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/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}