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