ssh_cli/cli/commands.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-COMP: top-level clap Command tree extracted from cli/mod (SRP; line budget).
3#![forbid(unsafe_code)]
4//! Top-level and nested clap action enums (except `vps` / `scp` / `sftp`).
5
6use super::scp_args::ScpAction;
7use super::sftp_args::SftpAction;
8use super::vps_action::VpsAction;
9use super::SshAuthArgs;
10use clap::{ArgAction, Subcommand, ValueHint};
11use clap_complete::Shell;
12use std::path::PathBuf;
13
14/// Top-level subcommands.
15#[derive(Debug, Subcommand)]
16pub enum Command {
17 /// Manages registered VPS hosts.
18 Vps {
19 /// Specific VPS CRUD action.
20 #[command(subcommand)]
21 action: VpsAction,
22 },
23
24 /// Sets the active VPS (writes sibling `active` file in the config directory).
25 Connect {
26 /// Name of the VPS previously added via `vps add`.
27 name: String,
28 },
29
30 /// Runs a command on the VPS over SSH (stdout/stderr captured).
31 ///
32 /// Positionals: `VPS COMMAND`, or with `--all`/`--hosts`, only `COMMAND`
33 /// (`ssh-cli exec --all uptime`, `ssh-cli exec --hosts a,b uptime`).
34 /// Extra steps on the **same** SSH session: `--step cmd2 --step cmd3` (G-O3).
35 Exec {
36 /// Run on every registered host (bounded concurrency). When set, pass
37 /// only the shell command as the single positional.
38 #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["hosts", "tags"])]
39 all: bool,
40 /// Comma-separated host subset (bounded fan-out). Batch JSON even for one name.
41 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "tags"])]
42 hosts: Option<String>,
43 /// Select hosts that have **any** of these tags (OR). Batch JSON (G-O2).
44 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "hosts"])]
45 tags: Option<String>,
46 /// `VPS COMMAND` (one host) or `COMMAND` only when `--all` / `--hosts` / `--tags`.
47 #[arg(required = true, num_args = 1..=2, value_names = ["VPS", "COMMAND"])]
48 target: Vec<String>,
49 /// Additional commands on the same SSH session after the primary (G-O3).
50 #[arg(long = "step", value_name = "CMD", action = ArgAction::Append)]
51 steps: Vec<String>,
52 /// JSON output (from global `--json` / format; G-AUD-01).
53 #[arg(from_global)]
54 json: bool,
55 /// SSH authentication overrides (password/key/passphrase).
56 #[command(flatten)]
57 auth: SshAuthArgs,
58 /// Timeout override in milliseconds.
59 #[arg(long, value_name = "MS")]
60 timeout: Option<u64>,
61 /// Shell comment appended for audit trails.
62 #[arg(long)]
63 description: Option<String>,
64 },
65
66 /// Runs a command with `sudo` (safe `sh -c` packing).
67 ///
68 /// Positionals: `VPS COMMAND` or, with `--all`/`--hosts`/`--tags`, only `COMMAND`.
69 SudoExec {
70 /// Run on every registered host (bounded concurrency).
71 #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["hosts", "tags"])]
72 all: bool,
73 /// Comma-separated host subset (bounded fan-out).
74 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "tags"])]
75 hosts: Option<String>,
76 /// Select hosts by tag (OR). Batch JSON (G-O2).
77 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "hosts"])]
78 tags: Option<String>,
79 /// `VPS COMMAND` (one host) or `COMMAND` only when batch selection.
80 #[arg(required = true, num_args = 1..=2, value_names = ["VPS", "COMMAND"])]
81 target: Vec<String>,
82 /// Extra commands on the same session (G-O3).
83 #[arg(long = "step", value_name = "CMD", action = ArgAction::Append)]
84 steps: Vec<String>,
85 /// JSON output (from global `--json` / format; G-AUD-01).
86 #[arg(from_global)]
87 json: bool,
88 /// SSH authentication overrides (password/key/passphrase).
89 #[command(flatten)]
90 auth: SshAuthArgs,
91 /// Sudo password override.
92 #[arg(
93 long,
94 alias = "sudoPassword",
95 alias = "sudo_password",
96 conflicts_with = "sudo_password_stdin"
97 )]
98 sudo_password: Option<String>,
99 /// Reads the sudo password from stdin.
100 #[arg(long, action = ArgAction::SetTrue)]
101 sudo_password_stdin: bool,
102 /// Timeout override in milliseconds.
103 #[arg(long, value_name = "MS")]
104 timeout: Option<u64>,
105 /// Shell comment appended for audit.
106 #[arg(long)]
107 description: Option<String>,
108 },
109
110 /// Runs a command with one-shot `su -` elevation.
111 ///
112 /// Positionals: `VPS COMMAND` or, with `--all`/`--hosts`, only `COMMAND`.
113 SuExec {
114 /// Run on every registered host (bounded concurrency).
115 #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["hosts", "tags"])]
116 all: bool,
117 /// Comma-separated host subset (bounded fan-out).
118 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "tags"])]
119 hosts: Option<String>,
120 /// Select hosts by tag (OR). Batch JSON (G-O2).
121 #[arg(long, value_name = "LIST", conflicts_with_all = ["all", "hosts"])]
122 tags: Option<String>,
123 /// `VPS COMMAND` (one host) or `COMMAND` only when batch selection.
124 #[arg(required = true, num_args = 1..=2, value_names = ["VPS", "COMMAND"])]
125 target: Vec<String>,
126 /// Extra commands on the same session (G-O3).
127 #[arg(long = "step", value_name = "CMD", action = ArgAction::Append)]
128 steps: Vec<String>,
129 /// JSON output (from global `--json` / format; G-AUD-01).
130 #[arg(from_global)]
131 json: bool,
132 /// SSH authentication overrides (password/key/passphrase).
133 #[command(flatten)]
134 auth: SshAuthArgs,
135 /// Su password override.
136 #[arg(
137 long,
138 alias = "suPassword",
139 alias = "su_password",
140 conflicts_with = "su_password_stdin"
141 )]
142 su_password: Option<String>,
143 /// Reads the su password from stdin.
144 #[arg(long, action = ArgAction::SetTrue)]
145 su_password_stdin: bool,
146 /// Timeout override.
147 #[arg(long, value_name = "MS")]
148 timeout: Option<u64>,
149 /// Shell comment appended for audit.
150 #[arg(long)]
151 description: Option<String>,
152 },
153
154 /// SCP file transfer (upload/download).
155 Scp {
156 /// Specific SCP action.
157 #[command(subcommand)]
158 action: ScpAction,
159 },
160
161 /// SFTP subsystem transfer and remote filesystem ops (G-SFTP).
162 Sftp {
163 /// Specific SFTP action.
164 #[command(subcommand)]
165 action: SftpAction,
166 },
167
168 /// SSH tunnel with mandatory deadline (bounded one-shot).
169 ///
170 /// Contract: **one** local bind + **one** SSH session per invocation (G-PAR-30).
171 /// Multi-host tunnels = N one-shots with distinct `--bind`/ports. Forward
172 /// accepts still use JoinSet + Semaphore (`--max-concurrency`).
173 Tunnel {
174 /// VPS name (single host only — no `--all` / `--hosts`).
175 vps_name: String,
176 /// Local port.
177 local_port: u16,
178 /// Remote host.
179 remote_host: String,
180 /// Remote port.
181 #[arg(value_parser = clap::value_parser!(u16).range(1..=65535))]
182 remote_port: u16,
183 /// Mandatory tunnel timeout in milliseconds.
184 #[arg(long, value_name = "MS")]
185 timeout_ms: u64,
186 /// SSH authentication overrides (password/key/passphrase).
187 #[command(flatten)]
188 auth: SshAuthArgs,
189 /// Agent-first JSON output when the local listener is up (GAP-SSH-IO-008).
190 #[arg(from_global)]
191 json: bool,
192 /// Local bind address (default loopback for security).
193 #[arg(long, default_value = crate::constants::DEFAULT_TUNNEL_BIND_ADDR, value_name = "ADDR")]
194 bind: String,
195 },
196
197 /// Checks SSH connectivity to a VPS (or multi-host with `--all` / `--hosts`).
198 HealthCheck {
199 /// VPS name (uses active if omitted; ignored with `--all` / `--hosts`).
200 #[arg(conflicts_with_all = ["all", "hosts"])]
201 vps_name: Option<String>,
202 /// Probe every registered host in parallel (bounded concurrency).
203 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "hosts")]
204 all: bool,
205 /// Comma-separated host subset (bounded fan-out). Batch JSON even for one name.
206 #[arg(long, value_name = "LIST", conflicts_with = "all")]
207 hosts: Option<String>,
208 /// JSON output (GAP-SSH-IO-002). Single host: classic object; multi: batch.
209 #[arg(from_global)]
210 json: bool,
211 /// SSH authentication overrides (password/key/passphrase).
212 #[command(flatten)]
213 auth: SshAuthArgs,
214 /// SSH timeout override in milliseconds (GAP-SSH-CLI-004).
215 #[arg(long, value_name = "MS")]
216 timeout: Option<u64>,
217 },
218
219 /// Manages the primary key and at-rest secret encryption (one-shot).
220 Secrets {
221 /// Secrets action.
222 #[command(subcommand)]
223 action: SecretsAction,
224 },
225
226 /// Generates shell completions.
227 Completions {
228 /// Target shell.
229 #[arg(value_enum)]
230 shell: Shell,
231 },
232
233 /// Emits the full command tree as JSON (agent discovery / rules `mycli commands`).
234 Commands {
235 /// JSON output (from global `--json`).
236 #[arg(from_global)]
237 json: bool,
238 },
239
240 /// Emits embedded JSON Schema catalog or one schema body (G-E2E-02).
241 Schema {
242 /// Schema name (omit to list catalog). Example: `vps-list`.
243 name: Option<String>,
244 /// JSON catalog envelope when listing (from global `--json`).
245 #[arg(from_global)]
246 json: bool,
247 },
248
249 /// Root alias for `vps doctor` (XDG / schema diagnostics; G-E2E-03).
250 Doctor {
251 /// JSON output (from global `--json`).
252 #[arg(from_global)]
253 json: bool,
254 /// Also probe SSH health on registered hosts.
255 #[arg(long, action = ArgAction::SetTrue)]
256 probe_ssh: bool,
257 /// Comma-separated host subset for `--probe-ssh`.
258 #[arg(long, value_name = "LIST")]
259 hosts: Option<String>,
260 },
261
262 /// Diagnoses and manages UI language (locale resolution / XDG preference).
263 Locale {
264 /// JSON diagnostics (from global `--json` / format).
265 #[arg(from_global)]
266 json: bool,
267 /// Optional locale action (default: show status).
268 #[command(subcommand)]
269 action: Option<LocaleAction>,
270 },
271 /// TLS stack: provider status, mTLS identities, ACME certs (XDG; rustls only).
272 Tls {
273 /// JSON output (from global `--json`).
274 #[arg(from_global)]
275 json: bool,
276 /// TLS action.
277 #[command(subcommand)]
278 action: TlsAction,
279 },
280}
281
282/// Actions of the `tls` subcommand (SSH-over-TLS / mTLS / ACME).
283#[derive(Debug, Subcommand)]
284pub enum TlsAction {
285 /// Shows rustls CryptoProvider status (`aws_lc_rs`).
286 Provider,
287 /// Prints XDG TLS directory layout paths.
288 Paths,
289 /// Manages imported mTLS client identities under XDG `tls/mtls/`.
290 Mtls {
291 /// mTLS action.
292 #[command(subcommand)]
293 action: TlsMtlsAction,
294 },
295 /// ACME (Let's Encrypt) account + DNS-01 certificate lifecycle.
296 Acme {
297 /// ACME action.
298 #[command(subcommand)]
299 action: TlsAcmeAction,
300 },
301}
302
303/// mTLS identity store actions.
304#[derive(Debug, Subcommand)]
305pub enum TlsMtlsAction {
306 /// Lists imported identity names.
307 List,
308 /// Imports PEM cert+key as a named identity.
309 Import {
310 /// Identity name (XDG leaf).
311 #[arg(long)]
312 name: String,
313 /// Certificate chain PEM path.
314 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
315 cert: PathBuf,
316 /// Private key PEM path.
317 #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
318 key: PathBuf,
319 },
320 /// Shows paths for one identity.
321 Show {
322 /// Identity name.
323 name: String,
324 },
325 /// Removes an identity directory.
326 Remove {
327 /// Identity name.
328 name: String,
329 },
330}
331
332/// ACME actions (DNS-01, agent two-step).
333#[derive(Debug, Subcommand)]
334pub enum TlsAcmeAction {
335 /// ACME account management.
336 Account {
337 /// Account action.
338 #[command(subcommand)]
339 action: TlsAcmeAccountAction,
340 },
341 /// Starts DNS-01 order and prints the TXT challenge (persists order URL under XDG).
342 Issue {
343 /// Domain name (DNS identifier).
344 #[arg(long)]
345 domain: String,
346 /// Use Let's Encrypt staging directory.
347 #[arg(long, action = ArgAction::SetTrue)]
348 staging: bool,
349 /// Required: print challenge and exit (agent-friendly; no interactive wait).
350 #[arg(long, action = ArgAction::SetTrue)]
351 print_challenge: bool,
352 },
353 /// Completes a pending order after DNS TXT is published.
354 Complete {
355 /// Domain name.
356 #[arg(long)]
357 domain: String,
358 },
359 /// Shows certificate / pending status for one domain or all.
360 Status {
361 /// Optional domain filter.
362 #[arg(long)]
363 domain: Option<String>,
364 },
365 /// Lists ACME domain directories under XDG.
366 List,
367}
368
369/// ACME account sub-actions.
370#[derive(Debug, Subcommand)]
371pub enum TlsAcmeAccountAction {
372 /// Creates an ACME account (credentials under XDG `tls/acme/account.json`, 0o600).
373 Create {
374 /// Use Let's Encrypt staging.
375 #[arg(long, action = ArgAction::SetTrue)]
376 staging: bool,
377 /// Contact URLs (e.g. `mailto:ops@example.com`). Required; repeatable (G-AUD-06).
378 #[arg(long = "contact", value_name = "URL", action = ArgAction::Append, required = true, num_args = 1..)]
379 contact: Vec<String>,
380 /// Replace existing account credentials.
381 #[arg(long, action = ArgAction::SetTrue)]
382 force: bool,
383 },
384 /// Shows whether an account exists and its path.
385 Show,
386}
387
388/// Actions of the `locale` subcommand.
389#[derive(Debug, Subcommand)]
390pub enum LocaleAction {
391 /// Shows resolved language, winning layer, and available locales (default).
392 Show,
393 /// Persists preferred language under the config directory (`lang` file, 0o600).
394 Set {
395 /// BCP47 tag that negotiates to a supported locale (`en`, `pt-BR`, …).
396 #[arg(value_name = "LOCALE", value_parser = crate::locale::parse_lang_cli_arg)]
397 lang: String,
398 },
399 /// Removes the persisted language preference.
400 Clear,
401}
402
403/// Actions of the `secrets` subcommand (primary-key / AEAD).
404#[derive(Debug, Subcommand)]
405pub enum SecretsAction {
406 /// Shows encryption status (no sensitive material).
407 Status {
408 /// JSON output (from global `--json`).
409 #[arg(from_global)]
410 json: bool,
411 },
412 /// Generates and stores the primary key (`secrets.key` or keyring). Never prints the key.
413 Init {
414 /// Store in the OS keyring instead of `secrets.key`.
415 #[arg(long)]
416 keyring: bool,
417 /// Overwrites an existing key.
418 #[arg(long)]
419 force: bool,
420 /// JSON success envelope (`event: secrets-init`; from global `--json`).
421 #[arg(from_global)]
422 json: bool,
423 },
424 /// Rewrites `config.toml` re-encrypting secrets with the current key.
425 Reencrypt {
426 /// JSON success envelope (`event: secrets-reencrypt`; from global `--json`).
427 #[arg(from_global)]
428 json: bool,
429 },
430}