@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,101 +1,145 @@
1
1
  import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
2
+ import { LOOKUP_EVIDENCE_SCHEMA_VERSION, PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../lib/evidence-export.mjs";
3
+ import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA, } from "../packages/contracts/risk-calibration.mjs";
4
+ import { CASE_SCHEMA_VERSION, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
5
+ const COMMAND_ORDER = Object.freeze([
6
+ 'completion',
7
+ 'doctor',
8
+ 'commands',
9
+ 'manual',
10
+ 'manifest',
11
+ 'map-observations',
12
+ 'oam-export',
13
+ 'lookup',
14
+ 'bulk',
15
+ 'ct-search',
16
+ 'ct-intake',
17
+ 'discover',
18
+ 'discover-scan',
19
+ 'posture',
20
+ 'http',
21
+ 'tls',
22
+ 'dnssec-validate',
23
+ 'mail-transport',
24
+ 'registry-support',
25
+ 'registry-doctor',
26
+ 'registry-cohort',
27
+ 'registry-scaffold',
28
+ 'risk-calibrate',
29
+ 'lookalike-calibrate',
30
+ 'verify-artifact',
31
+ 'interchange-report',
32
+ 'inspect-archive',
33
+ 'sign-artifact',
34
+ 'verify-signature',
35
+ 'source-report',
36
+ 'compare',
37
+ 'page-compare',
38
+ 'mail-review',
39
+ 'review-evidence',
40
+ 'brief',
41
+ 'case-pack',
42
+ 'domain-control',
43
+ 'monitor-once',
44
+ 'assurance',
45
+ 'change-packet',
46
+ 'sharing-review',
47
+ 'workflow-plan',
48
+ 'workflow-run',
49
+ 'diff',
50
+ 'reconcile',
51
+ 'timeline',
52
+ 'export',
53
+ ]);
54
+ const INVESTIGATION_PLAN_RECIPES = Object.freeze([
55
+ 'domain-triage',
56
+ 'lookalike-review',
57
+ 'owned-domain-review',
58
+ 'historical-comparison',
59
+ 'campaign-review',
60
+ 'certificate-anomaly',
61
+ 'registry-disagreement',
62
+ 'evidence-handoff',
63
+ 'planned-domain-change',
64
+ 'post-change-verification',
65
+ ]);
66
+ const RUNNABLE_INVESTIGATION_PLAN_RECIPES = Object.freeze([
67
+ 'domain-triage',
68
+ 'lookalike-review',
69
+ 'owned-domain-review',
70
+ 'historical-comparison',
71
+ ]);
72
+ const CLI_META_ACTIONS = Object.freeze([
73
+ Object.freeze({
74
+ id: 'help',
75
+ aliases: Object.freeze(['--help', '-h']),
76
+ scope: 'root_or_command',
77
+ precedence: 'before_command_grammar',
78
+ bypassesOrdinaryRequirements: true,
79
+ acceptsAdditionalArguments: false,
80
+ }),
81
+ Object.freeze({
82
+ id: 'version',
83
+ aliases: Object.freeze(['--version', '-V']),
84
+ scope: 'root_only',
85
+ precedence: 'before_command_grammar',
86
+ bypassesOrdinaryRequirements: true,
87
+ acceptsAdditionalArguments: false,
88
+ }),
89
+ ]);
90
+ const CLI_META_ACTION_BY_ID = Object.freeze(Object.fromEntries(CLI_META_ACTIONS.map((action) => [action.id, action])));
2
91
  // Human-facing command reference kept separate from command execution so
3
92
  // additions do not expand the already broad runtime controller.
4
- const HELP = `WHOISleuth CLI
5
- Source-aware domain investigation from your terminal.
93
+ const HELP_INTRO = `WHOISleuth CLI
94
+ Domain investigation from your terminal.
6
95
 
7
96
  Quick start:
8
- whoisleuth lookup example.test
97
+ whoisleuth
98
+ whoisleuth example.test
9
99
  whoisleuth lookup example.test --deep
10
100
  cat domains.txt | whoisleuth bulk --jsonl
11
-
12
- Investigate:
13
- lookup Collect one domain, IP, or ASN.
14
- bulk Triage newline-delimited targets with bounded concurrency.
15
- http Inspect one homepage request and redirect chain.
16
- tls Inspect one hostname's current certificate connection.
17
- dnssec-validate Validate one authorised DNSSEC chain in isolation.
18
- mail-transport Review selected authorised MX transports and STARTTLS.
19
- posture Review DNS mail and domain-control posture.
20
-
21
- Discover:
22
- ct-search Search certificate-transparency observations.
23
- ct-intake Normalise a local certificate event batch for case import.
24
- discover Generate offline lookalike candidates.
25
- discover-scan Collect a bounded candidate review queue.
26
- registry-support Explain local registry coverage without a request.
27
- registry-doctor Diagnose a saved registry collection against local policy.
28
- registry-cohort Build target-free registry quality timelines.
29
- registry-scaffold Create a sanitised synthetic fixture scaffold.
30
-
31
- Review saved evidence:
32
- map-observations Apply a declarative profile to local observations.
33
- oam-export Project external findings into an open asset graph.
34
- source-report Summarise source reliability without retaining targets.
35
- compare Compare saved registry publications.
36
- page-compare Compare saved static page and TLS evidence.
37
- mail-review Review saved passive mail exposure evidence.
38
- review-evidence Review supplied DNS, routing, cryptographic, GeoIP, RDAP, or trust-store evidence offline.
39
- brief Turn one saved Lookup into a bounded decision brief.
40
- case-pack Build a reviewed browser-importable case package.
41
- domain-control Build or review an integrity-protected desired-state manifest.
42
- monitor-once Run one bounded domain-control review and checkpoint.
43
- assurance Review change, recovery, concentration, or retirement plans.
44
- change-packet Assemble a digest-protected domain change review packet.
45
- sharing-review Lint a reviewed artifact before deliberate sharing.
46
- workflow-plan Plan a fixed investigation recipe without executing it.
47
- workflow-run Execute resumable approved steps from a fixed recipe.
48
- diff Compare two compatible retained artefacts.
49
- reconcile Reconcile independently labelled observations.
50
- timeline Compare a sequence of observations for one domain.
51
- export Convert a saved lookup into an evidence report.
52
- inspect-archive Inspect a workspace archive, redacted by default.
53
- verify-artifact Validate saved evidence or an integrity envelope offline.
54
- interchange-report Report preserved and excluded field groups offline.
55
-
56
- Integrity and calibration:
57
- manifest Record ordered artefact and configuration digests.
58
- sign-artifact Sign one reviewed packet or manifest locally.
59
- verify-signature Verify one signed evidence package locally.
60
- risk-calibrate Replay reviewed labels without changing the model.
61
- lookalike-calibrate Summarise reviewed mutation-family yield without tuning generation.
62
-
63
- Terminal:
64
- doctor Check the local runtime; network tests require --network.
65
- commands List command contracts for people or local tooling.
66
- completion Print completion for bash, zsh, fish, or PowerShell.
67
- manual Print the generated manual page.
68
-
101
+ `;
102
+ const HELP_FOOTER = `
69
103
  Run "whoisleuth <command> --help" for focused usage and an example.
104
+ With no arguments, an eligible interactive terminal opens a bounded launcher;
105
+ redirected or unsupported terminals continue to print this help. No request
106
+ starts until a Lookup plan is shown and the analyst confirms collection.
70
107
  Use --json or --jsonl where supported for machine-readable stdout.
71
108
  Use --output <file> for atomic private file output and --force to replace it.
109
+ Use --palette auto, light, or dark after the command to select a fixed terminal
110
+ colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
72
111
  Use --config <file> and --profile <name> for explicit versioned safe defaults.
112
+ Registry scaffold is the exception: its --profile selects a fixture capability
113
+ profile and --config is rejected, so bootstrap defaults cannot alter fixtures.
114
+ An ICANN-recognised public domain, reserved documentation domain, IP, or ASN
115
+ may replace "lookup"; URL-like or ambiguous input requires the explicit
116
+ command. Both forms use the same Lookup options.
73
117
  Diagnostics are written to stderr. Fast lookup is the default; deep collection
74
118
  must be requested explicitly and can disclose a target to additional sources.
75
119
 
76
120
  Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
77
121
  Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
78
122
  `;
79
- const COMMAND_USAGE = Object.freeze({
123
+ const COMMAND_USAGE_SEED = Object.freeze({
80
124
  completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
81
125
  doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
82
- commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
126
+ commands: 'whoisleuth commands [--common] [--group <group>] [--mode <offline|network>] [--json] [--quiet] [--no-color]',
83
127
  manual: 'whoisleuth manual',
84
128
  manifest: 'whoisleuth manifest <artefact.json> [...] --workflow <label> [--configuration-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
85
129
  'map-observations': 'whoisleuth map-observations [mapping.json] [--json] [--quiet] [--no-color]',
86
130
  'oam-export': 'whoisleuth oam-export [external-findings.json] [--json] [--quiet] [--no-color]',
87
- lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--junit|--markdown|--html] [--no-attribution] [--fast|--deep] [--observer <label>] [--vantage <label>] [--plan] [--summary|--verbose] [--strict-exit] [--fail-on <policies>] [--events] [--quiet] [--no-color]',
88
- bulk: 'whoisleuth bulk [file] [--json|--jsonl|--junit|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events] [--plan] [--fail-on <policies>]',
89
- 'ct-search': 'whoisleuth ct-search <keyword> [--json] [--quiet] [--no-color]',
131
+ lookup: 'whoisleuth lookup [domain|IP|ASN] [--json|--junit|--markdown|--html] [--no-attribution] [--fast|--deep] [--observer <label>] [--vantage <label>] [--plan] [--summary|--verbose|--browse [--save-lookup <file>]] [--palette <auto|light|dark>] [--strict-exit] [--fail-on <policies>] [--events] [--quiet] [--no-color]',
132
+ bulk: 'whoisleuth bulk [file] [--json|--jsonl|--junit|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events] [--plan] [--fail-on <policies>] [--quiet] [--no-color]',
133
+ 'ct-search': 'whoisleuth ct-search [keyword] [--json] [--quiet] [--no-color]',
90
134
  'ct-intake': 'whoisleuth ct-intake [events.json] [--json] [--quiet] [--no-color]',
91
- discover: 'whoisleuth discover <brand|domain> [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains]',
92
- 'discover-scan': 'whoisleuth discover-scan <brand|domain> [--fast|--deep] [--scan-limit <n>] [--chunk-size <n>] [--concurrency <n>] [--resolver <IPs>] [--allowlist <file>] [--checkpoint <file> [--resume]] [--observation-snapshot <file>] [--json|--jsonl|--csv|--domains] [--plan] [--fail-on <policies>]',
93
- posture: 'whoisleuth posture <domain> [--selectors <list>] [--retired-selectors <list>] [--mail-profile <profile>] [--json|--sarif --owned-domain] [--quiet] [--no-color]',
94
- http: 'whoisleuth http <domain> [--json] [--quiet] [--no-color]',
95
- tls: 'whoisleuth tls <hostname> [--json] [--quiet] [--no-color]',
135
+ discover: 'whoisleuth discover [brand|domain] [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains] [--quiet] [--no-color]',
136
+ 'discover-scan': 'whoisleuth discover-scan [brand|domain] [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--fast|--deep] [--scan-limit <n>] [--chunk-size <n>] [--concurrency <n>] [--resolver <IPs>] [--allowlist <file>] [--checkpoint <file> [--resume]] [--observation-snapshot <file>] [--registered-only|--inconclusive-only|--acquisition-only|--suppressed-only] [--events] [--json|--jsonl|--csv|--domains] [--plan] [--fail-on <policies>] [--quiet] [--no-color]',
137
+ posture: 'whoisleuth posture [domain] [--selectors <list>] [--retired-selectors <list>] [--mail-profile <profile>] [--json|--sarif --owned-domain] [--quiet] [--no-color]',
138
+ http: 'whoisleuth http [domain] [--json] [--quiet] [--no-color]',
139
+ tls: 'whoisleuth tls [hostname] [--json] [--quiet] [--no-color]',
96
140
  'dnssec-validate': 'whoisleuth dnssec-validate <domain> --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized [--json] [--quiet] [--no-color]',
97
141
  'mail-transport': 'whoisleuth mail-transport [input.json] --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized --active-probe [--json] [--quiet] [--no-color]',
98
- 'registry-support': 'whoisleuth registry-support <domain|suffix> [--json] [--quiet] [--no-color]',
142
+ 'registry-support': 'whoisleuth registry-support [domain|suffix] [--json] [--quiet] [--no-color]',
99
143
  'registry-doctor': 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
100
144
  'registry-cohort': 'whoisleuth registry-cohort [lookups-or-reports.json|jsonl] [--json] [--quiet] [--no-color]',
101
145
  'registry-scaffold': 'whoisleuth registry-scaffold --profile <id> --suffix <suffix> --scenario <registered|not_found|inconclusive>',
@@ -103,7 +147,7 @@ const COMMAND_USAGE = Object.freeze({
103
147
  'lookalike-calibrate': 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
104
148
  'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--manifest <manifest.json> --manifest-entry <artifact-N>] [--json] [--strict-exit] [--quiet] [--no-color]',
105
149
  'interchange-report': 'whoisleuth interchange-report [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
106
- 'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json]',
150
+ 'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
107
151
  'sign-artifact': 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
108
152
  'verify-signature': 'whoisleuth verify-signature [package.json] [--public-key-file <file>] [--json] [--quiet] [--no-color]',
109
153
  'source-report': 'whoisleuth source-report [lookup.json] [--json] [--quiet] [--no-color]',
@@ -117,15 +161,15 @@ const COMMAND_USAGE = Object.freeze({
117
161
  'monitor-once': 'whoisleuth monitor-once [manifest.json] [--previous <snapshot.json>] [--limit <1-20>] [--concurrency <1-3>] [--fail-on <policies>] [--json|--junit] [--quiet] [--no-color]',
118
162
  assurance: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
119
163
  'change-packet': 'whoisleuth change-packet [change-packet-input.json] [--json] [--quiet] [--no-color]',
120
- 'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json]',
121
- 'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> [--json] [--quiet] [--no-color]',
164
+ 'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json] [--quiet] [--no-color]',
165
+ 'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> | --list | --explain <recipe> [--json] [--quiet] [--no-color]',
122
166
  'workflow-run': 'whoisleuth workflow-run <recipe> <domain|brand> [--approve-network] [--resume <state.json>] [--json] [--quiet] [--no-color]',
123
167
  diff: 'whoisleuth diff <left.json> <right.json> [--left-session <id> --right-session <id>] [--json] [--quiet] [--no-color]',
124
168
  reconcile: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
125
169
  timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
126
170
  export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact] [--no-attribution]',
127
171
  });
128
- const COMMAND_DETAILS = Object.freeze({
172
+ const COMMAND_DETAILS_SEED = Object.freeze({
129
173
  completion: {
130
174
  description: 'Print a static shell-completion script for the installed CLI.',
131
175
  example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
@@ -163,8 +207,8 @@ const COMMAND_DETAILS = Object.freeze({
163
207
  },
164
208
  lookup: {
165
209
  description: 'Collect registration evidence for one domain, IP, or ASN.',
166
- example: 'whoisleuth lookup example.test --deep',
167
- boundary: 'Fast is the default. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable.',
210
+ example: 'whoisleuth lookup example.test --deep --browse',
211
+ boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
168
212
  },
169
213
  bulk: {
170
214
  description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
@@ -199,7 +243,7 @@ const COMMAND_DETAILS = Object.freeze({
199
243
  http: {
200
244
  description: 'Inspect one homepage request, redirects, and bounded response metadata.',
201
245
  example: 'whoisleuth http example.test --json',
202
- boundary: 'Requests use the shared public-address and redirect guards. This is not a rendered browser or vulnerability scan.',
246
+ boundary: 'Requests use the shared public-address and redirect guards. Fixed content-coding and cache-policy metadata describes only the selected response, excludes raw header values, and does not prove caching, transfer savings, performance, privacy, or safety. This is not a rendered browser or vulnerability scan.',
203
247
  },
204
248
  tls: {
205
249
  description: 'Inspect one hostname certificate through a bounded TLS connection.',
@@ -234,7 +278,7 @@ const COMMAND_DETAILS = Object.freeze({
234
278
  'registry-scaffold': {
235
279
  description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
236
280
  example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
237
- boundary: 'The output is a sanitised template only. Contributors must not paste live responses or personal registration data into fixtures.',
281
+ boundary: 'The output is a sanitised template only. Its command-owned --profile selects fixture capability, --config is rejected, and contributors must not paste live responses or personal registration data into fixtures.',
238
282
  },
239
283
  'risk-calibrate': {
240
284
  description: 'Replay reviewed labels against the current explainable Risk model.',
@@ -257,9 +301,9 @@ const COMMAND_DETAILS = Object.freeze({
257
301
  boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
258
302
  },
259
303
  'inspect-archive': {
260
- description: 'Summarise or search one workspace archive with redacted output by default.',
304
+ description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact public version-${PUBLIC_WORKSPACE_ARCHIVE_VERSION} support and redacted output by default.`,
261
305
  example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
262
- boundary: 'Exact matches require --reveal. Content identity excludes export time and formatting. The archive is read locally and is never uploaded.',
306
+ boundary: 'Exact matches require --reveal. Retired and future archive versions are rejected without changing data. The archive is read locally and is never uploaded.',
263
307
  },
