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