tsafe-cli 1.0.26

Secrets runtime for developers — inject credentials into processes via exec, never into shell history or .env files
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
use clap::{Parser, Subcommand, ValueEnum};
pub use clap_complete::Shell;

const 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.";

const 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";

const 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.";

const DOCTOR_AFTER_HELP: &str =
    "Examples:\n  tsafe doctor\n  tsafe doctor --json\n  tsafe --profile prod doctor";

const 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";

const ROTATE_DUE_AFTER_HELP: &str =
    "Examples:\n  tsafe rotate-due\n  tsafe rotate-due --json\n  tsafe rotate-due --fail   # for scripts: non-zero if overdue";

const BUILD_INFO_AFTER_HELP: &str = "Examples:\n  tsafe build-info\n  tsafe build-info --json";

#[derive(Parser)]
#[command(
    name = "tsafe",
    about = "tsafe — local encrypted secret runtime for vaults, exec/contracts, and operator workflows",
    long_about = ROOT_LONG_ABOUT,
    version,
    arg_required_else_help = true,
    after_help = ROOT_AFTER_HELP
)]
pub struct Cli {
    /// Named vault / profile. Defaults to the persisted default (or 'default'). Override with TSAFE_PROFILE env var.
    #[arg(short, long, global = true, env = "TSAFE_PROFILE")]
    pub profile: Option<String>,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Initialise a new encrypted vault for the current profile.
    ///
    /// Creates the vault file for this profile under the platform data directory. Prompts for a master password twice.
    ///
    /// On an interactive terminal, after the vault is created you may be offered "quick unlock":
    /// storing the password in the OS credential store (Touch ID / Face ID / Windows Hello / device PIN
    /// where the OS supports it). You can accept, defer, or skip; run `tsafe biometric enable` anytime.
    ///
    /// If `tsafe config set-backup-vault main` (or `default`) is set, the new vault's master password is
    /// also stored under `profile-passwords/<profile>` in that vault when possible.
    ///
    #[command(after_help = "Examples:\n  tsafe init\n  tsafe --profile prod init")]
    Init,

