feat(runtime): run local shell commands from UI
test / workspace (push) Successful in 12m36s

This commit is contained in:
Tom You
2026-07-09 00:17:45 -05:00
parent fc17c44c38
commit a215d91f40
6 changed files with 185 additions and 1 deletions
+1
View File
@@ -1,3 +1,4 @@
pub mod config_store;
pub mod local_terminal;
pub mod pty;
pub mod wallet_vault;
+118
View File
@@ -0,0 +1,118 @@
use serde::Serialize;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalCommand {
pub command: String,
pub cwd: Option<PathBuf>,
}
impl LocalCommand {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
cwd: None,
}
}
pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.cwd = Some(cwd.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LocalCommandOutput {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalTerminalError {
InvalidCommand(String),
Io(String),
}
#[derive(Debug, Default, Clone)]
pub struct LocalTerminal;
impl LocalTerminal {
pub fn run(&self, command: LocalCommand) -> Result<LocalCommandOutput, LocalTerminalError> {
if command.command.trim().is_empty() {
return Err(LocalTerminalError::InvalidCommand(
"command is required".to_string(),
));
}
let mut child = Command::new(default_shell());
child.arg("-lc").arg(&command.command);
if let Some(cwd) = command.cwd {
child.current_dir(cwd);
}
let output = child
.output()
.map_err(|err| LocalTerminalError::Io(err.to_string()))?;
Ok(LocalCommandOutput {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
})
}
}
fn default_shell() -> &'static str {
if cfg!(windows) {
"cmd"
} else {
"/bin/sh"
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_terminal_runs_shell_command_and_captures_stdout() {
let output = LocalTerminal::default()
.run(LocalCommand::new("printf 'rabby-local-terminal'"))
.unwrap();
assert_eq!(output.exit_code, 0);
assert_eq!(output.stdout, "rabby-local-terminal");
assert!(output.stderr.is_empty());
}
#[test]
fn local_terminal_reports_non_zero_exit() {
let output = LocalTerminal::default()
.run(LocalCommand::new("printf error >&2; exit 7"))
.unwrap();
assert_eq!(output.exit_code, 7);
assert_eq!(output.stderr, "error");
}
#[test]
fn local_terminal_rejects_blank_commands() {
let err = LocalTerminal::default()
.run(LocalCommand::new(" "))
.unwrap_err();
assert_eq!(
err,
LocalTerminalError::InvalidCommand("command is required".to_string())
);
}
#[test]
fn local_terminal_runs_from_working_directory() {
let dir = std::env::temp_dir();
let output = LocalTerminal::default()
.run(LocalCommand::new("pwd").with_cwd(&dir))
.unwrap();
assert_eq!(output.exit_code, 0);
assert_eq!(output.stdout.trim(), dir.display().to_string());
}
}