1use clap::{Parser, Subcommand};
2use 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 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 {
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,
50
51 Whoami,
53
54 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 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 {
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 {
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 {
102 #[arg(help = "Search query")]
103 query: String,
104 #[arg(long, help = "Namespace to search in")]
105 namespace: Option<String>,
106 },
107
108 Sync {
110 #[command(subcommand)]
111 action: SyncAction,
112 },
113
114 Roles {
116 #[command(subcommand)]
117 action: RoleAction,
118 },
119
120 Users {
122 #[command(subcommand)]
123 action: UserAction,
124 },
125
126 Audit {
128 #[command(subcommand)]
129 action: AuditAction,
130 },
131
132 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 {
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 Status,
154
155 Doctor,
157
158 Completions {
160 #[arg(help = "Shell type")]
161 shell: String,
162 },
163}
164
165#[derive(Subcommand)]
166pub enum SyncAction {
167 Push {
169 #[arg(long, help = "Force push even with conflicts")]
170 force: bool,
171 },
172 Pull {
174 #[arg(long, help = "Force pull and overwrite local changes")]
175 force: bool,
176 },
177 Auto {
179 #[arg(long, help = "Sync interval in minutes")]
180 interval: Option<u64>,
181 },
182 Status,
184 Configure,
186}
187
188#[derive(Subcommand)]
189pub enum RoleAction {
190 Add {
192 #[arg(long)]
193 tenant: String,
194 #[arg(long)]
195 user: String,
196 #[arg(long)]
197 role: String,
198 },
199 Remove {
201 #[arg(long)]
202 tenant: String,
203 #[arg(long)]
204 user: String,
205 },
206 List {
208 #[arg(long)]
209 tenant: String,
210 },
211}
212
213#[derive(Subcommand)]
214pub enum AuditAction {
215 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 {
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 {
237 #[arg(long)]
238 email: String,
239 #[arg(long)]
240 role: String,
241 },
242 List,
244 Remove {
246 #[arg(long)]
247 email: String,
248 },
249 ChangeRole {
251 #[arg(long)]
252 email: String,
253 #[arg(long)]
254 role: String,
255 },
256 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}