    /// View or change global settings (config.json): password backup target, default profile, etc.
    ///
    /// Use `config set-backup-vault main` so every new vault's master password is also stored under
    /// `profile-passwords/<profile>` in the `main` vault (requires that vault to exist and be unlockable when you create more profiles).
    #[command(
        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"
    )]
    Config {
        #[command(subcommand)]
        action: ConfigAction,
    },

    /// Store or update a secret in the vault.
    ///
    /// If VALUE is omitted on a TTY, you are prompted with masked input (typically `*` per character).
    /// Piped / non-interactive stdin reads a single line.
    /// Keys may be namespaced with `.` or `-` (e.g. `github.com.token`, `db-prod.PASSWORD`).
    ///
    /// If the key already exists the command will prompt for confirmation (on a TTY)
    /// or exit with an error (non-TTY). Pass --overwrite to skip the check.
    ///
    #[command(
        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"
    )]
    Set {
        /// Secret key (e.g. DB_PASSWORD, github.com.token).
        key: String,
        /// Secret value. Omit for a masked TTY prompt or a line from stdin when piped.
        value: Option<String>,
        /// Attach tags as KEY=VALUE pairs (repeatable).
        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
        tags: Vec<String>,
        /// Overwrite the key if it already exists — skips the confirmation prompt.
        #[arg(long)]
        overwrite: bool,
    },

    /// Retrieve a secret and print its plaintext value.
    ///
    /// Use --copy to copy to clipboard instead of printing; the clipboard is cleared after 30 s.
    /// Use --version to retrieve a previous version (0=current, 1=previous, etc.).
    ///
    #[command(
        after_help = "Examples:\n  tsafe get DB_PASSWORD\n  tsafe get API_KEY --copy\n  tsafe get DB_PASSWORD --version 1"
    )]
    Get {
        /// Secret key.
        key: String,
        /// Copy value to clipboard and clear after 30 seconds (does not print).
        #[arg(short, long)]
        copy: bool,
        /// Retrieve a previous version (0=current, 1=previous, etc.).
        #[arg(long)]
        version: Option<usize>,
    },

    /// Permanently remove a secret from the vault.
    ///
    /// The deletion is recorded in the audit log and a snapshot is taken before removal.
    ///
    #[command(after_help = "Examples:\n  tsafe delete OLD_TOKEN")]
    Delete {
        /// Secret key.
        key: String,
    },

    /// List all secret key names stored in the vault.
    ///
    /// Use --tag to filter by attached metadata.
    /// Use --ns to filter to a specific namespace (e.g. "cds-adf").
    ///
    #[command(
        after_help = "Examples:\n  tsafe list\n  tsafe list --tag env=prod\n  tsafe list --ns cds-adf"
    )]
    List {
        /// Filter to secrets with this tag (KEY=VALUE). Repeatable.
        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
        tags: Vec<String>,
        /// Filter to keys in this namespace (stored as "<ns>/<KEY>").
        #[arg(long)]
        ns: Option<String>,
    },

    /// Print secrets to stdout in the chosen format.
    ///
    /// Formats: env (default), dotenv, powershell, json, github-actions, yaml, docker-env.
    /// Use --ns to export only keys from a namespace; the prefix is stripped
    /// so the output contains plain KEY=VALUE (e.g. APP_PW not cds-adf/APP_PW).
    ///
    #[command(
        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"
    )]
    Export {
        /// Output format.
        #[arg(short, long, default_value = "env")]
        format: ExportFormat,
        /// Limit to specific keys (all keys if omitted).
        keys: Vec<String>,
        /// Filter to secrets with this tag (KEY=VALUE). Repeatable.
        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
        tags: Vec<String>,
        /// Filter to keys in this namespace; prefix is stripped in output.
        #[arg(long)]
        ns: Option<String>,
    },

    /// Execute a command with secrets injected into its environment.
    ///
    /// Secrets are injected as env vars; the child inherits all other env vars.
    /// Ctrl-C is forwarded to the child and tsafe exits with the child's exit code.
    /// Use --ns to inject only secrets from a namespace (prefix stripped from var names).
    ///
    /// Use --contract to load a named authority contract from the nearest .tsafe.yml manifest.
    /// A contract declares profile, namespace, allowed secrets, required secrets, allowed targets,
    /// and trust posture as a reusable, auditable policy. Explicit flags still override contract values.
    #[command(
        name = "exec",
        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"
    )]
    Exec {
        /// Load a named authority contract from the nearest .tsafe.yml (or .tsafe.json) manifest.
        /// The contract sets the profile, namespace, allowed/required secrets, allowed targets, and
        /// trust posture. Explicit flags (--ns, --keys, --mode, etc.) still override contract values.
        #[arg(long, value_name = "NAME")]
        contract: Option<String>,
        /// Inject only secrets from this namespace; prefix is stripped from env var names.
        #[arg(long)]
        ns: Option<String>,
        /// Inject only these vault keys (after `--ns` prefix stripping). Comma-separated or repeat flag.
        /// Missing selected keys abort the run so narrower injection does not silently degrade.
        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
        keys: Vec<String>,
        /// Trust preset for this run. `standard` keeps broad compatibility, `hardened` applies a stricter preset,
        /// and `custom` uses your persisted exec trust settings. Explicit flags still override the preset.
        #[arg(long)]
        mode: Option<ExecModeSetting>,
        /// Kill the child process after this many seconds and exit non-zero. Default: no timeout.
        #[arg(long, value_name = "SECONDS")]
        timeout: Option<u64>,
        /// Preset for inherited parent environment. `minimal` keeps only PATH and a safe core set
        /// (equivalent to --minimal). `full` inherits the full parent environment minus the strip list
        /// (equivalent to the default). Explicit --no-inherit, --minimal, and --only override this.
        #[arg(long, value_name = "PRESET")]
        preset: Option<ExecPresetSetting>,
        /// List env var names that would be injected (sorted, one per line) and exit 0; no command is run.
        #[arg(long)]
        dry_run: bool,
        /// Show a human-readable plan: profile, namespace, injected names, --require checks,
        /// parent env strips, and a copy-paste run line. Exit 0; no command is run.
        #[arg(long)]
        plan: bool,
        /// Start from a clean environment: no parent env vars are inherited. Only vault secrets
        /// (and any --only keys) are visible to the child. Mutually exclusive with --only and --minimal.
        #[arg(long, conflicts_with_all = ["only", "minimal"])]
        no_inherit: bool,
        /// Inherit only a safe minimal set of parent env vars (PATH, HOME, USER, TMPDIR, LANG,
        /// TERM, SSH_AUTH_SOCK, etc.) plus vault secrets. No tokens or credentials leak through.
        /// Mutually exclusive with --no-inherit and --only.
        #[arg(long, conflicts_with_all = ["no_inherit", "only"])]
        minimal: bool,
        /// Inherit only these parent env vars (comma-separated or repeat flag); all others are
        /// stripped. Vault secrets are then added on top. Mutually exclusive with --no-inherit and --minimal.
        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append, conflicts_with_all = ["no_inherit", "minimal"])]
        only: Vec<String>,
        /// Require these vault keys (after --ns mapping) to be present. Comma-separated or repeat flag.
        #[arg(long, value_name = "KEY", value_delimiter = ',', action = clap::ArgAction::Append)]
        require: Vec<String>,
        /// Map a vault key to a different env var name in the child process.
        /// Format: ENV_VAR=VAULT_KEY  (e.g. --env MY_DB=PROD_SECRET injects the vault value of
        /// PROD_SECRET under the name MY_DB). When --keys is also given, only vault keys that are
        /// in the --keys allowlist may be referenced; other vault keys are rejected with an error.
        /// Repeat the flag for multiple mappings.
        #[arg(long = "env", value_name = "ENV_VAR=VAULT_KEY", action = clap::ArgAction::Append)]
        env_mappings: Vec<String>,
        /// Abort if any injected name is a known high-risk env var (e.g. NODE_OPTIONS, LD_PRELOAD).
        /// Redundant: this is now the default. Kept for backwards compatibility.
        #[arg(long, conflicts_with = "allow_dangerous_env")]
        deny_dangerous_env: bool,
        /// Allow injection of known high-risk env var names (e.g. LD_PRELOAD, NODE_OPTIONS).
        /// By default, dangerous names abort exec. Use this flag to inject them with a warning instead.
        #[arg(long, conflicts_with = "deny_dangerous_env")]
        allow_dangerous_env: bool,
        /// Replace exact vault secret values in the child's stdout/stderr with [REDACTED].
        /// Useful for agent/tool wrappers where you trust the command less than the vault.
        #[arg(long, conflicts_with = "no_redact_output")]
        redact_output: bool,
        /// Force raw child stdout/stderr even if config enables exec output redaction by default.
        #[arg(long, conflicts_with = "redact_output")]
        no_redact_output: bool,
        /// Command and its arguments (omit when using --dry-run or --plan).
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        cmd: Vec<String>,
    },

    /// Import secrets from a `.env` file or another supported export source.
    ///
    /// `.env` paths work in every build. Some builds may also accept additional
    /// source names for password-manager or browser CSV exports.
    ///
    /// When `--from` is a named export source, `--file` is required.
    /// Skips keys that already exist unless --overwrite is passed.
    ///
    /// Use --ns to prefix all imported keys with a namespace, e.g. "cds-adf".
    /// Keys are stored as "<ns>/<KEY>" allowing multiple projects in one vault
    /// without collision (e.g. cds-adf/APP_PW vs mail-automation/APP_PW).
    ///
    #[command(
        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"
    )]
    /// If `--from` is a **relative** path that does not exist, the error includes extra hints and
    /// searches **downward** from the current directory (bounded depth; skips `target/`, `node_modules/`, `.git/`, etc.)
    /// for files with the **same name** (e.g. `.env`) so you can copy-paste a suggested `tsafe import --from '…'` line.
    Import {
        /// `.env` file path or another supported source name for this build.
        #[arg(long, default_value = ".env")]
        from: String,
        /// Export file path (required when `--from` is a named export source).
        #[arg(long)]
        file: Option<String>,
        /// Overwrite existing keys (skip by default).
        #[arg(long)]
        overwrite: bool,
        /// Skip duplicate keys silently instead of erroring (applies to both
        /// within-file duplicates and keys already in the vault).
        #[arg(long)]
        skip_duplicates: bool,
        /// Namespace prefix to prepend to imported keys (e.g. "cds-adf").
        /// Keys are stored as "<ns>/<KEY>", preventing collisions across projects.
        #[arg(long)]
        ns: Option<String>,
        /// Show what would be imported without writing any secrets to the vault.
        /// Prints each key and whether it would be skipped (existing) or imported.
        #[arg(long)]
        dry_run: bool,
    },

    /// Map browser domains to vault profiles for the browser extension.
    ///
    /// The extension uses these mappings to choose which vault profile to
    /// query when filling credentials on a given domain.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "browser")]
    #[command(name = "browser-profile")]
    BrowserProfile {
        #[command(subcommand)]
        action: BrowserProfileAction,
    },

    /// Register or unregister the native messaging host for the browser extension.
    ///
    /// Writes the per-user manifest/registration files needed by supported browsers on the
    /// current OS. This command does not require browser-profile mappings or vault access.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "nativehost")]
    #[command(name = "browser-native-host")]
    BrowserNativeHost {
        #[command(subcommand)]
        action: BrowserNativeHostAction,
    },

    /// Re-encrypt all secrets with a new master password (vault re-key).
    ///
    /// Prompts for the current password, then the new password twice (unless non-interactive).
    /// For automation / CI, set `TSAFE_PASSWORD` (current) and `TSAFE_NEW_MASTER_PASSWORD` (new);
    /// confirmation is skipped when both are set (no OS keychain prompt in that case — run `biometric enable` after).
    /// After interactive rotation, you are offered an OS keychain update so quick unlock matches the new password.
    /// A snapshot is taken automatically before rotation. `tsafe doctor` suggests periodic rotation.
    ///
    #[command(after_help = "Examples:\n  tsafe rotate\n  tsafe --profile prod rotate")]
    Rotate,

    /// Re-encrypt the vault with a new master password and update the biometric credential.
    ///
    /// Prompts for the current password (or reads from TSAFE_PASSWORD), then the new password
    /// twice (or reads from TSAFE_NEW_MASTER_PASSWORD).  The vault is written atomically via a
    /// temp-file rename.  If biometric quick-unlock is active, the stored credential is re-stored
    /// under the new password so subsequent unlocks continue to work.
    ///
    /// If the vault re-encryption succeeds but the biometric re-store fails, a warning is emitted
    /// directing the user to `tsafe biometric re-enroll`.
    ///
    #[command(
        name = "rotate-key",
        after_help = "Examples:\n  tsafe rotate-key\n  tsafe --profile prod rotate-key"
    )]
    RotateKey {
        /// Profile to re-key (defaults to the active profile).
        #[arg(short, long)]
        profile: Option<String>,
    },

    /// Manage profiles (named vaults).
    ///
    /// Each profile is an independent vault file under the platform data `vaults/` directory.
    ///
    #[command(
        after_help = "Examples:\n  tsafe profile list\n  tsafe profile delete staging\n  tsafe profile delete staging --force"
    )]
    Profile {
        #[command(subcommand)]
        action: ProfileAction,
    },

    /// Display recent audit log entries for the current profile in human-readable form.
    #[command(after_help = AUDIT_AFTER_HELP)]
    Audit {
        /// Number of entries to display.
        #[arg(short, long, default_value_t = 20)]
        limit: usize,
        /// Check all secret values against Have I Been Pwned (k-anonymity, no full hash sent).
        #[arg(long, conflicts_with = "explain")]
        hibp: bool,
        /// Show a session-style explanation (grouped operations, exec authority summaries).
        #[arg(long)]
        explain: bool,
        /// With `--explain`, print JSON instead of human text.
        #[arg(long, requires = "explain")]
        json: bool,
        /// Filter entries to those with this CellOS cell ID in their audit context.
        #[arg(long, value_name = "CELL_ID")]
        cell_id: Option<String>,
    },

    /// Cross-check authority contracts against a CellOS policy pack.
    ///
    /// Loads authority contracts from the nearest `.tsafe.yml` and compares each
    /// contract's `allowed_secrets` against `allowedSecretRefs` in the CellOS
    /// policy pack JSON.  Reports mismatches and exits non-zero if any are found.
    ///
    /// Use `--policy-file` as an alias for `--cellos-policy` (both accepted).
    ///
    #[command(
        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"
    )]
    Validate {
        /// Path to the CellOS policy pack JSON file.
        #[arg(long, value_name = "PATH", conflicts_with = "policy_file")]
        cellos_policy: Option<std::path::PathBuf>,
        /// Alias for --cellos-policy. Path to the policy pack JSON file.
        #[arg(long, value_name = "PATH", conflicts_with = "cellos_policy")]
        policy_file: Option<std::path::PathBuf>,
        /// Emit machine-readable JSON output (exit codes are preserved).
        #[arg(long)]
        json: bool,
    },

    /// Manage local vault snapshots.
    ///
    /// Snapshots are encrypted copies of the vault file, taken automatically before
    /// every write operation. Use them to recover from accidental changes.
    ///
    #[command(after_help = "Examples:\n  tsafe snapshot list\n  tsafe snapshot restore")]
    Snapshot {
        #[command(subcommand)]
        action: SnapshotAction,
    },

    /// Pull secrets from Azure Key Vault into the local vault.
    ///
    /// Requires TSAFE_AKV_URL and either a service principal
    /// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
    /// a managed identity (IMDS, automatic inside Azure VMs / ACI).
    ///
    #[command(
        after_help = "Examples:\n  tsafe kv-pull\n  tsafe kv-pull --prefix MYAPP_ --overwrite"
    )]
    #[cfg(feature = "akv-pull")]
    KvPull {
        /// Only import secrets whose names start with this prefix (case-insensitive).
        /// Omit to pull all secrets.
        #[arg(long)]
        prefix: Option<String>,

        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
    },

    /// Push local vault secrets to Azure Key Vault (upsert semantics).
    ///
    /// Requires TSAFE_AKV_URL and either a service principal
    /// (AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET) or
    /// a managed identity (IMDS, automatic inside Azure VMs / ACI).
    ///
    /// Local keys are reverse-normalised to Azure Key Vault format:
    /// MY_SECRET → my-secret. Two local keys that normalise to the same
    /// provider name are detected as a collision and abort pre-flight.
    ///
    /// Remote-only keys are left untouched unless --delete-missing is passed.
    /// A pre-flight diff is always shown before writing. No secret values
    /// are printed — only key names and 12-char SHA-256 hash prefixes.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "akv-pull")]
    KvPush {
        /// Only push secrets whose local key names start with this prefix (case-insensitive).
        #[arg(long)]
        prefix: Option<String>,

        /// Only push secrets in this namespace (stored as `<ns>/KEY`).
        #[arg(long)]
        ns: Option<String>,

        /// Show the diff without writing anything (always exits 0).
        #[arg(long)]
        dry_run: bool,

        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
        #[arg(long)]
        yes: bool,

        /// Also delete remote secrets that are absent locally within the filtered scope.
        /// Off by default — opt-in to avoid accidental mass deletion.
        /// AKV uses soft-delete (30-day recoverable window).
        #[arg(long)]
        delete_missing: bool,
    },

    /// Share a vault secret as a one-time HTTPS link via a configured OTS (one-time secret) service.
    ///
    /// Set `TSAFE_OTS_BASE_URL` to your service HTTPS origin (no default). The CLI POSTs JSON
    /// `{"secret","ttl"}` to `{base}{TSAFE_OTS_CREATE_PATH}` (default path `/create`) and prints the returned `url`.
    ///
    /// The one-time URL is printed to stdout — never the secret value. Use any server that implements this contract.
    ///
    #[command(
        after_help = "Examples:\n  tsafe share-once DB_PASSWORD\n  tsafe share-once API_KEY --ttl 10m"
    )]
    #[cfg(feature = "ots-sharing")]
    #[command(name = "share-once")]
    ShareOnce {
        /// Secret key to share.
        key: String,
        /// Link expiry (sent to the service; many accept 10m, 1h, 24h).
        #[arg(short, long, default_value = "1h")]
        ttl: String,
    },

    /// Receive a secret from a one-time link (from `share-once` or any compatible OTS server).
    ///
    /// POSTs to the exact HTTPS URL. The response may be JSON (`secret`, `plaintext`, or `value`) or HTML with
    /// `<div id="secret-content">...</div>`.
    ///
    /// Optionally store the retrieved value directly into the vault with --store.
    ///
    #[command(
        after_help = "Examples:\n  tsafe receive-once 'https://ots.example.com/s/abc123'\n  tsafe receive-once '<URL>' --store DB_PASSWORD"
    )]
    #[cfg(feature = "ots-sharing")]
    #[command(name = "receive-once", visible_alias = "snap-receive")]
    ReceiveOnce {
        /// The full one-time URL (fragment `#...` is ignored).
        url: String,
        /// Store the received secret in the vault under this key name instead of printing it.
        #[arg(long)]
        store: Option<String>,
    },

    /// Generate a cryptographically random secret and store it in the vault.
    ///
    /// Uses a CSPRNG. Default length 32, character set 'alnum'.
    ///
    #[command(
        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"
    )]
    Gen {
        /// Key name to store the generated secret under.
        key: String,
        /// Length of the generated secret in characters (ignored if --words is set).
        #[arg(short = 'l', long, default_value_t = 32)]
        length: usize,
        /// Character set: alnum (default), alpha, numeric, hex, symbol.
        #[arg(short = 'c', long, default_value = "alnum")]
        charset: String,
        /// Generate a passphrase of N random words instead of a random string.
        #[arg(short = 'w', long)]
        words: Option<usize>,
        /// Attach tags as KEY=VALUE pairs (repeatable).
        #[arg(short = 't', long = "tag", value_name = "KEY=VALUE")]
        tags: Vec<String>,
        /// Print the generated value to stdout (otherwise the value is only in the vault).
        #[arg(long)]
        print: bool,
        /// Remove visually ambiguous characters (0, O, l, 1, I) from the charset.
        /// Useful when the secret will be read aloud or transcribed manually.
        #[arg(long)]
        exclude_ambiguous: bool,
    },

    /// Show key-level changes between the current vault and its most-recent snapshot.
    ///
    /// Highlights added, removed, and modified keys — values are never shown.
    ///
    #[command(after_help = "Examples:\n  tsafe diff\n  tsafe --profile staging diff")]
    Diff,

    /// Compare key names across two profiles without decrypting any values.
    ///
    /// Highlights keys present in one profile but missing from the other.
    ///
    #[command(
        after_help = "Examples:\n  tsafe compare staging\n  tsafe --profile dev compare prod"
    )]
    Compare {
        /// Second profile to compare against the active --profile.
        profile_b: String,
    },

    /// Show version history for a secret.
    ///
    /// Lists all stored versions with timestamps. Version 0 is the current
    /// value; higher numbers are older. Use `tsafe get KEY --version N` to
    /// retrieve a specific version.
    ///
    #[command(after_help = "Examples:\n  tsafe history DB_PASSWORD")]
    History {
        /// Secret key.
        key: String,
    },

    /// Move or rename a secret within the vault, or to a different profile.
    ///
    /// Within a profile this is an atomic rename: key name, namespace prefix,
    /// tags and full version history are all preserved.
    ///
    #[command(
        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)"
    )]
    Mv {
        /// Source secret key.
        source: String,

        /// Destination key name.  Omit when using --to-profile to keep the same key name.
        dest: Option<String>,

        /// Move the secret to this profile (cross-profile move).
        #[arg(long, value_name = "PROFILE")]
        to_profile: Option<String>,

        /// Overwrite the destination key if it already exists.
        #[arg(long, short = 'f')]
        force: bool,
    },

    /// Install a secret-scanning git pre-commit hook in the current repo.
    ///
    /// Scans staged files for hardcoded secrets on every `git commit`.
    ///
    #[command(
        after_help = "Examples:\n  tsafe hook-install\n  tsafe hook-install --dir /path/to/repo"
    )]
    #[cfg(feature = "git-helpers")]
    HookInstall {
        /// Repo root directory; defaults to walking up from the current directory.
        #[arg(long)]
        dir: Option<String>,
    },

    /// Export audit log entries to stdout or a file as JSON or Splunk HEC events.
    #[command(
        after_help = "Examples:\n  tsafe audit-export --format json --output audit.jsonl\n  tsafe audit-export --format splunk"
    )]
    AuditExport {
        /// Output format.
        #[arg(short, long, value_enum, default_value = "json")]
        format: AuditExportFormat,
        /// Write to a file instead of stdout.
        #[arg(short, long)]
        output: Option<String>,
    },

    /// Report HMAC chain coverage for the audit log of the current profile.
    ///
    /// Reads all entries from the audit log file and counts how many carry a
    /// `prev_entry_hmac` field (written by a C8-capable tsafe build) versus
    /// how many are unchained (written before C8 or at a session boundary).
    ///
    /// IMPORTANT — ephemeral-key limitation: the HMAC chain key is generated
    /// fresh on every tsafe session and is never persisted.  This command
    /// cannot perform cryptographic verification of entries from a closed
    /// session; it can only report chain coverage (presence of the field).
    /// To detect within-session tampering, use AuditLog::verify_chain() from
    /// a live session handle.
    ///
    /// Exit codes: 0 = log is structurally valid (or empty), 2 = at least one
    /// entry could not be parsed as JSON.
    #[command(
        after_help = "Examples:\n  tsafe audit-verify\n  tsafe audit-verify --json\n  tsafe --profile prod audit-verify"
    )]
    AuditVerify {
        /// Emit machine-readable JSON output.
        #[arg(long)]
        json: bool,
    },

    /// Set or remove a rotation policy on a secret.
    ///
    /// Policies are stored as tags and checked by `tsafe doctor` and `tsafe rotate-due`.
    ///
    #[command(
        after_help = "Examples:\n  tsafe policy set DB_PASSWORD --rotate-every 90d\n  tsafe policy remove DB_PASSWORD"
    )]
    Policy {
        #[command(subcommand)]
        action: PolicyAction,
    },

    /// List secrets that are overdue for rotation (per `rotate_policy` tags).
    ///
    /// Checks the `rotate_policy` tag against the secret's `updated_at` timestamp.
    /// Use `--json` for automation; `--fail` exits with status 1 when anything is overdue (CI/cron).
    ///
    /// Set policies with: `tsafe policy set KEY --rotate-every 90d`
    #[command(after_help = ROTATE_DUE_AFTER_HELP)]
    RotateDue {
        /// Print JSON to stdout (`overdue_count` + `items` with key, days_overdue, policy).
        #[arg(long)]
        json: bool,
        /// Exit with status 1 when one or more secrets are overdue.
        #[arg(long)]
        fail: bool,
    },

    /// Pull secrets from a HashiCorp Vault KV v2 store.
    ///
    /// Requires TSAFE_HCP_URL or --addr and VAULT_TOKEN (or --token).
    ///
    #[command(
        after_help = "Examples:\n  tsafe vault-pull --addr http://vault:8200 --prefix myapp/\n  tsafe vault-pull  # uses TSAFE_HCP_URL + VAULT_TOKEN"
    )]
    #[cfg(feature = "cloud-pull-vault")]
    VaultPull {
        /// HashiCorp Vault address. Defaults to TSAFE_HCP_URL or http://127.0.0.1:8200.
        #[arg(long)]
        addr: Option<String>,
        /// Vault token. Defaults to VAULT_TOKEN env var.
        /// Deprecated: passing the token as a CLI argument exposes it in the process
        /// table. Store the token in tsafe and use `tsafe exec -- tsafe vault-pull`
        /// so the token is injected securely without appearing in the process table.
        #[arg(long)]
        token: Option<String>,
        /// KV v2 mount path. Defaults to "secret".
        #[arg(long)]
        mount: Option<String>,
        /// Only import secrets under this path prefix.
        #[arg(long)]
        prefix: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
    },

    /// Pull fields from a 1Password item via the `op` CLI.
    ///
    /// Requires the 1Password CLI (`op`) installed and authenticated.
    ///
    #[command(
        after_help = "Examples:\n  tsafe op-pull 'Database Credentials'\n  tsafe op-pull abc123xyz --op-vault Personal"
    )]
    #[cfg(feature = "cloud-pull-1password")]
    OpPull {
        /// Item title or ID.
        item: String,
        /// 1Password vault name (uses the default vault if omitted).
        #[arg(long = "op-vault")]
        op_vault: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
    },

    /// Import Login items from Bitwarden into the local vault via the `bw` CLI.
    ///
    /// Bitwarden REST API ciphers are always E2E encrypted client-side. This command
    /// shells to the `bw` CLI (which handles local decryption) rather than calling
    /// the REST API directly — the same pattern as `tsafe op-pull` for 1Password.
    ///
    /// Requires TSAFE_BW_CLIENT_ID, TSAFE_BW_CLIENT_SECRET, and TSAFE_BW_PASSWORD
    /// (master password for `bw unlock`). The `bw` CLI must be installed and on PATH.
    ///
    /// Item names are normalised: spaces and hyphens become underscores, uppercase.
    /// Login.Username → ITEM_NAME_USERNAME, Login.Password → ITEM_NAME_PASSWORD.
    /// Custom text/hidden fields → ITEM_NAME_<FIELD_NAME>. Boolean fields are skipped.
    ///
    #[command(
        name = "bw-pull",
        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"
    )]
    #[cfg(feature = "cloud-pull-bitwarden")]
    BwPull {
        /// Bitwarden API client ID. Reads TSAFE_BW_CLIENT_ID if not set.
        #[arg(long = "bw-client-id")]
        bw_client_id: Option<String>,
        /// Bitwarden API client secret. Reads TSAFE_BW_CLIENT_SECRET if not set.
        #[arg(long = "bw-client-secret")]
        bw_client_secret: Option<String>,
        /// Bitwarden API base URL (for self-hosted / Vaultwarden).
        /// Default: https://api.bitwarden.com
        #[arg(long = "bw-api-url")]
        bw_api_url: Option<String>,
        /// Bitwarden identity base URL (for self-hosted / Vaultwarden).
        /// Default: https://identity.bitwarden.com
        #[arg(long = "bw-identity-url")]
        bw_identity_url: Option<String>,
        /// Bitwarden folder ID to filter items. Imports all items when omitted.
        #[arg(long = "bw-folder")]
        bw_folder: Option<String>,
        /// Name of the env var holding the Bitwarden master password for `bw unlock`.
        /// Default: TSAFE_BW_PASSWORD
        #[arg(long = "bw-password-env")]
        bw_password_env: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
        /// Show which items would be imported without writing any secrets.
        #[arg(long)]
        dry_run: bool,
    },

    /// Import secrets from a KeePass `.kdbx` file into the local vault.
    ///
    /// Opens a local KeePass database using the master password (from the env var
    /// named by --kp-password-env, default TSAFE_KP_PASSWORD) and/or a key file.
    ///
    /// Entry titles are used as key prefixes. Standard fields (UserName, Password, URL)
    /// map to TITLE_USERNAME, TITLE_PASSWORD, TITLE_URL.  Custom fields map to
    /// TITLE_<FIELD_NAME_NORMALISED>.  Notes are skipped.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "cloud-pull-keepass")]
    KpPull {
        /// Absolute path to the `.kdbx` database file.
        #[arg(long = "kp-path")]
        kp_path: String,
        /// Name of the env var that holds the master password.
        /// Defaults to TSAFE_KP_PASSWORD.
        #[arg(long = "kp-password-env", default_value = "TSAFE_KP_PASSWORD")]
        kp_password_env: String,
        /// Path to a KeePass key file (optional).
        #[arg(long = "kp-keyfile")]
        kp_keyfile: Option<String>,
        /// Only import entries from this group name (case-insensitive).
        #[arg(long = "kp-group")]
        kp_group: Option<String>,
        /// When set, also traverse descendant groups under the matched group.
        #[arg(long = "kp-recursive")]
        kp_recursive: bool,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
    },

    /// Import secrets from AWS Secrets Manager into the local vault.
    ///
    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
    ///
    /// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
    ///
    /// Secret names are normalised: slashes and hyphens become underscores and
    /// the result is uppercased (e.g. `myapp/db-password` → `MYAPP_DB_PASSWORD`).
    ///
    #[command(
        after_help = "Examples:\n  tsafe aws-pull --region us-east-1\n  tsafe aws-pull --prefix myapp/ --overwrite"
    )]
    #[cfg(feature = "cloud-pull-aws")]
    AwsPull {
        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
        #[arg(long)]
        region: Option<String>,
        /// Only import secrets whose names start with this prefix.
        #[arg(long)]
        prefix: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
    },

    /// Import secrets from GCP Secret Manager into the local vault.
    ///
    /// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
    /// metadata server, or ADC file (gcloud auth application-default login).
    ///
    /// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
    ///
    /// Secret names are normalised: hyphens and dots become underscores and
    /// the result is uppercased (e.g. `db-password` → `DB_PASSWORD`).
    ///
    #[command(
        after_help = "Examples:\n  tsafe gcp-pull --project my-gcp-project\n  tsafe gcp-pull --prefix myapp- --overwrite"
    )]
    #[cfg(feature = "cloud-pull-gcp")]
    GcpPull {
        /// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
        #[arg(long)]
        project: Option<String>,
        /// Only import secrets whose names start with this prefix.
        #[arg(long)]
        prefix: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
    },

    /// Push local vault secrets to GCP Secret Manager (upsert semantics).
    ///
    /// Authenticates via (in order): GOOGLE_OAUTH_TOKEN env var, GCE/Cloud Run/GKE
    /// metadata server, or ADC file (gcloud auth application-default login).
    ///
    /// Project is read from GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT or --project.
    ///
    /// GCP Secret Manager uses a two-call pattern for new secrets: create the
    /// secret resource, then add a version. Existing secrets only need a new version.
    ///
    /// Local keys are reverse-normalised to GCP format:
    /// MY_SECRET → my-secret. Two local keys that normalise to the same
    /// provider name are detected as a collision and abort pre-flight.
    ///
    /// Remote-only keys are left untouched unless --delete-missing is passed.
    /// A pre-flight diff is always shown before writing. No secret values
    /// are printed — only key names and 12-char SHA-256 hash prefixes.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "cloud-pull-gcp")]
    GcpPush {
        /// GCP project ID (overrides GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT).
        #[arg(long)]
        project: Option<String>,

        /// Only push secrets whose local key names start with this prefix (case-insensitive).
        #[arg(long)]
        prefix: Option<String>,

        /// Only push secrets in this namespace (stored as `<ns>/KEY`).
        #[arg(long)]
        ns: Option<String>,

        /// Show the diff without writing anything (always exits 0).
        #[arg(long)]
        dry_run: bool,

        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
        #[arg(long)]
        yes: bool,

        /// Also delete remote secrets absent locally within the filtered scope.
        /// Off by default — opt-in to avoid accidental mass deletion.
        /// Note: GCP Secret Manager deletion requires the Secret Manager Admin API.
        #[arg(long)]
        delete_missing: bool,
    },

    /// Import parameters from AWS SSM Parameter Store into the local vault.
    ///
    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
    ///
    /// Region is read from AWS_DEFAULT_REGION / AWS_REGION or --region.
    /// Parameters are fetched recursively under the given path.
    /// SecureString parameters are decrypted automatically (WithDecryption=true).
    ///
    /// Parameter names are normalised: leading `/` stripped, remaining `/` and `-`
    /// become `_`, uppercased (e.g. `/myapp/db-password` → `MYAPP_DB_PASSWORD`).
    ///
    #[command(
        after_help = "Examples:\n  tsafe ssm-pull --region us-east-1 --path /myapp/prod/\n  tsafe ssm-pull --path /shared/ --overwrite"
    )]
    #[cfg(feature = "cloud-pull-aws")]
    SsmPull {
        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
        #[arg(long)]
        region: Option<String>,
        /// Parameter path prefix (e.g. `/myapp/prod/`). Defaults to `/` (all parameters).
        #[arg(long)]
        path: Option<String>,
        /// Overwrite existing local secrets (skip conflicts by default).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for provider/network errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
    },

    /// Push local vault secrets to AWS Secrets Manager (upsert semantics).
    ///
    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
    ///
    /// Local keys are reverse-normalised to AWS Secrets Manager format:
    /// MY_SECRET → my-secret. Two local keys that normalise to the same
    /// provider name are detected as a collision and abort pre-flight.
    ///
    /// Remote-only secrets are left untouched unless --delete-missing is passed.
    /// A pre-flight diff is always shown before writing. No secret values
    /// are printed — only key names and 12-char SHA-256 hash prefixes.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "cloud-pull-aws")]
    AwsPush {
        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
        #[arg(long)]
        region: Option<String>,
        /// Only push secrets whose local key names start with this prefix (case-insensitive).
        #[arg(long)]
        prefix: Option<String>,
        /// Show the diff without writing anything (always exits 0).
        #[arg(long)]
        dry_run: bool,
        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
        #[arg(long)]
        yes: bool,
        /// Also delete remote secrets absent locally within the filtered scope.
        /// Off by default — opt-in to avoid accidental mass deletion.
        #[arg(long)]
        delete_missing: bool,
    },

    /// Push local vault secrets to AWS SSM Parameter Store (upsert semantics).
    ///
    /// Authenticates via (in order): static env vars (AWS_ACCESS_KEY_ID +
    /// AWS_SECRET_ACCESS_KEY), ECS task role, or IMDSv2 (EC2 instance profile).
    ///
    /// Local keys are reverse-normalised to SSM parameter names:
    /// given `--path /myapp/`, MYAPP_DB_PASSWORD → /myapp/db-password.
    ///
    /// Remote-only parameters are left untouched unless --delete-missing is passed.
    /// A pre-flight diff is always shown before writing. No secret values
    /// are printed — only key names and 12-char SHA-256 hash prefixes.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "cloud-pull-aws")]
    SsmPush {
        /// AWS region (overrides AWS_DEFAULT_REGION / AWS_REGION).
        #[arg(long)]
        region: Option<String>,
        /// SSM path prefix that scopes the push (e.g. `/myapp/`).
        #[arg(long)]
        path: Option<String>,
        /// Show the diff without writing anything (always exits 0).
        #[arg(long)]
        dry_run: bool,
        /// Skip the confirmation prompt (required in non-TTY / CI contexts).
        #[arg(long)]
        yes: bool,
        /// Also delete remote parameters absent locally within the path scope.
        /// Off by default — opt-in to avoid accidental mass deletion.
        #[arg(long)]
        delete_missing: bool,
    },

    /// Print a shell completion script and exit.
    ///
    #[command(
        after_help = "Examples:\n  tsafe completions powershell | Out-String | Invoke-Expression"
    )]
    Completions {
        /// Shell to generate completions for.
        shell: Shell,
    },

    /// Output completion candidates for use by shell completion scripts (internal).
    ///
    /// Called by the patched completion scripts generated by `tsafe completions`.
    /// Not intended for direct use.
    #[command(name = "_completions-data", hide = true)]
    CompletionsData {
        /// Type of completion data to emit: `profiles` or `contracts`.
        data_type: String,
    },

    /// Diagnose vault health: file presence, snapshots, env vars, secret expiry, and operator-facing health hints.
    #[command(long_about = DOCTOR_LONG_ABOUT, after_help = DOCTOR_AFTER_HELP)]
    Doctor {
        /// Emit machine-readable JSON and use health exit codes (0=healthy, 1=warning, 2=critical).
        #[arg(long)]
        json: bool,
    },

    /// Explain a concept in the terminal (`exec`, namespaces, compiled agent/browser pull lanes, …).
    ///
    /// Omit the topic to list available explanations.
    ///
    #[command(
        after_help = "Examples:\n  tsafe explain\n  tsafe explain exec\n  tsafe explain exec-security"
    )]
    Explain {
        /// Topic to print (omit to list all topics).
        #[arg(value_name = "TOPIC")]
        topic: Option<crate::explain::ExplainTopic>,
    },

    /// Remove a stale vault lock file (use after a crash leaves the vault locked).
    ///
    /// Deletes `<profile>.vault.lock` if it exists. Safe to run — the lock is
    /// advisory only. Use when `tsafe` reports "vault is locked by another process"
    /// but no other process is actually running.
    ///
    #[command(after_help = "Examples:\n  tsafe unlock\n  tsafe --profile prod unlock")]
    Unlock,

    /// Launch the full-screen interactive terminal UI.
    ///
    /// Supports add/edit/delete/reveal/rotate/snapshot restore and audit log viewing.
    /// Press ? inside the TUI for a contextual keyboard reference.
    ///
    #[command(after_help = "Examples:\n  tsafe ui\n  tsafe --profile prod ui")]
    #[cfg(feature = "tui")]
    Ui,

    /// Render a secret value as a QR code in the terminal.
    ///
    /// Opens the vault, retrieves KEY, prints the QR code to stdout, then waits
    /// for Enter before clearing — so the value is never left on-screen.
    ///
    #[command(after_help = "Examples:\n  tsafe qr WIFI_PASSWORD\n  tsafe qr API_KEY")]
    Qr {
        /// Secret key whose value to render as a QR code.
        key: String,
    },

    /// Store a TOTP secret and retrieve live codes.
    ///
    /// add: store a TOTP seed for the given key
    /// get: compute and print the current 6-digit code
    ///
    #[command(
        after_help = "Examples:\n  tsafe totp add GITHUB_2FA JBSWY3DPEHPK3PXP\n  tsafe totp get GITHUB_2FA"
    )]
    Totp {
        #[command(subcommand)]
        action: TotpAction,
    },

    /// Pin a secret to the top of lists.
    ///
    #[command(
        after_help = "Examples:\n  tsafe pin DB_PASSWORD\n  tsafe --profile prod pin API_KEY"
    )]
    Pin { key: String },

    /// Remove pin from a secret.
    ///
    #[command(after_help = "Examples:\n  tsafe unpin DB_PASSWORD")]
    Unpin { key: String },

    /// Create an alias: ALIAS_NAME resolves to an existing KEY.
    ///
    /// tsafe get ALIAS_NAME returns the value of KEY.
    /// Use tsafe alias --list to view all aliases.
    ///
    #[command(
        after_help = "Examples:\n  tsafe alias DB_PASS DATABASE_PASSWORD\n  tsafe alias --list"
    )]
    Alias {
        /// Key this alias should resolve to (omit with --list to view all aliases).
        target_key: Option<String>,
        /// Name of the alias to create.
        alias_name: Option<String>,
        /// List all aliases in the vault.
        #[arg(long)]
        list: bool,
    },

    /// Replace `{{KEY}}` placeholders in a file with vault secret values.
    ///
    /// Reads the input file, replaces each `{{KEY}}` with the corresponding
    /// vault secret, and writes to stdout (or `--output PATH`).
    ///
    #[command(
        after_help = "Examples:\n  tsafe template config.yml.tmpl > config.yml\n  tsafe template app.conf.tmpl --output app.conf"
    )]
    Template {
        /// Input template file containing {{KEY}} placeholders.
        file: String,
        /// Write output to a file instead of stdout.
        #[arg(short, long)]
        output: Option<String>,
        /// Ignore missing keys instead of failing.
        #[arg(long)]
        ignore_missing: bool,
    },

    /// Read stdin and replace any vault secret values with [REDACTED].
    ///
    /// Useful for piping logs through to scrub sensitive values.
    ///
    #[command(
        after_help = "Examples:\n  cargo test 2>&1 | tsafe redact\n  tsafe exec -- myapp | tsafe redact"
    )]
    Redact,

    /// Show the active build profile label and compile-time capabilities.
    ///
    /// This reports the compiled truth for the running `tsafe` binary only.
    /// Companion runtimes such as `tsafe-agent` have separate install and release truth.
    #[command(name = "build-info", after_help = BUILD_INFO_AFTER_HELP)]
    BuildInfo {
        /// Emit machine-readable JSON output.
        #[arg(long)]
        json: bool,
    },

    #[cfg(feature = "plugins")]
    /// Run a tool with its required vault secrets injected automatically.
    ///
    /// Each plugin knows which vault keys map to which environment variables for the
    /// named tool.  Run `tsafe plugin` (no args) to list available plugins.
    ///
    /// Missing optional keys are silently skipped; missing required keys abort with an error.
    ///
    #[command(
        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)"
    )]
    Plugin {
        /// Tool name (e.g. gh, aws, az, docker, npm, pypi, terraform). Omit to list.
        tool: Option<String>,
        /// Arguments to pass to the tool.
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },

    /// Act as a git credential helper (install/get/store/erase protocol).
    ///
    /// Run `tsafe credential-helper install` once to configure git to use tsafe
    /// as the credential store. Git will then call `tsafe credential-helper get`,
    /// `store`, or `erase` automatically.
    ///
    /// In `get` mode, reads protocol/host from stdin and returns username/password
    /// from the vault. Keys are matched by `<HOST>_USERNAME` / `<HOST>_PASSWORD`
    /// pattern, or by tags `host=<HOST>`.
    #[cfg(feature = "git-helpers")]
    #[command(
        name = "credential-helper",
        after_help = "Examples:\n  tsafe credential-helper install\n  tsafe credential-helper install --global\n  git credential fill  # (git calls tsafe automatically after install)"
    )]
    CredentialHelper {
        /// Git credential helper action.
        #[arg(value_enum, default_value = "install")]
        action: CredentialHelperOperation,
        /// For `install`: configure at the --global level (user-wide).
        /// By default, configures the local repository git config.
        #[arg(long)]
        global: bool,
    },

    /// Collaboration service commands (team membership, DEK delivery, recovery).
    ///
    /// Scaffolding only in Tranche 2 — no network calls are made.
    /// Enable with `--features collab`.
    ///
    #[command(
        after_help = "Examples:\n  tsafe collab join <team-id>\n  tsafe collab status <team-id>"
    )]
    #[cfg(feature = "collab")]
    Collab {
        #[command(subcommand)]
        action: CollabAction,
    },

    /// Add an SSH key from the vault to the running ssh-agent.
    ///
    /// The key is passed via stdin to `ssh-add -` so it never touches disk.
    ///
    #[command(after_help = "Examples:\n  tsafe ssh-add SSH_KEY\n  tsafe ssh-add id_ed25519")]
    #[cfg(feature = "ssh")]
    SshAdd {
        /// Vault key name containing the SSH private key.
        key: String,
    },

    /// Import an SSH private key file into the vault.
    ///
    #[command(
        after_help = "Examples:\n  tsafe ssh-import ~/.ssh/id_ed25519\n  tsafe ssh-import ~/.ssh/id_rsa --name SSH_RSA_KEY"
    )]
    #[cfg(feature = "ssh")]
    SshImport {
        /// Path to the SSH private key file.
        path: String,
        /// Vault key name to store under (defaults to filename).
        #[arg(long)]
        name: Option<String>,
        /// Attach tags as KEY=VALUE pairs (repeatable).
        #[arg(short, long = "tag", value_name = "KEY=VALUE")]
        tags: Vec<String>,
    },

    /// SSH key inventory and operations.
    ///
    /// Subcommands: list, public-key, generate, config, agent
    ///
    #[command(
        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)"
    )]
    #[cfg(feature = "ssh")]
    Ssh {
        #[command(subcommand)]
        action: SshAction,
    },

    /// List namespaces or copy/move all keys under one prefix to another.
    ///
    /// A namespace is any key-prefix of the form "<name>/KEY".  They are not stored
    /// explicitly — this command introspects the key names in the vault.
    ///
    #[command(
        after_help = "Examples:\n  tsafe ns list\n  tsafe ns copy prod staging\n  tsafe ns move oldapp newapp --force"
    )]
    Ns {
        #[command(subcommand)]
        action: NsAction,
    },

    /// Pull secrets from all sources defined in `.tsafe.yml`.
    ///
    /// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
    /// and executes each pull source in manifest order (sequential; see ADR-012).
    ///
    /// Use --dry-run to preview which sources would be invoked without making any
    /// live API calls. Note: collision detection is not available in dry-run mode —
    /// detecting key conflicts requires fetching keys from each provider.
    ///
    /// Use --source to narrow execution to one or more named sources. Sources are
    /// named with the `name` field in the manifest. Multiple --source flags are OR'd.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "multi-pull")]
    Pull {
        /// Path to config file (auto-detected if omitted).
        #[arg(long)]
        config: Option<String>,
        /// Overwrite all existing secrets (overrides per-source settings).
        #[arg(long)]
        overwrite: bool,
        /// Failure handling mode for source errors in multi-source pull.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PullOnError,
        /// Preview which sources would be invoked without making any live API calls.
        /// Collision detection is not available in dry-run mode.
        #[arg(long)]
        dry_run: bool,
        /// Narrow execution to sources with this `name` label (repeatable).
        /// Sources without a `name` field are excluded when any --source filter is active.
        #[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
        sources: Vec<String>,
    },

    /// Push local vault secrets to all destinations defined in `.tsafe.yml`.
    ///
    /// Searches upward from the current directory for `.tsafe.yml` or `.tsafe.json`
    /// and executes each push destination in manifest order (sequential; see ADR-030).
    ///
    /// Use --dry-run to preview which destinations would be invoked without making
    /// any live API calls or writes.
    ///
    /// Use --source to narrow execution to one or more named destinations. Destinations
    /// are named with the `name` field in the manifest. Multiple --source flags are OR'd.
    ///
    /// A pre-flight diff is shown before any writes. Secret values are never printed —
    /// only key names and 12-char SHA-256 hash prefixes are shown (ADR-030).
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "akv-pull")]
    Push {
        /// Path to config file (auto-detected if omitted).
        #[arg(long, value_name = "PATH")]
        config: Option<std::path::PathBuf>,
        /// Narrow execution to destinations with this `name` label (repeatable).
        /// Destinations without a `name` field are excluded when any --source filter is active.
        #[arg(long = "source", value_name = "LABEL", action = clap::ArgAction::Append)]
        source: Vec<String>,
        /// Show the diff without writing anything (always exits 0).
        #[arg(long)]
        dry_run: bool,
        /// Skip confirmation prompts (required in non-TTY / CI contexts).
        #[arg(long)]
        yes: bool,
        /// Also delete remote secrets that are absent locally within each destination's scope.
        /// Off by default — opt-in to avoid accidental mass deletion (ADR-030).
        #[arg(long)]
        delete_missing: bool,
        /// Failure handling mode for destination errors.
        #[arg(long, value_enum, default_value = "fail-all")]
        on_error: PushOnError,
    },

    /// Synchronise a vault file with a git remote.
    ///
    /// Fetches the remote branch, performs a per-key three-way merge between
    /// the common ancestor, the local vault, and the remote vault, then commits
    /// and pushes the merged result.
    ///
    /// Conflicts (both sides edited the same key) are resolved by last-write-wins
    /// using the secret's `updated_at` timestamp. Conflicts are reported but do
    /// not block the sync.
    ///
    #[command(
        after_help = "Examples:\n  tsafe sync\n  tsafe sync --remote origin --branch main\n  tsafe sync --dry-run"
    )]
    #[cfg(feature = "git-helpers")]
    #[command(name = "sync")]
    Sync {
        /// Git remote name.
        #[arg(long, default_value = "origin")]
        remote: String,
        /// Git branch to sync with.
        #[arg(long, default_value = "main")]
        branch: String,
        /// Vault file path relative to repo root (auto-detected if omitted).
        #[arg(long)]
        file: Option<String>,
        /// Show what would change without modifying anything.
        #[arg(long)]
        dry_run: bool,
    },

    /// Manage team vaults (multi-recipient age encryption).
    ///
    /// Team vaults use X25519 (age) keypairs so multiple people can decrypt
    /// the same vault without sharing a password.
    ///
    #[command(
        after_help = "Examples:\n  tsafe team init --identity ~/.age/key.txt\n  tsafe team add-member age1qyqszqgpqyqszqgpqyqszqgpqyqszqgp...\n  tsafe team members"
    )]
    #[cfg(feature = "team-core")]
    Team {
        #[command(subcommand)]
        action: TeamAction,
    },

    /// Enable or disable biometric / keyring unlock for the current profile.
    ///
    /// When enabled, the vault password is stored in the OS credential store
    /// (macOS Keychain, Windows Credential Manager, Linux Secret Service).
    /// The credential store is itself protected by biometric or PIN.
    ///
    /// After `tsafe init`, the CLI may offer the same setup interactively ("quick unlock").
    /// You can always run `biometric enable` later if you skipped it.
    ///
    #[command(
        after_help = "Examples:\n  tsafe biometric enable\n  tsafe biometric disable\n  tsafe biometric status"
    )]
    #[cfg(feature = "biometric")]
    Biometric {
        #[command(subcommand)]
        action: BiometricAction,
    },

    /// Manage the per-process vault unlock agent.
    ///
    /// `tsafe agent unlock` prints terminal approval text, may show an OS notification,
    /// then prompts for the vault password once and starts a background agent that holds
    /// it in memory.  The token it prints must be set in the calling process's environment
    /// as `TSAFE_AGENT_SOCK` — all subsequent `tsafe` invocations that inherit that
    /// env var will be granted vault access without re-entering the password.
    ///
    /// Requests must present the session token and come from a live OS-reported peer
    /// PID; the unlock process PID is recorded for audit/context, not as the only
    /// process allowed to use the session.
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "agent")]
    Agent {
        #[command(subcommand)]
        action: AgentAction,
    },

    /// Run a git command with vault credentials injected automatically.
    ///
    /// Opens the vault, reads `ADO_PAT` (or the key named by TSAFE_GIT_PAT_KEY),
    /// and injects it as a git `http.extraHeader` so HTTPS remotes authenticate
    /// without embedding tokens in URLs.
    ///
    /// Detects the nearest `.git` directory automatically — no repo flags needed.
    /// Exits with git's exit code.
    ///
    /// Override the PAT key name:  $env:TSAFE_GIT_PAT_KEY = "MY_GIT_PAT"
    ///
    #[command(
        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"
    )]
    #[cfg(feature = "git-helpers")]
    #[command(name = "git")]
    Git {
        /// git subcommand and its arguments (e.g. `push ado main`).
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
}

