rvault/cli/
mod.rs

1use clap::{Parser, Subcommand};
2// use owo_colors::OwoColorize; // TODO: Use for colored output
3use anyhow::Result;
4
5mod commands;
6mod output;
7
8use crate::{storage::VaultStorage, config::Config};
9use commands::*;
10
11#[derive(Parser)]
12#[command(name = "vault")]
13#[command(about = "A local-first, multi-tenant password manager")]
14#[command(version)]
15pub struct VaultCli {
16    #[command(subcommand)]
17    pub command: Commands,
18    
19    #[arg(long, global = true, help = "Enable verbose output")]
20    pub verbose: bool,
21    
22    #[arg(long, global = true, help = "Configuration file path")]
23    pub config: Option<String>,
24}
25
26#[derive(Subcommand)]
27pub enum Commands {
28    /// Initialize a new vault
29    Init {
30        #[arg(long, help = "Tenant identifier")]
31        tenant: String,
32        #[arg(long, help = "Admin email address")]
33        admin: String,
34        #[arg(long, help = "Force initialization even if vault exists")]
35        force: bool,
36    },
37    
38    /// Login to a tenant
39    Login {
40        #[arg(long, help = "Tenant identifier")]
41        tenant: String,
42        #[arg(long, help = "User email (for collaborative mode)")]
43        email: Option<String>,
44        #[arg(long, help = "Remember session for longer")]
45        remember: bool,
46    },
47    
48    /// Logout from current session
49    Logout,
50    
51    /// Show current user info
52    Whoami,
53    
54    /// Store a secret
55    Put {
56        #[arg(help = "Secret key")]
57        key: String,
58        #[arg(long, help = "Namespace for the secret")]
59        namespace: Option<String>,
60        #[arg(long, help = "Secret value (will prompt if not provided)")]
61        value: Option<String>,
62        #[arg(long, help = "Tags for the secret")]
63        tags: Vec<String>,
64        #[arg(long, help = "Force overwrite existing secret")]
65        force: bool,
66    },
67    
68    /// Retrieve a secret
69    Get {
70        #[arg(help = "Secret key")]
71        key: String,
72        #[arg(long, help = "Namespace for the secret")]
73        namespace: Option<String>,
74        #[arg(long, help = "Copy to clipboard instead of printing")]
75        copy: bool,
76        #[arg(long, help = "Show secret metadata")]
77        metadata: bool,
78    },
79    
80    /// List secrets
81    List {
82        #[arg(long, help = "Namespace to list")]
83        namespace: Option<String>,
84        #[arg(long, help = "Filter by tag")]
85        tag: Option<String>,
86        #[arg(long, help = "Show detailed information")]
87        detailed: bool,
88    },
89    
90    /// Delete a secret
91    Delete {
92        #[arg(help = "Secret key")]
93        key: String,
94        #[arg(long, help = "Namespace for the secret")]
95        namespace: Option<String>,
96        #[arg(long, help = "Force deletion without confirmation")]
97        force: bool,
98    },
99    
100    /// Search secrets
101    Search {
102        #[arg(help = "Search query")]
103        query: String,
104        #[arg(long, help = "Namespace to search in")]
105        namespace: Option<String>,
106    },
107    
108    /// Sync with cloud
109    Sync {
110        #[command(subcommand)]
111        action: SyncAction,
112    },
113    
114    /// Manage roles and permissions
115    Roles {
116        #[command(subcommand)]
117        action: RoleAction,
118    },
119    
120    /// Manage users (collaborative mode only)
121    Users {
122        #[command(subcommand)]
123        action: UserAction,
124    },
125    
126    /// Audit operations
127    Audit {
128        #[command(subcommand)]
129        action: AuditAction,
130    },
131    
132    /// Export secrets
133    Export {
134        #[arg(long, help = "Output file path")]
135        output: String,
136        #[arg(long, help = "Export format", default_value = "json")]
137        format: String,
138        #[arg(long, help = "Namespace to export")]
139        namespace: Option<String>,
140    },
141    
142    /// Import secrets
143    Import {
144        #[arg(help = "Input file path")]
145        input: String,
146        #[arg(long, help = "Import format", default_value = "json")]
147        format: String,
148        #[arg(long, help = "Target namespace")]
149        namespace: Option<String>,
150    },
151    
152    /// Show vault status
153    Status,
154    
155    /// Run diagnostics
156    Doctor,
157    
158    /// Generate shell completions
159    Completions {
160        #[arg(help = "Shell type")]
161        shell: String,
162    },
163}
164
165#[derive(Subcommand)]
166pub enum SyncAction {
167    /// Push secrets to cloud
168    Push {
169        #[arg(long, help = "Force push even with conflicts")]
170        force: bool,
171    },
172    /// Pull secrets from cloud
173    Pull {
174        #[arg(long, help = "Force pull and overwrite local changes")]
175        force: bool,
176    },
177    /// Enable automatic sync
178    Auto {
179        #[arg(long, help = "Sync interval in minutes")]
180        interval: Option<u64>,
181    },
182    /// Show sync status
183    Status,
184    /// Configure sync backend
185    Configure,
186}
187
188#[derive(Subcommand)]
189pub enum RoleAction {
190    /// Add user to tenant
191    Add {
192        #[arg(long)]
193        tenant: String,
194        #[arg(long)]
195        user: String,
196        #[arg(long)]
197        role: String,
198    },
199    /// Remove user from tenant
200    Remove {
201        #[arg(long)]
202        tenant: String,
203        #[arg(long)]
204        user: String,
205    },
206    /// List users in tenant
207    List {
208        #[arg(long)]
209        tenant: String,
210    },
211}
212
213#[derive(Subcommand)]
214pub enum AuditAction {
215    /// Show recent audit logs
216    Tail {
217        #[arg(long, help = "Number of entries to show")]
218        lines: Option<usize>,
219        #[arg(long, help = "Follow log updates")]
220        follow: bool,
221    },
222    /// Search audit logs
223    Search {
224        #[arg(help = "Search query")]
225        query: String,
226        #[arg(long, help = "Start date")]
227        since: Option<String>,
228        #[arg(long, help = "End date")]
229        until: Option<String>,
230    },
231}
232
233#[derive(Subcommand)]
234pub enum UserAction {
235    /// Invite user to tenant
236    Invite {
237        #[arg(long)]
238        email: String,
239        #[arg(long)]
240        role: String,
241    },
242    /// List users in current tenant
243    List,
244    /// Remove user from tenant
245    Remove {
246        #[arg(long)]
247        email: String,
248    },
249    /// Change user role
250    ChangeRole {
251        #[arg(long)]
252        email: String,
253        #[arg(long)]
254        role: String,
255    },
256    /// Accept invitation
257    Accept {
258        #[arg(long)]
259        token: String,
260    },
261}
262
263impl VaultCli {
264    pub async fn run(self) -> Result<()> {
265        let config = Config::load(self.config.as_deref())?;
266        let mut storage = VaultStorage::new(&config.storage_path)?;
267        
268        match self.command {
269            Commands::Init { tenant, admin, force } => {
270                init_command(&mut storage, &tenant, &admin, force).await
271            }
272            Commands::Login { tenant, email, remember } => {
273                login_command(&mut storage, &config, &tenant, email.as_deref(), remember).await
274            }
275            Commands::Logout => {
276                logout_command().await
277            }
278            Commands::Put { key, namespace, value, tags, force } => {
279                put_command(&storage, &key, namespace.as_deref(), value.as_deref(), &tags, force).await
280            }
281            Commands::Get { key, namespace, copy, metadata } => {
282                get_command(&storage, &key, namespace.as_deref(), copy, metadata).await
283            }
284            Commands::List { namespace, tag, detailed } => {
285                list_command(&storage, namespace.as_deref(), tag.as_deref(), detailed).await
286            }
287            Commands::Delete { key, namespace, force } => {
288                delete_command(&storage, &key, namespace.as_deref(), force).await
289            }
290            Commands::Search { query, namespace } => {
291                search_command(&storage, &query, namespace.as_deref()).await
292            }
293            Commands::Status => {
294                status_command(&config, &storage).await
295            }
296            Commands::Whoami => {
297                whoami_command().await
298            }
299            Commands::Doctor => {
300                doctor_command(&config, &storage).await
301            }
302            Commands::Sync { action } => {
303                sync_command(action, &config).await
304            }
305            Commands::Roles { action } => {
306                roles_command(action).await
307            }
308            Commands::Audit { action } => {
309                audit_command(action).await
310            }
311            Commands::Users { action } => {
312                users_command(action, &storage, &config).await
313            }
314            Commands::Export { output, format, namespace } => {
315                export_command(&storage, &output, &format, namespace.as_deref()).await
316            }
317            Commands::Import { input, format, namespace } => {
318                import_command(&storage, &input, &format, namespace.as_deref()).await
319            }
320            Commands::Completions { shell } => {
321                completions_command(&shell).await
322            }
323        }
324    }
325}