sc/cli/commands/
remote.rs1use crate::cli::commands::config::{build_ssh_base_args, load_remote_config, shell_quote};
11use crate::error::{Error, Result};
12use std::path::PathBuf;
13use std::process::Command;
14use tracing::debug;
15
16pub fn execute(args: &[String], _db_path: Option<&PathBuf>, json: bool) -> Result<()> {
18 if args.is_empty() {
19 return Err(Error::InvalidArgument(
20 "No command specified. Usage: sc remote <command> [args...]".to_string(),
21 ));
22 }
23
24 let config = load_remote_config()?;
25
26 let sc_path = config.remote_sc_path.as_deref().unwrap_or("sc");
28 let mut quoted_parts: Vec<String> = vec![shell_quote(sc_path)];
29 for arg in args {
30 quoted_parts.push(shell_quote(arg));
31 }
32
33 let has_json_flag = args.iter().any(|a| a == "--json" || a == "--format=json");
35 if json && !has_json_flag {
36 quoted_parts.push(shell_quote("--json"));
37 }
38
39 let remote_cmd = quoted_parts.join(" ");
40 debug!(remote_cmd = %remote_cmd, "Executing remote command");
41
42 let mut ssh_args = build_ssh_base_args(&config);
44 ssh_args.push(remote_cmd);
45
46 debug!(ssh_args = ?ssh_args, "SSH command");
47
48 let output = Command::new("ssh")
49 .args(&ssh_args)
50 .output()
51 .map_err(|e| {
52 Error::Remote(format!(
53 "Failed to execute ssh: {e}. Is ssh installed and in PATH?"
54 ))
55 })?;
56
57 if !output.stdout.is_empty() {
59 print!("{}", String::from_utf8_lossy(&output.stdout));
60 }
61
62 if !output.stderr.is_empty() {
64 eprint!("{}", String::from_utf8_lossy(&output.stderr));
65 }
66
67 if !output.status.success() {
68 let code = output.status.code().unwrap_or(1);
69 return Err(Error::Remote(format!(
70 "Remote command failed with exit code {code}"
71 )));
72 }
73
74 Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::cli::commands::config::RemoteConfig;
81
82 #[test]
83 fn test_build_ssh_base_args_default_port() {
84 let config = RemoteConfig {
85 host: "example.com".to_string(),
86 user: "shane".to_string(),
87 port: 22,
88 identity_file: None,
89 remote_sc_path: Some("sc".to_string()),
90 remote_project_path: None,
91 remote_db_path: None,
92 };
93
94 let args = build_ssh_base_args(&config);
95 assert!(args.contains(&"shane@example.com".to_string()));
96 assert!(!args.contains(&"-p".to_string()));
97 }
98
99 #[test]
100 fn test_build_ssh_base_args_custom_port() {
101 let config = RemoteConfig {
102 host: "example.com".to_string(),
103 user: "shane".to_string(),
104 port: 2222,
105 identity_file: None,
106 remote_sc_path: None,
107 remote_project_path: None,
108 remote_db_path: None,
109 };
110
111 let args = build_ssh_base_args(&config);
112 assert!(args.contains(&"-p".to_string()));
113 assert!(args.contains(&"2222".to_string()));
114 }
115
116 #[test]
117 fn test_build_ssh_base_args_with_identity() {
118 let config = RemoteConfig {
119 host: "example.com".to_string(),
120 user: "shane".to_string(),
121 port: 22,
122 identity_file: Some("~/.ssh/id_rsa".to_string()),
123 remote_sc_path: None,
124 remote_project_path: None,
125 remote_db_path: None,
126 };
127
128 let args = build_ssh_base_args(&config);
129 assert!(args.contains(&"-i".to_string()));
130 assert!(args.contains(&"~/.ssh/id_rsa".to_string()));
131 }
132}