Skip to main content

ssh_cli/cli/
vps_action.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-COMP: clap `vps` action tree extracted from cli/mod (SRP; line budget).
3#![forbid(unsafe_code)]
4//! Clap types for `ssh-cli vps …`.
5
6use super::parse_cli_char_limit;
7use clap::{ArgAction, Subcommand, ValueHint};
8use std::path::PathBuf;
9
10/// Actions of the `vps` subcommand.
11#[derive(Debug, Subcommand)]
12pub enum VpsAction {
13    /// Adds a new VPS to the registry.
14    Add {
15        /// Unique VPS name.
16        #[arg(long)]
17        name: String,
18        /// Hostname or IP.
19        #[arg(long)]
20        host: String,
21        /// SSH port.
22        #[arg(
23            long,
24            default_value_t = crate::constants::DEFAULT_SSH_PORT,
25            value_parser = clap::value_parser!(u16).range(1..=65535)
26        )]
27        port: u16,
28        /// SSH username.
29        #[arg(long)]
30        user: String,
31        /// SSH password.
32        #[arg(long, conflicts_with = "password_stdin")]
33        password: Option<String>,
34        /// Reads the password from stdin.
35        #[arg(long)]
36        password_stdin: bool,
37        /// OpenSSH private key path.
38        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
39        key: Option<PathBuf>,
40        /// Key passphrase.
41        #[arg(long, conflicts_with = "key_passphrase_stdin")]
42        key_passphrase: Option<String>,
43        /// Reads the key passphrase from stdin.
44        ///
45        /// D13: `exec`, `scp`, `health-check` and `tunnel` all accepted
46        /// `--key-passphrase-stdin`, but `vps add` and `vps edit` — the only two
47        /// commands that *persist* the passphrase — did not. The one secret that
48        /// reaches disk was also the one that had to travel through argv, where
49        /// any local process can read it from `ps`. Found by running the E2E
50        /// matrix for real for the first time (D1).
51        #[arg(long, action = ArgAction::SetTrue)]
52        key_passphrase_stdin: bool,
53        /// Authenticate via SSH agent (mutually exclusive with --password / --key; G-E2E-19).
54        #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["password", "password_stdin", "key"])]
55        use_agent: bool,
56        /// Optional SSH agent socket path (defaults to platform agent when omitted).
57        #[arg(long, value_name = "PATH", value_hint = ValueHint::AnyPath)]
58        agent_socket: Option<PathBuf>,
59        /// Timeout in milliseconds (default [`crate::vps::model::DEFAULT_TIMEOUT_MS`]).
60        #[arg(long, default_value_t = crate::vps::model::DEFAULT_TIMEOUT_MS, value_name = "MS")]
61        timeout: u64,
62        /// Command character limit (input). Use `0` or `none` for unlimited.
63        #[arg(long, value_name = "N", value_parser = parse_cli_char_limit)]
64        max_command_chars: Option<usize>,
65        /// Output character limit. Use `0` or `none` for unlimited.
66        #[arg(long, value_name = "N", value_parser = parse_cli_char_limit)]
67        max_output_chars: Option<usize>,
68        /// Legacy alias: maps to max_command_chars.
69        #[arg(long, alias = "maxChars", value_name = "N", value_parser = parse_cli_char_limit)]
70        max_chars: Option<usize>,
71        /// Password for `sudo`.
72        #[arg(
73            long,
74            alias = "sudoPassword",
75            alias = "sudo_password",
76            conflicts_with = "sudo_password_stdin"
77        )]
78        sudo_password: Option<String>,
79        /// Reads the sudo password from stdin.
80        #[arg(long)]
81        sudo_password_stdin: bool,
82        /// Password for `su -`.
83        #[arg(
84            long,
85            alias = "suPassword",
86            alias = "su_password",
87            conflicts_with = "su_password_stdin"
88        )]
89        su_password: Option<String>,
90        /// Reads the su password from stdin.
91        #[arg(long)]
92        su_password_stdin: bool,
93        /// Disables sudo/su on this host.
94        #[arg(long, default_value_t = false)]
95        disable_sudo: bool,
96        /// Host tags for fleet selection (G-O2). Repeatable: `--tag prod --tag web`.
97        #[arg(long = "tag", value_name = "TAG", action = ArgAction::Append)]
98        tags: Vec<String>,
99        /// Enable SSH-over-TLS (rustls) for this host.
100        #[arg(long, action = ArgAction::SetTrue)]
101        tls: bool,
102        /// TLS SNI / cert name (defaults to `--host` when omitted).
103        #[arg(long, value_name = "NAME")]
104        tls_sni: Option<String>,
105        /// mTLS client certificate PEM path.
106        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
107        tls_client_cert: Option<PathBuf>,
108        /// mTLS client private key PEM path.
109        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
110        tls_client_key: Option<PathBuf>,
111        /// Runs health-check after add.
112        #[arg(long)]
113        check: bool,
114    },
115
116    /// Lists all VPS hosts (passwords masked).
117    List {
118        /// JSON output (from global `--json`).
119        #[arg(from_global)]
120        json: bool,
121        /// Filter hosts that have **any** of these tags (OR, G-O2).
122        #[arg(long = "tag", value_name = "TAG", action = ArgAction::Append)]
123        tags: Vec<String>,
124    },
125
126    /// Removes a VPS from the registry.
127    Remove {
128        /// VPS name to remove.
129        name: String,
130    },
131
132    /// Edits fields of an existing VPS.
133    Edit {
134        /// VPS name to edit.
135        name: String,
136        /// New hostname/IP.
137        #[arg(long)]
138        host: Option<String>,
139        /// New SSH port.
140        #[arg(long, value_parser = clap::value_parser!(u16).range(1..=65535))]
141        port: Option<u16>,
142        /// New username.
143        #[arg(long)]
144        user: Option<String>,
145        /// New password.
146        #[arg(long, conflicts_with = "password_stdin")]
147        password: Option<String>,
148        /// Reads the password from stdin.
149        #[arg(long)]
150        password_stdin: bool,
151        /// New private key path.
152        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
153        key: Option<PathBuf>,
154        /// New key passphrase.
155        #[arg(long, conflicts_with = "key_passphrase_stdin")]
156        key_passphrase: Option<String>,
157        /// Reads the new key passphrase from stdin (D13 — see `VpsAction::Add`).
158        #[arg(long, action = ArgAction::SetTrue)]
159        key_passphrase_stdin: bool,
160        /// Switch primary auth to SSH agent (clears password/key when set; G-E2E-19).
161        #[arg(long, action = ArgAction::SetTrue, conflicts_with_all = ["password", "password_stdin", "key"])]
162        use_agent: bool,
163        /// Optional SSH agent socket path.
164        #[arg(long, value_name = "PATH", value_hint = ValueHint::AnyPath)]
165        agent_socket: Option<PathBuf>,
166        /// New timeout.
167        #[arg(long, value_name = "MS")]
168        timeout: Option<u64>,
169        /// New max command chars. Use `0` or `none` for unlimited.
170        #[arg(long, value_name = "N", value_parser = parse_cli_char_limit)]
171        max_command_chars: Option<usize>,
172        /// New max output chars. Use `0` or `none` for unlimited.
173        #[arg(long, value_name = "N", value_parser = parse_cli_char_limit)]
174        max_output_chars: Option<usize>,
175        /// Legacy alias maxChars → command.
176        #[arg(long, alias = "maxChars", value_name = "N", value_parser = parse_cli_char_limit)]
177        max_chars: Option<usize>,
178        /// New sudo password.
179        #[arg(
180            long,
181            alias = "sudoPassword",
182            alias = "sudo_password",
183            conflicts_with = "sudo_password_stdin"
184        )]
185        sudo_password: Option<String>,
186        /// Reads the sudo password from stdin.
187        #[arg(long, action = ArgAction::SetTrue)]
188        sudo_password_stdin: bool,
189        /// New su password.
190        #[arg(
191            long,
192            alias = "suPassword",
193            alias = "su_password",
194            conflicts_with = "su_password_stdin"
195        )]
196        su_password: Option<String>,
197        /// Reads the su password from stdin.
198        #[arg(long, action = ArgAction::SetTrue)]
199        su_password_stdin: bool,
200        /// Disable sudo/su elevation for this host.
201        #[arg(long, action = ArgAction::SetTrue, conflicts_with = "enable_sudo")]
202        disable_sudo: bool,
203        /// Re-enable sudo/su elevation for this host.
204        #[arg(long, action = ArgAction::SetTrue, conflicts_with = "disable_sudo")]
205        enable_sudo: bool,
206        /// Enable SSH-over-TLS for this host.
207        #[arg(long, action = ArgAction::SetTrue, conflicts_with = "no_tls")]
208        tls: bool,
209        /// Disable SSH-over-TLS for this host.
210        #[arg(long, action = ArgAction::SetTrue, conflicts_with = "tls")]
211        no_tls: bool,
212        /// TLS SNI / cert name.
213        #[arg(long, value_name = "NAME")]
214        tls_sni: Option<String>,
215        /// mTLS client certificate PEM path.
216        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
217        tls_client_cert: Option<PathBuf>,
218        /// mTLS client private key PEM path.
219        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
220        tls_client_key: Option<PathBuf>,
221    },
222
223    /// Shows VPS details (passwords masked).
224    Show {
225        /// VPS name.
226        name: String,
227        /// JSON output (from global `--json`).
228        #[arg(from_global)]
229        json: bool,
230    },
231
232    /// Shows the configuration file path.
233    Path,
234
235    /// Diagnostics for XDG layers / path / schema.
236    Doctor {
237        /// JSON output (from global `--json`).
238        #[arg(from_global)]
239        json: bool,
240        /// After local diagnostics, probe hosts over SSH (bounded fan-out, G-PAR-29).
241        /// Default scope is every registered host; use `--hosts` for a subset (G-PAR-38).
242        #[arg(long, action = ArgAction::SetTrue)]
243        probe_ssh: bool,
244        /// Comma-separated host subset for `--probe-ssh` only (ignored without probe).
245        #[arg(long, value_name = "LIST")]
246        hosts: Option<String>,
247    },
248
249    /// Exports hosts (passwords redacted by default).
250    Export {
251        /// Include secrets in the export.
252        #[arg(long)]
253        include_secrets: bool,
254        /// Output file (stdout if omitted). Written atomically with mode 0o600.
255        #[arg(long, short, value_name = "PATH", value_hint = ValueHint::FilePath)]
256        output: Option<PathBuf>,
257        /// Agent-first JSON envelope (`event: vps-export`; from global `--json`).
258        /// Default body is **TOML** on text; JSON when format/json (G-AUD-03).
259        #[arg(from_global)]
260        json: bool,
261        /// Acknowledge writing plaintext secrets to stdout (pipe/non-TTY). Prefer `--output`.
262        #[arg(long)]
263        i_understand_secrets_on_stdout: bool,
264    },
265
266    /// Imports hosts from a TOML file or JSON `vps-export` envelope (EN + legacy PT keys).
267    Import {
268        /// Source file (TOML wire or JSON export envelope).
269        #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)]
270        file: PathBuf,
271        /// Allow hosts without full auth (redacted export / skeleton) — GAP-SSH-IMP-001.
272        #[arg(long)]
273        allow_incomplete: bool,
274    },
275}