Skip to main content

rush_sync_server/commands/remote/
command.rs

1use 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}