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