/// Collab subcommands — scaffolding for D3.5 (Tranche 2).
/// No network calls in this release; real implementation is Tranche 3+.
#[derive(Subcommand)]
#[cfg(feature = "collab")]
pub enum CollabAction {
    /// Join a collaboration team (scaffolding — prints status and exits 0).
    Join {
        /// Team ID to join.
        team_id: String,
    },
    /// Show collaboration status for a team (scaffolding — prints status and exits 0).
    Status {
        /// Team ID to query.
        team_id: String,
    },
}

#[derive(Subcommand)]
pub enum SnapshotAction {
    /// List snapshots for the current profile.
    List,
    /// Restore the most-recent snapshot, overwriting the current vault.
    Restore,
}

/// Global config.json settings (not tied to `--profile`).
#[derive(Subcommand)]
pub enum ConfigAction {
    /// Show config file path, default profile, exec trust settings, and password-backup settings.
    Show,
    /// 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.
    #[command(name = "set-backup-vault")]
    SetBackupVault {
        /// Target vault profile (`main`, `default`) or `off` to clear.
        target: String,
    },
    /// Persist whether normal vault opens should automatically try OS quick unlock.
    #[command(name = "set-auto-quick-unlock")]
    SetAutoQuickUnlock {
        /// `on` to allow automatic keychain reads, `off` to require agent / env / typed password instead.
        mode: ToggleSetting,
    },
    /// Persist the retry cooldown, in seconds, after an automatic quick-unlock failure.
    #[command(name = "set-quick-unlock-retry-cooldown")]
    SetQuickUnlockRetryCooldown {
        /// Seconds to wait before the next automatic keychain attempt. Use `0` to disable the cooldown.
        seconds: u64,
    },
    /// Persist the default exec trust mode.
    #[command(name = "set-exec-mode")]
    SetExecMode {
        /// One of: `standard`, `hardened`, `custom`.
        mode: ExecModeSetting,
    },
    /// Persist whether `tsafe exec` should redact child stdout/stderr by default.
    #[command(name = "set-exec-redact-output")]
    SetExecRedactOutput {
        /// `on` to redact child output by default, `off` to leave it raw unless `--redact-output` is passed.
        mode: ToggleSetting,
    },
    /// Persist the inherit strategy used when exec mode is `custom`.
    #[command(name = "set-exec-custom-inherit")]
    SetExecCustomInherit {
        /// One of: `full`, `minimal`, `clean`.
        mode: ExecCustomInheritSetting,
    },
    /// Persist whether dangerous injected env names should abort exec when mode is `custom`.
    #[command(name = "set-exec-custom-deny-dangerous-env")]
    SetExecCustomDenyDangerousEnv {
        /// `on` to abort, `off` to warn only.
        mode: ToggleSetting,
    },
    /// Add a parent environment variable name to the extra strip list for `tsafe exec`.
    #[command(name = "add-exec-extra-strip")]
    AddExecExtraStrip {
        /// Environment variable name, e.g. OPENAI_API_KEY.
        name: String,
    },
    /// Remove a parent environment variable name from the extra strip list for `tsafe exec`.
    #[command(name = "remove-exec-extra-strip")]
    RemoveExecExtraStrip {
        /// Environment variable name, e.g. OPENAI_API_KEY.
        name: String,
    },
}

