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    /// First-party MCP server — exposes tsafe to MCP-aware hosts (Claude
1459    /// Desktop, Cursor, Continue, Windsurf, Codex) over stdio JSON-RPC.
1460    ///
1461    /// Each `tsafe-mcp` process binds to exactly one profile; request-time
1462    /// profile or scope widening is rejected. See ADR-006
1463    /// (docs/architecture/ADR-006-mcp-server.md) and design doc §5.2 for the
1464    /// full surface.
1465    ///
1466    /// `tsafe mcp install <host>` writes the per-host MCP config file. The
1467    /// resulting host launch shells out to `tsafe-mcp serve` directly, not
1468    /// through this CLI.
1469    #[command(
1470        after_help = "Examples:\n  tsafe mcp install claude --allowed-keys \"demo/*\"\n  tsafe mcp install cursor --project . --allowed-keys \"demo/*\"\n  tsafe mcp serve --allowed-keys \"demo/*\" --allow-reveal\n  tsafe mcp uninstall claude\n  tsafe mcp status"
1471    )]
1472    #[cfg(feature = "mcp")]
1473    Mcp {
1474        #[command(subcommand)]
1475        action: McpAction,
1476    },
1477
1478    /// Run a git command with vault credentials injected automatically.
1479    ///
1480    /// Opens the vault, reads `ADO_PAT` (or the key named by TSAFE_GIT_PAT_KEY),
1481    /// and injects it as a git `http.extraHeader` so HTTPS remotes authenticate
1482    /// without embedding tokens in URLs.
1483    ///
1484    /// Detects the nearest `.git` directory automatically — no repo flags needed.
1485    /// Exits with git's exit code.
1486    ///
1487    /// Override the PAT key name:  $env:TSAFE_GIT_PAT_KEY = "MY_GIT_PAT"
1488    ///
1489    #[command(
1490        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"
1491    )]
1492    #[cfg(feature = "git-helpers")]
1493    #[command(name = "git")]
1494    Git {
1495        /// git subcommand and its arguments (e.g. `push ado main`).
1496        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
1497        args: Vec<String>,
1498    },
1499}
1500
1501/// Collab subcommands — scaffolding for D3.5 (Tranche 2).
1502/// No network calls in this release; real implementation is Tranche 3+.
1503#[derive(Subcommand)]
1504#[cfg(feature = "collab")]
1505pub enum CollabAction {
1506    /// Join a collaboration team (scaffolding — prints status and exits 0).
1507    Join {
1508        /// Team ID to join.
1509        team_id: String,
1510    },
1511    /// Show collaboration status for a team (scaffolding — prints status and exits 0).
1512    Status {
1513        /// Team ID to query.
1514        team_id: String,
1515    },
1516}
1517
1518#[derive(Subcommand)]
1519pub enum SnapshotAction {
1520    /// List snapshots for the current profile.
1521    List,
1522    /// Restore the most-recent snapshot, overwriting the current vault.
1523    Restore,
1524}
1525
1526/// Global config.json settings (not tied to `--profile`).
1527#[derive(Subcommand)]
1528pub enum ConfigAction {
1529    /// Show config file path, default profile, exec trust settings, and password-backup settings.
1530    Show,
1531    /// 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.
1532    #[command(name = "set-backup-vault")]
1533    SetBackupVault {
1534        /// Target vault profile (`main`, `default`) or `off` to clear.
1535        target: String,
1536    },
1537    /// Persist whether normal vault opens should automatically try OS quick unlock.
1538    #[command(name = "set-auto-quick-unlock")]
1539    SetAutoQuickUnlock {
1540        /// `on` to allow automatic keychain reads, `off` to require agent / env / typed password instead.
1541        mode: ToggleSetting,
1542    },
1543    /// Persist the retry cooldown, in seconds, after an automatic quick-unlock failure.
1544    #[command(name = "set-quick-unlock-retry-cooldown")]
1545    SetQuickUnlockRetryCooldown {
1546        /// Seconds to wait before the next automatic keychain attempt. Use `0` to disable the cooldown.
1547        seconds: u64,
1548    },
1549    /// Persist the default exec trust mode.
1550    #[command(name = "set-exec-mode")]
1551    SetExecMode {
1552        /// One of: `standard`, `hardened`, `custom`.
1553        mode: ExecModeSetting,
1554    },
1555    /// Persist whether `tsafe exec` should redact child stdout/stderr by default.
1556    #[command(name = "set-exec-redact-output")]
1557    SetExecRedactOutput {
1558        /// `on` to redact child output by default, `off` to leave it raw unless `--redact-output` is passed.
1559        mode: ToggleSetting,
1560    },
1561    /// Persist the inherit strategy used when exec mode is `custom`.
1562    #[command(name = "set-exec-custom-inherit")]
1563    SetExecCustomInherit {
1564        /// One of: `full`, `minimal`, `clean`.
1565        mode: ExecCustomInheritSetting,
1566    },
1567    /// Persist whether dangerous injected env names should abort exec when mode is `custom`.
1568    #[command(name = "set-exec-custom-deny-dangerous-env")]
1569    SetExecCustomDenyDangerousEnv {
1570        /// `on` to abort, `off` to warn only.
1571        mode: ToggleSetting,
1572    },
1573    /// Add a parent environment variable name to the extra strip list for `tsafe exec`.
1574    #[command(name = "add-exec-extra-strip")]
1575    AddExecExtraStrip {
1576        /// Environment variable name, e.g. OPENAI_API_KEY.
1577        name: String,
1578    },
1579    /// Remove a parent environment variable name from the extra strip list for `tsafe exec`.
1580    #[command(name = "remove-exec-extra-strip")]
1581    RemoveExecExtraStrip {
1582        /// Environment variable name, e.g. OPENAI_API_KEY.
1583        name: String,
1584    },
1585}
1586
1587#[derive(Clone, Copy, ValueEnum)]
1588pub enum ToggleSetting {
1589    /// Enable the setting.
1590    On,
1591    /// Disable the setting.
1592    Off,
1593}
1594
1595#[derive(Clone, Copy, ValueEnum)]
1596pub enum ExecModeSetting {
1597    /// Broad compatibility: full inherited env (minus strip list), raw output, and abort on dangerous injected names by default.
1598    Standard,
1599    /// Stricter preset: minimal inherited env, redacted output, and deny dangerous injected names.
1600    Hardened,
1601    /// Use persisted custom exec trust settings from config.json.
1602    Custom,
1603}
1604
1605/// Controls which host environment variables the child process inherits.
1606#[derive(Clone, Copy, ValueEnum)]
1607pub enum ExecPresetSetting {
1608    /// Inherit only PATH and a safe core set (HOME, USER, TMPDIR, LANG, TERM, SSH_AUTH_SOCK, etc.)
1609    /// plus vault secrets. No tokens or credentials from the parent environment leak through.
1610    Minimal,
1611    /// Inherit the full parent environment minus the known-sensitive strip list. This is the
1612    /// current default behavior when no preset or inheritance flag is given.
1613    Full,
1614}
1615
1616#[derive(Clone, Copy, ValueEnum)]
1617pub enum ExecCustomInheritSetting {
1618    /// Full inherited parent env (minus strip list).
1619    Full,
1620    /// Minimal inherited env plus vault secrets.
1621    Minimal,
1622    /// No inherited parent env; only vault secrets.
1623    Clean,
1624}
1625
1626#[derive(Subcommand)]
1627pub enum ProfileAction {
1628    /// List all profiles that have an existing vault.
1629    List,
1630    /// Permanently delete a profile vault.
1631    Delete {
1632        name: String,
1633        /// Skip the confirmation prompt.
1634        #[arg(long)]
1635        force: bool,
1636    },
1637    /// Set the default profile used when -p / TSAFE_PROFILE is not specified.
1638    ///
1639    #[command(after_help = "Examples:\n  tsafe profile set-default work")]
1640    SetDefault {
1641        /// Profile name to use as the new default.
1642        name: String,
1643    },
1644    /// Rename a profile (renames the vault file and updates the default if needed).
1645    ///
1646    #[command(after_help = "Examples:\n  tsafe profile rename old new")]
1647    Rename {
1648        /// Existing profile name.
1649        from: String,
1650        /// New profile name.
1651        to: String,
1652    },
1653}
1654
1655#[derive(Clone, ValueEnum)]
1656pub enum ExportFormat {
1657    /// KEY=VALUE (posix, one per line)
1658    Env,
1659    /// export KEY="VALUE" (bash/zsh source-able)
1660    Dotenv,
1661    /// $env:KEY = "VALUE" (PowerShell source-able)
1662    Powershell,
1663    /// JSON object
1664    Json,
1665    /// ::add-mask::VALUE + KEY=VALUE (GitHub Actions GITHUB_ENV format)
1666    GithubActions,
1667    /// YAML mapping (KEY: "VALUE" per entry)
1668    Yaml,
1669    /// KEY=VALUE per line suitable for Docker --env-file (alias for env, Docker-compatible)
1670    DockerEnv,
1671    /// TOML flat top-level table (KEY = "VALUE" per entry)
1672    Toml,
1673}
1674
1675#[derive(Clone, ValueEnum)]
1676pub enum AuditExportFormat {
1677    /// JSONL (one JSON object per line, same as stored on disk)
1678    Json,
1679    /// Splunk HEC-compatible JSON events
1680    Splunk,
1681    /// CloudEvents 1.0 JSONL (application/cloudevents+json per line)
1682    CloudEvents,
1683}
1684
1685/// Actions for `tsafe audit` subcommand.
1686///
1687/// `Rotate` is a stub reserved for the audit-log rotation handler implemented
1688/// in `cmd_audit_cmd.rs` by a separate agent.  The variant is declared here so
1689/// that `cli.rs` is the single source of truth for the CLI surface.
1690#[derive(Subcommand)]
1691#[allow(dead_code)]
1692pub enum AuditAction {
1693    /// Rotate (trim) the audit log to keep only the most-recent entries.
1694    ///
1695    /// Reserved — handler implemented in `cmd_audit_cmd.rs`.
1696    #[command(
1697        after_help = "Examples:\n  tsafe audit rotate --keep 1000\n  tsafe audit rotate --max-size-mb 10"
1698    )]
1699    Rotate {
1700        /// Maximum audit log size in megabytes before trimming.
1701        #[arg(long, default_value_t = 50)]
1702        max_size_mb: u64,
1703        /// Number of most-recent entries to keep after trimming.
1704        #[arg(long, default_value_t = 5000)]
1705        keep: u32,
1706    },
1707}
1708
1709#[derive(Clone, Copy, ValueEnum)]
1710pub enum CredentialHelperOperation {
1711    /// Install tsafe as the git credential helper in git config.
1712    Install,
1713    Get,
1714    Store,
1715    Erase,
1716}
1717
1718#[cfg(feature = "ssh")]
1719#[derive(Subcommand)]
1720pub enum SshAction {
1721    /// List SSH keys stored in the vault (tagged type=ssh or containing PRIVATE KEY).
1722    #[command(after_help = "Examples:\n  tsafe ssh list")]
1723    List,
1724
1725    /// Extract the public key from a stored SSH private key.
1726    ///
1727    /// Prints the OpenSSH public key in authorized_keys format to stdout.
1728    #[command(
1729        name = "public-key",
1730        after_help = "Examples:\n  tsafe ssh public-key my_ed25519_key\n  tsafe ssh public-key SSH_ID_ED25519"
1731    )]
1732    PublicKey {
1733        /// Vault key name containing the SSH private key.
1734        key: String,
1735    },
1736
1737    /// Generate a new SSH key pair and store the private key in the vault.
1738    ///
1739    /// Uses a CSPRNG (no subprocess). The private key is stored encrypted in
1740    /// the vault; the public key is printed to stdout.
1741    #[command(
1742        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"
1743    )]
1744    Generate {
1745        /// Vault key name to store the generated private key under.
1746        key: String,
1747        /// Key type: ed25519 (default, recommended) or rsa.
1748        #[arg(long, value_name = "TYPE", default_value = "ed25519")]
1749        r#type: SshKeyType,
1750        /// RSA key size in bits (only used with --type rsa; default 4096).
1751        #[arg(long, value_name = "BITS", default_value = "4096")]
1752        bits: u32,
1753        /// Comment to embed in the key (e.g. an email address).
1754        #[arg(long, value_name = "COMMENT")]
1755        comment: Option<String>,
1756        /// Print the public key to stdout after storing the private key.
1757        #[arg(long)]
1758        print: bool,
1759    },
1760
1761    /// Print an ~/.ssh/config snippet that points IdentityAgent at tsafe.
1762    ///
1763    /// Pipe or append the output to ~/.ssh/config.
1764    #[command(
1765        name = "config",
1766        after_help = "Examples:\n  tsafe ssh config\n  tsafe ssh config --host '*.corp.example'\n  tsafe ssh config >> ~/.ssh/config"
1767    )]
1768    Config {
1769        /// SSH Host pattern (defaults to `*`).
1770        #[arg(long, value_name = "PATTERN")]
1771        host: Option<String>,
1772    },
1773
1774    /// Start a persistent SSH agent serving vault keys on a Unix socket.
1775    ///
1776    /// Keys are loaded once at startup and served for the configured TTL.
1777    /// On Windows this subcommand prints a clear error — Unix socket required.
1778    ///
1779    /// Eval idiom:  eval $(tsafe ssh-agent)
1780    #[command(
1781        name = "agent",
1782        after_help = "Examples:\n  eval $(tsafe ssh agent)\n  tsafe ssh agent --ttl 4h\n  tsafe ssh agent --sock /run/user/1000/tsafe.sock"
1783    )]
1784    Agent {
1785        /// How long loaded keys remain valid (e.g. 8h, 30m, 1h30m). Default 8h.
1786        #[arg(long, value_name = "DURATION")]
1787        ttl: Option<String>,
1788        /// Override the Unix socket path.
1789        #[arg(long, value_name = "PATH")]
1790        sock: Option<String>,
1791    },
1792}
1793
1794#[cfg(feature = "ssh")]
1795#[derive(Clone, Copy, ValueEnum)]
1796pub enum SshKeyType {
1797    Ed25519,
1798    Rsa,
1799}
1800
1801#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1802pub enum PullOnError {
1803    /// Abort immediately on first source/provider error.
1804    FailAll,
1805    /// Skip failed source and continue remaining sources.
1806    SkipFailed,
1807    /// Continue and only warn on source/provider errors.
1808    WarnOnly,
1809}
1810
1811#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1812pub enum PushOnError {
1813    /// Abort immediately on first source error.
1814    FailAll,
1815    /// Log the error, skip the failed source, and continue with the next.
1816    SkipFailed,
1817}
1818
1819#[derive(Subcommand)]
1820pub enum BrowserProfileAction {
1821    /// Add or update a domain → vault-profile mapping.
1822    ///
1823    /// DOMAIN may be an exact hostname or a wildcard pattern (e.g. *.corp.example).
1824    /// Defaults to the active --profile if --profile is omitted.
1825    ///
1826    #[command(
1827        after_help = "Examples:\n  tsafe browser-profile add github.com\n  tsafe browser-profile add corp.example --profile work"
1828    )]
1829    Add {
1830        /// Domain or pattern (e.g. github.com, *.corp.example).
1831        domain: String,
1832        /// Vault profile to use for this domain. Defaults to the active --profile.
1833        #[arg(long)]
1834        profile: Option<String>,
1835    },
1836    /// List all domain → vault-profile mappings.
1837    ///
1838    #[command(after_help = "Examples:\n  tsafe browser-profile list")]
1839    List,
1840    /// Remove the mapping for DOMAIN.
1841    ///
1842    #[command(after_help = "Examples:\n  tsafe browser-profile remove github.com")]
1843    Remove {
1844        /// Domain or pattern to remove.
1845        domain: String,
1846    },
1847}
1848
1849#[derive(Subcommand)]
1850pub enum BrowserNativeHostAction {
1851    /// Write the per-browser native-messaging-host manifest pointing at
1852    /// `tsafe-nativehost`. Per-user; never elevates. On Windows it writes
1853    /// Chromium-family HKCU registry keys for a 32-char extension ID, or a
1854    /// Firefox filesystem manifest under `%APPDATA%\Mozilla\NativeMessagingHosts\`
1855    /// for an email-style or UUID-style Firefox addon ID. On macOS/Linux it
1856    /// skips browsers that are not installed.
1857    ///
1858    /// `--extension-id` is REQUIRED — defaulting to a known development ID
1859    /// would let any installed extension with that ID talk to your vault.
1860    /// Chromium ID: 32-char string at `chrome://extensions` (Developer mode).
1861    /// Firefox ID: the `gecko.id` value from `browser_specific_settings` in
1862    /// the extension manifest (e.g. `tsafe@tsafe.dev`).
1863    Register {
1864        /// The extension ID to allow. Chromium-family: 32-character lowercase
1865        /// ID from `chrome://extensions`. Firefox: email-style addon ID
1866        /// (e.g. `tsafe@tsafe.dev`) or UUID in curly braces.
1867        #[arg(long)]
1868        extension_id: String,
1869    },
1870    /// Remove the per-browser manifest files (and HKCU keys on Windows).
1871    Unregister,
1872    /// Detect the native-host binary location and print the manifest paths that
1873    /// `register` would write for each installed browser — without writing
1874    /// anything. Use this when you do not yet know your extension ID:
1875    ///
1876    ///   1. Run `tsafe browser-native-host detect` to confirm the binary is
1877    ///      found and see which browsers are detected.
1878    ///   2. Load the extension in your browser, find your extension ID at
1879    ///      `chrome://extensions` (Developer mode), then run:
1880    ///      tsafe browser-native-host register --extension-id <id>
1881    ///
1882    /// On Windows, prints the HKCU registry keys and manifest directory that
1883    /// would be written; never modifies registry or filesystem.
1884    Detect,
1885}
1886
1887#[derive(Clone, Copy, ValueEnum)]
1888pub enum TotpAlgorithm {
1889    /// HMAC-SHA1 (default; most compatible)
1890    Sha1,
1891    /// HMAC-SHA256
1892    Sha256,
1893    /// HMAC-SHA512
1894    Sha512,
1895}
1896
1897impl TotpAlgorithm {
1898    pub fn as_uri_str(self) -> &'static str {
1899        match self {
1900            Self::Sha1 => "SHA1",
1901            Self::Sha256 => "SHA256",
1902            Self::Sha512 => "SHA512",
1903        }
1904    }
1905}
1906
1907#[derive(Subcommand)]
1908pub enum TotpAction {
1909    /// Store a TOTP seed for KEY. Accepts a raw base32 secret or an otpauth:// URI.
1910    ///
1911    #[command(
1912        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"
1913    )]
1914    Add {
1915        /// Vault key name to store under.
1916        key: String,
1917        /// Base32-encoded TOTP secret or full otpauth:// URI.
1918        secret: String,
1919        /// HMAC algorithm to use (default: sha1, most widely supported).
1920        #[arg(long, default_value = "sha1")]
1921        algorithm: TotpAlgorithm,
1922        /// Number of digits in each OTP code (default: 6; some services use 8).
1923        #[arg(long, default_value_t = 6)]
1924        digits: u32,
1925        /// Time step in seconds (default: 30; some services use 60).
1926        #[arg(long, default_value_t = 30)]
1927        period: u64,
1928    },
1929    /// Print the current TOTP code + seconds remaining.
1930    ///
1931    #[command(after_help = "Examples:\n  tsafe totp get GITHUB_2FA")]
1932    Get {
1933        /// Vault key name where the TOTP seed is stored.
1934        key: String,
1935    },
1936}
1937
1938#[derive(Subcommand)]
1939pub enum NsAction {
1940    /// List all namespaces present in the vault (inferred from key prefixes).
1941    List,
1942    /// Copy every secret under FROM/ to TO/ (same suffix). Source keys stay.
1943    ///
1944    #[command(after_help = "Examples:\n  tsafe ns copy prod staging")]
1945    Copy {
1946        /// Namespace prefix to read from (keys must be `FROM/...`).
1947        from: String,
1948        /// Namespace prefix to write to (`TO/<same-suffix>`).
1949        to: String,
1950        /// Overwrite destination keys if they already exist.
1951        #[arg(long)]
1952        force: bool,
1953    },
1954    /// Move every secret under FROM/ to TO/ (same suffix). Source keys are removed.
1955    ///
1956    #[command(after_help = "Examples:\n  tsafe ns move prod staging")]
1957    Move {
1958        /// Namespace prefix to read from and delete after rename.
1959        from: String,
1960        /// Namespace prefix to write to (`TO/<same-suffix>`).
1961        to: String,
1962        /// Overwrite destination keys if they already exist.
1963        #[arg(long)]
1964        force: bool,
1965    },
1966}
1967
1968/// `tsafe mcp ...` subcommands per design §5.2 / ADR-006. Gated behind the
1969/// `mcp` feature.
1970#[derive(Subcommand)]
1971#[cfg(feature = "mcp")]
1972pub enum McpAction {
1973    /// Start the MCP stdio JSON-RPC server bound to the current profile.
1974    ///
1975    /// Hosts invoke this directly via the entry written by `tsafe mcp install`.
1976    /// Running it interactively is mainly useful for diagnostics.
1977    Serve {
1978        /// Comma-separated glob list of vault keys this server may expose.
1979        #[arg(long, value_name = "GLOBS")]
1980        allowed_keys: Option<String>,
1981        /// Comma-separated glob list of vault keys to exclude even when
1982        /// matched by `--allowed-keys` or the bound contract.
1983        #[arg(long, value_name = "GLOBS")]
1984        denied_keys: Option<String>,
1985        /// Bind an authority contract for stricter scope and per-key intent.
1986        #[arg(long, value_name = "NAME")]
1987        contract: Option<String>,
1988        /// Enable the opt-in `tsafe_reveal` tool. Off by default.
1989        #[arg(long)]
1990        allow_reveal: bool,
1991        /// Label written to the audit `source` field (e.g. `mcp:claude:1234`).
1992        #[arg(long, value_name = "LABEL")]
1993        audit_source: Option<String>,
1994    },
1995    /// Write an MCP server entry into the host's config file.
1996    ///
1997    /// Refuses to write without an explicit `--allowed-keys` or `--contract`.
1998    Install {
1999        /// Host name. One of: claude, cursor, continue, windsurf, codex.
2000        host: String,
2001        /// Server entry name. Defaults to `tsafe-<profile>`.
2002        #[arg(long)]
2003        name: Option<String>,
2004        /// Write to the global per-user config (default when no scope flag is set).
2005        #[arg(long)]
2006        global: bool,
2007        /// Write to a project-local config under the given directory.
2008        #[arg(long, value_name = "DIR")]
2009        project: Option<String>,
2010        /// Print the proposed file change without modifying disk.
2011        #[arg(long)]
2012        dry_run: bool,
2013        /// Comma-separated glob list of vault keys the server may expose.
2014        #[arg(long, value_name = "GLOBS")]
2015        allowed_keys: Option<String>,
2016        /// Comma-separated glob list of vault keys to exclude.
2017        #[arg(long, value_name = "GLOBS")]
2018        denied_keys: Option<String>,
2019        /// Bind an authority contract for stricter scope.
2020        #[arg(long, value_name = "NAME")]
2021        contract: Option<String>,
2022        /// Enable the opt-in `tsafe_reveal` tool on the installed entry.
2023        #[arg(long)]
2024        allow_reveal: bool,
2025    },
2026    /// Remove an MCP server entry from the host's config file.
2027    Uninstall {
2028        /// Host name. One of: claude, cursor, continue, windsurf, codex.
2029        host: String,
2030        /// Server entry name. Defaults to `tsafe-<profile>`.
2031        #[arg(long)]
2032        name: Option<String>,
2033    },
2034    /// Print binary version + resolved scope. Lightweight diagnostic;
2035    /// does not speak JSON-RPC.
2036    Status,
2037}
2038
2039#[derive(Subcommand)]
2040pub enum AgentAction {
2041    /// Prompt for approval + vault password, then start the background agent daemon.
2042    ///
2043    /// Prints a shell export line to stdout:
2044    ///   $env:TSAFE_AGENT_SOCK = "..."   # PowerShell
2045    ///   export TSAFE_AGENT_SOCK="..."   # bash/zsh
2046    ///
2047    /// Copy-paste or eval this line in the calling shell/process that needs access.
2048    Unlock {
2049        /// Idle TTL — how long the agent stays alive without a vault request.
2050        /// Resets on each vault access. Common values include 15m, 1h, and 4h. Default: 30m.
2051        #[arg(long, default_value = "30m")]
2052        ttl: String,
2053        /// Absolute TTL — hard cap regardless of activity. Default: 8h.
2054        /// Must be >= idle TTL. Common values include 8h, 12h, and 24h.
2055        #[arg(long, default_value = "8h")]
2056        absolute_ttl: String,
2057    },
2058    /// Immediately revoke the current session and stop the agent.
2059    Lock,
2060    /// Show whether the current agent socket is reachable.
2061    ///
2062    /// Use `--json` for a stable machine-readable output (ADR-029). Consumers such
2063    /// as the VS Code extension and tray agent depend on this flag. The schema
2064    /// `version` field must be checked before reading any other field.
2065    #[command(
2066        after_help = "Examples:\n  tsafe agent status\n  tsafe agent status --json\n  tsafe --profile prod agent status --json"
2067    )]
2068    Status {
2069        /// Emit a stable JSON object to stdout (schema version \"1\").
2070        /// See docs/decisions/agent-status-json-contract.md (ADR-029) for the full schema.
2071        #[arg(long)]
2072        json: bool,
2073    },
2074}
2075
2076#[derive(Subcommand)]
2077pub enum TeamAction {
2078    /// Create a new team vault encrypted to your age identity.
2079    ///
2080    #[command(after_help = "Examples:\n  tsafe team init --identity ~/.age/key.txt")]
2081    Init {
2082        /// Path to your age identity file (contains AGE-SECRET-KEY-1...).
2083        #[arg(long)]
2084        identity: String,
2085    },
2086    /// Add a team member by their age public key.
2087    ///
2088    #[command(after_help = "Examples:\n  tsafe team add-member age1qyqszqgp...")]
2089    AddMember {
2090        /// age X25519 public key (starts with "age1...").
2091        public_key: String,
2092        /// Path to your age identity file (for re-wrapping the DEK).
2093        #[arg(long)]
2094        identity: String,
2095    },
2096    /// Remove a team member and re-encrypt all secrets with a new key.
2097    ///
2098    #[command(after_help = "Examples:\n  tsafe team remove-member age1qyqszqgp...")]
2099    RemoveMember {
2100        /// age X25519 public key to remove.
2101        public_key: String,
2102        /// Path to your age identity file (for re-keying).
2103        #[arg(long)]
2104        identity: String,
2105    },
2106    /// List current team members (public keys).
2107    Members,
2108    /// Generate a new age identity (keypair) and print the JSON block to add
2109    /// to `.tsafe/team-keys.json` via a PR.
2110    ///
2111    /// The private key is saved to `~/.age/tsafe-<profile>.txt`.
2112    /// The public key is printed as a ready-to-paste JSON entry.
2113    ///
2114    #[command(
2115        after_help = "Examples:\n  tsafe team keygen\n  tsafe team keygen --name \"Alice Smith\" --email alice@corp.example"
2116    )]
2117    Keygen {
2118        /// Your display name for the team-keys entry.
2119        #[arg(long)]
2120        name: Option<String>,
2121        /// Your email for the team-keys entry.
2122        #[arg(long)]
2123        email: Option<String>,
2124    },
2125    /// Print your age public key from an existing identity file.
2126    ///
2127    #[command(
2128        after_help = "Examples:\n  tsafe team show-key\n  tsafe team show-key --identity ~/.age/key.txt"
2129    )]
2130    ShowKey {
2131        /// Path to identity file (default: ~/.age/tsafe-<profile>.txt).
2132        #[arg(long)]
2133        identity: Option<String>,
2134    },
2135    /// Reconcile vault recipients with `.tsafe/team-keys.json`.
2136    ///
2137    /// Adds any new members found in the keys file. Removes members no longer
2138    /// listed. Re-keys the vault if the member list changed.
2139    ///
2140    #[command(after_help = "Examples:\n  tsafe team sync-keys --identity ~/.age/key.txt")]
2141    SyncKeys {
2142        /// Path to your age identity file (required for re-wrapping the DEK).
2143        #[arg(long)]
2144        identity: String,
2145        /// Path to team-keys.json (auto-detected if omitted).
2146        #[arg(long)]
2147        keys_file: Option<String>,
2148    },
2149}
2150
2151#[derive(Subcommand)]
2152pub enum BiometricAction {
2153    /// Store the vault password in the OS credential store (same as accepting quick unlock after `tsafe init`).
2154    Enable,
2155    /// Remove the vault password from the OS credential store.
2156    Disable,
2157    /// Check if biometric/keyring unlock is configured for this profile.
2158    Status,
2159    /// Re-enroll biometric/keyring unlock after a stale-credential error.
2160    ///
2161    /// Use this when `tsafe` reports "stale biometric credential" — for example after
2162    /// rotating the vault password (`tsafe rotate`) or after enrolling a new fingerprint.
2163    /// This is equivalent to `tsafe biometric disable` followed by `tsafe biometric enable`
2164    /// but makes the recovery intent explicit and prints a confirmation.
2165    #[command(name = "re-enroll")]
2166    ReEnroll,
2167}
2168
2169#[cfg(test)]
2170mod tests {
2171    use super::*;
2172    use clap::CommandFactory;
2173
2174    fn has_subcommand(command: &clap::Command, name: &str) -> bool {
2175        command
2176            .get_subcommands()
2177            .any(|subcommand| subcommand.get_name() == name)
2178    }
2179
2180    #[test]
2181    fn root_command_visibility_matches_feature_gates() {
2182        let command = Cli::command();
2183
2184        assert_eq!(has_subcommand(&command, "ui"), cfg!(feature = "tui"));
2185        assert_eq!(
2186            has_subcommand(&command, "kv-pull"),
2187            cfg!(feature = "akv-pull")
2188        );
2189        assert_eq!(
2190            has_subcommand(&command, "aws-pull"),
2191            cfg!(feature = "cloud-pull-aws")
2192        );
2193        assert_eq!(
2194            has_subcommand(&command, "gcp-pull"),
2195            cfg!(feature = "cloud-pull-gcp")
2196        );
2197        assert_eq!(
2198            has_subcommand(&command, "gcp-push"),
2199            cfg!(feature = "cloud-pull-gcp")
2200        );
2201        assert_eq!(
2202            has_subcommand(&command, "ssm-pull"),
2203            cfg!(feature = "cloud-pull-aws")
2204        );
2205        assert_eq!(
2206            has_subcommand(&command, "aws-push"),
2207            cfg!(feature = "cloud-pull-aws")
2208        );
2209        assert_eq!(
2210            has_subcommand(&command, "ssm-push"),
2211            cfg!(feature = "cloud-pull-aws")
2212        );
2213        assert_eq!(
2214            has_subcommand(&command, "vault-pull"),
2215            cfg!(feature = "cloud-pull-vault")
2216        );
2217        assert_eq!(
2218            has_subcommand(&command, "op-pull"),
2219            cfg!(feature = "cloud-pull-1password")
2220        );
2221        assert_eq!(
2222            has_subcommand(&command, "pull"),
2223            cfg!(feature = "multi-pull")
2224        );
2225        assert_eq!(
2226            has_subcommand(&command, "share-once"),
2227            cfg!(feature = "ots-sharing")
2228        );
2229        assert_eq!(
2230            has_subcommand(&command, "receive-once"),
2231            cfg!(feature = "ots-sharing")
2232        );
2233        assert_eq!(
2234            has_subcommand(&command, "browser-profile"),
2235            cfg!(feature = "browser")
2236        );
2237        assert_eq!(
2238            has_subcommand(&command, "browser-native-host"),
2239            cfg!(feature = "nativehost")
2240        );
2241        assert_eq!(has_subcommand(&command, "ssh-add"), cfg!(feature = "ssh"));
2242        assert_eq!(
2243            has_subcommand(&command, "ssh-import"),
2244            cfg!(feature = "ssh")
2245        );
2246        assert_eq!(
2247            has_subcommand(&command, "plugin"),
2248            cfg!(feature = "plugins")
2249        );
2250        assert_eq!(
2251            has_subcommand(&command, "hook-install"),
2252            cfg!(feature = "git-helpers")
2253        );
2254        assert_eq!(
2255            has_subcommand(&command, "git"),
2256            cfg!(feature = "git-helpers")
2257        );
2258        assert_eq!(
2259            has_subcommand(&command, "sync"),
2260            cfg!(feature = "git-helpers")
2261        );
2262        assert_eq!(
2263            has_subcommand(&command, "credential-helper"),
2264            cfg!(feature = "git-helpers")
2265        );
2266        assert_eq!(
2267            has_subcommand(&command, "biometric"),
2268            cfg!(feature = "biometric")
2269        );
2270        assert_eq!(
2271            has_subcommand(&command, "team"),
2272            cfg!(feature = "team-core")
2273        );
2274        assert_eq!(has_subcommand(&command, "agent"), cfg!(feature = "agent"));
2275    }
2276}
2277
2278#[derive(Subcommand)]
2279pub enum PolicyAction {
2280    /// Set a rotation policy on a secret.
2281    ///
2282    #[command(after_help = "Examples:\n  tsafe policy set DB_PASSWORD --rotate-every 90d")]
2283    Set {
2284        /// Secret key.
2285        key: String,
2286        /// Rotation interval (e.g. 90d, 30d, 7d).
2287        #[arg(long)]
2288        rotate_every: String,
2289    },
2290    /// Remove the rotation policy from a secret.
2291    ///
2292    #[command(after_help = "Examples:\n  tsafe policy remove DB_PASSWORD")]
2293    Remove {
2294        /// Secret key.
2295        key: String,
2296    },
2297}