purple_ssh/cli_args.rs
1//! clap derive structs for the `purple` CLI.
2//!
3//! Everything related to argument parsing lives here so `main.rs` stays
4//! focused on orchestration. `Cli` is the top-level `Parser`; each
5//! `Commands` variant holds the subcommand-specific args.
6
7use clap::{Parser, Subcommand};
8use clap_complete::Shell;
9
10#[derive(Parser)]
11#[command(
12 name = "purple",
13 about = "Your SSH config is a mess. Purple fixes that.",
14 long_about = "Purple is a terminal SSH client for managing your hosts.\n\
15 Add, edit, delete and connect without opening a text editor.\n\n\
16 Life's too short for nano ~/.ssh/config.",
17 version
18)]
19pub struct Cli {
20 /// Connect to a host by alias, or filter the TUI
21 #[arg(value_name = "ALIAS")]
22 pub alias: Option<String>,
23
24 /// Connect directly to a host by alias (skip the TUI)
25 #[arg(short, long)]
26 pub connect: Option<String>,
27
28 /// List all configured hosts
29 #[arg(short, long)]
30 pub list: bool,
31
32 /// Path to SSH config file
33 #[arg(long, default_value = "~/.ssh/config")]
34 pub config: String,
35
36 /// Launch with demo data (no real config needed)
37 #[arg(long)]
38 pub demo: bool,
39
40 /// Generate shell completions
41 #[arg(long, value_name = "SHELL")]
42 pub completions: Option<Shell>,
43
44 /// Override theme for this session
45 #[arg(long)]
46 pub theme: Option<String>,
47
48 /// Enable verbose logging (debug level)
49 #[arg(
50 long,
51 long_help = "Enable verbose logging (debug level).\n\n\
52 Logs are written to purple.log in the state directory,\n\
53 ~/.purple by default (rotates at 5MB). `purple logs` prints\n\
54 the path; tail with `purple logs --tail`.\n\n\
55 Set PURPLE_LOG=trace|debug|info|warn|error to override the\n\
56 level without --verbose. PURPLE_LOG takes precedence."
57 )]
58 pub verbose: bool,
59
60 #[command(subcommand)]
61 pub command: Option<Commands>,
62}
63
64#[derive(Subcommand)]
65pub enum Commands {
66 /// Quick-add a host: purple add user@host:port --alias myserver
67 Add {
68 /// Target in user@hostname:port format
69 target: String,
70
71 /// Alias for the host (default: derived from hostname)
72 #[arg(short, long)]
73 alias: Option<String>,
74
75 /// Path to identity file (SSH key)
76 #[arg(short, long)]
77 key: Option<String>,
78 },
79 /// Import hosts from a file or known_hosts
80 Import {
81 /// File with one host per line (user@host:port format)
82 file: Option<String>,
83
84 /// Import from ~/.ssh/known_hosts instead
85 #[arg(long)]
86 known_hosts: bool,
87
88 /// Group label for imported hosts
89 #[arg(short, long)]
90 group: Option<String>,
91 },
92 /// Sync hosts from cloud providers (DigitalOcean, Vultr, Linode, Hetzner, UpCloud, Proxmox VE, AWS EC2, Scaleway, GCP, Azure, Tailscale, Oracle Cloud, OVHcloud, Leaseweb, i3D.net, TransIP, Teleport)
93 Sync {
94 /// Sync a specific provider (default: all configured)
95 provider: Option<String>,
96
97 /// Preview changes without modifying config
98 #[arg(long)]
99 dry_run: bool,
100
101 /// Remove hosts that no longer exist on the provider
102 #[arg(long)]
103 remove: bool,
104 },
105 /// Manage cloud provider configurations
106 Provider {
107 #[command(subcommand)]
108 command: ProviderCommands,
109 },
110 /// Manage SSH tunnels
111 Tunnel {
112 #[command(subcommand)]
113 command: TunnelCommands,
114 },
115 /// Manage passwords in the OS keychain for SSH hosts
116 Password {
117 #[command(subcommand)]
118 command: PasswordCommands,
119 },
120 /// Manage command snippets for quick execution on hosts
121 Snippet {
122 #[command(subcommand)]
123 command: SnippetCommands,
124 },
125 /// Update purple to the latest version
126 Update,
127 /// Start MCP server (Model Context Protocol) for AI agent integration
128 Mcp {
129 /// Restrict tools to read-only operations. Denies run_command and container_action,
130 /// and removes them from tools/list. Recommended when exposing purple to autonomous agents.
131 #[arg(long)]
132 read_only: bool,
133
134 /// Disable the MCP audit log. By default every tool call is appended to
135 /// mcp-audit.log in the state directory (~/.purple by default) as JSON Lines.
136 #[arg(long)]
137 no_audit: bool,
138
139 /// Custom path for the MCP audit log (default: mcp-audit.log in the state
140 /// directory, ~/.purple by default). Ignored when --no-audit is set.
141 #[arg(long, value_name = "PATH")]
142 audit_log: Option<String>,
143 },
144 /// Manage color themes
145 Theme {
146 #[command(subcommand)]
147 command: ThemeCommands,
148 },
149 /// HashiCorp Vault SSH secrets engine operations (signed SSH certificates)
150 Vault {
151 #[command(subcommand)]
152 command: VaultCommands,
153 },
154 /// View or manage log file
155 Logs {
156 /// Follow log output in real time
157 #[arg(long)]
158 tail: bool,
159
160 /// Delete the log file
161 #[arg(long)]
162 clear: bool,
163 },
164 /// Print release notes since a prior version
165 WhatsNew {
166 /// Only include entries newer than this version (e.g. 2.40.0)
167 #[arg(long)]
168 since: Option<String>,
169 },
170 /// Render marketing SVG screenshots from the demo TUI. Internal: drives the
171 /// release imagery pipeline so README and landing-page assets never go stale.
172 #[command(hide = true)]
173 GenAssets {
174 /// Output directory for the generated SVGs
175 #[arg(default_value = "assets/svg")]
176 out_dir: String,
177
178 /// Directory holding ui-mono-regular.woff2 and ui-mono-bold.woff2 to
179 /// embed. Pass an empty value to render with the system monospace.
180 #[arg(long, default_value = "assets/fonts")]
181 font_dir: String,
182
183 /// Also write the looping animated hero SVG (all five tabs with
184 /// detail panels, search typing itself) to this path.
185 #[arg(long)]
186 hero_out: Option<String>,
187 },
188}
189
190#[derive(Subcommand)]
191pub enum VaultCommands {
192 /// Sign an SSH certificate for a host (or --all) via the Vault SSH secrets engine
193 #[command(
194 long_about = "Sign one or more SSH certificates via the HashiCorp Vault SSH secrets engine.\n\n\
195 Prerequisites:\n\
196 - The `vault` CLI is installed and authenticated (run `vault login` or set VAULT_TOKEN)\n\
197 - VAULT_ADDR points at your Vault server\n\
198 - A role is configured on the host (Vault SSH role field in the host form) or\n \
199 on its provider (provider-level vault_role default)\n\
200 - The SSH secrets engine is enabled on Vault and your token has `update` capability\n \
201 on the role path\n\n\
202 Signed certificates are stored as certs/<alias>-cert.pub in the data\n\
203 directory (~/.purple by default) and `CertificateFile` is wired into the\n\
204 SSH config automatically.\n\n\
205 Distinct from the Vault KV secrets engine used as a password source (`vault:`\n\
206 askpass prefix); see `purple password` for that."
207 )]
208 Sign {
209 /// Host alias to sign (omit for --all)
210 alias: Option<String>,
211 /// Sign all hosts with a Vault SSH role configured
212 #[arg(long)]
213 all: bool,
214 /// Override VAULT_ADDR for this invocation only.
215 /// Highest precedence: flag > per-host comment > provider default > shell env.
216 #[arg(long, value_name = "URL")]
217 vault_addr: Option<String>,
218 },
219}
220
221#[derive(Subcommand)]
222pub enum ThemeCommands {
223 /// List available themes
224 List,
225 /// Set the active theme
226 Set {
227 /// Theme name
228 name: String,
229 },
230}
231
232#[derive(Subcommand)]
233#[allow(clippy::large_enum_variant)]
234pub enum ProviderCommands {
235 /// Add or update a provider configuration
236 Add {
237 /// Provider name (digitalocean, vultr, linode, hetzner, upcloud, proxmox, aws, scaleway, gcp, azure, tailscale, oracle, ovh, leaseweb, i3d, transip, teleport)
238 provider: String,
239
240 /// API token (or set PURPLE_TOKEN env var, or use --token-stdin)
241 #[arg(long)]
242 token: Option<String>,
243
244 /// Read token from stdin (e.g. from a password manager)
245 #[arg(long)]
246 token_stdin: bool,
247
248 /// Alias prefix (default: provider short label)
249 #[arg(long)]
250 prefix: Option<String>,
251
252 /// Default SSH user (default: root)
253 #[arg(long)]
254 user: Option<String>,
255
256 /// Default identity file
257 #[arg(long)]
258 key: Option<String>,
259
260 /// Base URL for self-hosted providers (required for Proxmox)
261 #[arg(long)]
262 url: Option<String>,
263
264 /// AWS credential profile from ~/.aws/credentials
265 #[arg(long)]
266 profile: Option<String>,
267
268 /// Comma-separated regions, zones or subscription IDs (e.g. us-east-1,eu-west-1 for AWS, fr-par-1,nl-ams-1 for Scaleway, us-central1-a for GCP zones or subscription UUIDs for Azure)
269 #[arg(long)]
270 regions: Option<String>,
271
272 /// GCP project ID
273 #[arg(long)]
274 project: Option<String>,
275
276 /// OCI compartment OCID (Oracle)
277 #[arg(long)]
278 compartment: Option<String>,
279
280 /// Skip TLS certificate verification (for self-signed certs)
281 #[arg(long, conflicts_with = "verify_tls")]
282 no_verify_tls: bool,
283
284 /// Explicitly enable TLS certificate verification (overrides stored setting)
285 #[arg(long, conflicts_with = "no_verify_tls")]
286 verify_tls: bool,
287
288 /// Enable automatic sync on startup
289 #[arg(long, conflicts_with = "no_auto_sync")]
290 auto_sync: bool,
291
292 /// Disable automatic sync on startup
293 #[arg(long, conflicts_with = "auto_sync")]
294 no_auto_sync: bool,
295
296 /// Optional label when adding a second config for the same
297 /// provider (e.g. --label work, --label personal). Required once
298 /// a provider already has a labeled config.
299 #[arg(long)]
300 label: Option<String>,
301 },
302 /// List configured providers
303 List,
304 /// Remove a provider configuration. Pass `provider` for ALL configs
305 /// of that provider, or `provider:label` for one specific config.
306 Remove {
307 /// Provider name (`digitalocean`) or `provider:label` (`digitalocean:work`)
308 provider: String,
309 },
310}
311
312#[derive(Subcommand)]
313pub enum TunnelCommands {
314 /// List configured tunnels
315 List {
316 /// Show tunnels for a specific host
317 alias: Option<String>,
318 },
319 /// Add a tunnel to a host
320 Add {
321 /// Host alias
322 alias: String,
323
324 /// Forward spec: L:port:host:port (local), R:port:host:port (remote) or D:port (SOCKS)
325 forward: String,
326 },
327 /// Remove a tunnel from a host
328 Remove {
329 /// Host alias
330 alias: String,
331
332 /// Forward spec: L:port:host:port (local), R:port:host:port (remote) or D:port (SOCKS)
333 forward: String,
334 },
335 /// Start a tunnel (foreground, Ctrl+C to stop)
336 Start {
337 /// Host alias
338 alias: String,
339 },
340}
341
342#[derive(Subcommand)]
343pub enum PasswordCommands {
344 /// Store a password in the OS keychain for a host
345 Set {
346 /// Host alias
347 alias: String,
348 },
349 /// Remove a password from the OS keychain
350 Remove {
351 /// Host alias
352 alias: String,
353 },
354}
355
356#[derive(Subcommand)]
357pub enum SnippetCommands {
358 /// List all saved snippets
359 List,
360 /// Add a new snippet
361 Add {
362 /// Snippet name
363 name: String,
364
365 /// Command to run on the remote host
366 command: String,
367
368 /// Short description
369 #[arg(long)]
370 description: Option<String>,
371 },
372 /// Remove a snippet
373 Remove {
374 /// Snippet name
375 name: String,
376 },
377 /// Run a snippet on one or more hosts
378 Run {
379 /// Snippet name
380 name: String,
381
382 /// Host alias (run on a single host)
383 alias: Option<String>,
384
385 /// Run on all hosts matching this tag
386 #[arg(long)]
387 tag: Option<String>,
388
389 /// Run on all hosts
390 #[arg(long)]
391 all: bool,
392
393 /// Run on hosts concurrently
394 #[arg(long)]
395 parallel: bool,
396 },
397}