#[derive(Clone, Copy, ValueEnum)]
pub enum ToggleSetting {
    /// Enable the setting.
    On,
    /// Disable the setting.
    Off,
}

#[derive(Clone, Copy, ValueEnum)]
pub enum ExecModeSetting {
    /// Broad compatibility: full inherited env (minus strip list), raw output, and abort on dangerous injected names by default.
    Standard,
    /// Stricter preset: minimal inherited env, redacted output, and deny dangerous injected names.
    Hardened,
    /// Use persisted custom exec trust settings from config.json.
    Custom,
}

/// Controls which host environment variables the child process inherits.
#[derive(Clone, Copy, ValueEnum)]
pub enum ExecPresetSetting {
    /// Inherit only PATH and a safe core set (HOME, USER, TMPDIR, LANG, TERM, SSH_AUTH_SOCK, etc.)
    /// plus vault secrets. No tokens or credentials from the parent environment leak through.
    Minimal,
    /// Inherit the full parent environment minus the known-sensitive strip list. This is the
    /// current default behavior when no preset or inheritance flag is given.
    Full,
}

#[derive(Clone, Copy, ValueEnum)]
pub enum ExecCustomInheritSetting {
    /// Full inherited parent env (minus strip list).
    Full,
    /// Minimal inherited env plus vault secrets.
    Minimal,
    /// No inherited parent env; only vault secrets.
    Clean,
}

