1use clap::{Args, Subcommand, ValueEnum};
2use std::{ffi::OsString, path::PathBuf};
3
4#[derive(Subcommand, Debug)]
5pub enum EnvCommands {
6 List {
8 #[arg(long)]
10 reveal: bool,
11 },
12 Set {
14 key: String,
16 value: String,
18 #[arg(long)]
21 force: bool,
22 },
23 Delete {
25 key: String,
27 #[arg(long)]
30 force: bool,
31 },
32 Get {
34 key: String,
36 },
37 Run(EnvRunCommand),
39 Workspace(EnvWorkspaceCommand),
41 Proxy(EnvProxyCommand),
43 Broker(EnvBrokerCommand),
45 Secret(EnvSecretCommand),
47}
48
49#[derive(Args, Debug)]
50pub struct EnvWorkspaceCommand {
51 #[command(subcommand)]
52 pub command: EnvWorkspaceSubcommand,
53}
54
55#[derive(Subcommand, Debug)]
56pub enum EnvWorkspaceSubcommand {
57 Init(EnvWorkspaceInitCommand),
59 Export(EnvWorkspaceExportCommand),
61}
62
63#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
64pub enum EnvWorkspaceExportFormat {
65 Dotenv,
67}
68
69#[derive(Args, Debug)]
70pub struct EnvWorkspaceExportCommand {
71 #[arg(long, value_enum, value_name = "FORMAT")]
73 pub format: EnvWorkspaceExportFormat,
74 #[arg(long, value_name = "FILE")]
76 pub workspace: Option<PathBuf>,
77 #[arg(long, value_name = "MODE")]
79 pub mode: String,
80 #[arg(long, value_name = "FILE")]
82 pub output: PathBuf,
83 #[arg(long)]
85 pub include_secrets: bool,
86 #[arg(long)]
88 pub force: bool,
89 #[arg(long)]
91 pub dry_run: bool,
92}
93
94#[derive(Args, Debug)]
95pub struct EnvWorkspaceInitCommand {
96 #[arg(long)]
98 pub from_dotenv: bool,
99 #[arg(long, value_name = "MODE")]
101 pub mode: Vec<String>,
102 #[arg(long, value_name = "KEY")]
104 pub secret: Vec<String>,
105 #[arg(long)]
107 pub force: bool,
108 #[arg(long)]
110 pub dry_run: bool,
111}
112
113#[derive(Args, Debug)]
114pub struct EnvProxyCommand {
115 #[command(subcommand)]
116 pub command: EnvProxySubcommand,
117}
118
119#[derive(Args, Debug)]
120pub struct EnvBrokerCommand {
121 #[command(subcommand)]
122 pub command: EnvBrokerSubcommand,
123}
124
125#[derive(Subcommand, Debug)]
126pub enum EnvBrokerSubcommand {
127 Describe {
129 #[arg(long, value_name = "FILE")]
130 workspace: Option<PathBuf>,
131 #[arg(long)]
132 mode: String,
133 #[arg(
134 long,
135 value_name = "KEY",
136 required_unless_present = "release_all_declared",
137 conflicts_with = "release_all_declared"
138 )]
139 release: Vec<String>,
140 #[arg(long)]
142 release_all_declared: bool,
143 #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
144 command: Vec<String>,
145 },
146 Policy(EnvBrokerPolicyCommand),
148}
149
150#[derive(Args, Debug)]
151pub struct EnvBrokerPolicyCommand {
152 #[command(subcommand)]
153 pub command: EnvBrokerPolicySubcommand,
154}
155
156#[derive(Args, Debug)]
157pub struct EnvBrokerPolicyInput {
158 #[arg(long)]
159 pub name: String,
160 #[arg(long)]
161 pub ssh_target: String,
162 #[arg(long, default_value = "")]
163 pub project: String,
164 #[arg(long, value_name = "FILE")]
165 pub workspace: PathBuf,
166 #[arg(long, value_name = "REMOTE_FILE")]
168 pub remote_workspace: Option<String>,
169 #[arg(long)]
170 pub mode: String,
171 #[arg(
172 long,
173 value_name = "KEY",
174 required_unless_present = "release_all_declared",
175 conflicts_with = "release_all_declared"
176 )]
177 pub release: Vec<String>,
178 #[arg(long)]
180 pub release_all_declared: bool,
181 #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
182 pub command: Vec<String>,
183}
184
185#[derive(Subcommand, Debug)]
186pub enum EnvBrokerPolicySubcommand {
187 Add(EnvBrokerPolicyInput),
189 Update(EnvBrokerPolicyInput),
191 Diff {
193 name: String,
194 #[arg(long, value_name = "FILE")]
195 workspace: PathBuf,
196 #[arg(long)]
197 mode: String,
198 #[arg(
199 long,
200 value_name = "KEY",
201 required_unless_present = "release_all_declared",
202 conflicts_with = "release_all_declared"
203 )]
204 release: Vec<String>,
205 #[arg(long)]
207 release_all_declared: bool,
208 #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
209 command: Vec<String>,
210 },
211 List,
213 Info { name: String },
215 Remove { name: String },
217}
218
219#[derive(Subcommand, Debug)]
220pub enum EnvProxySubcommand {
221 Install {
223 #[arg(value_name = "COMMAND")]
224 command: String,
225 #[arg(long = "with", value_name = "KEY[=ALIAS]", required = true)]
226 with: Vec<String>,
227 #[arg(long)]
229 project: bool,
230 },
231 List,
233 Uninstall {
235 #[arg(value_name = "COMMAND")]
236 command: String,
237 },
238 Enable {
240 #[arg(value_name = "COMMAND")]
241 command: String,
242 #[arg(long)]
244 project: bool,
245 },
246 Disable {
248 #[arg(value_name = "COMMAND")]
249 command: String,
250 #[arg(long)]
252 project: bool,
253 },
254 #[command(hide = true)]
255 Exec {
256 #[arg(long)]
257 target: PathBuf,
258 #[arg(value_name = "COMMAND")]
259 command: String,
260 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
261 args: Vec<OsString>,
262 },
263}
264
265#[derive(Args, Debug)]
266pub struct EnvSecretCommand {
267 #[command(subcommand)]
268 pub command: EnvSecretSubcommand,
269}
270
271#[derive(Subcommand, Debug)]
272pub enum EnvSecretSubcommand {
273 Decrypt {
275 key: String,
277 },
278 Export {
280 key: String,
282 #[arg(long = "as", value_name = "ALIAS")]
284 alias: Option<String>,
285 },
286 Encrypt(EnvEncryptCommand),
288 Seal(EnvSealCommand),
290 Identity(EnvIdentityCommand),
292}
293
294#[derive(Args, Debug)]
295pub struct EnvEncryptCommand {
296 #[arg(long)]
298 pub backend: Option<String>,
299 #[arg(short = 'r', long = "recipient")]
301 pub recipients: Vec<String>,
302 #[arg(long)]
304 pub set: Option<String>,
305 #[arg(long)]
307 pub from: Option<String>,
308 #[arg(long)]
311 pub force: bool,
312}
313
314#[derive(Args, Debug)]
315pub struct EnvSealCommand {
316 #[arg(value_name = "FILE")]
318 pub file: Option<PathBuf>,
319 #[arg(long, value_name = "FILE")]
321 pub workspace: Option<PathBuf>,
322 #[arg(long)]
324 pub backend: Option<String>,
325 #[arg(short = 'r', long = "recipient")]
327 pub recipients: Vec<String>,
328}
329
330#[derive(Args, Debug)]
331pub struct EnvIdentityCommand {
332 #[command(subcommand)]
333 pub command: EnvIdentitySubcommand,
334}
335
336#[derive(Clone, Copy, Debug, Default, ValueEnum)]
337pub enum PhoneIdentityTransport {
338 #[default]
339 Auto,
340 Adb,
341 Qr,
342}
343
344impl PhoneIdentityTransport {
345 pub fn as_str(self) -> &'static str {
346 match self {
347 Self::Auto => "auto",
348 Self::Adb => "adb",
349 Self::Qr => "qr",
350 }
351 }
352}
353
354#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
356pub enum PhoneRecipientType {
357 #[default]
358 Tag,
359 Phone,
360}
361
362impl PhoneRecipientType {
363 pub fn as_str(self) -> &'static str {
364 match self {
365 Self::Tag => "tag",
366 Self::Phone => "phone",
367 }
368 }
369}
370
371#[derive(Subcommand, Debug)]
372pub enum EnvIdentitySubcommand {
373 Init {
375 #[arg(long, conflicts_with = "phone")]
377 touch_id: bool,
378 #[arg(
380 long,
381 conflicts_with_all = ["touch_id", "access_control", "output", "force"]
382 )]
383 phone: bool,
384 #[arg(
386 long,
387 requires = "phone",
388 conflicts_with_all = ["touch_id", "access_control", "output", "force"],
389 value_enum,
390 value_name = "TYPE"
391 )]
392 recipient_type: Option<PhoneRecipientType>,
393 #[arg(long, requires = "phone", value_name = "LABEL")]
395 label: Option<String>,
396 #[arg(long, requires = "phone", value_enum, value_name = "TRANSPORT")]
398 transport: Option<PhoneIdentityTransport>,
399 #[arg(long, requires = "phone", value_name = "SERIAL")]
401 adb_serial: Option<String>,
402 #[arg(long, value_name = "POLICY")]
405 access_control: Option<String>,
406 #[arg(short = 'o', long, value_name = "PATH")]
408 output: Option<PathBuf>,
409 #[arg(long)]
411 force: bool,
412 },
413 List,
415}
416
417#[derive(Args, Debug)]
418pub struct EnvRunCommand {
419 #[arg(long, value_name = "FILE")]
421 pub workspace: Option<PathBuf>,
422 #[arg(long)]
424 pub mode: Option<String>,
425 #[arg(long, conflicts_with_all = ["workspace", "mode"])]
427 pub no_workspace: bool,
428 #[arg(long = "with", value_name = "KEY[=ALIAS]")]
430 pub with: Vec<String>,
431 #[arg(long)]
433 pub secret_broker: bool,
434 #[arg(
436 long = "secret",
437 value_name = "KEY[=ALIAS]",
438 requires = "secret_broker",
439 requires = "no_workspace"
440 )]
441 pub secret: Vec<String>,
442 #[arg(required = true, trailing_var_arg = true, allow_hyphen_values = true)]
444 pub command: Vec<OsString>,
445}
446
447#[cfg(test)]
448mod phone_recipient_tests {
449 use super::*;
450 use clap::Parser;
451
452 #[derive(Parser)]
453 struct IdentityCli {
454 #[command(flatten)]
455 identity: EnvIdentityCommand,
456 }
457
458 #[test]
459 fn phone_recipient_cli_defaults_and_explicit_types() {
460 for (extra, expected) in [
461 (vec![], PhoneRecipientType::Tag),
462 (vec!["--recipient-type", "tag"], PhoneRecipientType::Tag),
463 (vec!["--recipient-type", "phone"], PhoneRecipientType::Phone),
464 ] {
465 let mut args = vec!["identity", "init", "--phone"];
466 args.extend(extra);
467 let parsed = IdentityCli::try_parse_from(args).unwrap();
468 let EnvIdentitySubcommand::Init { recipient_type, .. } = parsed.identity.command else {
469 panic!("expected init")
470 };
471 assert_eq!(recipient_type.unwrap_or_default(), expected);
472 }
473 }
474
475 #[test]
476 fn phone_recipient_cli_rejects_invalid_combinations() {
477 for args in [
478 vec!["init", "--recipient-type", "tag"],
479 vec!["init", "--phone", "--recipient-type", "unknown"],
480 vec!["init", "--touch-id", "--recipient-type", "tag"],
481 vec![
482 "init",
483 "--recipient-type",
484 "tag",
485 "--output",
486 "identity.txt",
487 ],
488 vec!["init", "--recipient-type", "tag", "--force"],
489 vec![
490 "init",
491 "--recipient-type",
492 "tag",
493 "--access-control",
494 "passcode",
495 ],
496 vec!["init", "--phone", "--touch-id"],
497 vec!["init", "--phone", "--output", "identity.txt"],
498 vec!["init", "--phone", "--force"],
499 vec!["init", "--phone", "--access-control", "passcode"],
500 ] {
501 assert!(
502 IdentityCli::try_parse_from(
503 std::iter::once("identity").chain(args.iter().copied())
504 )
505 .is_err(),
506 "accepted {args:?}"
507 );
508 }
509 }
510}