264
308
  'sign-artifact': {
265
309
  description: 'Sign one reviewed response packet or supported manifest with a local private key.',
@@ -267,9 +311,9 @@ const COMMAND_DETAILS = Object.freeze({
267
311
  boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
268
312
  },
269
313
  'verify-signature': {
270
- description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artifact assurance separately.',
314
+ description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artefact assurance separately.',
271
315
  example: 'whoisleuth verify-signature packet.signed.json --json',
272
- boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-artifact assurance or establish the holder\'s real-world identity or authority.',
316
+ boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-artefact assurance or establish the holder\'s real-world identity or authority.',
273
317
  },
274
318
  'source-report': {
275
319
  description: 'Create a target-free reliability summary from a saved lookup.',
@@ -302,7 +346,7 @@ const COMMAND_DETAILS = Object.freeze({
302
346
  boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
303
347
  },
304
348
  'case-pack': {
305
- description: 'Build a reviewed, audience-specific and browser-importable case package.',
349
+ description: `Build a reviewed, audience-specific Case-pack v2 from an exact Case-schema-${CASE_SCHEMA_VERSION} export.`,
306
350
  example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
307
351
  boundary: 'The command is offline, creates a new package, never mutates the source archive, and requires an explicit review acknowledgement.',
308
352
  },
@@ -359,10 +403,10 @@ const COMMAND_DETAILS = Object.freeze({
359
403
  export: {
360
404
  description: 'Convert one saved lookup into a versioned evidence report.',
361
405
  example: 'whoisleuth export lookup.json --markdown',
362
- boundary: 'The saved Lookup is capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Exports preserve evidence-source attribution and limitations. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.',
406
+ boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; exact public schema ${PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remains readable with its strict source/publication binding, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
363
407
  },
364
408
  });
365
- const COMMAND_COLLECTION = Object.freeze({
409
+ const COMMAND_COLLECTION_SEED = Object.freeze({
366
410
  completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
367
411
  doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
368
412
  commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
@@ -376,7 +420,7 @@ const COMMAND_COLLECTION = Object.freeze({
376
420
  'ct-intake': { mode: 'offline', scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
377
421
  discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
378
422
  'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
379
- posture: { mode: 'network', scope: 'Accepts one domain and performs bounded DNS queries only.' },
423
+ posture: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests.' },
380
424
  http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
381
425
  tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
382
426
  'dnssec-validate': { mode: 'network', scope: 'Accepts one authorised domain, one public resolver IP, and one local trust-anchor file; DNS-over-TCP validation is capped at 32 queries and 15 seconds.' },
@@ -389,7 +433,7 @@ const COMMAND_COLLECTION = Object.freeze({
389
433
  'lookalike-calibrate': { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
390
434
  'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded artefact and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
391
435
  'interchange-report': { mode: 'offline', scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
392
- 'inspect-archive': { mode: 'offline', scope: 'Reads one selected bounded workspace archive with redacted output by default.' },
436
+ 'inspect-archive': { mode: 'offline', scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact v${PUBLIC_WORKSPACE_ARCHIVE_VERSION} compatibility, and redacts output by default.` },
393
437
  'sign-artifact': { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
394
438
  'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
395
439
  'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
@@ -397,8 +441,8 @@ const COMMAND_COLLECTION = Object.freeze({
397
441
  'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
398
442
  'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
399
443
  'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
400
- brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-aware decision brief.' },
401
- 'case-pack': { mode: 'offline', scope: 'Reads one bounded browser case export and writes a separate audience-specific package.' },
444
+ brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-attributed decision brief.' },
445
+ 'case-pack': { mode: 'offline', scope: `Reads one bounded Case-schema-${CASE_SCHEMA_VERSION} browser export and writes a separate audience-specific Case-pack v2.` },
402
446
  'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
403
447
  'monitor-once': { mode: 'network', scope: 'Runs deep collection for at most 20 manifest domains with concurrency capped at 3.' },
404
448
  assurance: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
@@ -411,9 +455,783 @@ const COMMAND_COLLECTION = Object.freeze({
411
455
  timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
412
456
  export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
413
457
  });
458
+ const COMMON_OPTIONS = Object.freeze(['--help', '--output', '--force', '--config', '--profile', '--palette']);
459
+ const REGISTRY_SCAFFOLD_COMMON_OPTIONS = Object.freeze(COMMON_OPTIONS.filter((option) => option !== '--config' && option !== '--profile'));
460
+ function commonOptionsSeedForCommand(command) {
461
+ return command === 'registry-scaffold' ? REGISTRY_SCAFFOLD_COMMON_OPTIONS : COMMON_OPTIONS;
462
+ }
463
+ const OPTIONS_BY_COMMAND_SEED = Object.freeze({
464
+ manifest: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
465
+ 'map-observations': ['--json', '--quiet', '--no-color'],
466
+ 'oam-export': ['--json', '--quiet', '--no-color'],
467
+ lookup: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
468
+ bulk: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
469
+ 'ct-search': ['--json', '--quiet', '--no-color'],
470
+ 'ct-intake': ['--json', '--quiet', '--no-color'],
471
+ discover: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--snapshot', '--json', '--jsonl', '--domains', '--quiet', '--no-color'],
472
+ 'discover-scan': ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
473
+ posture: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
474
+ http: ['--json', '--quiet', '--no-color'],
475
+ tls: ['--json', '--quiet', '--no-color'],
476
+ 'dnssec-validate': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
477
+ 'mail-transport': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
478
+ 'registry-support': ['--json', '--quiet', '--no-color'],
479
+ 'registry-doctor': ['--json', '--quiet', '--no-color'],
480
+ 'registry-cohort': ['--json', '--quiet', '--no-color'],
481
+ 'registry-scaffold': ['--profile', '--suffix', '--scenario'],
482
+ 'risk-calibrate': ['--json', '--summary-json', '--quiet', '--no-color'],
483
+ 'lookalike-calibrate': ['--json', '--quiet', '--no-color'],
484
+ 'verify-artifact': ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
485
+ 'interchange-report': ['--passphrase-file', '--json', '--quiet', '--no-color'],
486
+ 'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
487
+ 'sign-artifact': ['--private-key-file'],
488
+ 'verify-signature': ['--public-key-file', '--json', '--quiet', '--no-color'],
489
+ 'source-report': ['--json', '--quiet', '--no-color'],
490
+ compare: ['--json', '--quiet', '--no-color'],
491
+ 'page-compare': ['--json', '--quiet', '--no-color'],
492
+ 'mail-review': ['--json', '--quiet', '--no-color'],
493
+ 'review-evidence': ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
494
+ brief: ['--json', '--quiet', '--no-color'],
495
+ 'case-pack': ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
496
+ 'domain-control': ['--json', '--quiet', '--no-color'],
497
+ 'monitor-once': ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
498
+ assurance: ['--json', '--quiet', '--no-color'],
499
+ 'change-packet': ['--json', '--quiet', '--no-color'],
500
+ 'sharing-review': ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
501
+ 'workflow-plan': ['--list', '--explain', '--json', '--quiet', '--no-color'],
502
+ 'workflow-run': ['--approve-network', '--resume', '--json', '--quiet', '--no-color'],
503
+ diff: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
504
+ reconcile: ['--json', '--quiet', '--no-color'],
505
+ timeline: ['--json', '--quiet', '--no-color'],
506
+ export: ['--markdown', '--html', '--compact', '--no-attribution'],
507
+ completion: [],
508
+ commands: ['--common', '--group', '--mode', '--json', '--quiet', '--no-color'],
509
+ doctor: ['--network', '--json', '--quiet', '--no-color'],
510
+ manual: [],
511
+ });
512
+ const VALUE_OPTIONS = Object.freeze({
513
+ '--group': Object.freeze(['investigate', 'respond', 'assure', 'utilities']),
514
+ '--mode': Object.freeze(['offline', 'network']),
515
+ '--explain': INVESTIGATION_PLAN_RECIPES,
516
+ '--preset': Object.freeze(['common', 'impersonation', 'all']),
517
+ '--keyboard': Object.freeze(['qwerty', 'azerty', 'qwertz', 'all']),
518
+ '--mail-profile': Object.freeze(['standard', 'defensive-no-mail', 'parked']),
519
+ '--marking': Object.freeze(['clear', 'green', 'amber', 'amber-strict', 'red']),
520
+ '--recipient-scope': Object.freeze(['public', 'community', 'organization', 'named-recipients']),
521
+ '--audience': Object.freeze(['internal', 'trusted', 'public']),
522
+ '--fail-on': Object.freeze(['source-failure', 'inconclusive', 'danger', 'material-drift']),
523
+ '--manifest-entry': Object.freeze(Array.from({ length: 16 }, (_, index) => `artifact-${index + 1}`)),
524
+ '--palette': Object.freeze(['auto', 'light', 'dark']),
525
+ '--scenario': Object.freeze(['registered', 'not_found', 'inconclusive']),
526
+ });
527
+ const STANDARD_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3', '4', '5', '6', '7', '8']);
528
+ const LIMITED_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3']);
529
+ const FILE_OPTIONS = Object.freeze([
530
+ '--checkpoint',
531
+ '--config',
532
+ '--allowlist',
533
+ '--dictionary',
534
+ '--manifest',
535
+ '--mmdb',
536
+ '--output',
537
+ '--passphrase-file',
538
+ '--private-key-file',
539
+ '--public-key-file',
540
+ '--snapshot',
541
+ '--observation-snapshot',
542
+ '--previous',
543
+ '--save-lookup',
544
+ '--trust-anchor',
545
+ ]);
546
+ const FILE_OPTIONS_BY_COMMAND = Object.freeze({
547
+ 'workflow-run': Object.freeze(['--resume']),
548
+ });
549
+ function positional(name, valueKind, minimum, maximum, values = [], inputSource = 'argv', requiredWhenOptions = []) {
550
+ return Object.freeze({
551
+ name,
552
+ valueKind,
553
+ minimum,
554
+ maximum,
555
+ values: Object.freeze([...values]),
556
+ inputSource,
557
+ requiredWhenOptions: Object.freeze([...requiredWhenOptions]),
558
+ });
559
+ }
560
+ const NO_POSITIONALS = Object.freeze([]);
561
+ const OPTIONAL_FILE_POSITIONAL = Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin')]);
562
+ const OPTIONAL_TEXT_POSITIONAL = Object.freeze([positional('subject', 'text', 0, 1, [], 'argv_or_stdin')]);
563
+ const POSITIONALS_BY_COMMAND_SEED = Object.freeze({
564
+ completion: Object.freeze([positional('shell', 'enum', 1, 1, ['bash', 'zsh', 'fish', 'powershell'])]),
565
+ doctor: NO_POSITIONALS,
566
+ commands: NO_POSITIONALS,
567
+ manual: NO_POSITIONALS,
568
+ manifest: Object.freeze([positional('artefacts', 'file', 1, 16)]),
569
+ 'map-observations': OPTIONAL_FILE_POSITIONAL,
570
+ 'oam-export': OPTIONAL_FILE_POSITIONAL,
571
+ lookup: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
572
+ bulk: OPTIONAL_FILE_POSITIONAL,
573
+ 'ct-search': Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
574
+ 'ct-intake': OPTIONAL_FILE_POSITIONAL,
575
+ discover: OPTIONAL_TEXT_POSITIONAL,
576
+ 'discover-scan': OPTIONAL_TEXT_POSITIONAL,
577
+ posture: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
578
+ http: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
579
+ tls: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
580
+ 'dnssec-validate': Object.freeze([positional('domain', 'text', 1, 1)]),
581
+ 'mail-transport': OPTIONAL_FILE_POSITIONAL,
582
+ 'registry-support': Object.freeze([positional('domain-or-suffix', 'text', 0, 1, [], 'argv_or_stdin')]),
583
+ 'registry-doctor': OPTIONAL_FILE_POSITIONAL,
584
+ 'registry-cohort': OPTIONAL_FILE_POSITIONAL,
585
+ 'registry-scaffold': NO_POSITIONALS,
586
+ 'risk-calibrate': OPTIONAL_FILE_POSITIONAL,
587
+ 'lookalike-calibrate': OPTIONAL_FILE_POSITIONAL,
588
+ 'verify-artifact': OPTIONAL_FILE_POSITIONAL,
589
+ 'interchange-report': OPTIONAL_FILE_POSITIONAL,
590
+ 'inspect-archive': OPTIONAL_FILE_POSITIONAL,
591
+ 'sign-artifact': OPTIONAL_FILE_POSITIONAL,
592
+ 'verify-signature': OPTIONAL_FILE_POSITIONAL,
593
+ 'source-report': OPTIONAL_FILE_POSITIONAL,
594
+ compare: OPTIONAL_FILE_POSITIONAL,
595
+ 'page-compare': Object.freeze([positional('sources', 'file', 2, 2)]),
596
+ 'mail-review': OPTIONAL_FILE_POSITIONAL,
597
+ 'review-evidence': OPTIONAL_FILE_POSITIONAL,
598
+ brief: OPTIONAL_FILE_POSITIONAL,
599
+ 'case-pack': OPTIONAL_FILE_POSITIONAL,
600
+ 'domain-control': OPTIONAL_FILE_POSITIONAL,
601
+ 'monitor-once': OPTIONAL_FILE_POSITIONAL,
602
+ assurance: OPTIONAL_FILE_POSITIONAL,
603
+ 'change-packet': OPTIONAL_FILE_POSITIONAL,
604
+ 'sharing-review': OPTIONAL_FILE_POSITIONAL,
605
+ 'workflow-plan': Object.freeze([
606
+ positional('recipe', 'enum', 0, 1, INVESTIGATION_PLAN_RECIPES),
607
+ positional('subject', 'text', 0, 1),
608
+ ]),
609
+ 'workflow-run': Object.freeze([
610
+ positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
611
+ positional('subject', 'text', 1, 1),
612
+ ]),
613
+ diff: Object.freeze([positional('sources', 'file', 2, 2)]),
614
+ reconcile: Object.freeze([positional('sources', 'file', 2, 5)]),
615
+ timeline: Object.freeze([positional('sources', 'file', 2, 20)]),
616
+ export: OPTIONAL_FILE_POSITIONAL,
617
+ });
618
+ const TEXT_OPTIONS = Object.freeze([
619
+ '--families',
620
+ '--workflow',
621
+ '--configuration-digest',
622
+ '--resolver',
623
+ '--expect-content-digest',
624
+ '--profile',
625
+ '--suffix',
626
+ '--left-session',
627
+ '--right-session',
628
+ '--purpose',
629
+ '--observer',
630
+ '--retired-selectors',
631
+ '--search',
632
+ '--selectors',
633
+ '--tlds',
634
+ '--vantage',
635
+ ]);
636
+ const IDEMPOTENT_OPTIONS = Object.freeze([
637
+ '--human-reviewed',
638
+ '--no-color',
639
+ '--personal-data-reviewed',
640
+ '--quiet',
641
+ '--redactions-confirmed',
642
+ ]);
643
+ const INTEGER_RANGE_SEED = Object.freeze({
644
+ bulk: Object.freeze({
645
+ '--concurrency': Object.freeze([
646
+ Object.freeze({ minimum: 1, maximum: 8, whenOptionPresent: null }),
647
+ Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: '--deep' }),
648
+ ]),
649
+ }),
650
+ 'discover-scan': Object.freeze({
651
+ '--scan-limit': Object.freeze([
652
+ Object.freeze({ minimum: 1, maximum: 500, whenOptionPresent: null }),
653
+ Object.freeze({ minimum: 1, maximum: 50, whenOptionPresent: '--deep' }),
654
+ ]),
655
+ '--chunk-size': Object.freeze([
656
+ Object.freeze({ minimum: 1, maximum: 100, whenOptionPresent: null }),
657
+ ]),
658
+ '--concurrency': Object.freeze([
659
+ Object.freeze({ minimum: 1, maximum: 8, whenOptionPresent: null }),
660
+ Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: '--deep' }),
661
+ ]),
662
+ }),
663
+ 'monitor-once': Object.freeze({
664
+ '--limit': Object.freeze([
665
+ Object.freeze({ minimum: 1, maximum: 20, whenOptionPresent: null }),
666
+ ]),
667
+ '--concurrency': Object.freeze([
668
+ Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: null }),
669
+ ]),
670
+ }),
671
+ });
672
+ function constraint(value) {
673
+ if (value.kind === 'mutually_exclusive') {
674
+ return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
675
+ }
676
+ if (value.kind === 'required') {
677
+ return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
678
+ }
679
+ if (value.kind === 'excludes_all' || value.kind === 'value_excludes') {
680
+ return Object.freeze({ ...value, excludedOptions: Object.freeze([...value.excludedOptions]) });
681
+ }
682
+ return Object.freeze({ ...value, requiredOptions: Object.freeze([...value.requiredOptions]) });
683
+ }
684
+ const EMPTY_CONSTRAINTS = Object.freeze([]);
685
+ const FILE_OUTPUT_CONSTRAINTS = Object.freeze([
686
+ constraint({ kind: 'requires_all', option: '--force', requiredOptions: ['--output'] }),
687
+ ]);
688
+ const QUIET_OUTPUT_CONSTRAINT = constraint({
689
+ kind: 'mutually_exclusive',
690
+ options: ['--quiet', '--output'],
691
+ });
692
+ const MACHINE_OUTPUT_OPTIONS = Object.freeze([
693
+ '--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--markdown',
694
+ '--html', '--sarif', '--summary-json',
695
+ ]);
696
+ const GRAMMAR_CONSTRAINTS_SEED = Object.freeze({
697
+ commands: Object.freeze([]),
698
+ manifest: Object.freeze([
699
+ constraint({ kind: 'required', options: ['--workflow'] }),
700
+ ]),
701
+ lookup: Object.freeze([
702
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
703
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
704
+ constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
705
+ constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
706
+ constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
707
+ constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
708
+ constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
709
+ constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
710
+ constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--junit', '--markdown', '--html', '--summary', '--verbose', '--strict-exit', '--events', '--quiet', '--fail-on'] }),
711
+ ]),
712
+ bulk: Object.freeze([
713
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries'] }),
714
+ constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
715
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
716
+ constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
717
+ constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
718
+ ]),
719
+ discover: Object.freeze([
720
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--domains'] }),
721
+ constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
722
+ constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
723
+ ]),
724
+ 'discover-scan': Object.freeze([
725
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
726
+ constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
727
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
728
+ constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
729
+ constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
730
+ constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
731
+ constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
732
+ ]),
733
+ posture: Object.freeze([
734
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
735
+ constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
736
+ ]),
737
+ 'dnssec-validate': Object.freeze([
738
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
739
+ ]),
740
+ 'mail-transport': Object.freeze([
741
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
742
+ ]),
743
+ 'registry-scaffold': Object.freeze([
744
+ constraint({ kind: 'required', options: ['--profile', '--suffix', '--scenario'] }),
745
+ ]),
746
+ 'risk-calibrate': Object.freeze([
747
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--summary-json'] }),
748
+ ]),
749
+ 'verify-artifact': Object.freeze([
750
+ constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
751
+ constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
752
+ ]),
753
+ 'inspect-archive': Object.freeze([
754
+ constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
755
+ constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
756
+ ]),
757
+ 'sign-artifact': Object.freeze([
758
+ constraint({ kind: 'required', options: ['--private-key-file'] }),
759
+ ]),
760
+ 'case-pack': Object.freeze([
761
+ constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
762
+ ]),
763
+ 'monitor-once': Object.freeze([
764
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit'] }),
765
+ ]),
766
+ 'sharing-review': Object.freeze([
767
+ constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
768
+ ]),
769
+ 'workflow-plan': Object.freeze([
770
+ constraint({ kind: 'mutually_exclusive', options: ['--list', '--explain'] }),
771
+ ]),
772
+ export: Object.freeze([
773
+ constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
774
+ constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
775
+ constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
776
+ ]),
777
+ });
778
+ function optionValueKind(command, option) {
779
+ if (Object.hasOwn(VALUE_OPTIONS, option))
780
+ return option === '--fail-on' ? 'policy_list' : 'enum';
781
+ if (INTEGER_RANGE_SEED[command]?.[option])
782
+ return 'integer';
783
+ if (FILE_OPTIONS.includes(option) || FILE_OPTIONS_BY_COMMAND[command]?.includes(option))
784
+ return 'file';
785
+ if (TEXT_OPTIONS.includes(option))
786
+ return 'text';
787
+ return 'flag';
788
+ }
789
+ function optionSpec(command, option, scope) {
790
+ const valueKind = optionValueKind(command, option);
791
+ return Object.freeze({
792
+ option,
793
+ scope,
794
+ arity: valueKind === 'flag' ? 0 : 1,
795
+ valueKind,
796
+ values: Object.freeze([...(VALUE_OPTIONS[option] ?? [])]),
797
+ integerRanges: Object.freeze([...(INTEGER_RANGE_SEED[command]?.[option] ?? [])]),
798
+ occurrence: IDEMPOTENT_OPTIONS.includes(option) ? 'idempotent' : 'once',
799
+ metaAction: option === '--help' ? 'help' : null,
800
+ });
801
+ }
802
+ function grammarConstraints(command) {
803
+ const commandOptions = OPTIONS_BY_COMMAND_SEED[command];
804
+ const machineOutputOptions = commandOptions.filter((option) => MACHINE_OUTPUT_OPTIONS.includes(option));
805
+ return Object.freeze([
806
+ ...FILE_OUTPUT_CONSTRAINTS,
807
+ ...(commandOptions.includes('--quiet') ? [QUIET_OUTPUT_CONSTRAINT] : []),
808
+ ...(commandOptions.includes('--quiet') && machineOutputOptions.length > 0
809
+ ? [constraint({ kind: 'excludes_all', option: '--quiet', excludedOptions: machineOutputOptions })]
810
+ : []),
811
+ ...(commandOptions.includes('--events')
812
+ ? [constraint({ kind: 'mutually_exclusive', options: ['--events', '--output'] })]
813
+ : []),
814
+ ...(commandOptions.includes('--browse')
815
+ ? [constraint({ kind: 'mutually_exclusive', options: ['--browse', '--output'] })]
816
+ : []),
817
+ ...(GRAMMAR_CONSTRAINTS_SEED[command] ?? EMPTY_CONSTRAINTS),
818
+ ]);
819
+ }
820
+ const COMMAND_DESCRIPTIONS_SEED = Object.freeze({
821
+ manifest: 'Build an evidence manifest offline',
822
+ 'map-observations': 'Apply a declarative observation map offline',
823
+ 'oam-export': 'Project external findings to Open Asset Model',
824
+ lookup: 'Collect one domain, IP, or ASN',
825
+ bulk: 'Run bounded multi-target collection',
826
+ 'ct-search': 'Search certificate observations',
827
+ 'ct-intake': 'Normalise certificate observations offline',
828
+ discover: 'Generate lookalike candidates offline',
829
+ 'discover-scan': 'Collect a supervised candidate review queue',
830
+ posture: 'Review DNS and mail posture',
831
+ http: 'Inspect one homepage request',
832
+ tls: 'Inspect one TLS connection',
833
+ 'dnssec-validate': 'Validate an authorised DNSSEC chain',
834
+ 'mail-transport': 'Review selected authorised SMTP transports',
835
+ 'registry-support': 'Explain local registry coverage',
836
+ 'registry-doctor': 'Diagnose saved registry collection',
837
+ 'registry-cohort': 'Build target-free registry quality timelines',
838
+ 'registry-scaffold': 'Create a sanitised registry fixture scaffold',
839
+ 'risk-calibrate': 'Replay reviewed Risk labels offline',
840
+ 'lookalike-calibrate': 'Summarise reviewed lookalike yield offline',
841
+ 'verify-artifact': 'Validate saved evidence offline',
842
+ 'interchange-report': 'Report portable artefact fidelity offline',
843
+ 'inspect-archive': 'Inspect an archive locally',
844
+ 'sign-artifact': 'Sign a reviewed artefact locally',
845
+ 'verify-signature': 'Verify a signed evidence package',
846
+ 'source-report': 'Build a target-free source report',
847
+ compare: 'Compare registry publications in one lookup',
848
+ 'page-compare': 'Compare saved static page evidence',
849
+ 'mail-review': 'Review saved passive mail evidence',
850
+ 'review-evidence': 'Review supplied evidence offline',
851
+ brief: 'Build a decision brief from a saved lookup',
852
+ 'case-pack': 'Build a reviewed case package',
853
+ 'domain-control': 'Build or review a domain control manifest',
854
+ 'monitor-once': 'Run one bounded domain control review',
855
+ assurance: 'Review domain change, recovery, or retirement plans',
856
+ 'change-packet': 'Build a reviewed change packet offline',
857
+ 'sharing-review': 'Lint an artefact before deliberate sharing',
858
+ 'workflow-plan': 'Plan a fixed investigation recipe',
859
+ 'workflow-run': 'Execute approved fixed-recipe steps',
860
+ diff: 'Compare two compatible retained artefacts',
861
+ reconcile: 'Reconcile independently labelled observations',
862
+ timeline: 'Build same-domain history from saved lookups',
863
+ export: 'Convert a lookup to an evidence report',
864
+ completion: 'Print shell completion',
865
+ commands: 'List installed command contracts',
866
+ doctor: 'Check the local CLI runtime',
867
+ manual: 'Print the generated manual page',
868
+ });
869
+ const HELP_COMMANDS_BY_GROUP = Object.freeze({
870
+ investigate: Object.freeze([
871
+ 'lookup', 'bulk', 'ct-search', 'ct-intake', 'discover', 'discover-scan',
872
+ 'posture', 'http', 'tls', 'registry-support', 'registry-doctor',
873
+ 'registry-cohort', 'source-report', 'compare', 'page-compare', 'mail-review',
874
+ 'review-evidence', 'brief',
875
+ ]),
876
+ respond: Object.freeze([
877
+ 'map-observations', 'oam-export', 'case-pack', 'change-packet',
878
+ 'sharing-review', 'export',
879
+ ]),
880
+ assure: Object.freeze([
881
+ 'dnssec-validate', 'mail-transport', 'domain-control', 'monitor-once',
882
+ 'assurance', 'workflow-plan', 'workflow-run', 'diff', 'reconcile', 'timeline',
883
+ 'inspect-archive', 'verify-artifact', 'interchange-report', 'manifest',
884
+ 'sign-artifact', 'verify-signature', 'risk-calibrate', 'lookalike-calibrate',
885
+ ]),
886
+ utilities: Object.freeze([
887
+ 'registry-scaffold', 'doctor', 'commands', 'completion', 'manual',
888
+ ]),
889
+ });
890
+ const HELP_GROUP_BY_COMMAND = Object.freeze(Object.fromEntries(Object.entries(HELP_COMMANDS_BY_GROUP).flatMap(([group, commands]) => (commands.map((command) => [command, group])))));
891
+ const HANDLER_OWNER_BY_COMMAND = Object.freeze({
892
+ completion: 'inline',
893
+ doctor: 'inline',
894
+ commands: 'inline',
895
+ manual: 'inline',
896
+ manifest: 'inline',
897
+ 'map-observations': 'inline',
898
+ 'oam-export': 'inline',
899
+ lookup: 'lookup',
900
+ bulk: 'bulk',
901
+ 'ct-search': 'network',
902
+ 'ct-intake': 'inline',
903
+ discover: 'discovery',
904
+ 'discover-scan': 'discovery_scan',
905
+ posture: 'network',
906
+ http: 'network',
907
+ tls: 'network',
908
+ 'dnssec-validate': 'network',
909
+ 'mail-transport': 'network',
910
+ 'registry-support': 'inline',
911
+ 'registry-doctor': 'inline',
912
+ 'registry-cohort': 'inline',
913
+ 'registry-scaffold': 'inline',
914
+ 'risk-calibrate': 'inline',
915
+ 'lookalike-calibrate': 'inline',
916
+ 'verify-artifact': 'inline',
917
+ 'interchange-report': 'inline',
918
+ 'inspect-archive': 'evidence',
919
+ 'sign-artifact': 'evidence',
920
+ 'verify-signature': 'evidence',
921
+ 'source-report': 'inline',
922
+ compare: 'inline',
923
+ 'page-compare': 'inline',
924
+ 'mail-review': 'inline',
925
+ 'review-evidence': 'inline',
926
+ brief: 'inline',
927
+ 'case-pack': 'inline',
928
+ 'domain-control': 'inline',
929
+ 'monitor-once': 'inline',
930
+ assurance: 'inline',
931
+ 'change-packet': 'inline',
932
+ 'sharing-review': 'inline',
933
+ 'workflow-plan': 'inline',
934
+ 'workflow-run': 'inline',
935
+ diff: 'inline',
936
+ reconcile: 'inline',
937
+ timeline: 'inline',
938
+ export: 'inline',
939
+ });
940
+ const NETWORK_EFFECT_BY_COMMAND = Object.freeze({
941
+ completion: 'offline',
942
+ doctor: 'conditional_network',
943
+ commands: 'offline',
944
+ manual: 'offline',
945
+ manifest: 'offline',
946
+ 'map-observations': 'offline',
947
+ 'oam-export': 'offline',
948
+ lookup: 'conditional_network',
949
+ bulk: 'conditional_network',
950
+ 'ct-search': 'always_network',
951
+ 'ct-intake': 'offline',
952
+ discover: 'offline',
953
+ 'discover-scan': 'conditional_network',
954
+ posture: 'always_network',
955
+ http: 'always_network',
956
+ tls: 'always_network',
957
+ 'dnssec-validate': 'always_network',
958
+ 'mail-transport': 'always_network',
959
+ 'registry-support': 'offline',
960
+ 'registry-doctor': 'offline',
961
+ 'registry-cohort': 'offline',
962
+ 'registry-scaffold': 'offline',
963
+ 'risk-calibrate': 'offline',
964
+ 'lookalike-calibrate': 'offline',
965
+ 'verify-artifact': 'offline',
966
+ 'interchange-report': 'offline',
967
+ 'inspect-archive': 'offline',
968
+ 'sign-artifact': 'offline',
969
+ 'verify-signature': 'offline',
970
+ 'source-report': 'offline',
971
+ compare: 'offline',
972
+ 'page-compare': 'offline',
973
+ 'mail-review': 'offline',
974
+ 'review-evidence': 'offline',
975
+ brief: 'offline',
976
+ 'case-pack': 'offline',
977
+ 'domain-control': 'offline',
978
+ 'monitor-once': 'always_network',
979
+ assurance: 'offline',
980
+ 'change-packet': 'offline',
981
+ 'sharing-review': 'offline',
982
+ 'workflow-plan': 'offline',
983
+ 'workflow-run': 'conditional_network',
984
+ diff: 'offline',
985
+ reconcile: 'offline',
986
+ timeline: 'offline',
987
+ export: 'offline',
988
+ });
989
+ const COMMON_COMMANDS = Object.freeze([
990
+ 'doctor', 'commands', 'lookup', 'bulk', 'discover', 'discover-scan', 'verify-artifact',
991
+ 'review-evidence', 'case-pack', 'workflow-plan', 'diff', 'export',
992
+ ]);
993
+ const SCHEMA_IDENTIFIERS_BY_COMMAND = Object.freeze({
994
+ commands: Object.freeze(['whoisleuth.cli.command-catalogue']),
995
+ doctor: Object.freeze(['whoisleuth.cli.doctor']),
996
+ manifest: Object.freeze(['whoisleuth.investigation-manifest']),
997
+ 'map-observations': Object.freeze(['whoisleuth.external-observation-mapping']),
998
+ 'oam-export': Object.freeze(['whoisleuth.open-asset-model-bridge']),
999
+ lookup: Object.freeze(['whoisleuth.cli.lookup', 'whoisleuth.cli.lookup-plan']),
1000
+ bulk: Object.freeze(['whoisleuth.cli.bulk', 'whoisleuth.cli.bulk.item', 'whoisleuth.cli.bulk-checkpoint']),
1001
+ 'ct-search': Object.freeze(['whoisleuth.cli.ct-search']),
1002
+ 'ct-intake': Object.freeze(['whoisleuth.ct-event-batch', 'whoisleuth.external-findings']),
1003
+ discover: Object.freeze(['whoisleuth.cli.discover', 'whoisleuth.cli.discover.item', 'whoisleuth.cli.discovery-snapshot']),
1004
+ 'discover-scan': Object.freeze(['whoisleuth.cli.discovery-scan', 'whoisleuth.cli.discovery-scan.item', 'whoisleuth.cli.discovery-observation-snapshot']),
1005
+ posture: Object.freeze(['whoisleuth.cli.posture']),
1006
+ http: Object.freeze(['whoisleuth.cli.http']),
1007
+ tls: Object.freeze(['whoisleuth.cli.tls']),
1008
+ 'dnssec-validate': Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
1009
+ 'mail-transport': Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
1010
+ 'registry-support': Object.freeze(['whoisleuth.cli.registry-support', 'whoisleuth.registry-standards-coverage']),
1011
+ 'registry-doctor': Object.freeze(['whoisleuth.cli.registry-doctor']),
1012
+ 'registry-cohort': Object.freeze(['whoisleuth.cli.registry-cohort']),
1013
+ 'risk-calibrate': Object.freeze([RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA]),
1014
+ 'lookalike-calibrate': Object.freeze(['whoisleuth.lookalike-calibration-input', 'whoisleuth.lookalike-calibration']),
1015
+ 'verify-artifact': Object.freeze(['whoisleuth.offline-artifact-verification']),
1016
+ 'interchange-report': Object.freeze(['whoisleuth.interchange-fidelity-report']),
1017
+ 'inspect-archive': Object.freeze(['whoisleuth.workspace-archive-inspection']),
1018
+ 'sign-artifact': Object.freeze(['whoisleuth.signed-evidence-package']),
1019
+ 'verify-signature': Object.freeze(['whoisleuth.evidence-signature-verification']),
1020
+ 'source-report': Object.freeze(['whoisleuth.source-reliability-report']),
1021
+ compare: Object.freeze(['whoisleuth.cli.compare']),
1022
+ 'page-compare': Object.freeze(['whoisleuth.cli.page-compare']),
1023
+ 'mail-review': Object.freeze(['whoisleuth.cli.mail-review']),
1024
+ 'review-evidence': Object.freeze([
1025
+ 'whoisleuth.cli.offline-evidence-review',
1026
+ 'whoisleuth.rdap-search-input',
1027
+ 'whoisleuth.dnssec-evidence-input',
1028
+ 'whoisleuth.tlsa-evidence-input',
1029
+ 'whoisleuth.rpki-route-input',
1030
+ 'whoisleuth.local-geoip-query',
1031
+ 'whoisleuth.encrypted-dns-plan-input',
1032
+ ]),
1033
+ brief: Object.freeze(['whoisleuth.cli.lookup-brief']),
1034
+ 'case-pack': Object.freeze(['whoisleuth.cli.case-pack', 'whoisleuth.case-report']),
1035
+ 'domain-control': Object.freeze(['whoisleuth.cli.domain-control-review-input', 'whoisleuth.cli.domain-control-review']),
1036
+ 'monitor-once': Object.freeze(['whoisleuth.cli.domain-control-monitor', 'whoisleuth.domain-control-flight-recorder.input']),
1037
+ assurance: Object.freeze(['whoisleuth.domain-assurance.input', 'whoisleuth.domain-assurance']),
1038
+ 'change-packet': Object.freeze(['whoisleuth.domain-change-packet.input', 'whoisleuth.domain-change-packet']),
1039
+ 'sharing-review': Object.freeze(['whoisleuth.cli.sharing-review']),
1040
+ 'workflow-plan': Object.freeze(['whoisleuth.cli.investigation-plan', 'whoisleuth.cli.workflow-recipe-catalogue']),
1041
+ 'workflow-run': Object.freeze(['whoisleuth.cli.investigation-run']),
1042
+ diff: Object.freeze(['whoisleuth.cli.lookup-diff']),
1043
+ reconcile: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
1044
+ timeline: Object.freeze(['whoisleuth.cli.lookup-timeline']),
1045
+ export: Object.freeze(['whoisleuth.lookup-evidence']),
1046
+ });
1047
+ const PRIMARY_ARTEFACTS_BY_COMMAND = Object.freeze({
1048
+ lookup: Object.freeze(['Source-qualified Lookup', 'Lookup request plan']),
1049
+ bulk: Object.freeze(['Bulk result', 'Bulk checkpoint']),
1050
+ discover: Object.freeze(['Candidate set', 'Discovery snapshot']),
1051
+ 'discover-scan': Object.freeze(['Reviewed candidate queue', 'Observation snapshot']),
1052
+ 'verify-artifact': Object.freeze(['Offline verification report']),
1053
+ 'case-pack': Object.freeze(['Reviewed Case-pack v2']),
1054
+ 'workflow-plan': Object.freeze(['Plan-only workflow document']),
1055
+ 'workflow-run': Object.freeze(['Resumable workflow state']),
1056
+ diff: Object.freeze(['Retained-evidence comparison']),
1057
+ timeline: Object.freeze(['Bounded retained-observation timeline']),
1058
+ export: Object.freeze(['Portable evidence report']),
1059
+ });
1060
+ function documentationMetadata(command, commandOptions, positionals) {
1061
+ const effect = NETWORK_EFFECT_BY_COMMAND[command];
1062
+ const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
1063
+ const outputOptionFormats = [
1064
+ ['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
1065
+ ['--csv', 'CSV'], ['--domains', 'domain list'], ['--queries', 'query list'],
1066
+ ['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
1067
+ ['--summary-json', 'summary JSON'],
1068
+ ];
1069
+ const outputFormats = new Set(['terminal']);
1070
+ for (const [option, label] of outputOptionFormats) {
1071
+ if (commandOptions.includes(option))
1072
+ outputFormats.add(label);
1073
+ }
1074
+ if (command === 'export')
1075
+ outputFormats.add('JSON');
1076
+ const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
1077
+ return Object.freeze({
1078
+ common: COMMON_COMMANDS.includes(command),
1079
+ disclosureClass: effect === 'offline'
1080
+ ? 'none'
1081
+ : explicitAuthorisationRequired
1082
+ ? 'bounded_authorised_active'
1083
+ : effect === 'conditional_network'
1084
+ ? 'conditional_bounded_passive'
1085
+ : 'bounded_passive',
1086
+ explicitAuthorisationRequired,
1087
+ planSupport: commandOptions.includes('--plan') || command === 'workflow-plan',
1088
+ failurePolicySupport: commandOptions.includes('--fail-on') || commandOptions.includes('--strict-exit'),
1089
+ supportedSchemaIdentifiers: Object.freeze([...(SCHEMA_IDENTIFIERS_BY_COMMAND[command] ?? [])]),
1090
+ inputLimits: Object.freeze([COMMAND_COLLECTION_SEED[command].scope, ...positionalLimits]),
1091
+ outputLimits: Object.freeze([
1092
+ 'Output is bounded by the command-owned formatter and document contract.',
1093
+ ...(commonOptionsSeedForCommand(command).includes('--output')
1094
+ ? ['Selected file output is atomic and replacement requires --force.']
1095
+ : []),
1096
+ ]),
1097
+ outputFormats: Object.freeze([...outputFormats]),
1098
+ primaryEvidenceArtefacts: Object.freeze([...(PRIMARY_ARTEFACTS_BY_COMMAND[command] ?? [])]),
1099
+ });
1100
+ }
1101
+ const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) => {
1102
+ const commonOptions = Object.freeze([...commonOptionsSeedForCommand(command)]);
1103
+ const commandOptions = Object.freeze([...OPTIONS_BY_COMMAND_SEED[command]]);
1104
+ const grammarOptions = Object.freeze([
1105
+ ...commonOptions.map((option) => optionSpec(command, option, 'common')),
1106
+ ...commandOptions.map((option) => optionSpec(command, option, 'command')),
1107
+ ]);
1108
+ return Object.freeze({
1109
+ command,
1110
+ order,
1111
+ reference: Object.freeze({
1112
+ usage: COMMAND_USAGE_SEED[command],
1113
+ ...COMMAND_DETAILS_SEED[command],
1114
+ }),
1115
+ collection: Object.freeze({ ...COMMAND_COLLECTION_SEED[command] }),
1116
+ completion: Object.freeze({
1117
+ description: COMMAND_DESCRIPTIONS_SEED[command],
1118
+ commonOptions,
1119
+ options: commandOptions,
1120
+ }),
1121
+ grammar: Object.freeze({
1122
+ parserKey: command,
1123
+ bootstrapProfile: command === 'registry-scaffold' ? 'command_owned' : 'allowed',
1124
+ options: grammarOptions,
1125
+ positionals: POSITIONALS_BY_COMMAND_SEED[command],
1126
+ constraints: grammarConstraints(command),
1127
+ metaActions: Object.freeze(['help']),
1128
+ }),
1129
+ execution: Object.freeze({
1130
+ handlerOwner: HANDLER_OWNER_BY_COMMAND[command],
1131
+ networkEffect: NETWORK_EFFECT_BY_COMMAND[command],
1132
+ }),
1133
+ help: Object.freeze({
1134
+ group: HELP_GROUP_BY_COMMAND[command],
1135
+ summary: COMMAND_DESCRIPTIONS_SEED[command],
1136
+ }),
1137
+ documentation: documentationMetadata(command, commandOptions, POSITIONALS_BY_COMMAND_SEED[command]),
1138
+ });
1139
+ }));
1140
+ const CLI_COMMANDS = Object.freeze(CLI_COMMAND_REGISTRY.map((definition) => definition.command));
1141
+ const FILE_POSITIONAL_COMMANDS = Object.freeze(CLI_COMMAND_REGISTRY
1142
+ .filter((definition) => definition.grammar.positionals.some((item) => item.valueKind === 'file'))
1143
+ .map((definition) => definition.command));
1144
+ const CLI_COMMAND_BY_NAME = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition])));
1145
+ function isCliCommand(value) {
1146
+ return typeof value === 'string' && Object.hasOwn(CLI_COMMAND_BY_NAME, value);
1147
+ }
1148
+ function commandDefinition(command) {
1149
+ return CLI_COMMAND_BY_NAME[command];
1150
+ }
1151
+ function metaActionDefinition(id) {
1152
+ return CLI_META_ACTION_BY_ID[id];
1153
+ }
1154
+ function cliMetaActionForInvocation(argv) {
1155
+ for (const action of CLI_META_ACTIONS) {
1156
+ const matches = action.scope === 'root_only'
1157
+ ? action.aliases.includes(argv[0] ?? '')
1158
+ : argv.some((argument) => action.aliases.includes(argument));
1159
+ if (matches)
1160
+ return action;
1161
+ }
1162
+ return null;
1163
+ }
1164
+ const COMMAND_USAGE = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.reference.usage])));
1165
+ const COMMAND_DETAILS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, Object.freeze({
1166
+ description: definition.reference.description,
1167
+ example: definition.reference.example,
1168
+ boundary: definition.reference.boundary,
1169
+ })])));
1170
+ const COMMAND_COLLECTION = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.collection])));
1171
+ const COMMAND_DESCRIPTIONS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.completion.description])));
1172
+ const OPTIONS_BY_COMMAND = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, Object.freeze(definition.grammar.options
1173
+ .filter((option) => option.scope === 'command')
1174
+ .map((option) => option.option))])));
1175
+ function commonOptionsForCommand(command) {
1176
+ return Object.freeze(commandDefinition(command).grammar.options
1177
+ .filter((option) => option.scope === 'common')
1178
+ .map((option) => option.option));
1179
+ }
1180
+ function commandOptionSpec(command, option) {
1181
+ return commandDefinition(command).grammar.options.find((candidate) => candidate.option === option) ?? null;
1182
+ }
1183
+ function commandPositionalSpecs(command) {
1184
+ return commandDefinition(command).grammar.positionals;
1185
+ }
1186
+ const HELP_GROUP_LABELS = Object.freeze({
1187
+ investigate: 'Investigate',
1188
+ respond: 'Respond',
1189
+ assure: 'Assure',
1190
+ utilities: 'Utilities',
1191
+ });
1192
+ function renderRootHelpCommands() {
1193
+ return Object.keys(HELP_COMMANDS_BY_GROUP).map((group) => {
1194
+ const lines = HELP_COMMANDS_BY_GROUP[group].map((command) => {
1195
+ const definition = commandDefinition(command);
1196
+ return ` ${command.padEnd(20, ' ')} ${definition.help.summary}.`;
1197
+ });
1198
+ return `${HELP_GROUP_LABELS[group]}:\n${lines.join('\n')}`;
1199
+ }).join('\n\n');
1200
+ }
1201
+ const HELP = `${HELP_INTRO}\n${renderRootHelpCommands()}\n${HELP_FOOTER}`;
1202
+ function commandOwnsOption(command, option) {
1203
+ return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
1204
+ }
1205
+ function invocationHasFlag(command, args, flag) {
1206
+ for (let index = 0; index < args.length; index += 1) {
1207
+ const argument = args[index];
1208
+ if (argument === flag)
1209
+ return true;
1210
+ const specification = argument ? commandOptionSpec(command, argument) : null;
1211
+ if (specification?.arity === 1)
1212
+ index += 1;
1213
+ }
1214
+ return false;
1215
+ }
1216
+ function cliInvocationNetworkEffect(command, args) {
1217
+ const effect = commandDefinition(command).execution.networkEffect;
1218
+ if (effect === 'offline')
1219
+ return 'offline';
1220
+ if (effect === 'always_network')
1221
+ return 'network';
1222
+ if (command === 'doctor')
1223
+ return invocationHasFlag(command, args, '--network') ? 'network' : 'offline';
1224
+ if (command === 'lookup' || command === 'bulk' || command === 'discover-scan') {
1225
+ return invocationHasFlag(command, args, '--plan') ? 'offline' : 'network';
1226
+ }
1227
+ if (command === 'workflow-run') {
1228
+ return invocationHasFlag(command, args, '--approve-network') ? 'network' : 'offline';
1229
+ }
1230
+ throw new Error(`Conditional network effect is not implemented for ${command}.`);
1231
+ }
414
1232
  function commandHelp(command) {
415
1233
  const detail = COMMAND_DETAILS[command];
416
1234
  const collection = COMMAND_COLLECTION[command];
417
1235
  return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
418
1236
  }
419
- export { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, commandHelp };
1237
+ export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, COMMON_OPTIONS, FILE_OPTIONS, FILE_OPTIONS_BY_COMMAND, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, LIMITED_CONCURRENCY_VALUES, OPTIONS_BY_COMMAND, STANDARD_CONCURRENCY_VALUES, TEXT_OPTIONS, VALUE_OPTIONS, cliMetaActionForInvocation, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };