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