Skip to main content

tsafe_cli/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2pub use clap_complete::Shell;
3
4const ROOT_LONG_ABOUT: &str = "Manage secrets in a local encrypted vault instead of scattering them across `.env` files, shell history, and ad-hoc runtime exports.\n\nThe core-only release family centers on local encrypted vault CRUD, `exec`/contracts, profiles, snapshots, audit, and `doctor`, plus the default-core Azure Key Vault pull, biometric/quick-unlock, and team workflows when they are compiled into this `tsafe` binary. Some named stack shapes also include the terminal UI and/or the `agent` workflow as explicit companion/runtime claims. Broader gated non-core lanes such as AWS, GCP, browser/nativehost, plugins, and other additive surfaces appear only when they are compiled into this binary and shipped by the chosen stack. Companion runtimes such as `tsafe-agent` are installed and released separately. Use `tsafe build-info` when you need the compiled truth for the running binary.";
5
6const ROOT_AFTER_HELP: &str = "Compiled truth:\n  tsafe build-info\n\nCompanion note:\n  `tsafe-agent` is installed and released separately from the `tsafe` CLI binary.\n\nSee also:\n  man tsafe\n  tsafe explain\n  tsafe <command> --help\n  docs/index.md in the repository";
7
8const DOCTOR_LONG_ABOUT: &str = "Diagnose vault health: file presence, snapshots, env vars, secret expiry, and operator-facing health hints.\n\nPrints a colour-coded report. Use `--json` for machine-readable monitoring output.";
9
10const DOCTOR_AFTER_HELP: &str =
11    "Examples:\n  tsafe doctor\n  tsafe doctor --json\n  tsafe --profile prod doctor";
12
13const AUDIT_AFTER_HELP: &str = "Examples:\n  tsafe audit\n  tsafe audit --limit 100\n  tsafe audit --explain\n  tsafe audit --explain --json\n  tsafe audit --cell-id doom-cell-001\n  tsafe audit-verify\n  tsafe audit-verify --json";
14
15const ROTATE_DUE_AFTER_HELP: &str =
16    "Examples:\n  tsafe rotate-due\n  tsafe rotate-due --json\n  tsafe rotate-due --fail   # for scripts: non-zero if overdue";
17
18const BUILD_INFO_AFTER_HELP: &str = "Examples:\n  tsafe build-info\n  tsafe build-info --json";
19
20#[derive(Parser)]
21#[command(
22    name = "tsafe",
23    about = "tsafe — local encrypted secret runtime for vaults, exec/contracts, and operator workflows",
24    long_about = ROOT_LONG_ABOUT,
25    version,
26    arg_required_else_help = true,
27    after_help = ROOT_AFTER_HELP
28)]
29pub struct Cli {
30    /// Named vault / profile. Defaults to the persisted default (or 'default'). Override with TSAFE_PROFILE env var.
31    #[arg(short, long, global = true, env = "TSAFE_PROFILE")]
32    pub profile: Option<String>,
33
34    #[command(subcommand)]
35    pub command: Commands,
36}
37
38#[derive(Subcommand)]
39pub enum Commands {
40    /// Initialise a new encrypted vault for the current profile.
41    ///
42    /// Creates the vault file for this profile under the platform data directory. Prompts for a master password twice.
43    ///
44    /// On an interactive terminal, after the vault is created you may be offered "quick unlock":
45    /// storing the password in the OS credential store (Touch ID / Face ID / Windows Hello / device PIN
46    /// where the OS supports it). You can accept, defer, or skip; run `tsafe biometric enable` anytime.
47    ///
48    /// If `tsafe config set-backup-vault main` (or `default`) is set, the new vault's master password is
49    /// also stored under `profile-passwords/<profile>` in that vault when possible.
50    ///
51    #[command(after_help = "Examples:\n  tsafe init\n  tsafe --profile prod init")]
52    Init,
53
54    /// View or change global settings (config.json): password backup target, default profile, etc.
55    ///
56    /// Use `config set-backup-vault main` so every new vault's master password is also stored under
57    /// `profile-passwords/<profile>` in the `main` vault (requires that vault to exist and be unlockable when you create more profiles).
58    #[command(
59        after_help = "Examples:\n  tsafe config show\n  tsafe config set-backup-vault main\n  tsafe config set-backup-vault default\n  tsafe config set-backup-vault off\n  tsafe config set-exec-mode hardened\n  tsafe config set-exec-redact-output on\n  tsafe config add-exec-extra-strip OPENAI_API_KEY"
60    )]
61    Config {
62        #[command(subcommand)]
63        action: ConfigAction,
64    },
65
66    /// Store or update a secret in the vault.
67    ///
68    /// If VALUE is omitted on a TTY, you are prompted with masked input (typically `*` per character).
69    /// Piped / non-interactive stdin reads a single line.
70    /// Keys may be namespaced with `.` or `-` (e.g. `github.com.token`, `db-prod.PASSWORD`).
71    ///
72    /// If the key already exists the command will prompt for confirmation (on a TTY)
73    /// or exit with an error (non-TTY). Pass --overwrite to skip the check.
74    ///
75    #[command(
76        after_help = "Examples:\n  tsafe set DB_PASSWORD supersecret\n  tsafe set github.com.token ghp_xxx --tag env=prod\n  tsafe set API_KEY --overwrite  # replace existing without prompt"
77    )]
78    Set {
79        /// Secret key (e.g. DB_PASSWORD, github.com.token).
80        key: String,
81        /// Secret value. Omit for a masked TTY prompt or a line from stdin when piped.
82        value: Option<String>,
83        /// Attach tags as KEY=VALUE pairs (repeatable).
84        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
85        tags: Vec<String>,
86        /// Overwrite the key if it already exists — skips the confirmation prompt.
87        #[arg(long)]
88        overwrite: bool,
89    },
90
91    /// Retrieve a secret and print its plaintext value.
92    ///
93    /// Use --copy to copy to clipboard instead of printing; the clipboard is cleared after 30 s.
94    /// Use --version to retrieve a previous version (0=current, 1=previous, etc.).
95    ///
96    #[command(
97        after_help = "Examples:\n  tsafe get DB_PASSWORD\n  tsafe get API_KEY --copy\n  tsafe get DB_PASSWORD --version 1"
98    )]
99    Get {
100        /// Secret key.
101        key: String,
102        /// Copy value to clipboard and clear after 30 seconds (does not print).
103        #[arg(short, long)]
104        copy: bool,
105        /// Retrieve a previous version (0=current, 1=previous, etc.).
106        #[arg(long)]
107        version: Option<usize>,
108    },
109
110    /// Permanently remove a secret from the vault.
111    ///
112    /// The deletion is recorded in the audit log and a snapshot is taken before removal.
113    ///
114    #[command(after_help = "Examples:\n  tsafe delete OLD_TOKEN")]
115    Delete {
116        /// Secret key.
117        key: String,
118    },
119
120    /// List all secret key names stored in the vault.
121    ///
122    /// Use --tag to filter by attached metadata.
123    /// Use --ns to filter to a specific namespace (e.g. "cds-adf").
124    ///
125    #[command(
126        after_help = "Examples:\n  tsafe list\n  tsafe list --tag env=prod\n  tsafe list --ns cds-adf"
127    )]
128    List {
129        /// Filter to secrets with this tag (KEY=VALUE). Repeatable.
130        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
131        tags: Vec<String>,
132        /// Filter to keys in this namespace (stored as "<ns>/<KEY>").
133        #[arg(long)]
134        ns: Option<String>,
135    },
136
137    /// Print secrets to stdout in the chosen format.
138    ///
139    /// Formats: env (default), dotenv, powershell, json, github-actions, yaml, docker-env.
140    /// Use --ns to export only keys from a namespace; the prefix is stripped
141    /// so the output contains plain KEY=VALUE (e.g. APP_PW not cds-adf/APP_PW).
142    ///
143    #[command(
144        after_help = "Examples:\n  tsafe export\n  tsafe export --format powershell > secrets.ps1\n  tsafe export --format github-actions --tag env=ci\n  tsafe export --ns cds-adf --format dotenv > .env\n  tsafe export --format yaml > secrets.yaml\n  tsafe export --format docker-env > .env"
145    )]
146    Export {
147        /// Output format.
148        #[arg(short, long, default_value = "env")]
149        format: ExportFormat,
150        /// Limit to specific keys (all keys if omitted).
151        keys: Vec<String>,
152        /// Filter to secrets with this tag (KEY=VALUE). Repeatable.
153        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
154        tags: Vec<String>,
155        /// Filter to keys in this namespace; prefix is stripped in output.
156        #[arg(long)]
157        ns: Option<String>,
158    },
159
160    /// Execute a command with secrets injected into its environment.
161    ///
162    /// Secrets are injected as env vars; the child inherits all other env vars.
163    /// Ctrl-C is forwarded to the child and tsafe exits with the child's exit code.
164    /// Use --ns to inject only secrets from a namespace (prefix stripped from var names).
165    ///
166    /// Use --contract to load a named authority contract from the nearest .tsafe.yml manifest.
167    /// A contract declares profile, namespace, allowed secrets, required secrets, allowed targets,
168    /// and trust posture as a reusable, auditable policy. Explicit flags still override contract values.
169    #[command(
170        name = "exec",
171        after_help = "Examples:\n  tsafe exec -- dotnet run\n  tsafe exec -- docker-compose up\n  tsafe exec --ns cds-adf -- python pipeline.py\n  tsafe exec --dry-run\n  tsafe exec --plan -- npm start\n  tsafe exec --require API_KEY,DB_URL -- npm test\n  tsafe exec --no-inherit -- node index.js\n  tsafe exec --only PATH,HOME -- python script.py\n  tsafe exec --minimal -- pytest\n  tsafe exec --mode hardened -- npm test\n  tsafe exec --keys OPENAI_API_KEY,DB_URL -- npm test\n  tsafe exec --env MY_API_KEY=VAULT_API_KEY -- npm test\n  tsafe exec --preset minimal -- npm test\n  tsafe exec --timeout 30 -- npm test\n  tsafe exec --redact-output -- npm test\n  tsafe exec --contract deploy -- terraform apply\n  tsafe exec --contract ci-tests --dry-run"
172    )]
173    Exec {
174        /// Load a named authority contract from the nearest .tsafe.yml (or .tsafe.json) manifest.
175        /// The contract sets the profile, namespace, allowed/required secrets, allowed targets, and
176        /// trust posture. Explicit flags (--ns, --keys, --mode, etc.) still override contract values.
177        #[arg(long, value_name = "NAME")]
178        contract: Option<String>,
179        /// Inject only secrets from this namespace; prefix is stripped from env var names.
180        #[arg(long)]
181        ns: Option<String>,
182        /// Inject only these vault keys (after `--ns` prefix stripping). Comma-separated or repeat flag.
183        /// Missing selected keys abort the run so narrower injection does not silently degrade.
184        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
185        keys: Vec<String>,
186        /// Trust preset for this run. `standard` keeps broad compatibility, `hardened` applies a stricter preset,
187        /// and `custom` uses your persisted exec trust settings. Explicit flags still override the preset.
188        #[arg(long)]
189        mode: Option<ExecModeSetting>,
190        /// Kill the child process after this many seconds and exit non-zero. Default: no timeout.
191        #[arg(long, value_name = "SECONDS")]
192        timeout: Option<u64>,
193        /// Preset for inherited parent environment. `minimal` keeps only PATH and a safe core set
194        /// (equivalent to --minimal). `full` inherits the full parent environment minus the strip list
195        /// (equivalent to the default). Explicit --no-inherit, --minimal, and --only override this.
196        #[arg(long, value_name = "PRESET")]
197        preset: Option<ExecPresetSetting>,
198        /// List env var names that would be injected (sorted, one per line) and exit 0; no command is run.
199        #[arg(long)]
200        dry_run: bool,
201        /// Show a human-readable plan: profile, namespace, injected names, --require checks,
202        /// parent env strips, and a copy-paste run line. Exit 0; no command is run.
203        #[arg(long)]
204        plan: bool,
205        /// Start from a clean environment: no parent env vars are inherited. Only vault secrets
206        /// (and any --only keys) are visible to the child. Mutually exclusive with --only and --minimal.
207        #[arg(long, conflicts_with_all = ["only", "minimal"])]
208        no_inherit: bool,
209        /// Inherit only a safe minimal set of parent env vars (PATH, HOME, USER, TMPDIR, LANG,
210        /// TERM, SSH_AUTH_SOCK, etc.) plus vault secrets. No tokens or credentials leak through.
211        /// Mutually exclusive with --no-inherit and --only.
212        #[arg(long, conflicts_with_all = ["no_inherit", "only"])]
213        minimal: bool,
214        /// Inherit only these parent env vars (comma-separated or repeat flag); all others are
215        /// stripped. Vault secrets are then added on top. Mutually exclusive with --no-inherit and --minimal.
216        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append, conflicts_with_all = ["no_inherit", "minimal"])]
217        only: Vec<String>,
218        /// Require these vault keys (after --ns mapping) to be present. Comma-separated or repeat flag.
219        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
220        require: Vec<String>,
221        /// Map a vault key to a different env var name in the child process.
222        /// Format: ENV_VAR=VAULT_KEY  (e.g. --env MY_DB=PROD_SECRET injects the vault value of
223        /// PROD_SECRET under the name MY_DB). When --keys is also given, only vault keys that are
224        /// in the --keys allowlist may be referenced; other vault keys are rejected with an error.
225        /// Repeat the flag for multiple mappings.
226        #[arg(long = "env", value_name = "ENV_VAR=VAULT_KEY", action = clap::ArgAction::Append)]
227        env_mappings: Vec<String>,
228        /// Abort if any injected name is a known high-risk env var (e.g. NODE_OPTIONS, LD_PRELOAD).
229        /// Redundant: this is now the default. Kept for backwards compatibility.
230        #[arg(long, conflicts_with = "allow_dangerous_env")]
231        deny_dangerous_env: bool,
232        /// Allow injection of known high-risk env var names (e.g. LD_PRELOAD, NODE_OPTIONS).
233        /// By default, dangerous names abort exec. Use this flag to inject them with a warning instead.
234        #[arg(long, conflicts_with = "deny_dangerous_env")]
235        allow_dangerous_env: bool,
236        /// Replace exact vault secret values in the child's stdout/stderr with [REDACTED].
237        /// Useful for agent/tool wrappers where you trust the command less than the vault.
238        #[arg(long, conflicts_with = "no_redact_output")]
239        redact_output: bool,
240        /// Force raw child stdout/stderr even if config enables exec output redaction by default.
241        #[arg(long, conflicts_with = "redact_output")]
242        no_redact_output: bool,
243        /// Command and its arguments (omit when using --dry-run or --plan).
244        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
245        cmd: Vec<String>,
246    },
247
248    /// Import secrets from a `.env` file or another supported export source.
249    ///
250    /// `.env` paths work in every build. Some builds may also accept additional
251    /// source names for password-manager or browser CSV exports.
252    ///
253    /// When `--from` is a named export source, `--file` is required.
254    /// Skips keys that already exist unless --overwrite is passed.
255    ///
256    /// Use --ns to prefix all imported keys with a namespace, e.g. "cds-adf".
257    /// Keys are stored as "<ns>/<KEY>" allowing multiple projects in one vault
258    /// without collision (e.g. cds-adf/APP_PW vs mail-automation/APP_PW).
259    ///
260    #[command(
261        after_help = "Examples:\n  tsafe import --from .env\n  tsafe import --from .env.production --overwrite\n  tsafe import --from ../cds-adf/.env --ns cds-adf\n  tsafe import --from .env --dry-run"
262    )]
263    /// If `--from` is a **relative** path that does not exist, the error includes extra hints and
264    /// searches **downward** from the current directory (bounded depth; skips `target/`, `node_modules/`, `.git/`, etc.)
265    /// for files with the **same name** (e.g. `.env`) so you can copy-paste a suggested `tsafe import --from '…'` line.
266    Import {
267        /// `.env` file path or another supported source name for this build.
268        #[arg(long, default_value = ".env")]
269        from: String,
270        /// Export file path (required when `--from` is a named export source).
271        #[arg(long)]
272        file: Option<String>,
273        /// Overwrite existing keys (skip by default).
274        #[arg(long)]
275        overwrite: bool,
276        /// Skip duplicate keys silently instead of erroring (applies to both
277        /// within-file duplicates and keys already in the vault).
278        #[arg(long)]
279        skip_duplicates: bool,
280        /// Namespace prefix to prepend to imported keys (e.g. "cds-adf").
281        /// Keys are stored as "<ns>/<KEY>", preventing collisions across projects.
282        #[arg(long)]
283        ns: Option<String>,
284        /// Show what would be imported without writing any secrets to the vault.
285        /// Prints each key and whether it would be skipped (existing) or imported.
286        #[arg(long)]
287        dry_run: bool,
288    },
289
290    /// Map browser domains to vault profiles for the browser extension.
291    ///
292    /// The extension uses these mappings to choose which vault profile to
293    /// query when filling credentials on a given domain.
294    ///
295    #[command(
296        after_help = "Examples:\n  tsafe browser-profile add github.com\n  tsafe browser-profile add paypal.com --profile finance\n  tsafe browser-profile list\n  tsafe browser-profile remove paypal.com"
297    )]
298    #[cfg(feature = "browser")]
299    #[command(name = "browser-profile")]
300    BrowserProfile {
301        #[command(subcommand)]
302        action: BrowserProfileAction,
303    },
304
305    /// Register or unregister the native messaging host for the browser extension.
306    ///
307    /// Writes the per-user manifest/registration files needed by supported browsers on the
308    /// current OS. This command does not require browser-profile mappings or vault access.
309    ///
310    #[command(
311        after_help = "Examples:\n  tsafe browser-native-host detect\n  tsafe browser-native-host register --extension-id <chromium-id>\n  tsafe browser-native-host unregister"
312    )]
313    #[cfg(feature = "nativehost")]
314    #[command(name = "browser-native-host")]
315    BrowserNativeHost {
316        #[command(subcommand)]
317        action: BrowserNativeHostAction,
318    },
319
320    /// Re-encrypt all secrets with a new master password (vault re-key).
321    ///
322    /// Prompts for the current password, then the new password twice (unless non-interactive).
323    /// For automation / CI, set `TSAFE_PASSWORD` (current) and `TSAFE_NEW_MASTER_PASSWORD` (new);
324    /// confirmation is skipped when both are set (no OS keychain prompt in that case — run `biometric enable` after).
325    /// After interactive rotation, you are offered an OS keychain update so quick unlock matches the new password.
326    /// A snapshot is taken automatically before rotation. `tsafe doctor` suggests periodic rotation.
327    ///
328    #[command(after_help = "Examples:\n  tsafe rotate\n  tsafe --profile prod rotate")]
329    Rotate,
330
331    /// Re-encrypt the vault with a new master password and update the biometric credential.
332    ///
333    /// Prompts for the current password (or reads from TSAFE_PASSWORD), then the new password
334    /// twice (or reads from TSAFE_NEW_MASTER_PASSWORD).  The vault is written atomically via a
335    /// temp-file rename.  If biometric quick-unlock is active, the stored credential is re-stored
336    /// under the new password so subsequent unlocks continue to work.
337    ///
338    /// If the vault re-encryption succeeds but the biometric re-store fails, a warning is emitted
339    /// directing the user to `tsafe biometric re-enroll`.
340    ///
341    #[command(
342        name = "rotate-key",
343        after_help = "Examples:\n  tsafe rotate-key\n  tsafe --profile prod rotate-key"
344    )]
345    RotateKey {
346        /// Profile to re-key (defaults to the active profile).
347        #[arg(short, long)]
348        profile: Option<String>,
349    },
350
351    /// Manage profiles (named vaults).
352    ///
353    /// Each profile is an independent vault file under the platform data `vaults/` directory.
354    ///
355    #[command(
356        after_help = "Examples:\n  tsafe profile list\n  tsafe profile delete staging\n  tsafe profile delete staging --force"
357    )]
358    Profile {
359        #[command(subcommand)]
360        action: ProfileAction,
361    },
362
363    /// Display recent audit log entries for the current profile in human-readable form.
364    #[command(after_help = AUDIT_AFTER_HELP)]
365    Audit {
366        /// Number of entries to display.
367        #[arg(short, long, default_value_t = 20)]
368        limit: usize,
369        /// Check all secret values against Have I Been Pwned (k-anonymity, no full hash sent).
370        #[arg(long, conflicts_with = "explain")]
371        hibp: bool,
372        /// Show a session-style explanation (grouped operations, exec authority summaries).
373        #[arg(long)]
374        explain: bool,
375        /// With `--explain`, print JSON instead of human text.
376        #[arg(long, requires = "explain")]
377        json: bool,
378        /// Filter entries to those with this CellOS cell ID in their audit context.
379        #[arg(long, value_name = "CELL_ID")]
380        cell_id: Option<String>,
381    },
382
383    /// Cross-check authority contracts against a CellOS policy pack.
384    ///
385    /// Loads authority contracts from the nearest `.tsafe.yml` and compares each
386    /// contract's `allowed_secrets` against `allowedSecretRefs` in the CellOS
387    /// policy pack JSON.  Reports mismatches and exits non-zero if any are found.
388    ///
389    /// Use `--policy-file` as an alias for `--cellos-policy` (both accepted).
390    ///
391    #[command(
392        after_help = "Examples:\n  tsafe validate --cellos-policy doom-airgapped-policy.json\n  tsafe validate --policy-file policy.json\n  tsafe validate --policy-file policy.json --json"
393    )]
394    Validate {
395        /// Path to the CellOS policy pack JSON file.
396        #[arg(long, value_name = "PATH", conflicts_with = "policy_file")]
397        cellos_policy: Option<std::path::PathBuf>,
398        /// Alias for --cellos-policy. Path to the policy pack JSON file.
399        #[arg(long, value_name = "PATH", conflicts_with = "cellos_policy")]
400        policy_file: Option<std::path::PathBuf>,
401        /// Emit machine-readable JSON output (exit codes are preserved).
402        #[arg(long)]
403        json: bool,
404    },
405
406    /// Manage local vault snapshots.
407    ///
408    /// Snapshots are encrypted copies of the vault file, taken automatically before
409    /// every write operation. Use them to recover from accidental changes.
410    ///
411    #[command(after_help = "Examples:\n  tsafe snapshot list\n  tsafe snapshot restore")]
412    Snapshot {
413        #[command(subcommand)]
414        action: SnapshotAction,
415    },
416
417    /// Pull secrets from Azure Key Vault into the local vault.
418    ///
419    /// Requires TSAFE_AKV_URL and either a service principal
420    /// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
421    /// a managed identity (IMDS, automatic inside Azure VMs / ACI).
422    ///
423    #[command(
424        after_help = "Examples:\n  tsafe kv-pull\n  tsafe kv-pull --prefix MYAPP_ --overwrite"
425    )]
426    #[cfg(feature = "akv-pull")]
427    KvPull {
428        /// Only import secrets whose names start with this prefix (case-insensitive).
429        /// Omit to pull all secrets.
430        #[arg(long)]
431        prefix: Option<String>,
432
433        /// Overwrite existing local secrets (skip conflicts by default).
434        #[arg(long)]
435        overwrite: bool,
436        /// Failure handling mode for provider/network errors.
437        #[arg(long, value_enum, default_value = "fail-all")]
438        on_error: PullOnError,
439    },
440
441    /// Push local vault secrets to Azure Key Vault (upsert semantics).
442    ///
443    /// Requires TSAFE_AKV_URL and either a service principal
444    /// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
445    /// a managed identity (IMDS, automatic inside Azure VMs / ACI).
446    ///
447    /// Local keys are reverse-normalised to Azure Key Vault format:
448    /// MY_SECRET → my-secret. Two local keys that normalise to the same
449    /// provider name are detected as a collision and abort pre-flight.
450    ///
451    /// Remote-only keys are left untouched unless --delete-missing is passed.
452    /// A pre-flight diff is always shown before writing. No secret values
453    /// are printed — only key names and 12-char SHA-256 hash prefixes.
454    ///
455    #[command(
456        after_help = "Examples:\n  tsafe kv-push --dry-run\n  tsafe kv-push --yes\n  tsafe kv-push --prefix MYAPP_ --yes\n  tsafe kv-push --delete-missing --yes"
457    )]
458    #[cfg(feature = "akv-pull")]
459    KvPush {
460        /// Only push secrets whose local key names start with this prefix (case-insensitive).
461        #[arg(long)]
462        prefix: Option<String>,
463
464        /// Only push secrets in this namespace (stored as `<ns>/KEY`).
465        #[arg(long)]
466        ns: Option<String>,
467
468        /// Show the diff without writing anything (always exits 0).
469        #[arg(long)]
470        dry_run: bool,
471
472        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
473        #[arg(long)]
474        yes: bool,
475
476        /// Also delete remote secrets that are absent locally within the filtered scope.
477        /// Off by default — opt-in to avoid accidental mass deletion.
478        /// AKV uses soft-delete (30-day recoverable window).
479        #[arg(long)]
480        delete_missing: bool,
481    },
482
483    /// Share a vault secret as a one-time HTTPS link via a configured OTS (one-time secret) service.
484    ///
485    /// Set `TSAFE_OTS_BASE_URL` to your service HTTPS origin (no default). The CLI POSTs JSON
486    /// `{"secret","ttl"}` to `{base}{TSAFE_OTS_CREATE_PATH}` (default path `/create`) and prints the returned `url`.
487    ///
488    /// The one-time URL is printed to stdout — never the secret value. Use any server that implements this contract.
489    ///
490    #[command(
491        after_help = "Examples:\n  tsafe share-once DB_PASSWORD\n  tsafe share-once API_KEY --ttl 10m"
492    )]
493    #[cfg(feature = "ots-sharing")]
494    #[command(name = "share-once")]
495    ShareOnce {
496        /// Secret key to share.
497        key: String,
498        /// Link expiry (sent to the service; many accept 10m, 1h, 24h).
499        #[arg(short, long, default_value = "1h")]
500        ttl: String,
501    },
502
503    /// Receive a secret from a one-time link (from `share-once` or any compatible OTS server).
504    ///
505    /// POSTs to the exact HTTPS URL. The response may be JSON (`secret`, `plaintext`, or `value`) or HTML with
506    /// `<div id="secret-content">...</div>`.
507    ///
508    /// Optionally store the retrieved value directly into the vault with --store.
509    ///
510    #[command(
511        after_help = "Examples:\n  tsafe receive-once 'https://ots.example.com/s/abc123'\n  tsafe receive-once '<URL>' --store DB_PASSWORD"
512    )]
513    #[cfg(feature = "ots-sharing")]
514    #[command(name = "receive-once", visible_alias = "snap-receive")]
515    ReceiveOnce {
516        /// The full one-time URL (fragment `#...` is ignored).
517        url: String,
518        /// Store the received secret in the vault under this key name instead of printing it.
519        #[arg(long)]
520        store: Option<String>,
521    },
522
523    /// Generate a cryptographically random secret and store it in the vault.
524    ///
525    /// Uses a CSPRNG. Default length 32, character set 'alnum'.
526    ///
527    #[command(
528        after_help = "Examples:\n  tsafe gen DB_PASSWORD\n  tsafe gen SESSION_KEY --length 64 --charset hex --print\n  tsafe gen TEMP_PASSWORD --exclude-ambiguous --print"
529    )]
530    Gen {
531        /// Key name to store the generated secret under.
532        key: String,
533        /// Length of the generated secret in characters (ignored if --words is set).
534        #[arg(short = 'l', long, default_value_t = 32)]
535        length: usize,
536        /// Character set: alnum (default), alpha, numeric, hex, symbol.
537        #[arg(short = 'c', long, default_value = "alnum")]
538        charset: String,
539        /// Generate a passphrase of N random words instead of a random string.
540        #[arg(short = 'w', long)]
541        words: Option<usize>,
542        /// Attach tags as KEY=VALUE pairs (repeatable).
543        #[arg(short = 't', long = "tag", value_name = "KEY=VALUE")]
544        tags: Vec<String>,
545        /// Print the generated value to stdout (otherwise the value is only in the vault).
546        #[arg(long)]
547        print: bool,
548        /// Remove visually ambiguous characters (0, O, l, 1, I) from the charset.
549        /// Useful when the secret will be read aloud or transcribed manually.
550        #[arg(long)]
551        exclude_ambiguous: bool,
552    },
553
554    /// Show key-level changes between the current vault and its most-recent snapshot.
555    ///
556    /// Highlights added, removed, and modified keys — values are never shown.
557    ///
558    #[command(after_help = "Examples:\n  tsafe diff\n  tsafe --profile staging diff")]
559    Diff,
560
561    /// Compare key names across two profiles without decrypting any values.
562    ///
563    /// Highlights keys present in one profile but missing from the other.
564    ///
565    #[command(
566        after_help = "Examples:\n  tsafe compare staging\n  tsafe --profile dev compare prod"
567    )]
568    Compare {
569        /// Second profile to compare against the active --profile.
570        profile_b: String,
571    },
572
573    /// Show version history for a secret.
574    ///
575    /// Lists all stored versions with timestamps. Version 0 is the current
576    /// value; higher numbers are older. Use `tsafe get KEY --version N` to
577    /// retrieve a specific version.
578    ///
579    #[command(after_help = "Examples:\n  tsafe history DB_PASSWORD")]
580    History {
581        /// Secret key.
582        key: String,
583    },
584
585    /// Move or rename a secret within the vault, or to a different profile.
586    ///
587    /// Within a profile this is an atomic rename: key name, namespace prefix,
588    /// tags and full version history are all preserved.
589    ///
590    #[command(
591        after_help = "Examples:\n  tsafe mv DB_HOST infra/DB_HOST        (add namespace)\n  tsafe mv infra/DB_HOST DB_HOST        (remove namespace)\n  tsafe mv DB_HOST --to-profile prod    (move to other profile, same key)\n  tsafe mv DB_HOST --to-profile prod NEW_NAME  (move + rename)"
592    )]
593    Mv {
594        /// Source secret key.
595        source: String,
596
597        /// Destination key name.  Omit when using --to-profile to keep the same key name.
598        dest: Option<String>,
599
600        /// Move the secret to this profile (cross-profile move).
601        #[arg(long, value_name = "PROFILE")]
602        to_profile: Option<String>,
603
604        /// Overwrite the destination key if it already exists.
605        #[arg(long, short = 'f')]
606        force: bool,
607    },
608
609    /// Install a secret-scanning git pre-commit hook in the current repo.
610    ///
611    /// Scans staged files for hardcoded secrets on every `git commit`.
612    ///
613    #[command(
614        after_help = "Examples:\n  tsafe hook-install\n  tsafe hook-install --dir /path/to/repo"
615    )]
616    #[cfg(feature = "git-helpers")]
617    HookInstall {
618        /// Repo root directory; defaults to walking up from the current directory.
619        #[arg(long)]
620        dir: Option<String>,
621    },
622
623    /// Export audit log entries to stdout or a file as JSON or Splunk HEC events.
624    #[command(
625        after_help = "Examples:\n  tsafe audit-export --format json --output audit.jsonl\n  tsafe audit-export --format splunk"
626    )]
627    AuditExport {
628        /// Output format.
629        #[arg(short, long, value_enum, default_value = "json")]
630        format: AuditExportFormat,
631        /// Write to a file instead of stdout.
632        #[arg(short, long)]
633        output: Option<String>,
634    },
635
636    /// Report HMAC chain coverage for the audit log of the current profile.
637    ///
638    /// Reads all entries from the audit log file and counts how many carry a
639    /// `prev_entry_hmac` field (written by a C8-capable tsafe build) versus
640    /// how many are unchained (written before C8 or at a session boundary).
641    ///
642    /// IMPORTANT — ephemeral-key limitation: the HMAC chain key is generated
643    /// fresh on every tsafe session and is never persisted.  This command
644    /// cannot perform cryptographic verification of entries from a closed
645    /// session; it can only report chain coverage (presence of the field).
646    /// To detect within-session tampering, use AuditLog::verify_chain() from
647    /// a live session handle.
648    ///
649    /// Exit codes: 0 = log is structurally valid (or empty), 2 = at least one
650    /// entry could not be parsed as JSON.
651    #[command(
652        after_help = "Examples:\n  tsafe audit-verify\n  tsafe audit-verify --json\n  tsafe --profile prod audit-verify"
653    )]
654    AuditVerify {
655        /// Emit machine-readable JSON output.
656        #[arg(long)]
657        json: bool,
658    },
659
660    /// Set or remove a rotation policy on a secret.
661    ///
662    /// Policies are stored as tags and checked by `tsafe doctor` and `tsafe rotate-due`.
663    ///
664    #[command(
665        after_help = "Examples:\n  tsafe policy set DB_PASSWORD --rotate-every 90d\n  tsafe policy remove DB_PASSWORD"
666    )]
667    Policy {
668        #[command(subcommand)]
669        action: PolicyAction,
670    },
671
672    /// List secrets that are overdue for rotation (per `rotate_policy` tags).
673    ///
674    /// Checks the `rotate_policy` tag against the secret's `updated_at` timestamp.
675    /// Use `--json` for automation; `--fail` exits with status 1 when anything is overdue (CI/cron).
676    ///
677    /// Set policies with: `tsafe policy set KEY --rotate-every 90d`
678    #[command(after_help = ROTATE_DUE_AFTER_HELP)]
679    RotateDue {
680        /// Print JSON to stdout (`overdue_count` + `items` with key, days_overdue, policy).
681        #[arg(long)]
682        json: bool,
683        /// Exit with status 1 when one or more secrets are overdue.
684        #[arg(long)]
685        fail: bool,
686    },
687
688    /// Pull secrets from a HashiCorp Vault KV v2 store.
689    ///
690    /// Requires TSAFE_HCP_URL or --addr and VAULT_TOKEN (or --token).
691    ///
692    #[command(
693        after_help = "Examples:\n  tsafe vault-pull --addr http://vault:8200 --prefix myapp/\n  tsafe vault-pull  # uses TSAFE_HCP_URL + VAULT_TOKEN"
694    )]
695    #[cfg(feature = "cloud-pull-vault")]
696    VaultPull {
697        /// HashiCorp Vault address. Defaults to TSAFE_HCP_URL or http://127.0.0.1:8200.
698        #[arg(long)]
699        addr: Option<String>,
700        /// Vault token. Defaults to VAULT_TOKEN env var.
701        /// Deprecated: passing the token as a CLI argument exposes it in the process
702        /// table. Store the token in tsafe and use `tsafe exec -- tsafe vault-pull`
703        /// so the token is injected securely without appearing in the process table.
704        #[arg(long)]
705        token: Option<String>,
706        /// KV v2 mount path. Defaults to "secret".
707        #[arg(long)]
708        mount: Option<String>,
709        /// Only import secrets under this path prefix.
710        #[arg(long)]
711        prefix: Option<String>,
712        /// Overwrite existing local secrets (skip conflicts by default).
713        #[arg(long)]
714        overwrite: bool,
715    },
716
717    /// Pull fields from a 1Password item via the `op` CLI.
718    ///
719    /// Requires the 1Password CLI (`op`) installed and authenticated.
720    ///
721    #[command(
722        after_help = "Examples:\n  tsafe op-pull 'Database Credentials'\n  tsafe op-pull abc123xyz --op-vault Personal"
723    )]
724    #[cfg(feature = "cloud-pull-1password")]
725    OpPull {
726        /// Item title or ID.
727        item: String,
728        /// 1Password vault name (uses the default vault if omitted).
729        #[arg(long = "op-vault")]
730        op_vault: Option<String>,
731        /// Overwrite existing local secrets (skip conflicts by default).
732        #[arg(long)]
733        overwrite: bool,
734    },
735
736    /// Import Login items from Bitwarden into the local vault via the `bw` CLI.
737    ///
738    /// Bitwarden REST API ciphers are always E2E encrypted client-side. This command
739    /// shells to the `bw` CLI (which handles local decryption) rather than calling
740    /// the REST API directly — the same pattern as `tsafe op-pull` for 1Password.
741    ///
742    /// Requires TSAFE_BW_CLIENT_ID, TSAFE_BW_CLIENT_SECRET, and TSAFE_BW_PASSWORD
743    /// (master password for `bw unlock`). The `bw` CLI must be installed and on PATH.
744    ///
745    /// Item names are normalised: spaces and hyphens become underscores, uppercase.
746    /// Login.Username → ITEM_NAME_USERNAME, Login.Password → ITEM_NAME_PASSWORD.
747    /// Custom text/hidden fields → ITEM_NAME_<FIELD_NAME>. Boolean fields are skipped.
748    ///
749    #[command(
750        name = "bw-pull",
751        after_help = "Examples:\n  tsafe bw-pull\n  tsafe bw-pull --bw-folder my-folder-id --overwrite\n  tsafe bw-pull --bw-client-id org.abc --bw-password-env MY_BW_PW"
752    )]
753    #[cfg(feature = "cloud-pull-bitwarden")]
754    BwPull {
755        /// Bitwarden API client ID. Reads TSAFE_BW_CLIENT_ID if not set.
756        #[arg(long = "bw-client-id")]
757        bw_client_id: Option<String>,
758        /// Bitwarden API client secret. Reads TSAFE_BW_CLIENT_SECRET if not set.
759        #[arg(long = "bw-client-secret")]
760        bw_client_secret: Option<String>,
761        /// Bitwarden API base URL (for self-hosted / Vaultwarden).
762        /// Default: https://api.bitwarden.com
763        #[arg(long = "bw-api-url")]
764        bw_api_url: Option<String>,
765        /// Bitwarden identity base URL (for self-hosted / Vaultwarden).
766        /// Default: https://identity.bitwarden.com
767        #[arg(long = "bw-identity-url")]
768        bw_identity_url: Option<String>,
769        /// Bitwarden folder ID to filter items. Imports all items when omitted.
770        #[arg(long = "bw-folder")]
771        bw_folder: Option<String>,
772        /// Name of the env var holding the Bitwarden master password for `bw unlock`.
773        /// Default: TSAFE_BW_PASSWORD
774        #[arg(long = "bw-password-env")]
775        bw_password_env: Option<String>,
776        /// Overwrite existing local secrets (skip conflicts by default).
777        #[arg(long)]
778        overwrite: bool,
779        /// Failure handling mode for provider/network errors.
780        #[arg(long, value_enum, default_value = "fail-all")]
781        on_error: PullOnError,
782        /// Show which items would be imported without writing any secrets.
783        #[arg(long)]
784        dry_run: bool,
785    },
786
787    /// Import secrets from a KeePass `.kdbx` file into the local vault.
788    ///
789    /// Opens a local KeePass database using the master password (from the env var
790    /// named by --kp-password-env, default TSAFE_KP_PASSWORD) and/or a key file.
791    ///
792    /// Entry titles are used as key prefixes. Standard fields (UserName, Password, URL)
793    /// map to TITLE_USERNAME, TITLE_PASSWORD, TITLE_URL.  Custom fields map to
794    /// TITLE_<FIELD_NAME_NORMALISED>.  Notes are skipped.
795    ///
796    #[command(
797        after_help = "Examples:\n  tsafe kp-pull --kp-path /home/user/vault.kdbx\n  tsafe kp-pull --kp-path ~/db.kdbx --kp-password-env MY_KP_PW --kp-group Infra\n  tsafe kp-pull --kp-path db.kdbx --kp-keyfile ~/my.keyx"
798    )]
799    #[cfg(feature = "cloud-pull-keepass")]
800    KpPull {
801        /// Absolute path to the `.kdbx` database file.
802        #[arg(long = "kp-path")]
803        kp_path: String,
804        /// Name of the env var that holds the master password.
805        /// Defaults to TSAFE_KP_PASSWORD.
806        #[arg(long = "kp-password-env", default_value = "TSAFE_KP_PASSWORD")]
807        kp_password_env: String,
808        /// Path to a KeePass key file (optional).
809        #[arg(long = "kp-keyfile")]
810        kp_keyfile: Option<String>,
811        /// Only import entries from this group name (case-insensitive).
812        #[arg(long = "kp-group")]
813        kp_group: Option<String>,
814        /// When set, also traverse descendant groups under the matched group.
815        #[arg(long = "kp-recursive")]
816        kp_recursive: bool,
817        /// Overwrite existing local secrets (skip conflicts by default).
818        #[arg(long)]
819        overwrite: bool,
820        /// Failure handling mode for provider/network errors.
821        #[arg(long, value_enum, default_value = "fail-all")]
822        on_error: PullOnError,
823    },
824
825    /// Import secrets from AWS Secrets Manager into the local vault.
826    ///
827    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
828    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
829    ///
830    /// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
831    ///
832    /// Secret names are normalised: slashes and hyphens become underscores and
833    /// the result is uppercased (e.g. `myapp/db-password` → `MYAPP_DB_PASSWORD`).
834    ///
835    #[command(
836        after_help = "Examples:\n  tsafe aws-pull --region us-east-1\n  tsafe aws-pull --prefix myapp/ --overwrite"
837    )]
838    #[cfg(feature = "cloud-pull-aws")]
839    AwsPull {
840        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
841        #[arg(long)]
842        region: Option<String>,
843        /// Only import secrets whose names start with this prefix.
844        #[arg(long)]
845        prefix: Option<String>,
846        /// Overwrite existing local secrets (skip conflicts by default).
847        #[arg(long)]
848        overwrite: bool,
849        /// Failure handling mode for provider/network errors.
850        #[arg(long, value_enum, default_value = "fail-all")]
851        on_error: PullOnError,
852    },
853
854    /// Import secrets from GCP Secret Manager into the local vault.
855    ///
856    /// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
857    /// metadata server, or ADC file (gcloud auth application-default login).
858    ///
859    /// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
860    ///
861    /// Secret names are normalised: hyphens and dots become underscores and
862    /// the result is uppercased (e.g. `db-password` → `DB_PASSWORD`).
863    ///
864    #[command(
865        after_help = "Examples:\n  tsafe gcp-pull --project my-gcp-project\n  tsafe gcp-pull --prefix myapp- --overwrite"
866    )]
867    #[cfg(feature = "cloud-pull-gcp")]
868    GcpPull {
869        /// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
870        #[arg(long)]
871        project: Option<String>,
872        /// Only import secrets whose names start with this prefix.
873        #[arg(long)]
874        prefix: Option<String>,
875        /// Overwrite existing local secrets (skip conflicts by default).
876        #[arg(long)]
877        overwrite: bool,
878        /// Failure handling mode for provider/network errors.
879        #[arg(long, value_enum, default_value = "fail-all")]
880        on_error: PullOnError,
881    },
882
883    /// Push local vault secrets to GCP Secret Manager (upsert semantics).
884    ///
885    /// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
886    /// metadata server, or ADC file (gcloud auth application-default login).
887    ///
888    /// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
889    ///
890    /// GCP Secret Manager uses a two-call pattern for new secrets: create the
891    /// secret resource, then add a version. Existing secrets only need a new version.
892    ///
893    /// Local keys are reverse-normalised to GCP format:
894    /// MY_SECRET → my-secret. Two local keys that normalise to the same
895    /// provider name are detected as a collision and abort pre-flight.
896    ///
897    /// Remote-only keys are left untouched unless --delete-missing is passed.
898    /// A pre-flight diff is always shown before writing. No secret values
899    /// are printed — only key names and 12-char SHA-256 hash prefixes.
900    ///
901    #[command(
902        after_help = "Examples:\n  tsafe gcp-push --project my-project --dry-run\n  tsafe gcp-push --project my-project --yes\n  tsafe gcp-push --prefix MYAPP_ --yes\n  tsafe gcp-push --delete-missing --yes"
903    )]
904    #[cfg(feature = "cloud-pull-gcp")]
905    GcpPush {
906        /// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
907        #[arg(long)]
908        project: Option<String>,
909
910        /// Only push secrets whose local key names start with this prefix (case-insensitive).
911        #[arg(long)]
912        prefix: Option<String>,
913
914        /// Only push secrets in this namespace (stored as `<ns>/KEY`).
915        #[arg(long)]
916        ns: Option<String>,
917
918        /// Show the diff without writing anything (always exits 0).
919        #[arg(long)]
920        dry_run: bool,
921
922        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
923        #[arg(long)]
924        yes: bool,
925
926        /// Also delete remote secrets absent locally within the filtered scope.
927        /// Off by default — opt-in to avoid accidental mass deletion.
928        /// Note: GCP Secret Manager deletion requires the Secret Manager Admin API.
929        #[arg(long)]
930        delete_missing: bool,
931    },
932
933    /// Import parameters from AWS SSM Parameter Store into the local vault.
934    ///
935    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
936    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
937    ///
938    /// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
939    /// Parameters are fetched recursively under the given path.
940    /// SecureString parameters are decrypted automatically (WithDecryption=true).
941    ///
942    /// Parameter names are normalised: leading `/` stripped, remaining `/` and `-`
943    /// become `_`, uppercased (e.g. `/myapp/db-password` → `MYAPP_DB_PASSWORD`).
944    ///
945    #[command(
946        after_help = "Examples:\n  tsafe ssm-pull --region us-east-1 --path /myapp/prod/\n  tsafe ssm-pull --path /shared/ --overwrite"
947    )]
948    #[cfg(feature = "cloud-pull-aws")]
949    SsmPull {
950        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
951        #[arg(long)]
952        region: Option<String>,
953        /// Parameter path prefix (e.g. `/myapp/prod/`). Defaults to `/` (all parameters).
954        #[arg(long)]
955        path: Option<String>,
956        /// Overwrite existing local secrets (skip conflicts by default).
957        #[arg(long)]
958        overwrite: bool,
959        /// Failure handling mode for provider/network errors.
960        #[arg(long, value_enum, default_value = "fail-all")]
961        on_error: PullOnError,
962    },
963
964    /// Push local vault secrets to AWS Secrets Manager (upsert semantics).
965    ///
966    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
967    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
968    ///
969    /// Local keys are reverse-normalised to AWS Secrets Manager format:
970    /// MY_SECRET → my-secret. Two local keys that normalise to the same
971    /// provider name are detected as a collision and abort pre-flight.
972    ///
973    /// Remote-only secrets are left untouched unless --delete-missing is passed.
974    /// A pre-flight diff is always shown before writing. No secret values
975    /// are printed — only key names and 12-char SHA-256 hash prefixes.
976    ///
977    #[command(
978        after_help = "Examples:\n  tsafe aws-push --dry-run\n  tsafe aws-push --yes\n  tsafe aws-push --prefix myapp/ --yes\n  tsafe aws-push --delete-missing --yes"
979    )]
980    #[cfg(feature = "cloud-pull-aws")]
981    AwsPush {
982        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
983        #[arg(long)]
984        region: Option<String>,
985        /// Only push secrets whose local key names start with this prefix (case-insensitive).
986        #[arg(long)]
987        prefix: Option<String>,
988        /// Show the diff without writing anything (always exits 0).
989        #[arg(long)]
990        dry_run: bool,
991        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
992        #[arg(long)]
993        yes: bool,
994        /// Also delete remote secrets absent locally within the filtered scope.
995        /// Off by default — opt-in to avoid accidental mass deletion.
996        #[arg(long)]
997        delete_missing: bool,
998    },
999
1000    /// Push local vault secrets to AWS SSM Parameter Store (upsert semantics).
1001    ///
1002    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
1003    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
1004    ///
1005    /// Local keys are reverse-normalised to SSM parameter names:
1006    /// given `--path /myapp/`, MYAPP_DB_PASSWORD → /myapp/db-password.
1007    ///
1008    /// Remote-only parameters are left untouched unless --delete-missing is passed.
1009    /// A pre-flight diff is always shown before writing. No secret values
1010    /// are printed — only key names and 12-char SHA-256 hash prefixes.
1011    ///
1012    #[command(
1013        after_help = "Examples:\n  tsafe ssm-push --path /myapp/ --dry-run\n  tsafe ssm-push --path /myapp/ --yes\n  tsafe ssm-push --path /myapp/ --delete-missing --yes"
1014    )]
1015    #[cfg(feature = "cloud-pull-aws")]
1016    SsmPush {
1017        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
1018        #[arg(long)]
1019        region: Option<String>,
1020        /// SSM path prefix that scopes the push (e.g. `/myapp/`).
1021        #[arg(long)]
1022        path: Option<String>,
1023        /// Show the diff without writing anything (always exits 0).
1024        #[arg(long)]
1025        dry_run: bool,
1026        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
1027        #[arg(long)]
1028        yes: bool,
1029        /// Also delete remote parameters absent locally within the path scope.
1030        /// Off by default — opt-in to avoid accidental mass deletion.
1031        #[arg(long)]
1032        delete_missing: bool,
1033    },
1034
1035    /// Print a shell completion script and exit.
1036    ///
1037    #[command(
1038        after_help = "Examples:\n  tsafe completions powershell | Out-String | Invoke-Expression"
1039    )]
1040    Completions {
1041        /// Shell to generate completions for.
1042        shell: Shell,
1043    },
1044
1045    /// Output completion candidates for use by shell completion scripts (internal).
1046    ///
1047    /// Called by the patched completion scripts generated by `tsafe completions`.
1048    /// Not intended for direct use.
1049    #[command(name = "_completions-data", hide = true)]
1050    CompletionsData {
1051        /// Type of completion data to emit: `profiles` or `contracts`.
1052        data_type: String,
1053    },
1054
1055    /// Diagnose vault health: file presence, snapshots, env vars, secret expiry, and operator-facing health hints.
1056    #[command(long_about = DOCTOR_LONG_ABOUT, after_help = DOCTOR_AFTER_HELP)]
1057    Doctor {
1058        /// Emit machine-readable JSON and use health exit codes (0=healthy, 1=warning, 2=critical).
1059        #[arg(long)]
1060        json: bool,
1061    },
1062
1063    /// Explain a concept in the terminal (`exec`, namespaces, compiled agent/browser pull lanes, …).
1064    ///
1065    /// Omit the topic to list available explanations.
1066    ///
1067    #[command(
1068        after_help = "Examples:\n  tsafe explain\n  tsafe explain exec\n  tsafe explain exec-security"
1069    )]
1070    Explain {
1071        /// Topic to print (omit to list all topics).
1072        #[arg(value_name = "TOPIC")]
1073        topic: Option<crate::explain::ExplainTopic>,
1074    },
1075
1076    /// Remove a stale vault lock file (use after a crash leaves the vault locked).
1077    ///
1078    /// Deletes `<profile>.vault.lock` if it exists. Safe to run — the lock is
1079    /// advisory only. Use when `tsafe` reports "vault is locked by another process"
1080    /// but no other process is actually running.
1081    ///
1082    #[command(after_help = "Examples:\n  tsafe unlock\n  tsafe --profile prod unlock")]
1083    Unlock,
1084
1085    /// Launch the full-screen interactive terminal UI.
1086    ///
1087    /// Supports add/edit/delete/reveal/rotate/snapshot restore and audit log viewing.
1088    /// Press ? inside the TUI for a contextual keyboard reference.
1089    ///
1090    #[command(after_help = "Examples:\n  tsafe ui\n  tsafe --profile prod ui")]
1091    #[cfg(feature = "tui")]
1092    Ui,
1093
1094    /// Render a secret value as a QR code in the terminal.
1095    ///
1096    /// Opens the vault, retrieves KEY, prints the QR code to stdout, then waits
1097    /// for Enter before clearing — so the value is never left on-screen.
1098    ///
1099    #[command(after_help = "Examples:\n  tsafe qr WIFI_PASSWORD\n  tsafe qr API_KEY")]
1100    Qr {
1101        /// Secret key whose value to render as a QR code.
1102        key: String,
1103    },
1104
1105    /// Store a TOTP secret and retrieve live codes.
1106    ///
1107    /// add: store a TOTP seed for the given key
1108    /// get: compute and print the current 6-digit code
1109    ///
1110    #[command(
1111        after_help = "Examples:\n  tsafe totp add GITHUB_2FA JBSWY3DPEHPK3PXP\n  tsafe totp get GITHUB_2FA"
1112    )]
1113    Totp {
1114        #[command(subcommand)]
1115        action: TotpAction,
1116    },
1117
1118    /// Pin a secret to the top of lists.
1119    ///
1120    #[command(
1121        after_help = "Examples:\n  tsafe pin DB_PASSWORD\n  tsafe --profile prod pin API_KEY"
1122    )]
1123    Pin { key: String },
1124
1125    /// Remove pin from a secret.
1126    ///
1127    #[command(after_help = "Examples:\n  tsafe unpin DB_PASSWORD")]
1128    Unpin { key: String },
1129
1130    /// Create an alias: ALIAS_NAME resolves to an existing KEY.
1131    ///
1132    /// tsafe get ALIAS_NAME returns the value of KEY.
1133    /// Use tsafe alias --list to view all aliases.
1134    ///
1135    #[command(
1136        after_help = "Examples:\n  tsafe alias DB_PASS DATABASE_PASSWORD\n  tsafe alias --list"
1137    )]
1138    Alias {
1139        /// Key this alias should resolve to (omit with --list to view all aliases).
1140        target_key: Option<String>,
1141        /// Name of the alias to create.
1142        alias_name: Option<String>,
1143        /// List all aliases in the vault.
1144        #[arg(long)]
1145        list: bool,
1146    },
1147
1148    /// Replace `{{KEY}}` placeholders in a file with vault secret values.
1149    ///
1150    /// Reads the input file, replaces each `{{KEY}}` with the corresponding
1151    /// vault secret, and writes to stdout (or `--output PATH`).
1152    ///
1153    #[command(
1154        after_help = "Examples:\n  tsafe template config.yml.tmpl > config.yml\n  tsafe template app.conf.tmpl --output app.conf"
1155    )]
1156    Template {
1157        /// Input template file containing {{KEY}} placeholders.
1158        file: String,
1159        /// Write output to a file instead of stdout.
1160        #[arg(short, long)]
1161        output: Option<String>,
1162        /// Ignore missing keys instead of failing.
1163        #[arg(long)]
1164        ignore_missing: bool,
1165    },
1166
1167    /// Read stdin and replace any vault secret values with [REDACTED].
1168    ///
1169    /// Useful for piping logs through to scrub sensitive values.
1170    ///
1171    #[command(
1172        after_help = "Examples:\n  cargo test 2>&1 | tsafe redact\n  tsafe exec -- myapp | tsafe redact"
1173    )]
1174    Redact,
1175
1176    /// Show the active build profile label and compile-time capabilities.
1177    ///
1178    /// This reports the compiled truth for the running `tsafe` binary only.
1179    /// Companion runtimes such as `tsafe-agent` have separate install and release truth.
1180    #[command(name = "build-info", after_help = BUILD_INFO_AFTER_HELP)]
1181    BuildInfo {
1182        /// Emit machine-readable JSON output.
1183        #[arg(long)]
1184        json: bool,
1185    },
1186
1187    #[cfg(feature = "plugins")]
1188    /// Run a tool with its required vault secrets injected automatically.
1189    ///
1190    /// Each plugin knows which vault keys map to which environment variables for the
1191    /// named tool.  Run `tsafe plugin` (no args) to list available plugins.
1192    ///
1193    /// Missing optional keys are silently skipped; missing required keys abort with an error.
1194    ///
1195    #[command(
1196        after_help = "Examples:\n  tsafe plugin gh repo list\n  tsafe plugin aws s3 ls --bucket my-bucket\n  tsafe plugin az group list --subscription my-sub\n  tsafe plugin          (list all available plugins)"
1197    )]
1198    Plugin {
1199        /// Tool name (e.g. gh, aws, az, docker, npm, pypi, terraform). Omit to list.
1200        tool: Option<String>,
1201        /// Arguments to pass to the tool.
1202        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1203        args: Vec<String>,
1204    },
1205
1206    /// Act as a git credential helper (install/get/store/erase protocol).
1207    ///
1208    /// Run `tsafe credential-helper install` once to configure git to use tsafe
1209    /// as the credential store. Git will then call `tsafe credential-helper get`,
1210    /// `store`, or `erase` automatically.
1211    ///
1212    /// In `get` mode, reads protocol/host from stdin and returns username/password
1213    /// from the vault. Keys are matched by `<HOST>_USERNAME` / `<HOST>_PASSWORD`
1214    /// pattern, or by tags `host=<HOST>`.
1215    #[cfg(feature = "git-helpers")]
1216    #[command(
1217        name = "credential-helper",
1218        after_help = "Examples:\n  tsafe credential-helper install\n  tsafe credential-helper install --global\n  git credential fill  # (git calls tsafe automatically after install)"
1219    )]
1220    CredentialHelper {
1221        /// Git credential helper action.
1222        #[arg(value_enum, default_value = "install")]
1223        action: CredentialHelperOperation,
1224        /// For `install`: configure at the --global level (user-wide).
1225        /// By default, configures the local repository git config.
1226        #[arg(long)]
1227        global: bool,
1228    },
1229
1230    /// Collaboration service commands (team membership, DEK delivery, recovery).
1231    ///
1232    /// Scaffolding only in Tranche 2 — no network calls are made.
1233    /// Enable with `--features collab`.
1234    ///
1235    #[command(
1236        after_help = "Examples:\n  tsafe collab join <team-id>\n  tsafe collab status <team-id>"
1237    )]
1238    #[cfg(feature = "collab")]
1239    Collab {
1240        #[command(subcommand)]
1241        action: CollabAction,
1242    },
1243
1244    /// Add an SSH key from the vault to the running ssh-agent.
1245    ///
1246    /// The key is passed via stdin to `ssh-add -` so it never touches disk.
1247    ///
1248    #[command(after_help = "Examples:\n  tsafe ssh-add SSH_KEY\n  tsafe ssh-add id_ed25519")]
1249    #[cfg(feature = "ssh")]
1250    SshAdd {
1251        /// Vault key name containing the SSH private key.
1252        key: String,
1253    },
1254
1255    /// Import an SSH private key file into the vault.
1256    ///
1257    #[command(
1258        after_help = "Examples:\n  tsafe ssh-import ~/.ssh/id_ed25519\n  tsafe ssh-import ~/.ssh/id_rsa --name SSH_RSA_KEY"
1259    )]
1260    #[cfg(feature = "ssh")]
1261    SshImport {
1262        /// Path to the SSH private key file.
1263        path: String,
1264        /// Vault key name to store under (defaults to filename).
1265        #[arg(long)]
1266        name: Option<String>,
1267        /// Attach tags as KEY=VALUE pairs (repeatable).
1268        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
1269        tags: Vec<String>,
1270    },
1271
1272    /// SSH key inventory and operations.
1273    ///
1274    /// Subcommands: list, public-key, generate, config, agent
1275    ///
1276    #[command(
1277        after_help = "Examples:\n  tsafe ssh list\n  tsafe ssh public-key my_ed25519_key\n  tsafe ssh generate my_key\n  tsafe ssh generate my_key --type rsa\n  tsafe ssh config\n  eval $(tsafe ssh-agent)"
1278    )]
1279    #[cfg(feature = "ssh")]
1280    Ssh {
1281        #[command(subcommand)]
1282        action: SshAction,
1283    },
1284
1285    /// List namespaces or copy/move all keys under one prefix to another.
1286    ///
1287    /// A namespace is any key-prefix of the form "<name>/KEY".  They are not stored
1288    /// explicitly — this command introspects the key names in the vault.
1289    ///
1290    #[command(
1291        after_help = "Examples:\n  tsafe ns list\n  tsafe ns copy prod staging\n  tsafe ns move oldapp newapp --force"
1292    )]
1293    Ns {
1294        #[command(subcommand)]
1295        action: NsAction,
1296    },
1297
1298    /// Pull secrets from all sources defined in `.tsafe.yml`.
1299    ///
1300    /// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
1301    /// and executes each pull source in manifest order (sequential; see ADR-012).
1302    ///
1303    /// Use --dry-run to preview which sources would be invoked without making any
1304    /// live API calls. Note: collision detection is not available in dry-run mode —
1305    /// detecting key conflicts requires fetching keys from each provider.
1306    ///
1307    /// Use --source to narrow execution to one or more named sources. Sources are
1308    /// named with the `name` field in the manifest. Multiple --source flags are OR'd.
1309    ///
1310    #[command(
1311        after_help = "Examples:\n  tsafe pull\n  tsafe pull --config path/to/.tsafe.yml\n  tsafe pull --dry-run\n  tsafe pull --source prod-akv\n  tsafe pull --source prod-akv --source staging-aws"
1312    )]
1313    #[cfg(feature = "multi-pull")]
1314    Pull {
1315        /// Path to config file (auto-detected if omitted).
1316        #[arg(long)]
1317        config: Option<String>,
1318        /// Overwrite all existing secrets (overrides per-source settings).
1319        #[arg(long)]
1320        overwrite: bool,
1321        /// Failure handling mode for source errors in multi-source pull.
1322        #[arg(long, value_enum, default_value = "fail-all")]
1323        on_error: PullOnError,
1324        /// Preview which sources would be invoked without making any live API calls.
1325        /// Collision detection is not available in dry-run mode.
1326        #[arg(long)]
1327        dry_run: bool,
1328        /// Narrow execution to sources with this `name` label (repeatable).
1329        /// Sources without a `name` field are excluded when any --source filter is active.
1330        #[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
1331        sources: Vec<String>,
1332    },
1333
1334    /// Push local vault secrets to all destinations defined in `.tsafe.yml`.
1335    ///
1336    /// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
1337    /// and executes each push destination in manifest order (sequential; see ADR-030).
1338    ///
1339    /// Use --dry-run to preview which destinations would be invoked without making
1340    /// any live API calls or writes.
1341    ///
1342    /// Use --source to narrow execution to one or more named destinations. Destinations
1343    /// are named with the `name` field in the manifest. Multiple --source flags are OR'd.
1344    ///
1345    /// A pre-flight diff is shown before any writes. Secret values are never printed —
1346    /// only key names and 12-char SHA-256 hash prefixes are shown (ADR-030).
1347    ///
1348    #[command(
1349        after_help = "Examples:\n  tsafe push\n  tsafe push --config path/to/.tsafe.yml\n  tsafe push --dry-run\n  tsafe push --source prod-akv\n  tsafe push --yes\n  tsafe push --on-error skip-failed"
1350    )]
1351    #[cfg(feature = "akv-pull")]
1352    Push {
1353        /// Path to config file (auto-detected if omitted).
1354        #[arg(long, value_name = "PATH")]
1355        config: Option<std::path::PathBuf>,
1356        /// Narrow execution to destinations with this `name` label (repeatable).
1357        /// Destinations without a `name` field are excluded when any --source filter is active.
1358        #[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
1359        source: Vec<String>,
1360        /// Show the diff without writing anything (always exits 0).
1361        #[arg(long)]
1362        dry_run: bool,
1363        /// Skip confirmation prompts (required in non-TTY / CI contexts).
1364        #[arg(long)]
1365        yes: bool,
1366        /// Also delete remote secrets that are absent locally within each destination's scope.
1367        /// Off by default — opt-in to avoid accidental mass deletion (ADR-030).
1368        #[arg(long)]
1369        delete_missing: bool,
1370        /// Failure handling mode for destination errors.
1371        #[arg(long, value_enum, default_value = "fail-all")]
1372        on_error: PushOnError,
1373    },
1374
1375    /// Synchronise a vault file with a git remote.
1376    ///
1377    /// Fetches the remote branch, performs a per-key three-way merge between
1378    /// the common ancestor, the local vault, and the remote vault, then commits
1379    /// and pushes the merged result.
1380    ///
1381    /// Conflicts (both sides edited the same key) are resolved by last-write-wins
1382    /// using the secret's `updated_at` timestamp. Conflicts are reported but do
1383    /// not block the sync.
1384    ///
1385    #[command(
1386        after_help = "Examples:\n  tsafe sync\n  tsafe sync --remote origin --branch main\n  tsafe sync --dry-run"
1387    )]
1388    #[cfg(feature = "git-helpers")]
1389    #[command(name = "sync")]
1390    Sync {
1391        /// Git remote name.
1392        #[arg(long, default_value = "origin")]
1393        remote: String,
1394        /// Git branch to sync with.
1395        #[arg(long, default_value = "main")]
1396        branch: String,
1397        /// Vault file path relative to repo root (auto-detected if omitted).
1398        #[arg(long)]
1399        file: Option<String>,
1400        /// Show what would change without modifying anything.
1401        #[arg(long)]
1402        dry_run: bool,
1403    },
1404
1405    /// Manage team vaults (multi-recipient age encryption).
1406    ///
1407    /// Team vaults use X25519 (age) keypairs so multiple people can decrypt
1408    /// the same vault without sharing a password.
1409    ///
1410    #[command(
1411        after_help = "Examples:\n  tsafe team init --identity ~/.age/key.txt\n  tsafe team add-member age1qyqszqgpqyqszqgpqyqszqgpqyqszqgp...\n  tsafe team members"
1412    )]
1413    #[cfg(feature = "team-core")]
1414    Team {
1415        #[command(subcommand)]
1416        action: TeamAction,
1417    },
1418
1419    /// Enable or disable biometric / keyring unlock for the current profile.
1420    ///
1421    /// When enabled, the vault password is stored in the OS credential store
1422    /// (macOS Keychain, Windows Credential Manager, Linux Secret Service).
1423    /// The credential store is itself protected by biometric or PIN.
1424    ///
1425    /// After `tsafe init`, the CLI may offer the same setup interactively ("quick unlock").
1426    /// You can always run `biometric enable` later if you skipped it.
1427    ///
1428    #[command(
1429        after_help = "Examples:\n  tsafe biometric enable\n  tsafe biometric disable\n  tsafe biometric status"
1430    )]
1431    #[cfg(feature = "biometric")]
1432    Biometric {
1433        #[command(subcommand)]
1434        action: BiometricAction,
1435    },
1436
1437    /// Manage the per-process vault unlock agent.
1438    ///
1439    /// `tsafe agent unlock` prints terminal approval text, may show an OS notification,
1440    /// then prompts for the vault password once and starts a background agent that holds
1441    /// it in memory.  The token it prints must be set in the calling process's environment
1442    /// as `TSAFE_AGENT_SOCK` — all subsequent `tsafe` invocations that inherit that
1443    /// env var will be granted vault access without re-entering the password.
1444    ///
1445    /// Requests must present the session token and come from a live OS-reported peer
1446    /// PID; the unlock process PID is recorded for audit/context, not as the only
1447    /// process allowed to use the session.
1448    ///
1449    #[command(
1450        after_help = "Examples:\n  tsafe agent unlock              # unlock for 30 minutes (default)\n  tsafe agent unlock --ttl 8h     # unlock for 8 hours\n  tsafe agent unlock --ttl 30m --absolute-ttl 8h\n  tsafe agent status              # check whether the current agent socket is reachable\n  tsafe agent lock                # immediately revoke the session"
1451    )]
1452    #[cfg(feature = "agent")]
1453    Agent {
1454        #[command(subcommand)]
1455        action: AgentAction,
1456    },
1457
1458    /// Run a git command with vault credentials injected automatically.
1459    ///
1460    /// Opens the vault, reads `ADO_PAT` (or the key named by TSAFE_GIT_PAT_KEY),
1461    /// and injects it as a git `http.extraHeader` so HTTPS remotes authenticate
1462    /// without embedding tokens in URLs.
1463    ///
1464    /// Detects the nearest `.git` directory automatically — no repo flags needed.
1465    /// Exits with git's exit code.
1466    ///
1467    /// Override the PAT key name:  $env:TSAFE_GIT_PAT_KEY = "MY_GIT_PAT"
1468    ///
1469    #[command(
1470        after_help = "Examples:\n  tsafe git push ado main\n  tsafe git pull\n  tsafe git fetch --all\n  tsafe -p work git push origin main"
1471    )]
1472    #[cfg(feature = "git-helpers")]
1473    #[command(name = "git")]
1474    Git {
1475        /// git subcommand and its arguments (e.g. `push ado main`).
1476        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1477        args: Vec<String>,
1478    },
1479}
1480
1481/// Collab subcommands — scaffolding for D3.5 (Tranche 2).
1482/// No network calls in this release; real implementation is Tranche 3+.
1483#[derive(Subcommand)]
1484#[cfg(feature = "collab")]
1485pub enum CollabAction {
1486    /// Join a collaboration team (scaffolding — prints status and exits 0).
1487    Join {
1488        /// Team ID to join.
1489        team_id: String,
1490    },
1491    /// Show collaboration status for a team (scaffolding — prints status and exits 0).
1492    Status {
1493        /// Team ID to query.
1494        team_id: String,
1495    },
1496}
1497
1498#[derive(Subcommand)]
1499pub enum SnapshotAction {
1500    /// List snapshots for the current profile.
1501    List,
1502    /// Restore the most-recent snapshot, overwriting the current vault.
1503    Restore,
1504}
1505
1506/// Global config.json settings (not tied to `--profile`).
1507#[derive(Subcommand)]
1508pub enum ConfigAction {
1509    /// Show config file path, default profile, exec trust settings, and password-backup settings.
1510    Show,
1511    /// After each new password vault is created, copy its master password into this vault at `profile-passwords/<new-profile>` (recovery / main-vault bridging). Common values: `main`, `default`. Use `off` to disable.
1512    #[command(name = "set-backup-vault")]
1513    SetBackupVault {
1514        /// Target vault profile (`main`, `default`) or `off` to clear.
1515        target: String,
1516    },
1517    /// Persist whether normal vault opens should automatically try OS quick unlock.
1518    #[command(name = "set-auto-quick-unlock")]
1519    SetAutoQuickUnlock {
1520        /// `on` to allow automatic keychain reads, `off` to require agent / env / typed password instead.
1521        mode: ToggleSetting,
1522    },
1523    /// Persist the retry cooldown, in seconds, after an automatic quick-unlock failure.
1524    #[command(name = "set-quick-unlock-retry-cooldown")]
1525    SetQuickUnlockRetryCooldown {
1526        /// Seconds to wait before the next automatic keychain attempt. Use `0` to disable the cooldown.
1527        seconds: u64,
1528    },
1529    /// Persist the default exec trust mode.
1530    #[command(name = "set-exec-mode")]
1531    SetExecMode {
1532        /// One of: `standard`, `hardened`, `custom`.
1533        mode: ExecModeSetting,
1534    },
1535    /// Persist whether `tsafe exec` should redact child stdout/stderr by default.
1536    #[command(name = "set-exec-redact-output")]
1537    SetExecRedactOutput {
1538        /// `on` to redact child output by default, `off` to leave it raw unless `--redact-output` is passed.
1539        mode: ToggleSetting,
1540    },
1541    /// Persist the inherit strategy used when exec mode is `custom`.
1542    #[command(name = "set-exec-custom-inherit")]
1543    SetExecCustomInherit {
1544        /// One of: `full`, `minimal`, `clean`.
1545        mode: ExecCustomInheritSetting,
1546    },
1547    /// Persist whether dangerous injected env names should abort exec when mode is `custom`.
1548    #[command(name = "set-exec-custom-deny-dangerous-env")]
1549    SetExecCustomDenyDangerousEnv {
1550        /// `on` to abort, `off` to warn only.
1551        mode: ToggleSetting,
1552    },
1553    /// Add a parent environment variable name to the extra strip list for `tsafe exec`.
1554    #[command(name = "add-exec-extra-strip")]
1555    AddExecExtraStrip {
1556        /// Environment variable name, e.g. OPENAI_API_KEY.
1557        name: String,
1558    },
1559    /// Remove a parent environment variable name from the extra strip list for `tsafe exec`.
1560    #[command(name = "remove-exec-extra-strip")]
1561    RemoveExecExtraStrip {
1562        /// Environment variable name, e.g. OPENAI_API_KEY.
1563        name: String,
1564    },
1565}
1566
1567#[derive(Clone, Copy, ValueEnum)]
1568pub enum ToggleSetting {
1569    /// Enable the setting.
1570    On,
1571    /// Disable the setting.
1572    Off,
1573}
1574
1575#[derive(Clone, Copy, ValueEnum)]
1576pub enum ExecModeSetting {
1577    /// Broad compatibility: full inherited env (minus strip list), raw output, and abort on dangerous injected names by default.
1578    Standard,
1579    /// Stricter preset: minimal inherited env, redacted output, and deny dangerous injected names.
1580    Hardened,
1581    /// Use persisted custom exec trust settings from config.json.
1582    Custom,
1583}
1584
1585/// Controls which host environment variables the child process inherits.
1586#[derive(Clone, Copy, ValueEnum)]
1587pub enum ExecPresetSetting {
1588    /// Inherit only PATH and a safe core set (HOME, USER, TMPDIR, LANG, TERM, SSH_AUTH_SOCK, etc.)
1589    /// plus vault secrets. No tokens or credentials from the parent environment leak through.
1590    Minimal,
1591    /// Inherit the full parent environment minus the known-sensitive strip list. This is the
1592    /// current default behavior when no preset or inheritance flag is given.
1593    Full,
1594}
1595
1596#[derive(Clone, Copy, ValueEnum)]
1597pub enum ExecCustomInheritSetting {
1598    /// Full inherited parent env (minus strip list).
1599    Full,
1600    /// Minimal inherited env plus vault secrets.
1601    Minimal,
1602    /// No inherited parent env; only vault secrets.
1603    Clean,
1604}
1605
1606#[derive(Subcommand)]
1607pub enum ProfileAction {
1608    /// List all profiles that have an existing vault.
1609    List,
1610    /// Permanently delete a profile vault.
1611    Delete {
1612        name: String,
1613        /// Skip the confirmation prompt.
1614        #[arg(long)]
1615        force: bool,
1616    },
1617    /// Set the default profile used when -p / TSAFE_PROFILE is not specified.
1618    ///
1619    #[command(after_help = "Examples:\n  tsafe profile set-default work")]
1620    SetDefault {
1621        /// Profile name to use as the new default.
1622        name: String,
1623    },
1624    /// Rename a profile (renames the vault file and updates the default if needed).
1625    ///
1626    #[command(after_help = "Examples:\n  tsafe profile rename old new")]
1627    Rename {
1628        /// Existing profile name.
1629        from: String,
1630        /// New profile name.
1631        to: String,
1632    },
1633}
1634
1635#[derive(Clone, ValueEnum)]
1636pub enum ExportFormat {
1637    /// KEY=VALUE (posix, one per line)
1638    Env,
1639    /// export KEY="VALUE" (bash/zsh source-able)
1640    Dotenv,
1641    /// $env:KEY = "VALUE" (PowerShell source-able)
1642    Powershell,
1643    /// JSON object
1644    Json,
1645    /// ::add-mask::VALUE + KEY=VALUE (GitHub Actions GITHUB_ENV format)
1646    GithubActions,
1647    /// YAML mapping (KEY: "VALUE" per entry)
1648    Yaml,
1649    /// KEY=VALUE per line suitable for Docker --env-file (alias for env, Docker-compatible)
1650    DockerEnv,
1651    /// TOML flat top-level table (KEY = "VALUE" per entry)
1652    Toml,
1653}
1654
1655#[derive(Clone, ValueEnum)]
1656pub enum AuditExportFormat {
1657    /// JSONL (one JSON object per line, same as stored on disk)
1658    Json,
1659    /// Splunk HEC-compatible JSON events
1660    Splunk,
1661    /// CloudEvents 1.0 JSONL (application/cloudevents+json per line)
1662    CloudEvents,
1663}
1664
1665/// Actions for `tsafe audit` subcommand.
1666///
1667/// `Rotate` is a stub reserved for the audit-log rotation handler implemented
1668/// in `cmd_audit_cmd.rs` by a separate agent.  The variant is declared here so
1669/// that `cli.rs` is the single source of truth for the CLI surface.
1670#[derive(Subcommand)]
1671#[allow(dead_code)]
1672pub enum AuditAction {
1673    /// Rotate (trim) the audit log to keep only the most-recent entries.
1674    ///
1675    /// Reserved — handler implemented in `cmd_audit_cmd.rs`.
1676    #[command(
1677        after_help = "Examples:\n  tsafe audit rotate --keep 1000\n  tsafe audit rotate --max-size-mb 10"
1678    )]
1679    Rotate {
1680        /// Maximum audit log size in megabytes before trimming.
1681        #[arg(long, default_value_t = 50)]
1682        max_size_mb: u64,
1683        /// Number of most-recent entries to keep after trimming.
1684        #[arg(long, default_value_t = 5000)]
1685        keep: u32,
1686    },
1687}
1688
1689#[derive(Clone, Copy, ValueEnum)]
1690pub enum CredentialHelperOperation {
1691    /// Install tsafe as the git credential helper in git config.
1692    Install,
1693    Get,
1694    Store,
1695    Erase,
1696}
1697
1698#[cfg(feature = "ssh")]
1699#[derive(Subcommand)]
1700pub enum SshAction {
1701    /// List SSH keys stored in the vault (tagged type=ssh or containing PRIVATE KEY).
1702    #[command(after_help = "Examples:\n  tsafe ssh list")]
1703    List,
1704
1705    /// Extract the public key from a stored SSH private key.
1706    ///
1707    /// Prints the OpenSSH public key in authorized_keys format to stdout.
1708    #[command(
1709        name = "public-key",
1710        after_help = "Examples:\n  tsafe ssh public-key my_ed25519_key\n  tsafe ssh public-key SSH_ID_ED25519"
1711    )]
1712    PublicKey {
1713        /// Vault key name containing the SSH private key.
1714        key: String,
1715    },
1716
1717    /// Generate a new SSH key pair and store the private key in the vault.
1718    ///
1719    /// Uses a CSPRNG (no subprocess). The private key is stored encrypted in
1720    /// the vault; the public key is printed to stdout.
1721    #[command(
1722        after_help = "Examples:\n  tsafe ssh generate my_deploy_key\n  tsafe ssh generate my_rsa_key --type rsa --bits 4096\n  tsafe ssh generate ci_key --comment \"ci@example.com\" --print"
1723    )]
1724    Generate {
1725        /// Vault key name to store the generated private key under.
1726        key: String,
1727        /// Key type: ed25519 (default, recommended) or rsa.
1728        #[arg(long, value_name = "TYPE", default_value = "ed25519")]
1729        r#type: SshKeyType,
1730        /// RSA key size in bits (only used with --type rsa; default 4096).
1731        #[arg(long, value_name = "BITS", default_value = "4096")]
1732        bits: u32,
1733        /// Comment to embed in the key (e.g. an email address).
1734        #[arg(long, value_name = "COMMENT")]
1735        comment: Option<String>,
1736        /// Print the public key to stdout after storing the private key.
1737        #[arg(long)]
1738        print: bool,
1739    },
1740
1741    /// Print an ~/.ssh/config snippet that points IdentityAgent at tsafe.
1742    ///
1743    /// Pipe or append the output to ~/.ssh/config.
1744    #[command(
1745        name = "config",
1746        after_help = "Examples:\n  tsafe ssh config\n  tsafe ssh config --host '*.corp.example'\n  tsafe ssh config >> ~/.ssh/config"
1747    )]
1748    Config {
1749        /// SSH Host pattern (defaults to `*`).
1750        #[arg(long, value_name = "PATTERN")]
1751        host: Option<String>,
1752    },
1753
1754    /// Start a persistent SSH agent serving vault keys on a Unix socket.
1755    ///
1756    /// Keys are loaded once at startup and served for the configured TTL.
1757    /// On Windows this subcommand prints a clear error — Unix socket required.
1758    ///
1759    /// Eval idiom:  eval $(tsafe ssh-agent)
1760    #[command(
1761        name = "agent",
1762        after_help = "Examples:\n  eval $(tsafe ssh agent)\n  tsafe ssh agent --ttl 4h\n  tsafe ssh agent --sock /run/user/1000/tsafe.sock"
1763    )]
1764    Agent {
1765        /// How long loaded keys remain valid (e.g. 8h, 30m, 1h30m). Default 8h.
1766        #[arg(long, value_name = "DURATION")]
1767        ttl: Option<String>,
1768        /// Override the Unix socket path.
1769        #[arg(long, value_name = "PATH")]
1770        sock: Option<String>,
1771    },
1772}
1773
1774#[cfg(feature = "ssh")]
1775#[derive(Clone, Copy, ValueEnum)]
1776pub enum SshKeyType {
1777    Ed25519,
1778    Rsa,
1779}
1780
1781#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1782pub enum PullOnError {
1783    /// Abort immediately on first source/provider error.
1784    FailAll,
1785    /// Skip failed source and continue remaining sources.
1786    SkipFailed,
1787    /// Continue and only warn on source/provider errors.
1788    WarnOnly,
1789}
1790
1791#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1792pub enum PushOnError {
1793    /// Abort immediately on first source error.
1794    FailAll,
1795    /// Log the error, skip the failed source, and continue with the next.
1796    SkipFailed,
1797}
1798
1799#[derive(Subcommand)]
1800pub enum BrowserProfileAction {
1801    /// Add or update a domain → vault-profile mapping.
1802    ///
1803    /// DOMAIN may be an exact hostname or a wildcard pattern (e.g. *.corp.example).
1804    /// Defaults to the active --profile if --profile is omitted.
1805    ///
1806    #[command(
1807        after_help = "Examples:\n  tsafe browser-profile add github.com\n  tsafe browser-profile add corp.example --profile work"
1808    )]
1809    Add {
1810        /// Domain or pattern (e.g. github.com, *.corp.example).
1811        domain: String,
1812        /// Vault profile to use for this domain. Defaults to the active --profile.
1813        #[arg(long)]
1814        profile: Option<String>,
1815    },
1816    /// List all domain → vault-profile mappings.
1817    ///
1818    #[command(after_help = "Examples:\n  tsafe browser-profile list")]
1819    List,
1820    /// Remove the mapping for DOMAIN.
1821    ///
1822    #[command(after_help = "Examples:\n  tsafe browser-profile remove github.com")]
1823    Remove {
1824        /// Domain or pattern to remove.
1825        domain: String,
1826    },
1827}
1828
1829#[derive(Subcommand)]
1830pub enum BrowserNativeHostAction {
1831    /// Write the per-browser native-messaging-host manifest pointing at
1832    /// `tsafe-nativehost`. Per-user; never elevates. On Windows it writes
1833    /// Chromium-family HKCU registry keys for a 32-char extension ID, or a
1834    /// Firefox filesystem manifest under `%APPDATA%\Mozilla\NativeMessagingHosts\`
1835    /// for an email-style or UUID-style Firefox addon ID. On macOS/Linux it
1836    /// skips browsers that are not installed.
1837    ///
1838    /// `--extension-id` is REQUIRED — defaulting to a known development ID
1839    /// would let any installed extension with that ID talk to your vault.
1840    /// Chromium ID: 32-char string at `chrome://extensions` (Developer mode).
1841    /// Firefox ID: the `gecko.id` value from `browser_specific_settings` in
1842    /// the extension manifest (e.g. `tsafe@tsafe.dev`).
1843    Register {
1844        /// The extension ID to allow. Chromium-family: 32-character lowercase
1845        /// ID from `chrome://extensions`. Firefox: email-style addon ID
1846        /// (e.g. `tsafe@tsafe.dev`) or UUID in curly braces.
1847        #[arg(long)]
1848        extension_id: String,
1849    },
1850    /// Remove the per-browser manifest files (and HKCU keys on Windows).
1851    Unregister,
1852    /// Detect the native-host binary location and print the manifest paths that
1853    /// `register` would write for each installed browser — without writing
1854    /// anything. Use this when you do not yet know your extension ID:
1855    ///
1856    ///   1. Run `tsafe browser-native-host detect` to confirm the binary is
1857    ///      found and see which browsers are detected.
1858    ///   2. Load the extension in your browser, find your extension ID at
1859    ///      `chrome://extensions` (Developer mode), then run:
1860    ///      tsafe browser-native-host register --extension-id <id>
1861    ///
1862    /// On Windows, prints the HKCU registry keys and manifest directory that
1863    /// would be written; never modifies registry or filesystem.
1864    Detect,
1865}
1866
1867#[derive(Clone, Copy, ValueEnum)]
1868pub enum TotpAlgorithm {
1869    /// HMAC-SHA1 (default; most compatible)
1870    Sha1,
1871    /// HMAC-SHA256
1872    Sha256,
1873    /// HMAC-SHA512
1874    Sha512,
1875}
1876
1877impl TotpAlgorithm {
1878    pub fn as_uri_str(self) -> &'static str {
1879        match self {
1880            Self::Sha1 => "SHA1",
1881            Self::Sha256 => "SHA256",
1882            Self::Sha512 => "SHA512",
1883        }
1884    }
1885}
1886
1887#[derive(Subcommand)]
1888pub enum TotpAction {
1889    /// Store a TOTP seed for KEY. Accepts a raw base32 secret or an otpauth:// URI.
1890    ///
1891    #[command(
1892        after_help = "Examples:\n  tsafe totp add GITHUB_2FA JBSWY3DPEHPK3PXP\n  tsafe totp add CORP_2FA JBSWY3DPEHPK3PXP --digits 8 --period 60\n  tsafe totp add CORP_2FA JBSWY3DPEHPK3PXP --algorithm sha256"
1893    )]
1894    Add {
1895        /// Vault key name to store under.
1896        key: String,
1897        /// Base32-encoded TOTP secret or full otpauth:// URI.
1898        secret: String,
1899        /// HMAC algorithm to use (default: sha1, most widely supported).
1900        #[arg(long, default_value = "sha1")]
1901        algorithm: TotpAlgorithm,
1902        /// Number of digits in each OTP code (default: 6; some services use 8).
1903        #[arg(long, default_value_t = 6)]
1904        digits: u32,
1905        /// Time step in seconds (default: 30; some services use 60).
1906        #[arg(long, default_value_t = 30)]
1907        period: u64,
1908    },
1909    /// Print the current TOTP code + seconds remaining.
1910    ///
1911    #[command(after_help = "Examples:\n  tsafe totp get GITHUB_2FA")]
1912    Get {
1913        /// Vault key name where the TOTP seed is stored.
1914        key: String,
1915    },
1916}
1917
1918#[derive(Subcommand)]
1919pub enum NsAction {
1920    /// List all namespaces present in the vault (inferred from key prefixes).
1921    List,
1922    /// Copy every secret under FROM/ to TO/ (same suffix). Source keys stay.
1923    ///
1924    #[command(after_help = "Examples:\n  tsafe ns copy prod staging")]
1925    Copy {
1926        /// Namespace prefix to read from (keys must be `FROM/...`).
1927        from: String,
1928        /// Namespace prefix to write to (`TO/<same-suffix>`).
1929        to: String,
1930        /// Overwrite destination keys if they already exist.
1931        #[arg(long)]
1932        force: bool,
1933    },
1934    /// Move every secret under FROM/ to TO/ (same suffix). Source keys are removed.
1935    ///
1936    #[command(after_help = "Examples:\n  tsafe ns move prod staging")]
1937    Move {
1938        /// Namespace prefix to read from and delete after rename.
1939        from: String,
1940        /// Namespace prefix to write to (`TO/<same-suffix>`).
1941        to: String,
1942        /// Overwrite destination keys if they already exist.
1943        #[arg(long)]
1944        force: bool,
1945    },
1946}
1947
1948#[derive(Subcommand)]
1949pub enum AgentAction {
1950    /// Prompt for approval + vault password, then start the background agent daemon.
1951    ///
1952    /// Prints a shell export line to stdout:
1953    ///   $env:TSAFE_AGENT_SOCK = "..."   # PowerShell
1954    ///   export TSAFE_AGENT_SOCK="..."   # bash/zsh
1955    ///
1956    /// Copy-paste or eval this line in the calling shell/process that needs access.
1957    Unlock {
1958        /// Idle TTL — how long the agent stays alive without a vault request.
1959        /// Resets on each vault access. Common values include 15m, 1h, and 4h. Default: 30m.
1960        #[arg(long, default_value = "30m")]
1961        ttl: String,
1962        /// Absolute TTL — hard cap regardless of activity. Default: 8h.
1963        /// Must be >= idle TTL. Common values include 8h, 12h, and 24h.
1964        #[arg(long, default_value = "8h")]
1965        absolute_ttl: String,
1966    },
1967    /// Immediately revoke the current session and stop the agent.
1968    Lock,
1969    /// Show whether the current agent socket is reachable.
1970    ///
1971    /// Use `--json` for a stable machine-readable output (ADR-029). Consumers such
1972    /// as the VS Code extension and tray agent depend on this flag. The schema
1973    /// `version` field must be checked before reading any other field.
1974    #[command(
1975        after_help = "Examples:\n  tsafe agent status\n  tsafe agent status --json\n  tsafe --profile prod agent status --json"
1976    )]
1977    Status {
1978        /// Emit a stable JSON object to stdout (schema version \"1\").
1979        /// See docs/decisions/agent-status-json-contract.md (ADR-029) for the full schema.
1980        #[arg(long)]
1981        json: bool,
1982    },
1983}
1984
1985#[derive(Subcommand)]
1986pub enum TeamAction {
1987    /// Create a new team vault encrypted to your age identity.
1988    ///
1989    #[command(after_help = "Examples:\n  tsafe team init --identity ~/.age/key.txt")]
1990    Init {
1991        /// Path to your age identity file (contains AGE-SECRET-KEY-1...).
1992        #[arg(long)]
1993        identity: String,
1994    },
1995    /// Add a team member by their age public key.
1996    ///
1997    #[command(after_help = "Examples:\n  tsafe team add-member age1qyqszqgp...")]
1998    AddMember {
1999        /// age X25519 public key (starts with "age1...").
2000        public_key: String,
2001        /// Path to your age identity file (for re-wrapping the DEK).
2002        #[arg(long)]
2003        identity: String,
2004    },
2005    /// Remove a team member and re-encrypt all secrets with a new key.
2006    ///
2007    #[command(after_help = "Examples:\n  tsafe team remove-member age1qyqszqgp...")]
2008    RemoveMember {
2009        /// age X25519 public key to remove.
2010        public_key: String,
2011        /// Path to your age identity file (for re-keying).
2012        #[arg(long)]
2013        identity: String,
2014    },
2015    /// List current team members (public keys).
2016    Members,
2017    /// Generate a new age identity (keypair) and print the JSON block to add
2018    /// to `.tsafe/team-keys.json` via a PR.
2019    ///
2020    /// The private key is saved to `~/.age/tsafe-<profile>.txt`.
2021    /// The public key is printed as a ready-to-paste JSON entry.
2022    ///
2023    #[command(
2024        after_help = "Examples:\n  tsafe team keygen\n  tsafe team keygen --name \"Alice Smith\" --email alice@corp.example"
2025    )]
2026    Keygen {
2027        /// Your display name for the team-keys entry.
2028        #[arg(long)]
2029        name: Option<String>,
2030        /// Your email for the team-keys entry.
2031        #[arg(long)]
2032        email: Option<String>,
2033    },
2034    /// Print your age public key from an existing identity file.
2035    ///
2036    #[command(
2037        after_help = "Examples:\n  tsafe team show-key\n  tsafe team show-key --identity ~/.age/key.txt"
2038    )]
2039    ShowKey {
2040        /// Path to identity file (default: ~/.age/tsafe-<profile>.txt).
2041        #[arg(long)]
2042        identity: Option<String>,
2043    },
2044    /// Reconcile vault recipients with `.tsafe/team-keys.json`.
2045    ///
2046    /// Adds any new members found in the keys file. Removes members no longer
2047    /// listed. Re-keys the vault if the member list changed.
2048    ///
2049    #[command(after_help = "Examples:\n  tsafe team sync-keys --identity ~/.age/key.txt")]
2050    SyncKeys {
2051        /// Path to your age identity file (required for re-wrapping the DEK).
2052        #[arg(long)]
2053        identity: String,
2054        /// Path to team-keys.json (auto-detected if omitted).
2055        #[arg(long)]
2056        keys_file: Option<String>,
2057    },
2058}
2059
2060#[derive(Subcommand)]
2061pub enum BiometricAction {
2062    /// Store the vault password in the OS credential store (same as accepting quick unlock after `tsafe init`).
2063    Enable,
2064    /// Remove the vault password from the OS credential store.
2065    Disable,
2066    /// Check if biometric/keyring unlock is configured for this profile.
2067    Status,
2068    /// Re-enroll biometric/keyring unlock after a stale-credential error.
2069    ///
2070    /// Use this when `tsafe` reports "stale biometric credential" — for example after
2071    /// rotating the vault password (`tsafe rotate`) or after enrolling a new fingerprint.
2072    /// This is equivalent to `tsafe biometric disable` followed by `tsafe biometric enable`
2073    /// but makes the recovery intent explicit and prints a confirmation.
2074    #[command(name = "re-enroll")]
2075    ReEnroll,
2076}
2077
2078#[cfg(test)]
2079mod tests {
2080    use super::*;
2081    use clap::CommandFactory;
2082
2083    fn has_subcommand(command: &clap::Command, name: &str) -> bool {
2084        command
2085            .get_subcommands()
2086            .any(|subcommand| subcommand.get_name() == name)
2087    }
2088
2089    #[test]
2090    fn root_command_visibility_matches_feature_gates() {
2091        let command = Cli::command();
2092
2093        assert_eq!(has_subcommand(&command, "ui"), cfg!(feature = "tui"));
2094        assert_eq!(
2095            has_subcommand(&command, "kv-pull"),
2096            cfg!(feature = "akv-pull")
2097        );
2098        assert_eq!(
2099            has_subcommand(&command, "aws-pull"),
2100            cfg!(feature = "cloud-pull-aws")
2101        );
2102        assert_eq!(
2103            has_subcommand(&command, "gcp-pull"),
2104            cfg!(feature = "cloud-pull-gcp")
2105        );
2106        assert_eq!(
2107            has_subcommand(&command, "gcp-push"),
2108            cfg!(feature = "cloud-pull-gcp")
2109        );
2110        assert_eq!(
2111            has_subcommand(&command, "ssm-pull"),
2112            cfg!(feature = "cloud-pull-aws")
2113        );
2114        assert_eq!(
2115            has_subcommand(&command, "aws-push"),
2116            cfg!(feature = "cloud-pull-aws")
2117        );
2118        assert_eq!(
2119            has_subcommand(&command, "ssm-push"),
2120            cfg!(feature = "cloud-pull-aws")
2121        );
2122        assert_eq!(
2123            has_subcommand(&command, "vault-pull"),
2124            cfg!(feature = "cloud-pull-vault")
2125        );
2126        assert_eq!(
2127            has_subcommand(&command, "op-pull"),
2128            cfg!(feature = "cloud-pull-1password")
2129        );
2130        assert_eq!(
2131            has_subcommand(&command, "pull"),
2132            cfg!(feature = "multi-pull")
2133        );
2134        assert_eq!(
2135            has_subcommand(&command, "share-once"),
2136            cfg!(feature = "ots-sharing")
2137        );
2138        assert_eq!(
2139            has_subcommand(&command, "receive-once"),
2140            cfg!(feature = "ots-sharing")
2141        );
2142        assert_eq!(
2143            has_subcommand(&command, "browser-profile"),
2144            cfg!(feature = "browser")
2145        );
2146        assert_eq!(
2147            has_subcommand(&command, "browser-native-host"),
2148            cfg!(feature = "nativehost")
2149        );
2150        assert_eq!(has_subcommand(&command, "ssh-add"), cfg!(feature = "ssh"));
2151        assert_eq!(
2152            has_subcommand(&command, "ssh-import"),
2153            cfg!(feature = "ssh")
2154        );
2155        assert_eq!(
2156            has_subcommand(&command, "plugin"),
2157            cfg!(feature = "plugins")
2158        );
2159        assert_eq!(
2160            has_subcommand(&command, "hook-install"),
2161            cfg!(feature = "git-helpers")
2162        );
2163        assert_eq!(
2164            has_subcommand(&command, "git"),
2165            cfg!(feature = "git-helpers")
2166        );
2167        assert_eq!(
2168            has_subcommand(&command, "sync"),
2169            cfg!(feature = "git-helpers")
2170        );
2171        assert_eq!(
2172            has_subcommand(&command, "credential-helper"),
2173            cfg!(feature = "git-helpers")
2174        );
2175        assert_eq!(
2176            has_subcommand(&command, "biometric"),
2177            cfg!(feature = "biometric")
2178        );
2179        assert_eq!(
2180            has_subcommand(&command, "team"),
2181            cfg!(feature = "team-core")
2182        );
2183        assert_eq!(has_subcommand(&command, "agent"), cfg!(feature = "agent"));
2184    }
2185}
2186
2187#[derive(Subcommand)]
2188pub enum PolicyAction {
2189    /// Set a rotation policy on a secret.
2190    ///
2191    #[command(after_help = "Examples:\n  tsafe policy set DB_PASSWORD --rotate-every 90d")]
2192    Set {
2193        /// Secret key.
2194        key: String,
2195        /// Rotation interval (e.g. 90d, 30d, 7d).
2196        #[arg(long)]
2197        rotate_every: String,
2198    },
2199    /// Remove the rotation policy from a secret.
2200    ///
2201    #[command(after_help = "Examples:\n  tsafe policy remove DB_PASSWORD")]
2202    Remove {
2203        /// Secret key.
2204        key: String,
2205    },
2206}