#[derive(Subcommand)]
pub enum ProfileAction {
    /// List all profiles that have an existing vault.
    List,
    /// Permanently delete a profile vault.
    Delete {
        name: String,
        /// Skip the confirmation prompt.
        #[arg(long)]
        force: bool,
    },
    /// Set the default profile used when -p / TSAFE_PROFILE is not specified.
    ///
    #[command(after_help = "Examples:\n  tsafe profile set-default work")]
    SetDefault {
        /// Profile name to use as the new default.
        name: String,
    },
    /// Rename a profile (renames the vault file and updates the default if needed).
    ///
    #[command(after_help = "Examples:\n  tsafe profile rename old new")]
    Rename {
        /// Existing profile name.
        from: String,
        /// New profile name.
        to: String,
    },
}

#[derive(Clone, ValueEnum)]
pub enum ExportFormat {
    /// KEY=VALUE (posix, one per line)
    Env,
    /// export KEY="VALUE" (bash/zsh source-able)
    Dotenv,
    /// $env:KEY = "VALUE" (PowerShell source-able)
    Powershell,
    /// JSON object
    Json,
    /// ::add-mask::VALUE + KEY=VALUE (GitHub Actions GITHUB_ENV format)
    GithubActions,
    /// YAML mapping (KEY: "VALUE" per entry)
    Yaml,
    /// KEY=VALUE per line suitable for Docker --env-file (alias for env, Docker-compatible)
    DockerEnv,
    /// TOML flat top-level table (KEY = "VALUE" per entry)
    Toml,
}

