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