Skip to main content

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}