#[derive(Clone, ValueEnum)]
pub enum AuditExportFormat {
    /// JSONL (one JSON object per line, same as stored on disk)
    Json,
    /// Splunk HEC-compatible JSON events
    Splunk,
    /// CloudEvents 1.0 JSONL (application/cloudevents+json per line)
    CloudEvents,
}

/// Actions for `tsafe audit` subcommand.
///
/// `Rotate` is a stub reserved for the audit-log rotation handler implemented
/// in `cmd_audit_cmd.rs` by a separate agent.  The variant is declared here so
/// that `cli.rs` is the single source of truth for the CLI surface.
#[derive(Subcommand)]
#[allow(dead_code)]
pub enum AuditAction {
    /// Rotate (trim) the audit log to keep only the most-recent entries.
    ///
    /// Reserved — handler implemented in `cmd_audit_cmd.rs`.
    #[command(
        after_help = "Examples:\n  tsafe audit rotate --keep 1000\n  tsafe audit rotate --max-size-mb 10"
    )]
    Rotate {
        /// Maximum audit log size in megabytes before trimming.
        #[arg(long, default_value_t = 50)]
        max_size_mb: u64,
        /// Number of most-recent entries to keep after trimming.
        #[arg(long, default_value_t = 5000)]
        keep: u32,
    },
}

#[derive(Clone, Copy, ValueEnum)]
pub enum CredentialHelperOperation {
    /// Install tsafe as the git credential helper in git config.
    Install,
    Get,
    Store,
    Erase,
}

#[cfg(feature = "ssh")]
#[derive(Subcommand)]
pub enum SshAction {
    /// List SSH keys stored in the vault (tagged type=ssh or containing PRIVATE KEY).
    #[command(after_help = "Examples:\n  tsafe ssh list")]
    List,

    /// Extract the public key from a stored SSH private key.
    ///
    /// Prints the OpenSSH public key in authorized_keys format to stdout.
    #[command(
        name = "public-key",
        after_help = "Examples:\n  tsafe ssh public-key my_ed25519_key\n  tsafe ssh public-key SSH_ID_ED25519"
    )]
    PublicKey {
        /// Vault key name containing the SSH private key.
        key: String,
    },

    /// Generate a new SSH key pair and store the private key in the vault.
    ///
    /// Uses a CSPRNG (no subprocess). The private key is stored encrypted in
    /// the vault; the public key is printed to stdout.
    #[command(
        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"
    )]
    Generate {
        /// Vault key name to store the generated private key under.
        key: String,
        /// Key type: ed25519 (default, recommended) or rsa.
        #[arg(long, value_name = "TYPE", default_value = "ed25519")]
        r#type: SshKeyType,
        /// RSA key size in bits (only used with --type rsa; default 4096).
        #[arg(long, value_name = "BITS", default_value = "4096")]
        bits: u32,
        /// Comment to embed in the key (e.g. an email address).
        #[arg(long, value_name = "COMMENT")]
        comment: Option<String>,
        /// Print the public key to stdout after storing the private key.
        #[arg(long)]
        print: bool,
    },

    /// Print an ~/.ssh/config snippet that points IdentityAgent at tsafe.
    ///
    /// Pipe or append the output to ~/.ssh/config.
    #[command(
        name = "config",
        after_help = "Examples:\n  tsafe ssh config\n  tsafe ssh config --host '*.corp.example'\n  tsafe ssh config >> ~/.ssh/config"
    )]
    Config {
        /// SSH Host pattern (defaults to `*`).
        #[arg(long, value_name = "PATTERN")]
        host: Option<String>,
    },

    /// Start a persistent SSH agent serving vault keys on a Unix socket.
    ///
    /// Keys are loaded once at startup and served for the configured TTL.
    /// On Windows this subcommand prints a clear error — Unix socket required.
    ///
    /// Eval idiom:  eval $(tsafe ssh-agent)
    #[command(
        name = "agent",
        after_help = "Examples:\n  eval $(tsafe ssh agent)\n  tsafe ssh agent --ttl 4h\n  tsafe ssh agent --sock /run/user/1000/tsafe.sock"
    )]
    Agent {
        /// How long loaded keys remain valid (e.g. 8h, 30m, 1h30m). Default 8h.
        #[arg(long, value_name = "DURATION")]
        ttl: Option<String>,
        /// Override the Unix socket path.
        #[arg(long, value_name = "PATH")]
        sock: Option<String>,
    },
}

#[cfg(feature = "ssh")]
#[derive(Clone, Copy, ValueEnum)]
pub enum SshKeyType {
    Ed25519,
    Rsa,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum PullOnError {
    /// Abort immediately on first source/provider error.
    FailAll,
    /// Skip failed source and continue remaining sources.
    SkipFailed,
    /// Continue and only warn on source/provider errors.
    WarnOnly,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum PushOnError {
    /// Abort immediately on first source error.
    FailAll,
    /// Log the error, skip the failed source, and continue with the next.
    SkipFailed,
}

#[derive(Subcommand)]
pub enum BrowserProfileAction {
    /// Add or update a domain → vault-profile mapping.
    ///
    /// DOMAIN may be an exact hostname or a wildcard pattern (e.g. *.corp.example).
    /// Defaults to the active --profile if --profile is omitted.
    ///
    #[command(
        after_help = "Examples:\n  tsafe browser-profile add github.com\n  tsafe browser-profile add corp.example --profile work"
    )]
    Add {
        /// Domain or pattern (e.g. github.com, *.corp.example).
        domain: String,
        /// Vault profile to use for this domain. Defaults to the active --profile.
        #[arg(long)]
        profile: Option<String>,
    },
    /// List all domain → vault-profile mappings.
    ///
    #[command(after_help = "Examples:\n  tsafe browser-profile list")]
    List,
    /// Remove the mapping for DOMAIN.
    ///
    #[command(after_help = "Examples:\n  tsafe browser-profile remove github.com")]
    Remove {
        /// Domain or pattern to remove.
        domain: String,
    },
}

#[derive(Subcommand)]
pub enum BrowserNativeHostAction {
    /// Write the per-browser native-messaging-host manifest pointing at
    /// `tsafe-nativehost`. Per-user; never elevates. On Windows it writes
    /// Chromium-family HKCU registry keys for a 32-char extension ID, or a
    /// Firefox filesystem manifest under `%APPDATA%\Mozilla\NativeMessagingHosts\`
    /// for an email-style or UUID-style Firefox addon ID. On macOS/Linux it
    /// skips browsers that are not installed.
    ///
    /// `--extension-id` is REQUIRED — defaulting to a known development ID
    /// would let any installed extension with that ID talk to your vault.
    /// Chromium ID: 32-char string at `chrome://extensions` (Developer mode).
    /// Firefox ID: the `gecko.id` value from `browser_specific_settings` in
    /// the extension manifest (e.g. `tsafe@tsafe.dev`).
    Register {
        /// The extension ID to allow. Chromium-family: 32-character lowercase
        /// ID from `chrome://extensions`. Firefox: email-style addon ID
        /// (e.g. `tsafe@tsafe.dev`) or UUID in curly braces.
        #[arg(long)]
        extension_id: String,
    },
    /// Remove the per-browser manifest files (and HKCU keys on Windows).
    Unregister,
    /// Detect the native-host binary location and print the manifest paths that
    /// `register` would write for each installed browser — without writing
    /// anything. Use this when you do not yet know your extension ID:
    ///
    ///   1. Run `tsafe browser-native-host detect` to confirm the binary is
    ///      found and see which browsers are detected.
    ///   2. Load the extension in your browser, find your extension ID at
    ///      `chrome://extensions` (Developer mode), then run:
    ///      tsafe browser-native-host register --extension-id <id>
    ///
    /// On Windows, prints the HKCU registry keys and manifest directory that
    /// would be written; never modifies registry or filesystem.
    Detect,
}

#[derive(Clone, Copy, ValueEnum)]
pub enum TotpAlgorithm {
    /// HMAC-SHA1 (default; most compatible)
    Sha1,
    /// HMAC-SHA256
    Sha256,
    /// HMAC-SHA512
    Sha512,
}

impl TotpAlgorithm {
    pub fn as_uri_str(self) -> &'static str {
        match self {
            Self::Sha1 => "SHA1",
            Self::Sha256 => "SHA256",
            Self::Sha512 => "SHA512",
        }
    }
}

#[derive(Subcommand)]
pub enum TotpAction {
    /// Store a TOTP seed for KEY. Accepts a raw base32 secret or an otpauth:// URI.
    ///
    #[command(
        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"
    )]
    Add {
        /// Vault key name to store under.
        key: String,
        /// Base32-encoded TOTP secret or full otpauth:// URI.
        secret: String,
        /// HMAC algorithm to use (default: sha1, most widely supported).
        #[arg(long, default_value = "sha1")]
        algorithm: TotpAlgorithm,
        /// Number of digits in each OTP code (default: 6; some services use 8).
        #[arg(long, default_value_t = 6)]
        digits: u32,
        /// Time step in seconds (default: 30; some services use 60).
        #[arg(long, default_value_t = 30)]
        period: u64,
    },
    /// Print the current TOTP code + seconds remaining.
    ///
    #[command(after_help = "Examples:\n  tsafe totp get GITHUB_2FA")]
    Get {
        /// Vault key name where the TOTP seed is stored.
        key: String,
    },
}

