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}
170
171#[derive(Subcommand)]
172pub enum VaultCommands {
173 /// Sign an SSH certificate for a host (or --all) via the Vault SSH secrets engine
174 #[command(
175 long_about = "Sign one or more SSH certificates via the HashiCorp Vault SSH secrets engine.\n\n\
176 Prerequisites:\n\
177 - The `vault` CLI is installed and authenticated (run `vault login` or set VAULT_TOKEN)\n\
178 - VAULT_ADDR points at your Vault server\n\
179 - A role is configured on the host (Vault SSH role field in the host form) or\n \
180 on its provider (provider-level vault_role default)\n\
181 - The SSH secrets engine is enabled on Vault and your token has `update` capability\n \
182 on the role path\n\n\
183 Signed certificates are cached under ~/.purple/certs/<alias>-cert.pub and\n\
184 `CertificateFile` is wired into the SSH config automatically.\n\n\
185 Distinct from the Vault KV secrets engine used as a password source (`vault:`\n\
186 askpass prefix); see `purple password` for that."
187 )]
188 Sign {
189 /// Host alias to sign (omit for --all)
190 alias: Option<String>,
191 /// Sign all hosts with a Vault SSH role configured
192 #[arg(long)]
193 all: bool,
194 /// Override VAULT_ADDR for this invocation only.
195 /// Highest precedence: flag > per-host comment > provider default > shell env.
196 #[arg(long, value_name = "URL")]
197 vault_addr: Option<String>,
198 },
199}
200
201#[derive(Subcommand)]
202pub enum ThemeCommands {
203 /// List available themes
204 List,
205 /// Set the active theme
206 Set {
207 /// Theme name
208 name: String,
209 },
210}
211
212#[derive(Subcommand)]
213#[allow(clippy::large_enum_variant)]
214pub enum ProviderCommands {
215 /// Add or update a provider configuration
216 Add {
217 /// Provider name (digitalocean, vultr, linode, hetzner, upcloud, proxmox, aws, scaleway, gcp, azure, tailscale, oracle, ovh, leaseweb, i3d, transip)
218 provider: String,
219
220 /// API token (or set PURPLE_TOKEN env var, or use --token-stdin)
221 #[arg(long)]
222 token: Option<String>,
223
224 /// Read token from stdin (e.g. from a password manager)
225 #[arg(long)]
226 token_stdin: bool,
227
228 /// Alias prefix (default: provider short label)
229 #[arg(long)]
230 prefix: Option<String>,
231
232 /// Default SSH user (default: root)
233 #[arg(long)]
234 user: Option<String>,
235
236 /// Default identity file
237 #[arg(long)]
238 key: Option<String>,
239
240 /// Base URL for self-hosted providers (required for Proxmox)
241 #[arg(long)]
242 url: Option<String>,
243
244 /// AWS credential profile from ~/.aws/credentials
245 #[arg(long)]
246 profile: Option<String>,
247
248 /// 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)
249 #[arg(long)]
250 regions: Option<String>,
251
252 /// GCP project ID
253 #[arg(long)]
254 project: Option<String>,
255
256 /// OCI compartment OCID (Oracle)
257 #[arg(long)]
258 compartment: Option<String>,
259
260 /// Skip TLS certificate verification (for self-signed certs)
261 #[arg(long, conflicts_with = "verify_tls")]
262 no_verify_tls: bool,
263
264 /// Explicitly enable TLS certificate verification (overrides stored setting)
265 #[arg(long, conflicts_with = "no_verify_tls")]
266 verify_tls: bool,
267
268 /// Enable automatic sync on startup
269 #[arg(long, conflicts_with = "no_auto_sync")]
270 auto_sync: bool,
271
272 /// Disable automatic sync on startup
273 #[arg(long, conflicts_with = "auto_sync")]
274 no_auto_sync: bool,
275
276 /// Optional label when adding a second config for the same
277 /// provider (e.g. --label work, --label personal). Required once
278 /// a provider already has a labeled config.
279 #[arg(long)]
280 label: Option<String>,
281 },
282 /// List configured providers
283 List,
284 /// Remove a provider configuration. Pass `provider` for ALL configs
285 /// of that provider, or `provider:label` for one specific config.
286 Remove {
287 /// Provider name (`digitalocean`) or `provider:label` (`digitalocean:work`)
288 provider: String,
289 },
290}
291
292#[derive(Subcommand)]
293pub enum TunnelCommands {
294 /// List configured tunnels
295 List {
296 /// Show tunnels for a specific host
297 alias: Option<String>,
298 },
299 /// Add a tunnel to a host
300 Add {
301 /// Host alias
302 alias: String,
303
304 /// Forward spec: L:port:host:port (local), R:port:host:port (remote) or D:port (SOCKS)
305 forward: String,
306 },
307 /// Remove a tunnel from a host
308 Remove {
309 /// Host alias
310 alias: String,
311
312 /// Forward spec: L:port:host:port (local), R:port:host:port (remote) or D:port (SOCKS)
313 forward: String,
314 },
315 /// Start a tunnel (foreground, Ctrl+C to stop)
316 Start {
317 /// Host alias
318 alias: String,
319 },
320}
321
322#[derive(Subcommand)]
323pub enum PasswordCommands {
324 /// Store a password in the OS keychain for a host
325 Set {
326 /// Host alias
327 alias: String,
328 },
329 /// Remove a password from the OS keychain
330 Remove {
331 /// Host alias
332 alias: String,
333 },
334}
335
336#[derive(Subcommand)]
337pub enum SnippetCommands {
338 /// List all saved snippets
339 List,
340 /// Add a new snippet
341 Add {
342 /// Snippet name
343 name: String,
344
345 /// Command to run on the remote host
346 command: String,
347
348 /// Short description
349 #[arg(long)]
350 description: Option<String>,
351 },
352 /// Remove a snippet
353 Remove {
354 /// Snippet name
355 name: String,
356 },
357 /// Run a snippet on one or more hosts
358 Run {
359 /// Snippet name
360 name: String,
361
362 /// Host alias (run on a single host)
363 alias: Option<String>,
364
365 /// Run on all hosts matching this tag
366 #[arg(long)]
367 tag: Option<String>,
368
369 /// Run on all hosts
370 #[arg(long)]
371 all: bool,
372
373 /// Run on hosts concurrently
374 #[arg(long)]
375 parallel: bool,
376 },
377}