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}