#[derive(Subcommand)]
pub enum NsAction {
    /// List all namespaces present in the vault (inferred from key prefixes).
    List,
    /// Copy every secret under FROM/ to TO/ (same suffix). Source keys stay.
    ///
    #[command(after_help = "Examples:\n  tsafe ns copy prod staging")]
    Copy {
        /// Namespace prefix to read from (keys must be `FROM/...`).
        from: String,
        /// Namespace prefix to write to (`TO/<same-suffix>`).
        to: String,
        /// Overwrite destination keys if they already exist.
        #[arg(long)]
        force: bool,
    },
    /// Move every secret under FROM/ to TO/ (same suffix). Source keys are removed.
    ///
    #[command(after_help = "Examples:\n  tsafe ns move prod staging")]
    Move {
        /// Namespace prefix to read from and delete after rename.
        from: String,
        /// Namespace prefix to write to (`TO/<same-suffix>`).
        to: String,
        /// Overwrite destination keys if they already exist.
        #[arg(long)]
        force: bool,
    },
}

#[derive(Subcommand)]
pub enum AgentAction {
    /// Prompt for approval + vault password, then start the background agent daemon.
    ///
    /// Prints a shell export line to stdout:
    ///   $env:TSAFE_AGENT_SOCK = "..."   # PowerShell
    ///   export TSAFE_AGENT_SOCK="..."   # bash/zsh
    ///
    /// Copy-paste or eval this line in the calling shell/process that needs access.
    Unlock {
        /// Idle TTL — how long the agent stays alive without a vault request.
        /// Resets on each vault access. Common values include 15m, 1h, and 4h. Default: 30m.
        #[arg(long, default_value = "30m")]
        ttl: String,
        /// Absolute TTL — hard cap regardless of activity. Default: 8h.
        /// Must be >= idle TTL. Common values include 8h, 12h, and 24h.
        #[arg(long, default_value = "8h")]
        absolute_ttl: String,
    },
    /// Immediately revoke the current session and stop the agent.
    Lock,
    /// Show whether the current agent socket is reachable.
    ///
    /// Use `--json` for a stable machine-readable output (ADR-029). Consumers such
    /// as the VS Code extension and tray agent depend on this flag. The schema
    /// `version` field must be checked before reading any other field.
    #[command(
        after_help = "Examples:\n  tsafe agent status\n  tsafe agent status --json\n  tsafe --profile prod agent status --json"
    )]
    Status {
        /// Emit a stable JSON object to stdout (schema version \"1\").
        /// See docs/decisions/agent-status-json-contract.md (ADR-029) for the full schema.
        #[arg(long)]
        json: bool,
    },
}

#[derive(Subcommand)]
pub enum TeamAction {
    /// Create a new team vault encrypted to your age identity.
    ///
    #[command(after_help = "Examples:\n  tsafe team init --identity ~/.age/key.txt")]
    Init {
        /// Path to your age identity file (contains AGE-SECRET-KEY-1...).
        #[arg(long)]
        identity: String,
    },
    /// Add a team member by their age public key.
    ///
    #[command(after_help = "Examples:\n  tsafe team add-member age1qyqszqgp...")]
    AddMember {
        /// age X25519 public key (starts with "age1...").
        public_key: String,
        /// Path to your age identity file (for re-wrapping the DEK).
        #[arg(long)]
        identity: String,
    },
    /// Remove a team member and re-encrypt all secrets with a new key.
    ///
    #[command(after_help = "Examples:\n  tsafe team remove-member age1qyqszqgp...")]
    RemoveMember {
        /// age X25519 public key to remove.
        public_key: String,
        /// Path to your age identity file (for re-keying).
        #[arg(long)]
        identity: String,
    },
    /// List current team members (public keys).
    Members,
    /// Generate a new age identity (keypair) and print the JSON block to add
    /// to `.tsafe/team-keys.json` via a PR.
    ///
    /// The private key is saved to `~/.age/tsafe-<profile>.txt`.
    /// The public key is printed as a ready-to-paste JSON entry.
    ///
    #[command(
        after_help = "Examples:\n  tsafe team keygen\n  tsafe team keygen --name \"Alice Smith\" --email alice@corp.example"
    )]
    Keygen {
        /// Your display name for the team-keys entry.
        #[arg(long)]
        name: Option<String>,
        /// Your email for the team-keys entry.
        #[arg(long)]
        email: Option<String>,
    },
    /// Print your age public key from an existing identity file.
    ///
    #[command(
        after_help = "Examples:\n  tsafe team show-key\n  tsafe team show-key --identity ~/.age/key.txt"
    )]
    ShowKey {
        /// Path to identity file (default: ~/.age/tsafe-<profile>.txt).
        #[arg(long)]
        identity: Option<String>,
    },
    /// Reconcile vault recipients with `.tsafe/team-keys.json`.
    ///
    /// Adds any new members found in the keys file. Removes members no longer
    /// listed. Re-keys the vault if the member list changed.
    ///
    #[command(after_help = "Examples:\n  tsafe team sync-keys --identity ~/.age/key.txt")]
    SyncKeys {
        /// Path to your age identity file (required for re-wrapping the DEK).
        #[arg(long)]
        identity: String,
        /// Path to team-keys.json (auto-detected if omitted).
        #[arg(long)]
        keys_file: Option<String>,
    },
}

#[derive(Subcommand)]
pub enum BiometricAction {
    /// Store the vault password in the OS credential store (same as accepting quick unlock after `tsafe init`).
    Enable,
    /// Remove the vault password from the OS credential store.
    Disable,
    /// Check if biometric/keyring unlock is configured for this profile.
    Status,
    /// Re-enroll biometric/keyring unlock after a stale-credential error.
    ///
    /// Use this when `tsafe` reports "stale biometric credential" — for example after
    /// rotating the vault password (`tsafe rotate`) or after enrolling a new fingerprint.
    /// This is equivalent to `tsafe biometric disable` followed by `tsafe biometric enable`
    /// but makes the recovery intent explicit and prints a confirmation.
    #[command(name = "re-enroll")]
    ReEnroll,
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::CommandFactory;

    fn has_subcommand(command: &clap::Command, name: &str) -> bool {
        command
            .get_subcommands()
            .any(|subcommand| subcommand.get_name() == name)
    }

    #[test]
    fn root_command_visibility_matches_feature_gates() {
        let command = Cli::command();

        assert_eq!(has_subcommand(&command, "ui"), cfg!(feature = "tui"));
        assert_eq!(
            has_subcommand(&command, "kv-pull"),
            cfg!(feature = "akv-pull")
        );
        assert_eq!(
            has_subcommand(&command, "aws-pull"),
            cfg!(feature = "cloud-pull-aws")
        );
        assert_eq!(
            has_subcommand(&command, "gcp-pull"),
            cfg!(feature = "cloud-pull-gcp")
        );
        assert_eq!(
            has_subcommand(&command, "gcp-push"),
            cfg!(feature = "cloud-pull-gcp")
        );
        assert_eq!(
            has_subcommand(&command, "ssm-pull"),
            cfg!(feature = "cloud-pull-aws")
        );
        assert_eq!(
            has_subcommand(&command, "aws-push"),
            cfg!(feature = "cloud-pull-aws")
        );
        assert_eq!(
            has_subcommand(&command, "ssm-push"),
            cfg!(feature = "cloud-pull-aws")
        );
        assert_eq!(
            has_subcommand(&command, "vault-pull"),
            cfg!(feature = "cloud-pull-vault")
        );
        assert_eq!(
            has_subcommand(&command, "op-pull"),
            cfg!(feature = "cloud-pull-1password")
        );
        assert_eq!(
            has_subcommand(&command, "pull"),
            cfg!(feature = "multi-pull")
        );
        assert_eq!(
            has_subcommand(&command, "share-once"),
            cfg!(feature = "ots-sharing")
        );
        assert_eq!(
            has_subcommand(&command, "receive-once"),
            cfg!(feature = "ots-sharing")
        );
        assert_eq!(
            has_subcommand(&command, "browser-profile"),
            cfg!(feature = "browser")
        );
        assert_eq!(
            has_subcommand(&command, "browser-native-host"),
            cfg!(feature = "nativehost")
        );
        assert_eq!(has_subcommand(&command, "ssh-add"), cfg!(feature = "ssh"));
        assert_eq!(
            has_subcommand(&command, "ssh-import"),
            cfg!(feature = "ssh")
        );
        assert_eq!(
            has_subcommand(&command, "plugin"),
            cfg!(feature = "plugins")
        );
        assert_eq!(
            has_subcommand(&command, "hook-install"),
            cfg!(feature = "git-helpers")
        );
        assert_eq!(
            has_subcommand(&command, "git"),
            cfg!(feature = "git-helpers")
        );
        assert_eq!(
            has_subcommand(&command, "sync"),
            cfg!(feature = "git-helpers")
        );
        assert_eq!(
            has_subcommand(&command, "credential-helper"),
            cfg!(feature = "git-helpers")
        );
        assert_eq!(
            has_subcommand(&command, "biometric"),
            cfg!(feature = "biometric")
        );
        assert_eq!(
            has_subcommand(&command, "team"),
            cfg!(feature = "team-core")
        );
        assert_eq!(has_subcommand(&command, "agent"), cfg!(feature = "agent"));
    }
}

#[derive(Subcommand)]
pub enum PolicyAction {
    /// Set a rotation policy on a secret.
    ///
    #[command(after_help = "Examples:\n  tsafe policy set DB_PASSWORD --rotate-every 90d")]
    Set {
        /// Secret key.
        key: String,
        /// Rotation interval (e.g. 90d, 30d, 7d).
        #[arg(long)]
        rotate_every: String,
    },
    /// Remove the rotation policy from a secret.
    ///
    #[command(after_help = "Examples:\n  tsafe policy remove DB_PASSWORD")]
    Remove {
        /// Secret key.
        key: String,
    },
}