1#![forbid(unsafe_code)]
7
8use super::{
9 command_tree_json, effective_timeout_ms, generate_completions, parse_exec_target,
10 parse_hosts_list, parse_remote_steps, read_stdin_if, warn_if_password_argv, CliArgs, Command,
11 LocaleAction, OutputFormat, ScpAction, SftpAction,
12};
13use anyhow::Result;
14
15pub async fn dispatch(args: CliArgs) -> Result<()> {
20 dispatch_impl(args).await
21}
22
23pub async fn dispatch_impl(args: CliArgs) -> Result<()> {
25 let config_override = args.config_dir.clone();
26 crate::secrets::set_config_dir(config_override.clone());
28 crate::secrets::set_runtime_flags(
29 args.allow_plaintext_secrets,
30 args.secrets_key_file.clone(),
31 args.use_keyring,
32 );
33 let limit = crate::concurrency::resolve_limit(args.max_concurrency.map(usize::from));
35 crate::concurrency::install_process_limit(limit);
36 crate::concurrency::install_fail_fast(args.fail_fast);
37 if let Some(n) = args.scp_file_concurrency {
38 crate::concurrency::install_scp_file_concurrency(usize::from(n));
39 }
40 let formato = super::resolve_format_from_cli(args.json, args.output_format)?;
42 crate::output::set_quiet(args.quiet);
44 crate::output::set_json_errors(formato == OutputFormat::Json);
45 let disable_sudo = args.disable_sudo;
46 let replace_host_key = args.replace_host_key;
47 let global_timeout = args.timeout;
49
50 warn_if_password_argv(&args);
52
53 match args.command {
54 Command::Vps { action } => {
55 crate::vps::run_vps_command(action, config_override, formato).await
58 }
59 Command::Connect { name } => {
60 crate::vps::run_connect(&name, config_override, formato).await
62 }
63 Command::Exec {
64 all,
65 hosts,
66 tags,
67 target,
68 steps,
69 json,
70 auth,
71 timeout,
72 description,
73 } => {
74 let (selection, command) = parse_exec_target(
75 all,
76 hosts,
77 tags,
78 target,
79 crate::vps::read_active_vps(config_override.as_deref())?,
80 )
81 .map_err(|s| {
82 if s.starts_with("no active VPS") {
83 crate::errors::SshCliError::NoActiveVps
84 } else {
85 crate::errors::SshCliError::InvalidArgument(s)
86 }
87 })?;
88 let key = auth.key_path_string();
89 let password = read_stdin_if(auth.password_stdin, auth.password)?;
90 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
91 let steps =
92 parse_remote_steps(steps).map_err(crate::errors::SshCliError::InvalidArgument)?;
93 let opts = crate::vps::ExecOptions {
94 password,
95 key,
96 key_passphrase,
97 timeout: effective_timeout_ms(timeout, global_timeout)
98 .map_err(crate::errors::SshCliError::InvalidArgument)?,
99 description,
100 replace_host_key,
101 disable_sudo,
102 steps,
103 use_agent: auth.use_agent,
104 agent_socket: auth
105 .agent_socket
106 .as_ref()
107 .map(|p| p.to_string_lossy().into_owned()),
108 ..Default::default()
109 };
110 crate::vps::run_exec(selection, &command, config_override, formato, json, opts).await
111 }
112 Command::SudoExec {
113 all,
114 hosts,
115 tags,
116 target,
117 steps,
118 json,
119 auth,
120 sudo_password,
121 sudo_password_stdin,
122 timeout,
123 description,
124 } => {
125 let (selection, command) = parse_exec_target(
126 all,
127 hosts,
128 tags,
129 target,
130 crate::vps::read_active_vps(config_override.as_deref())?,
131 )
132 .map_err(|s| {
133 if s.starts_with("no active VPS") {
134 crate::errors::SshCliError::NoActiveVps
135 } else {
136 crate::errors::SshCliError::InvalidArgument(s)
137 }
138 })?;
139 let key = auth.key_path_string();
140 let password = read_stdin_if(auth.password_stdin, auth.password)?;
141 let sudo_password = read_stdin_if(sudo_password_stdin, sudo_password)?;
142 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
143 let steps =
144 parse_remote_steps(steps).map_err(crate::errors::SshCliError::InvalidArgument)?;
145 let opts = crate::vps::ExecOptions {
146 password,
147 sudo_password,
148 key,
149 key_passphrase,
150 timeout: effective_timeout_ms(timeout, global_timeout)
151 .map_err(crate::errors::SshCliError::InvalidArgument)?,
152 description,
153 replace_host_key,
154 disable_sudo,
155 steps,
156 use_agent: auth.use_agent,
157 agent_socket: auth
158 .agent_socket
159 .as_ref()
160 .map(|p| p.to_string_lossy().into_owned()),
161 ..Default::default()
162 };
163 crate::vps::run_sudo_exec(selection, &command, config_override, formato, json, opts)
164 .await
165 }
166 Command::SuExec {
167 all,
168 hosts,
169 tags,
170 target,
171 steps,
172 json,
173 auth,
174 su_password,
175 su_password_stdin,
176 timeout,
177 description,
178 } => {
179 let (selection, command) = parse_exec_target(
180 all,
181 hosts,
182 tags,
183 target,
184 crate::vps::read_active_vps(config_override.as_deref())?,
185 )
186 .map_err(|s| {
187 if s.starts_with("no active VPS") {
188 crate::errors::SshCliError::NoActiveVps
189 } else {
190 crate::errors::SshCliError::InvalidArgument(s)
191 }
192 })?;
193 let key = auth.key_path_string();
194 let password = read_stdin_if(auth.password_stdin, auth.password)?;
195 let su_password = read_stdin_if(su_password_stdin, su_password)?;
196 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
197 let steps =
198 parse_remote_steps(steps).map_err(crate::errors::SshCliError::InvalidArgument)?;
199 let opts = crate::vps::ExecOptions {
200 password,
201 su_password,
202 key,
203 key_passphrase,
204 timeout: effective_timeout_ms(timeout, global_timeout)
205 .map_err(crate::errors::SshCliError::InvalidArgument)?,
206 description,
207 replace_host_key,
208 disable_sudo,
209 steps,
210 use_agent: auth.use_agent,
211 agent_socket: auth
212 .agent_socket
213 .as_ref()
214 .map(|p| p.to_string_lossy().into_owned()),
215 ..Default::default()
216 };
217 crate::vps::run_su_exec(selection, &command, config_override, formato, json, opts).await
218 }
219 Command::Scp { action } => {
220 let (auth, timeout, json_local) = match &action {
221 ScpAction::Upload {
222 auth,
223 timeout,
224 json,
225 ..
226 }
227 | ScpAction::Download {
228 auth,
229 timeout,
230 json,
231 ..
232 } => (auth.clone(), *timeout, *json),
233 };
234 let key = auth.key_path_string();
235 let password = read_stdin_if(auth.password_stdin, auth.password)?;
236 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
237 let json_efetivo = json_local || formato == OutputFormat::Json;
239 if json_efetivo {
240 crate::output::set_json_errors(true);
241 }
242 crate::scp::run_scp(
243 action,
244 config_override,
245 crate::scp::ScpOptions {
246 password,
247 key,
248 key_passphrase,
249 timeout: effective_timeout_ms(timeout, global_timeout)
250 .map_err(crate::errors::SshCliError::InvalidArgument)?,
251 replace_host_key,
252 json: json_efetivo,
253 use_agent: auth.use_agent,
254 agent_socket: auth
255 .agent_socket
256 .as_ref()
257 .map(|p| p.to_string_lossy().into_owned()),
258 },
259 )
260 .await
261 }
262 Command::Sftp { action } => {
263 let (auth, timeout, json_local) = match &action {
264 SftpAction::Upload {
265 auth,
266 timeout,
267 json,
268 ..
269 }
270 | SftpAction::Download {
271 auth,
272 timeout,
273 json,
274 ..
275 }
276 | SftpAction::Ls {
277 auth,
278 timeout,
279 json,
280 ..
281 }
282 | SftpAction::Mkdir {
283 auth,
284 timeout,
285 json,
286 ..
287 }
288 | SftpAction::Rmdir {
289 auth,
290 timeout,
291 json,
292 ..
293 }
294 | SftpAction::Rm {
295 auth,
296 timeout,
297 json,
298 ..
299 }
300 | SftpAction::Stat {
301 auth,
302 timeout,
303 json,
304 ..
305 }
306 | SftpAction::Rename {
307 auth,
308 timeout,
309 json,
310 ..
311 } => (auth.clone(), *timeout, *json),
312 };
313 let key = auth.key_path_string();
314 let password = read_stdin_if(auth.password_stdin, auth.password)?;
315 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
316 let json_efetivo = json_local || formato == OutputFormat::Json;
317 if json_efetivo {
318 crate::output::set_json_errors(true);
319 }
320 crate::sftp::run_sftp(
321 action,
322 config_override,
323 crate::sftp::SftpOptions {
324 password,
325 key,
326 key_passphrase,
327 timeout: effective_timeout_ms(timeout, global_timeout)
328 .map_err(crate::errors::SshCliError::InvalidArgument)?,
329 replace_host_key,
330 json: json_efetivo,
331 use_agent: auth.use_agent,
332 agent_socket: auth
333 .agent_socket
334 .as_ref()
335 .map(|p| p.to_string_lossy().into_owned()),
336 recursive: false, },
338 )
339 .await
340 }
341 Command::Tunnel {
342 vps_name,
343 local_port,
344 remote_host,
345 remote_port,
346 timeout_ms,
347 auth,
348 json,
349 bind,
350 } => {
351 let json_efetivo = json || formato == OutputFormat::Json;
353 if json_efetivo {
354 crate::output::set_json_errors(true);
355 }
356 let key = auth.key_path_string();
360 let password = read_stdin_if(auth.password_stdin, auth.password)?;
361 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
362 crate::tunnel::run_tunnel(
363 &vps_name,
364 local_port,
365 &remote_host,
366 remote_port,
367 config_override,
368 password,
369 key,
370 key_passphrase,
371 timeout_ms,
372 replace_host_key,
373 json_efetivo,
374 &bind,
375 )
376 .await
377 }
378 Command::HealthCheck {
379 vps_name,
380 all,
381 hosts,
382 json,
383 auth,
384 timeout,
385 } => {
386 let key = auth.key_path_string();
388 let password = read_stdin_if(auth.password_stdin, auth.password)?;
389 let key_passphrase = read_stdin_if(auth.key_passphrase_stdin, auth.key_passphrase)?;
390 let selection = if all {
391 crate::vps::HostSelection::All
392 } else if let Some(h) = hosts {
393 let names = parse_hosts_list(&h);
394 if names.is_empty() {
395 return Err(crate::errors::SshCliError::InvalidArgument(
396 "--hosts requires at least one host name".into(),
397 )
398 .into());
399 }
400 let names = names
401 .into_iter()
402 .map(crate::domain::VpsName::try_new)
403 .collect::<Result<Vec<_>, _>>()
404 .map_err(|e| crate::errors::SshCliError::InvalidArgument(e.to_string()))?;
405 crate::vps::HostSelection::Named(names)
406 } else {
407 let name = match vps_name {
408 Some(n) => n,
409 None => {
410 let active = crate::vps::read_active_vps(config_override.as_deref())?;
412 active.ok_or(crate::errors::SshCliError::NoActiveVps)?
413 }
414 };
415 let name = crate::domain::VpsName::try_new(name)
416 .map_err(|e| crate::errors::SshCliError::InvalidArgument(e.to_string()))?;
417 crate::vps::HostSelection::Single(name)
418 };
419 crate::vps::run_health_check(
420 selection,
421 config_override,
422 formato,
423 json,
424 password,
425 effective_timeout_ms(timeout, global_timeout)
426 .map_err(crate::errors::SshCliError::InvalidArgument)?,
427 key,
428 key_passphrase,
429 replace_host_key,
430 )
431 .await
432 }
433 Command::Secrets { action } => {
434 crate::vps::run_secrets_command(action, config_override, formato).await
436 }
437 Command::Completions { shell } => {
438 generate_completions(shell)
440 }
441 Command::Commands { json: _ } => {
442 crate::output::print_json_value(&command_tree_json())?;
445 Ok(())
446 }
447 Command::Schema { name, json } => {
448 let wants_json = json || formato == OutputFormat::Json;
450 crate::cli::run_schema(name.as_deref(), wants_json).map_err(Into::into)
451 }
452 Command::Doctor {
453 json,
454 probe_ssh,
455 hosts,
456 } => {
457 crate::vps::run_vps_command(
459 crate::cli::VpsAction::Doctor {
460 json,
461 probe_ssh,
462 hosts,
463 },
464 config_override,
465 formato,
466 )
467 .await
468 }
469 Command::Tls { json, action } => {
470 #[cfg(feature = "tls")]
471 {
472 crate::tls::commands::run_tls_command(action, config_override, formato, json).await
473 }
474 #[cfg(not(feature = "tls"))]
475 {
476 let _ = (action, json);
477 Err(crate::errors::SshCliError::tls_msg(
478 "TLS feature disabled; rebuild with --features tls (default)".into(),
479 )
480 .into())
481 }
482 }
483 Command::Locale { json, action } => {
484 run_locale_command(
486 action,
487 config_override.as_deref(),
488 formato,
489 args.lang.as_deref(),
490 json,
491 )
492 }
493 }
494}
495
496pub(crate) fn run_locale_command(
498 action: Option<LocaleAction>,
499 config_override: Option<&std::path::Path>,
500 formato: OutputFormat,
501 force_lang: Option<&str>,
502 json_flag: bool,
503) -> Result<()> {
504 use crate::i18n::{self, Message};
505 use crate::locale::{
506 clear_persisted_lang, current_language, lang_preference_path, negotiate_code,
507 resolve_language_detailed, write_persisted_lang,
508 };
509
510 let action = action.unwrap_or(LocaleAction::Show);
511 let override_ref = config_override;
512 let wants_json = json_flag || formato == OutputFormat::Json;
513
514 match action {
515 LocaleAction::Show => {
516 let detailed = resolve_language_detailed(force_lang, override_ref);
518 let available: Vec<&str> = crate::i18n::Language::AVAILABLE
519 .iter()
520 .map(|l| l.bcp47())
521 .collect();
522 let pref_path = lang_preference_path(override_ref)
523 .map(|p| p.display().to_string())
524 .unwrap_or_else(|| String::from("(unavailable)"));
525
526 if wants_json {
527 let body = serde_json::json!({
528 "resolved": detailed.language.bcp47(),
529 "current": current_language().bcp47(),
530 "source": detailed.source.as_str(),
531 "available": available,
532 "system_raw": detailed.system_raw,
533 "persisted_raw": detailed.persisted_raw,
534 "preference_path": pref_path,
535 "direction": match detailed.language.direction() {
536 crate::i18n::TextDirection::Ltr => "ltr",
537 crate::i18n::TextDirection::Rtl => "rtl",
538 },
539 "script": detailed.language.script(),
540 });
541 crate::output::print_json_value(&body)?;
542 } else {
543 crate::output::write_line(&i18n::t(Message::LocaleStatusTitle))?;
544 crate::output::write_line_fmt(format_args!(
545 " resolved: {} ({})",
546 detailed.language.bcp47(),
547 detailed.source.as_str()
548 ))?;
549 crate::output::write_line_fmt(format_args!(
550 " current: {}",
551 current_language().bcp47()
552 ))?;
553 crate::output::write_line_fmt(format_args!(
554 " available: {}",
555 available.join(", ")
556 ))?;
557 crate::output::write_line_fmt(format_args!(
558 " system: {}",
559 detailed.system_raw.as_deref().unwrap_or("(none)")
560 ))?;
561 crate::output::write_line_fmt(format_args!(
562 " persisted: {}",
563 detailed.persisted_raw.as_deref().unwrap_or("(none)")
564 ))?;
565 crate::output::write_line_fmt(format_args!(" pref_path: {pref_path}"))?;
566 }
567 Ok(())
568 }
569 LocaleAction::Set { lang } => {
570 let language = negotiate_code(&lang)
571 .ok_or_else(|| anyhow::anyhow!("unsupported language after validation: {lang}"))?;
572 let path = write_persisted_lang(language, override_ref)?;
573 if wants_json {
576 crate::output::print_json_value(&serde_json::json!({
577 "ok": true,
578 "lang": language.bcp47(),
579 "path": path.display().to_string(),
580 "applies": "next_invocation_unless_overridden",
581 }))?;
582 } else {
583 crate::output::print_success(&i18n::t(Message::LocalePreferenceSaved {
584 lang: language.bcp47().to_string(),
585 path: path.display().to_string(),
586 }));
587 }
588 Ok(())
589 }
590 LocaleAction::Clear => {
591 clear_persisted_lang(override_ref)?;
592 if wants_json {
593 crate::output::print_json_value(&serde_json::json!({
594 "ok": true,
595 "cleared": true,
596 }))?;
597 } else {
598 crate::output::print_success(&i18n::t(Message::LocalePreferenceCleared));
599 }
600 Ok(())
601 }
602 }
603}