Skip to main content

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}