rush_sync_server/commands/remote/
command.rs1use crate::commands::command::Command;
2use crate::core::prelude::*;
3use crate::sync::profiles::{parse_user_host, RemoteProfile, RemoteProfileStore};
4use crate::sync::transport::test_connection;
5
6#[derive(Debug, Default)]
7pub struct RemoteCommand;
8
9impl RemoteCommand {
10 pub fn new() -> Self {
11 Self
12 }
13}
14
15impl Command for RemoteCommand {
16 fn name(&self) -> &'static str {
17 "remote"
18 }
19
20 fn description(&self) -> &'static str {
21 "Manage SSH remote profiles for sync/deploy"
22 }
23
24 fn matches(&self, command: &str) -> bool {
25 command.trim().to_lowercase().starts_with("remote")
26 }
27
28 fn execute_sync(&self, args: &[&str]) -> Result<String> {
29 let store = RemoteProfileStore::new()?;
30
31 match args.first().copied() {
32 None | Some("-h" | "--help" | "help") => Ok(self.help_text(store.path())),
33 Some("list" | "ls") => self.list_profiles(&store),
34 Some("add") => self.add_profile(&store, args),
35 Some("show") => self.show_profile(&store, args),
36 Some("remove" | "rm" | "delete") => self.remove_profile(&store, args),
37 Some("test") => self.test_profile(&store, args),
38 Some(sub) => Err(AppError::Validation(format!(
39 "Unknown remote subcommand '{}'. Use 'remote help'.",
40 sub
41 ))),
42 }
43 }
44
45 fn priority(&self) -> u8 {
46 72
47 }
48}
49
50impl RemoteCommand {
51 fn add_profile(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
52 if args.len() < 4 {
53 return Err(AppError::Validation(
54 "Usage: remote add <name> <user@host> <remote_path> [port] [identity_file]"
55 .to_string(),
56 ));
57 }
58
59 let name = args[1];
60 let (user, host) = parse_user_host(args[2])?;
61 let remote_path = args[3].to_string();
62
63 let (port, identity_file) = match args.get(4) {
64 None => (22, None),
65 Some(port_or_identity) => {
66 if let Ok(port) = port_or_identity.parse::<u16>() {
67 let identity = args.get(5).map(|s| (*s).to_string());
68 (port, identity)
69 } else {
70 (22, Some((*port_or_identity).to_string()))
71 }
72 }
73 };
74
75 let profile = RemoteProfile::new(user, host, remote_path, port, identity_file)?;
76 let existed = store.exists(name)?;
77 store.upsert(name, profile)?;
78
79 Ok(if existed {
80 format!("Remote '{}' updated", name)
81 } else {
82 format!("Remote '{}' added", name)
83 })
84 }
85
86 fn list_profiles(&self, store: &RemoteProfileStore) -> Result<String> {
87 let profiles = store.list()?;
88 if profiles.is_empty() {
89 return Ok(format!(
90 "No remotes configured yet.\nFile: {}",
91 store.path().display()
92 ));
93 }
94
95 let mut out = String::from("Configured remotes:\n");
96 for (name, profile) in profiles {
97 let identity = profile.identity_file.as_deref().unwrap_or("-");
98
99 out.push_str(&format!(
100 " {} -> {}@{}:{} {}\n",
101 name, profile.user, profile.host, profile.port, profile.remote_path
102 ));
103 out.push_str(&format!(" identity: {}\n", identity));
104 }
105 out.push_str(&format!("\nFile: {}", store.path().display()));
106 Ok(out)
107 }
108
109 fn show_profile(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
110 let name = args
111 .get(1)
112 .ok_or_else(|| AppError::Validation("Usage: remote show <name>".to_string()))?;
113
114 let profile = store.get(name)?;
115 Ok(format!(
116 "Remote '{}'\n user: {}\n host: {}\n port: {}\n remote_path: {}\n identity_file: {}",
117 name,
118 profile.user,
119 profile.host,
120 profile.port,
121 profile.remote_path,
122 profile.identity_file.as_deref().unwrap_or("-")
123 ))
124 }
125
126 fn remove_profile(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
127 let name = args
128 .get(1)
129 .ok_or_else(|| AppError::Validation("Usage: remote remove <name>".to_string()))?;
130
131 store.remove(name)?;
132 Ok(format!("Remote '{}' removed", name))
133 }
134
135 fn test_profile(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
136 let name = args
137 .get(1)
138 .ok_or_else(|| AppError::Validation("Usage: remote test <name>".to_string()))?;
139
140 let profile = store.get(name)?;
141 let message = test_connection(&profile)?;
142 Ok(format!("{} [{}]", message, name))
143 }
144
145 fn help_text(&self, file_path: &std::path::Path) -> String {
146 format!(
147 "Remote profile management\n\n\
148 Commands:\n\
149 remote list\n\
150 remote add <name> <user@host> <remote_path> [port] [identity_file]\n\
151 remote show <name>\n\
152 remote remove <name>\n\
153 remote test <name>\n\n\
154 Example:\n\
155 remote add prod deploy@example.com /opt/rush-sync 22 ~/.ssh/id_ed25519\n\n\
156 Storage:\n\
157 {}",
158 file_path.display()
159 )
160 }
161}