@slicedearth/whoisleuth-cli 1.47.4 → 2.0.0

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 (235) 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/provider-policy-admission.mjs +3 -1
  100. package/lib/rdap-bootstrap.mjs +76 -16
  101. package/lib/rdap-client.mjs +20 -12
  102. package/lib/rdap-normalization.mjs +306 -75
  103. package/lib/rdap-transport.mjs +2 -2
  104. package/lib/rdap-validation.mjs +65 -14
  105. package/lib/rdap.mjs +2 -2
  106. package/lib/registrable-domain.mjs +36 -0
  107. package/lib/registry-insights.mjs +7 -0
  108. package/lib/response-policy.mjs +27 -4
  109. package/lib/risk-calibration-summary.mjs +35 -33
  110. package/lib/risk-scoring.mjs +4 -3
  111. package/lib/safe-fetch.mjs +62 -40
  112. package/lib/security-txt.mjs +1 -1
  113. package/lib/smtp-transport-review.mjs +8 -5
  114. package/lib/sslbl-intelligence.mjs +6 -8
  115. package/lib/static-html-analysis.mjs +301 -4
  116. package/lib/structured-data-identity.mjs +51 -19
  117. package/lib/threat-intelligence-fixtures.mjs +2 -4
  118. package/lib/threat-intelligence-manifests.mjs +2 -7
  119. package/lib/threat-intelligence-runtime.mjs +3 -5
  120. package/lib/threat-intelligence-types.mjs +3 -0
  121. package/lib/threatfox-intelligence.mjs +7 -3
  122. package/lib/tls-intelligence.mjs +4 -4
  123. package/lib/urlhaus-intelligence.mjs +4 -2
  124. package/lib/urlscan-intelligence.mjs +6 -5
  125. package/lib/website-security-posture.mjs +1 -1
  126. package/lib/website-technology.mjs +1 -1
  127. package/lib/whois-authority.mjs +43 -8
  128. package/lib/whois-chain.mjs +25 -3
  129. package/lib/whois-normalization.mjs +3 -1
  130. package/lib/whois-transport.mjs +12 -3
  131. package/lib/zone-intent-review.mjs +1 -1
  132. package/package.json +12 -4
  133. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  134. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  135. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  136. package/packages/cases/case-migration-model.mjs +476 -0
  137. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  138. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  139. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  140. package/packages/cases/case-record-model.mjs +9 -0
  141. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  142. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  143. package/packages/cases/case-response-model.mjs +1604 -0
  144. package/packages/cases/case-response-packet.mjs +1312 -0
  145. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  146. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  147. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  148. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  149. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  150. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  151. package/packages/comparison/favicon-similarity.mjs +78 -0
  152. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  153. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  154. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  155. package/packages/contracts/analyst-review-state.mjs +50 -0
  156. package/packages/contracts/case-portability.mjs +1310 -0
  157. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  158. package/packages/contracts/cli-lookup.mjs +394 -0
  159. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  160. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  161. package/packages/contracts/domain-control-monitor.mjs +310 -0
  162. package/packages/contracts/domain-control-review.mjs +828 -0
  163. package/packages/contracts/domain-name.mjs +4 -0
  164. package/packages/contracts/external-observation-interchange.mjs +224 -0
  165. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  166. package/packages/contracts/investigation-portability.mjs +331 -0
  167. package/packages/contracts/offline-comparison.mjs +160 -0
  168. package/packages/contracts/risk-calibration.mjs +1288 -0
  169. package/packages/contracts/schema-compatibility.mjs +71 -0
  170. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  171. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  172. package/packages/contracts/workspace-portability.mjs +1347 -0
  173. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  174. package/packages/evidence/decision-fact.mjs +735 -0
  175. package/packages/evidence/domain-control-runtime.mjs +547 -0
  176. package/packages/evidence/domain-name.mjs +41 -0
  177. package/packages/evidence/observation.mjs +384 -0
  178. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  179. package/packages/interchange/retained-text.mjs +8 -0
  180. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  181. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  182. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  183. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  184. package/packages/investigation/bulk-review-policy.mjs +1 -0
  185. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  186. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  187. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  188. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  189. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  190. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  191. package/packages/monitoring/analyst-review-state.mjs +680 -0
  192. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +70 -47
  193. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  194. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  195. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  196. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  197. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  198. package/packages/workspace/hostile-input.mjs +172 -0
  199. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  200. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  201. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  202. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  203. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  204. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  205. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  206. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  207. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  208. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  209. package/third-party-notices.txt +14 -14
  210. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  211. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  212. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  213. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  214. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  215. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  216. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  217. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  218. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  219. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  220. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  221. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  222. package/frontend/src/lib/analysis/scoring.js +0 -55
  223. package/frontend/src/lib/analysis/utils.js +0 -255
  224. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  225. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  226. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  227. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  228. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  229. package/lib/bounded-json-response.mjs +0 -170
  230. package/lib/lookup-response-contract.mjs +0 -1034
  231. package/lib/observation.mjs +0 -115
  232. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  233. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,186 +1,149 @@
1
- import { CLI_COMMANDS } from "./arguments.mjs";
2
- import { INVESTIGATION_PLAN_RECIPES } from "./investigation-plan.mjs";
3
- const COMMON_OPTIONS = Object.freeze(['--help', '--output', '--force', '--config', '--profile']);
4
- const OPTIONS_BY_COMMAND = Object.freeze({
5
- manifest: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
6
- 'map-observations': ['--json', '--quiet', '--no-color'],
7
- 'oam-export': ['--json', '--quiet', '--no-color'],
8
- lookup: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
9
- 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'],
10
- 'ct-search': ['--json', '--quiet', '--no-color'],
11
- 'ct-intake': ['--json', '--quiet', '--no-color'],
12
- discover: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--snapshot', '--json', '--jsonl', '--domains', '--quiet', '--no-color'],
13
- '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'],
14
- posture: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
15
- http: ['--json', '--quiet', '--no-color'],
16
- tls: ['--json', '--quiet', '--no-color'],
17
- 'dnssec-validate': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
18
- 'mail-transport': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
19
- 'registry-support': ['--json', '--quiet', '--no-color'],
20
- 'registry-doctor': ['--json', '--quiet', '--no-color'],
21
- 'registry-cohort': ['--json', '--quiet', '--no-color'],
22
- 'registry-scaffold': ['--profile', '--suffix', '--scenario'],
23
- 'risk-calibrate': ['--json', '--summary-json', '--quiet', '--no-color'],
24
- 'lookalike-calibrate': ['--json', '--quiet', '--no-color'],
25
- 'verify-artifact': ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
26
- 'interchange-report': ['--passphrase-file', '--json', '--quiet', '--no-color'],
27
- 'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
28
- 'sign-artifact': ['--private-key-file'],
29
- 'verify-signature': ['--public-key-file', '--json', '--quiet', '--no-color'],
30
- 'source-report': ['--json', '--quiet', '--no-color'],
31
- compare: ['--json', '--quiet', '--no-color'],
32
- 'page-compare': ['--json', '--quiet', '--no-color'],
33
- 'mail-review': ['--json', '--quiet', '--no-color'],
34
- 'review-evidence': ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
35
- brief: ['--json', '--quiet', '--no-color'],
36
- 'case-pack': ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
37
- 'domain-control': ['--json', '--quiet', '--no-color'],
38
- 'monitor-once': ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
39
- assurance: ['--json', '--quiet', '--no-color'],
40
- 'change-packet': ['--json', '--quiet', '--no-color'],
41
- 'sharing-review': ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
42
- 'workflow-plan': ['--json', '--quiet', '--no-color'],
43
- 'workflow-run': ['--approve-network', '--resume', '--json', '--quiet', '--no-color'],
44
- diff: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
45
- reconcile: ['--json', '--quiet', '--no-color'],
46
- timeline: ['--json', '--quiet', '--no-color'],
47
- export: ['--markdown', '--html', '--compact', '--no-attribution'],
48
- completion: [],
49
- commands: ['--json', '--quiet', '--no-color'],
50
- doctor: ['--network', '--json', '--quiet', '--no-color'],
51
- manual: [],
52
- });
53
- const COMMAND_DESCRIPTIONS = Object.freeze({
54
- manifest: 'Build an evidence manifest offline',
55
- 'map-observations': 'Normalise passive DNS observations offline',
56
- 'oam-export': 'Export an observation-archive map offline',
57
- lookup: 'Collect one domain, IP, or ASN',
58
- bulk: 'Run bounded multi-target collection',
59
- 'ct-search': 'Search certificate observations',
60
- 'ct-intake': 'Normalise certificate observations offline',
61
- discover: 'Generate lookalike candidates offline',
62
- 'discover-scan': 'Collect a supervised candidate review queue',
63
- posture: 'Review DNS and mail posture',
64
- http: 'Inspect one homepage request',
65
- tls: 'Inspect one TLS connection',
66
- 'dnssec-validate': 'Validate an authorised DNSSEC chain',
67
- 'mail-transport': 'Review selected authorised SMTP transports',
68
- 'registry-support': 'Explain local registry coverage',
69
- 'registry-doctor': 'Diagnose saved registry collection',
70
- 'registry-cohort': 'Build target-free registry quality timelines',
71
- 'registry-scaffold': 'Create a sanitised registry fixture scaffold',
72
- 'risk-calibrate': 'Replay reviewed Risk labels offline',
73
- 'lookalike-calibrate': 'Summarise reviewed lookalike yield offline',
74
- 'verify-artifact': 'Validate saved evidence offline',
75
- 'interchange-report': 'Report portable artefact fidelity offline',
76
- 'inspect-archive': 'Inspect an archive locally',
77
- 'sign-artifact': 'Sign a reviewed artefact locally',
78
- 'verify-signature': 'Verify a signed evidence package',
79
- 'source-report': 'Build a target-free source report',
80
- compare: 'Compare registry publications in one lookup',
81
- 'page-compare': 'Compare saved static page evidence',
82
- 'mail-review': 'Review saved passive mail evidence',
83
- 'review-evidence': 'Review supplied evidence offline',
84
- brief: 'Build a decision brief from a saved lookup',
85
- 'case-pack': 'Build a reviewed case package',
86
- 'domain-control': 'Build or review a domain control manifest',
87
- 'monitor-once': 'Run one bounded domain control review',
88
- assurance: 'Review domain change, recovery, or retirement plans',
89
- 'change-packet': 'Build a reviewed change packet offline',
90
- 'sharing-review': 'Lint an artefact before deliberate sharing',
91
- 'workflow-plan': 'Plan a fixed investigation recipe',
92
- 'workflow-run': 'Execute approved fixed-recipe steps',
93
- diff: 'Compare two compatible retained artefacts',
94
- reconcile: 'Reconcile independently labelled observations',
95
- timeline: 'Build same-domain history from saved lookups',
96
- export: 'Convert a lookup to an evidence report',
97
- completion: 'Print shell completion',
98
- commands: 'List installed command contracts',
99
- doctor: 'Check the local CLI runtime',
100
- manual: 'Print the generated manual page',
101
- });
102
- const VALUE_OPTIONS = Object.freeze({
103
- '--preset': ['common', 'impersonation', 'all'],
104
- '--keyboard': ['qwerty', 'azerty', 'qwertz', 'all'],
105
- '--mail-profile': ['standard', 'defensive-no-mail', 'parked'],
106
- '--marking': ['clear', 'green', 'amber', 'amber-strict', 'red'],
107
- '--recipient-scope': ['public', 'community', 'organization', 'named-recipients'],
108
- '--audience': ['internal', 'trusted', 'public'],
109
- '--concurrency': ['1', '2', '3', '4', '5', '6', '7', '8'],
110
- });
111
- const FILE_OPTIONS = Object.freeze([
112
- '--checkpoint',
113
- '--config',
114
- '--allowlist',
115
- '--dictionary',
116
- '--output',
117
- '--passphrase-file',
118
- '--private-key-file',
119
- '--public-key-file',
120
- '--snapshot',
121
- '--observation-snapshot',
122
- '--previous',
123
- '--resume',
124
- '--trust-anchor',
125
- ]);
126
- const FILE_POSITIONAL_COMMANDS = new Set([
127
- 'manifest',
128
- 'map-observations',
129
- 'oam-export',
130
- 'ct-intake',
131
- 'change-packet',
132
- 'mail-transport',
133
- ]);
134
- const TEXT_OPTIONS = Object.freeze([
135
- '--families',
136
- '--resolver',
137
- '--fail-on',
138
- '--expect-content-digest',
139
- '--profile',
140
- '--purpose',
141
- '--observer',
142
- '--retired-selectors',
143
- '--search',
144
- '--selectors',
145
- '--tlds',
146
- '--vantage',
1
+ import { CLI_COMMAND_REGISTRY, CLI_COMMANDS, COMMAND_DESCRIPTIONS, FILE_POSITIONAL_COMMANDS, commandOptionSpec, commandDefinition, commandPositionalSpecs, metaActionDefinition, } from "./command-reference.mjs";
2
+ const MAX_CLI_COMPLETION_BYTES = 32 * 1024;
3
+ const HELP_META_ACTION = metaActionDefinition('help');
4
+ const VERSION_META_ACTION = metaActionDefinition('version');
5
+ const COMMAND_META_ALIASES = Object.freeze([...HELP_META_ACTION.aliases]);
6
+ const ROOT_META_ALIASES = Object.freeze([
7
+ ...HELP_META_ACTION.aliases,
8
+ ...VERSION_META_ACTION.aliases,
147
9
  ]);
10
+ function longMetaAlias(action) {
11
+ const alias = action.aliases.find((candidate) => candidate.startsWith('--'));
12
+ if (!alias)
13
+ throw new Error(`Missing long CLI meta-action alias for ${action.id}.`);
14
+ return alias;
15
+ }
16
+ function shortMetaAlias(action) {
17
+ const alias = action.aliases.find((candidate) => /^-[^-]$/u.test(candidate));
18
+ if (!alias)
19
+ throw new Error(`Missing short CLI meta-action alias for ${action.id}.`);
20
+ return alias;
21
+ }
22
+ const HELP_LONG_ALIAS = longMetaAlias(HELP_META_ACTION);
23
+ const HELP_SHORT_ALIAS = shortMetaAlias(HELP_META_ACTION);
24
+ function optionNames(command) {
25
+ const definition = commandDefinition(command);
26
+ return Object.freeze([...new Set([
27
+ ...definition.grammar.options.map((option) => option.option),
28
+ ...definition.grammar.metaActions.flatMap((id) => metaActionDefinition(id).aliases),
29
+ ])]);
30
+ }
31
+ function optionNamesByKind(command, kinds) {
32
+ return commandDefinition(command).grammar.options
33
+ .filter((option) => kinds.includes(option.valueKind))
34
+ .map((option) => option.option);
35
+ }
36
+ function positionalValues(command, oneBasedIndex) {
37
+ let offset = 0;
38
+ for (const specification of commandPositionalSpecs(command)) {
39
+ if (oneBasedIndex > offset && oneBasedIndex <= offset + specification.maximum) {
40
+ return specification.values;
41
+ }
42
+ offset += specification.maximum;
43
+ }
44
+ return Object.freeze([]);
45
+ }
46
+ function maximumFilePositionals(command) {
47
+ const positionals = commandPositionalSpecs(command);
48
+ return positionals.length > 0 && positionals.every((specification) => specification.valueKind === 'file')
49
+ ? positionals.reduce((total, specification) => total + specification.maximum, 0)
50
+ : 0;
51
+ }
52
+ function positionalEnumValues(command) {
53
+ return positionalValues(command, 1);
54
+ }
55
+ const VALUE_OPTIONS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options)
56
+ .filter((option) => option.values.length > 0)
57
+ .map((option) => [option.option, option.values])));
58
+ function integerValues(command, option, whenOptionPresent) {
59
+ const range = commandOptionSpec(command, option)?.integerRanges
60
+ .find((candidate) => candidate.whenOptionPresent === whenOptionPresent);
61
+ if (!range)
62
+ throw new Error(`Missing completion range for ${command} ${option}.`);
63
+ return Object.freeze(Array.from({ length: range.maximum - range.minimum + 1 }, (_, index) => String(range.minimum + index)));
64
+ }
148
65
  function commandOptions(command) {
149
- return [...COMMON_OPTIONS, ...OPTIONS_BY_COMMAND[command]].join(' ');
66
+ return optionNames(command).join(' ');
67
+ }
68
+ function commandOptionPatterns(kinds) {
69
+ return CLI_COMMANDS.flatMap((command) => optionNamesByKind(command, kinds)
70
+ .map((option) => `${command}:${option}`)).join('|');
150
71
  }
151
72
  function bashCompletion() {
152
- const cases = CLI_COMMANDS.map((command) => ` ${command}) options="${commandOptions(command)}" ;;`).join('\n');
73
+ const cases = CLI_COMMANDS.map((command) => ` ${command}) options="${commandOptions(command)}"; value_options="${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}"; file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
153
74
  const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) COMPREPLY=( $(compgen -W "${values.join(' ')}" -- "\${current}") ); return ;;`).join('\n');
75
+ const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
76
+ .filter((option) => option.valueKind === 'integer')
77
+ .map((option) => {
78
+ const ordinary = integerValues(definition.command, option.option, null).join(' ');
79
+ const conditional = option.integerRanges.find((range) => range.whenOptionPresent !== null);
80
+ if (!conditional) {
81
+ return ` ${definition.command}:${option.option}) COMPREPLY=( $(compgen -W "${ordinary}" -- "\${current}") ); return ;;`;
82
+ }
83
+ const narrowed = integerValues(definition.command, option.option, conditional.whenOptionPresent).join(' ');
84
+ return ` ${definition.command}:${option.option}) if [[ " \${seen_options} " == *" ${conditional.whenOptionPresent} "* ]]; then integer_values="${narrowed}"; else integer_values="${ordinary}"; fi; COMPREPLY=( $(compgen -W "\${integer_values}" -- "\${current}") ); return ;;`;
85
+ })).join('\n');
86
+ const positionalValueCases = CLI_COMMANDS.flatMap((command) => {
87
+ const values = positionalEnumValues(command);
88
+ return values.length > 0
89
+ ? [` ${command}:0) if (( foreign_option == 0 && expect_value == 0 )) && [[ "\${current}" != -* ]]; then COMPREPLY=( $(compgen -W "${values.join(' ')}" -- "\${current}") ); return; fi ;;`]
90
+ : [];
91
+ }).join('\n');
154
92
  return `# WHOISleuth bash completion
93
+ _whoisleuth_direct_lookup_target() {
94
+ local candidate="\${1}"
95
+ [[ "\${candidate}" != -* ]] || return 1
96
+ case " ${CLI_COMMANDS.join(' ')} " in *" \${candidate} "*) return 1 ;; esac
97
+ "\${COMP_WORDS[0]}" "\${candidate}" --plan --json >/dev/null 2>&1
98
+ }
155
99
  _whoisleuth_completion() {
156
- local current previous command options
100
+ local current previous command options value_options file_limit positional_count expect_value foreign_option word integer_values seen_options i
157
101
  current="\${COMP_WORDS[COMP_CWORD]}"
158
102
  previous="\${COMP_WORDS[COMP_CWORD-1]}"
159
103
  command="\${COMP_WORDS[1]}"
160
104
  if [[ \${COMP_CWORD} -eq 1 ]]; then
161
- COMPREPLY=( $(compgen -W "${CLI_COMMANDS.join(' ')} --help --version" -- "\${current}") )
162
- return
163
- fi
164
- if [[ "\${command}" == "completion" && \${COMP_CWORD} -eq 2 ]]; then
165
- COMPREPLY=( $(compgen -W "bash zsh fish powershell" -- "\${current}") )
105
+ COMPREPLY=( $(compgen -W "${CLI_COMMANDS.join(' ')} ${ROOT_META_ALIASES.join(' ')}" -- "\${current}") )
166
106
  return
167
107
  fi
168
- if [[ "\${command}" == "workflow-plan" && \${COMP_CWORD} -eq 2 ]]; then
169
- COMPREPLY=( $(compgen -W "${INVESTIGATION_PLAN_RECIPES.join(' ')}" -- "\${current}") )
170
- return
108
+ if _whoisleuth_direct_lookup_target "\${command}"; then
109
+ command="lookup"
171
110
  fi
172
- case "\${previous}" in
173
- ${valueCases}
174
- ${FILE_OPTIONS.join('|')}) COMPREPLY=( $(compgen -f -- "\${current}") ); return ;;
175
- ${TEXT_OPTIONS.join('|')}) COMPREPLY=(); return ;;
176
- esac
177
111
  case "\${command}" in
178
112
  ${cases}
179
- *) options="--help" ;;
113
+ *) options="${COMMAND_META_ALIASES.join(' ')}"; value_options=""; file_limit=0 ;;
180
114
  esac
181
- case " ${[...FILE_POSITIONAL_COMMANDS].join(' ')} " in
182
- *" \${command} "*) [[ "\${current}" != -* ]] && COMPREPLY=( $(compgen -f -- "\${current}") ) && return ;;
115
+ positional_count=0
116
+ expect_value=0
117
+ foreign_option=0
118
+ seen_options=""
119
+ for ((i=2; i<COMP_CWORD; i++)); do
120
+ word="\${COMP_WORDS[i]}"
121
+ if (( expect_value )); then expect_value=0; continue; fi
122
+ if [[ "\${word}" == -* ]]; then
123
+ if [[ " \${options} " != *" \${word} "* ]]; then foreign_option=1; continue; fi
124
+ seen_options="\${seen_options} \${word}"
125
+ [[ " \${value_options} " == *" \${word} "* ]] && expect_value=1
126
+ else
127
+ ((positional_count+=1))
128
+ fi
129
+ done
130
+ if [[ " \${options} " == *" \${previous} "* ]]; then
131
+ case "\${previous}" in
132
+ ${valueCases}
133
+ esac
134
+ case "\${command}:\${previous}" in
135
+ ${commandOptionPatterns(['file'])}) COMPREPLY=( $(compgen -f -- "\${current}") ); return ;;
136
+ ${integerCases}
137
+ ${commandOptionPatterns(['text'])}) COMPREPLY=(); return ;;
138
+ esac
139
+ fi
140
+ case "\${command}:\${positional_count}" in
141
+ ${positionalValueCases}
183
142
  esac
143
+ if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && [[ "\${current}" != -* ]]; then
144
+ COMPREPLY=( $(compgen -f -- "\${current}") )
145
+ return
146
+ fi
184
147
  COMPREPLY=( $(compgen -W "\${options}" -- "\${current}") )
185
148
  }
186
149
  complete -F _whoisleuth_completion whoisleuth
@@ -188,37 +151,77 @@ complete -F _whoisleuth_completion whoisleuth
188
151
  }
189
152
  function zshCompletion() {
190
153
  const commandEntries = CLI_COMMANDS.map((command) => `'${command}:${COMMAND_DESCRIPTIONS[command] || command}'`).join(' ');
191
- const cases = CLI_COMMANDS.map((command) => ` ${command}) options=(${commandOptions(command)}) ;;`).join('\n');
154
+ const cases = CLI_COMMANDS.map((command) => ` ${command}) options=(${commandOptions(command)}); value_options=(${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}); file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
192
155
  const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) compadd -- ${values.join(' ')}; return ;;`).join('\n');
156
+ const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
157
+ .filter((option) => option.valueKind === 'integer')
158
+ .map((option) => {
159
+ const ordinary = integerValues(definition.command, option.option, null).join(' ');
160
+ const conditional = option.integerRanges.find((range) => range.whenOptionPresent !== null);
161
+ if (!conditional)
162
+ return ` ${definition.command}:${option.option}) compadd -- ${ordinary}; return ;;`;
163
+ const narrowed = integerValues(definition.command, option.option, conditional.whenOptionPresent).join(' ');
164
+ return ` ${definition.command}:${option.option}) if [[ " \${seen_options[*]} " == *" ${conditional.whenOptionPresent} "* ]]; then compadd -- ${narrowed}; else compadd -- ${ordinary}; fi; return ;;`;
165
+ })).join('\n');
166
+ const positionalValueCases = CLI_COMMANDS.flatMap((command) => {
167
+ const values = positionalEnumValues(command);
168
+ return values.length > 0
169
+ ? [` ${command}:0) if (( foreign_option == 0 && expect_value == 0 )) && [[ "\${words[CURRENT]}" != -* ]]; then compadd -- ${values.join(' ')}; return; fi ;;`]
170
+ : [];
171
+ }).join('\n');
193
172
  return `#compdef whoisleuth
173
+ _whoisleuth_direct_lookup_target() {
174
+ local candidate="\${1}"
175
+ [[ "\${candidate}" != -* ]] || return 1
176
+ [[ " ${CLI_COMMANDS.join(' ')} " == *" \${candidate} "* ]] && return 1
177
+ "\${words[1]}" "\${candidate}" --plan --json >/dev/null 2>&1
178
+ }
194
179
  _whoisleuth() {
195
- local command previous
196
- local -a options commands
197
- commands=(${commandEntries})
180
+ local command previous word
181
+ local -a options commands value_options seen_options
182
+ integer file_limit=0 positional_count=0 expect_value=0 foreign_option=0 i
183
+ commands=(${commandEntries} ${[
184
+ ...HELP_META_ACTION.aliases.map((alias) => `'${alias}:Show help'`),
185
+ ...VERSION_META_ACTION.aliases.map((alias) => `'${alias}:Show version'`),
186
+ ].join(' ')})
198
187
  if (( CURRENT == 2 )); then
199
188
  _describe 'command' commands
200
189
  return
201
190
  fi
202
191
  command="\${words[2]}"
203
192
  previous="\${words[CURRENT-1]}"
204
- if [[ "\${command}" == "completion" && CURRENT -eq 3 ]]; then
205
- compadd -- bash zsh fish powershell
206
- return
207
- fi
208
- if [[ "\${command}" == "workflow-plan" && CURRENT -eq 3 ]]; then
209
- compadd -- ${INVESTIGATION_PLAN_RECIPES.join(' ')}
210
- return
193
+ if _whoisleuth_direct_lookup_target "\${command}"; then
194
+ command="lookup"
211
195
  fi
212
- case "\${previous}" in
213
- ${valueCases}
214
- ${FILE_OPTIONS.join('|')}) _files; return ;;
215
- ${TEXT_OPTIONS.join('|')}) _message 'value'; return ;;
216
- esac
217
196
  case "\${command}" in
218
197
  ${cases}
219
- *) options=(--help) ;;
198
+ *) options=(${COMMAND_META_ALIASES.join(' ')}); value_options=(); file_limit=0 ;;
220
199
  esac
221
- if [[ " ${[...FILE_POSITIONAL_COMMANDS].join(' ')} " == *" \${command} "* && "\${words[CURRENT]}" != -* ]]; then
200
+ for ((i=3; i<CURRENT; i++)); do
201
+ word="\${words[i]}"
202
+ if (( expect_value )); then expect_value=0; continue; fi
203
+ if [[ "\${word}" == -* ]]; then
204
+ if [[ " \${options[*]} " != *" \${word} "* ]]; then foreign_option=1; continue; fi
205
+ seen_options+=("\${word}")
206
+ [[ " \${value_options[*]} " == *" \${word} "* ]] && expect_value=1
207
+ else
208
+ ((positional_count+=1))
209
+ fi
210
+ done
211
+ if [[ " \${options[*]} " == *" \${previous} "* ]]; then
212
+ case "\${previous}" in
213
+ ${valueCases}
214
+ esac
215
+ case "\${command}:\${previous}" in
216
+ ${commandOptionPatterns(['file'])}) _files; return ;;
217
+ ${integerCases}
218
+ ${commandOptionPatterns(['text'])}) _message 'value'; return ;;
219
+ esac
220
+ fi
221
+ case "\${command}:\${positional_count}" in
222
+ ${positionalValueCases}
223
+ esac
224
+ if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && [[ "\${words[CURRENT]}" != -* ]]; then
222
225
  _files
223
226
  return
224
227
  fi
@@ -232,84 +235,359 @@ fi
232
235
  `;
233
236
  }
234
237
  function fishCompletion() {
235
- const commandLines = CLI_COMMANDS.map((command) => (`complete -c whoisleuth -n '__fish_use_subcommand' -a '${command}' -d '${COMMAND_DESCRIPTIONS[command]}'${FILE_POSITIONAL_COMMANDS.has(command) ? ' -F' : ''}`));
236
- const optionLine = (condition, option) => {
237
- const name = option.replace(/^--/u, '');
238
- const fileValue = FILE_OPTIONS.includes(option);
239
- const requiresValue = fileValue || [...Object.keys(VALUE_OPTIONS), ...TEXT_OPTIONS].includes(option);
240
- return `complete -c whoisleuth -n '${condition}' -l ${name}${requiresValue ? ' -r' : ''}${fileValue ? ' -F' : ''}`;
241
- };
242
- const commonCondition = 'not __fish_use_subcommand';
243
- const commonOptionLines = COMMON_OPTIONS.map((option) => optionLine(commonCondition, option));
244
- const optionLines = Object.entries(OPTIONS_BY_COMMAND).flatMap(([command, options]) => (options.map((option) => optionLine(`__fish_seen_subcommand_from ${command}`, option))));
245
- const valueLines = Object.entries(VALUE_OPTIONS).flatMap(([option, values]) => (values.map((value) => `complete -c whoisleuth -n '__fish_prev_arg_in ${option}' -a '${value}'`)));
238
+ const commandLines = CLI_COMMANDS.map((command) => (`complete -c whoisleuth -n '__fish_use_subcommand' -a '${command}' -d '${COMMAND_DESCRIPTIONS[command]}'`));
239
+ const optionGroups = new Map();
240
+ for (const definition of CLI_COMMAND_REGISTRY) {
241
+ for (const option of definition.grammar.options) {
242
+ const key = `${option.option}\u0000${option.arity}\u0000${option.valueKind === 'file'}`;
243
+ const group = optionGroups.get(key) ?? {
244
+ option: option.option,
245
+ arity: option.arity,
246
+ file: option.valueKind === 'file',
247
+ commands: [],
248
+ };
249
+ group.commands.push(definition.command);
250
+ optionGroups.set(key, group);
251
+ }
252
+ }
253
+ const optionLines = [...optionGroups.values()].map((group) => {
254
+ const condition = `__whoisleuth_command_is ${group.commands.join(' ')}`
255
+ + (group.commands.includes('lookup') ? '; or __whoisleuth_direct_lookup_target' : '');
256
+ return `complete -c whoisleuth -n '${condition}'${group.option === HELP_LONG_ALIAS ? ` -s ${HELP_SHORT_ALIAS.slice(1)}` : ''} -l ${group.option.slice(2)}${group.arity === 1 ? ' -r' : ''}${group.file ? ' -F' : ''}`;
257
+ });
258
+ const valueLines = Object.entries(VALUE_OPTIONS).flatMap(([option, values]) => {
259
+ const commands = CLI_COMMANDS.filter((command) => commandOptionSpec(command, option) !== null);
260
+ const namedCondition = commands.length === CLI_COMMANDS.length
261
+ ? `__fish_prev_arg_in ${option}`
262
+ : `__whoisleuth_command_is ${commands.join(' ')}; and __fish_prev_arg_in ${option}`;
263
+ const lines = commands.length > 0
264
+ ? [`complete -c whoisleuth -n '${namedCondition}' -a '${values.join(' ')}'`]
265
+ : [];
266
+ if (commands.includes('lookup')) {
267
+ lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${option}' -a '${values.join(' ')}'`);
268
+ }
269
+ return lines;
270
+ });
271
+ const integerLines = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
272
+ .filter((option) => option.valueKind === 'integer')
273
+ .flatMap((option) => option.integerRanges.map((range) => {
274
+ const qualifiers = [
275
+ `__fish_prev_arg_in ${option.option}`,
276
+ `and __whoisleuth_command_is ${definition.command}`,
277
+ ...(range.whenOptionPresent
278
+ ? [`and __whoisleuth_seen ${definition.command} ${range.whenOptionPresent}`]
279
+ : option.integerRanges.length > 1
280
+ ? [`and not __whoisleuth_seen ${definition.command} ${option.integerRanges.find((candidate) => candidate.whenOptionPresent)?.whenOptionPresent}`]
281
+ : []),
282
+ ].join('; ');
283
+ return `complete -c whoisleuth -n '${qualifiers}' -a '(__whoisleuth_integer_values ${range.minimum} ${range.maximum})'`;
284
+ })));
285
+ const positionalCases = [...new Set([
286
+ ...FILE_POSITIONAL_COMMANDS,
287
+ ...CLI_COMMANDS.filter((command) => positionalEnumValues(command).length > 0),
288
+ ...CLI_COMMAND_REGISTRY.filter((definition) => definition.grammar.options
289
+ .some((option) => option.integerRanges.some((range) => range.whenOptionPresent !== null)))
290
+ .map((definition) => definition.command),
291
+ ])].map((command) => {
292
+ const definition = commandDefinition(command);
293
+ const commandValueOptions = definition.grammar.options
294
+ .filter((option) => option.scope === 'command' && option.arity === 1)
295
+ .map((option) => option.option);
296
+ const positioning = FILE_POSITIONAL_COMMANDS.includes(command)
297
+ || positionalEnumValues(command).length > 0;
298
+ return ` case ${command}\n${positioning
299
+ ? ` set options $options ${definition.completion.options.join(' ')}\n`
300
+ : ' set strict 0\n'} set value_options $value_options ${commandValueOptions.join(' ')}\n set limit ${maximumFilePositionals(command)}`;
301
+ }).join('\n');
302
+ const positionalEnumLines = CLI_COMMANDS.flatMap((command) => {
303
+ const values = positionalEnumValues(command);
304
+ return values.length > 0
305
+ ? [`complete -c whoisleuth -n '__whoisleuth_position_is 0 ${command}' -a '${values.join(' ')}'`]
306
+ : [];
307
+ });
246
308
  return `# WHOISleuth fish completion
309
+ function __whoisleuth_clear_direct_lookup_cache --on-event fish_prompt
310
+ set -e __whoisleuth_direct_lookup_cache_key
311
+ set -e __whoisleuth_direct_lookup_cache_status
312
+ end
313
+ function __whoisleuth_direct_lookup_target
314
+ set -l words (commandline -opc)
315
+ if test (count \$words) -lt 2
316
+ return 1
317
+ end
318
+ set -l first \$words[2]
319
+ string match -q -- '-*' \$first; and return 1
320
+ contains -- \$first ${CLI_COMMANDS.join(' ')}; and return 1
321
+ set -l cache_key "\$words[1]:\$first"
322
+ if test "\$__whoisleuth_direct_lookup_cache_key" = "\$cache_key"
323
+ return \$__whoisleuth_direct_lookup_cache_status
324
+ end
325
+ \$words[1] \$first --plan --json >/dev/null 2>/dev/null
326
+ set -l result \$status
327
+ set -g __whoisleuth_direct_lookup_cache_key \$cache_key
328
+ set -g __whoisleuth_direct_lookup_cache_status \$result
329
+ return \$result
330
+ end
331
+ function __whoisleuth_command_is
332
+ set -l words (commandline -opc)
333
+ test (count \$words) -ge 2; or return 1
334
+ contains -- \$words[2] $argv
335
+ end
336
+ function __whoisleuth_integer_values
337
+ set -l value $argv[1]
338
+ while test $value -le $argv[2]
339
+ echo $value
340
+ set value (math $value + 1)
341
+ end
342
+ end
343
+ function __whoisleuth_position_state
344
+ set -l expected_command $argv[1]
345
+ set -l words (commandline -opc)
346
+ test (count $words) -ge 2; or return 1
347
+ set -l command $words[2]
348
+ test "$command" = "$expected_command"; or return 1
349
+ set -l options ${COMMAND_META_ALIASES.join(' ')} --output --force --config --profile --palette
350
+ set -l value_options --output --config --profile --palette
351
+ set -l limit 0
352
+ set -l strict 1
353
+ switch $command
354
+ ${positionalCases}
355
+ case '*'
356
+ return 1
357
+ end
358
+ set -l expect_value 0
359
+ set -l positional_count 0
360
+ set -l seen
361
+ for word in $words[3..-1]
362
+ if test $expect_value -eq 1
363
+ set expect_value 0
364
+ continue
365
+ end
366
+ if string match -q -- '-*' $word
367
+ if test $strict -eq 1
368
+ contains -- $word $options; or return 1
369
+ end
370
+ set -a seen $word
371
+ contains -- $word $value_options; and set expect_value 1
372
+ else
373
+ set positional_count (math $positional_count + 1)
374
+ end
375
+ end
376
+ test $expect_value -eq 0; or return 1
377
+ echo $positional_count $limit $seen
378
+ end
379
+ function __whoisleuth_position_is
380
+ set -l state (__whoisleuth_position_state $argv[2]); or return 1
381
+ test $state[1] -eq $argv[1]
382
+ end
383
+ function __whoisleuth_file_position
384
+ set -l words (commandline -opc)
385
+ test (count $words) -ge 2; or return 1
386
+ set -l state (__whoisleuth_position_state $words[2]); or return 1
387
+ test $state[2] -gt $state[1]; or return 1
388
+ not string match -qr -- ^- (commandline -ct)
389
+ end
390
+ function __whoisleuth_seen
391
+ set -l s (__whoisleuth_position_state $argv[1]); or return 1
392
+ contains -- $argv[2] $s[3..-1]
393
+ end
247
394
  complete -c whoisleuth -f
248
- complete -c whoisleuth -n '__fish_use_subcommand' -l help
249
- complete -c whoisleuth -n '__fish_use_subcommand' -l version
395
+ ${[HELP_META_ACTION, VERSION_META_ACTION].map((action) => (`complete -c whoisleuth -n '__fish_use_subcommand' -s ${shortMetaAlias(action).slice(1)} -l ${longMetaAlias(action).slice(2)}`)).join('\n')}
250
396
  ${commandLines.join('\n')}
251
- complete -c whoisleuth -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish powershell'
252
- complete -c whoisleuth -n '__fish_seen_subcommand_from workflow-plan' -a '${INVESTIGATION_PLAN_RECIPES.join(' ')}'
253
- ${commonOptionLines.join('\n')}
397
+ ${positionalEnumLines.join('\n')}
398
+ complete -c whoisleuth -n '__whoisleuth_file_position' -F
254
399
  ${optionLines.join('\n')}
255
400
  ${valueLines.join('\n')}
401
+ ${integerLines.join('\n')}
256
402
  `;
257
403
  }
258
404
  function powershellCompletion() {
259
405
  const commandOptions = Object.fromEntries(CLI_COMMANDS.map((command) => [
260
406
  command,
261
- [...COMMON_OPTIONS, ...OPTIONS_BY_COMMAND[command]],
407
+ optionNames(command),
262
408
  ]));
409
+ const positionalValuesByCommand = Object.fromEntries(CLI_COMMANDS.flatMap((command) => {
410
+ const values = positionalEnumValues(command);
411
+ return values.length > 0 ? [[`${command}:0`, values]] : [];
412
+ }));
413
+ const integerRanges = Object.fromEntries(CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
414
+ .filter((option) => option.valueKind === 'integer')
415
+ .map((option) => {
416
+ const range = option.integerRanges.find((candidate) => candidate.whenOptionPresent === null);
417
+ if (!range)
418
+ throw new Error(`Missing ordinary completion range for ${definition.command} ${option.option}.`);
419
+ return [`${definition.command}:${option.option}`, Object.freeze({
420
+ minimum: range.minimum,
421
+ maximum: range.maximum,
422
+ })];
423
+ })));
424
+ const conditionalIntegerRanges = Object.fromEntries(CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
425
+ .filter((option) => option.valueKind === 'integer')
426
+ .flatMap((option) => option.integerRanges
427
+ .filter((range) => range.whenOptionPresent !== null)
428
+ .map((range) => [`${definition.command}:${option.option}`, Object.freeze({
429
+ option: range.whenOptionPresent,
430
+ minimum: range.minimum,
431
+ maximum: range.maximum,
432
+ })]))));
263
433
  return `# WHOISleuth PowerShell completion
264
434
  Register-ArgumentCompleter -Native -CommandName whoisleuth -ScriptBlock {
265
435
  param($wordToComplete, $commandAst, $cursorPosition)
436
+ $noMatch = { [System.Management.Automation.CompletionResult]::new(' ', 'no matches', 'ParameterValue', 'No completion available') }
266
437
  $commands = @(${CLI_COMMANDS.map((command) => `'${command}'`).join(', ')})
267
438
  $options = @{
268
439
  ${Object.entries(commandOptions).map(([command, options]) => ` '${command}' = @(${options.map((option) => `'${option}'`).join(', ')})`).join('\n')}
269
440
  }
270
441
  $values = @{
271
442
  ${Object.entries(VALUE_OPTIONS).map(([option, values]) => ` '${option}' = @(${values.map((value) => `'${value}'`).join(', ')})`).join('\n')}
272
- 'completion' = @('bash', 'zsh', 'fish', 'powershell')
273
- 'workflow-plan' = @(${INVESTIGATION_PLAN_RECIPES.map((value) => `'${value}'`).join(', ')})
274
443
  }
275
- $fileCommands = @(${[...FILE_POSITIONAL_COMMANDS].map((command) => `'${command}'`).join(', ')})
276
- $elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
444
+ $fileOptions = @{
445
+ ${CLI_COMMANDS.map((command) => ` '${command}' = @(${optionNamesByKind(command, ['file']).map((option) => `'${option}'`).join(', ')})`).join('\n')}
446
+ }
447
+ $textOptions = @{
448
+ ${CLI_COMMANDS.map((command) => ` '${command}' = @(${optionNamesByKind(command, ['text']).map((option) => `'${option}'`).join(', ')})`).join('\n')}
449
+ }
450
+ $valueOptions = @{
451
+ ${CLI_COMMANDS.map((command) => ` '${command}' = @(${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).map((option) => `'${option}'`).join(', ')})`).join('\n')}
452
+ }
453
+ $fileLimits = @{
454
+ ${FILE_POSITIONAL_COMMANDS.map((command) => ` '${command}' = ${maximumFilePositionals(command)}`).join('\n')}
455
+ }
456
+ $positionValues = @{
457
+ ${Object.entries(positionalValuesByCommand).map(([key, values]) => ` '${key}' = @(${values.map((value) => `'${value}'`).join(', ')})`).join('\n')}
458
+ }
459
+ $integerRanges = @{
460
+ ${Object.entries(integerRanges).map(([key, range]) => ` '${key}' = @(${range.minimum}, ${range.maximum})`).join('\n')}
461
+ }
462
+ $conditionalIntegerRanges = @{
463
+ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' = @('${range.option}', ${range.minimum}, ${range.maximum})`).join('\n')}
464
+ }
465
+ $elements = @($commandAst.CommandElements | ForEach-Object {
466
+ if ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $_.Value }
467
+ else { $_.Extent.Text }
468
+ })
277
469
  $command = if ($elements.Count -gt 1) { $elements[1] } else { '' }
278
- $previous = if ($elements.Count -gt 1) { $elements[$elements.Count - 1] } else { '' }
279
- if ($fileCommands -contains $command -and -not $wordToComplete.StartsWith('-')) {
280
- Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue | ForEach-Object {
470
+ $previousIndex = if ($wordToComplete) { $elements.Count - 2 } else { $elements.Count - 1 }
471
+ $previous = if ($previousIndex -ge 0) { $elements[$previousIndex] } else { '' }
472
+ $rootCompletion = $elements.Count -eq 1 -or (
473
+ $elements.Count -eq 2 -and $cursorPosition -le $commandAst.Extent.EndOffset
474
+ )
475
+ $directLookup = $false
476
+ if ($command -notmatch '^-' -and $elements.Count -gt 1 -and $commands -notcontains $command) {
477
+ & $elements[0] $command '--plan' '--json' *> $null
478
+ $directLookup = $LASTEXITCODE -eq 0
479
+ }
480
+ if ($directLookup) { $command = 'lookup' }
481
+ $commandKnown = $options.ContainsKey($command)
482
+ $activeOptions = if ($commandKnown) { $options[$command] } else { @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')}) }
483
+ $previousOwned = $activeOptions -contains $previous
484
+ if ($previousOwned -and $fileOptions.ContainsKey($command) -and $fileOptions[$command] -contains $previous) {
485
+ $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
486
+ $files | ForEach-Object {
281
487
  [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
282
488
  }
489
+ if ($files.Count -eq 0) { & $noMatch }
490
+ return
491
+ }
492
+ if ($previousOwned -and $textOptions.ContainsKey($command) -and $textOptions[$command] -contains $previous) {
493
+ $completionText = if ([string]::IsNullOrEmpty($wordToComplete)) { ' ' } else { [string]$wordToComplete }
494
+ $listItemText = if ([string]::IsNullOrEmpty($wordToComplete)) { 'value' } else { [string]$wordToComplete }
495
+ [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterValue', 'Enter a value')
283
496
  return
284
497
  }
285
- $candidates = if ($elements.Count -le 2) {
286
- @($commands) + @('--help', '--version')
287
- } elseif ($command -eq 'completion' -and $elements.Count -le 3) {
288
- $values['completion']
289
- } elseif ($command -eq 'workflow-plan' -and $elements.Count -le 3) {
290
- $values['workflow-plan']
291
- } elseif ($values.ContainsKey($previous)) {
292
- $values[$previous]
293
- } elseif ($options.ContainsKey($command)) {
294
- $options[$command]
498
+ $positionCount = 0
499
+ $expectValue = $false
500
+ $foreignOption = $false
501
+ $seenOptions = @()
502
+ for ($index = 2; $index -le $previousIndex; $index += 1) {
503
+ $word = $elements[$index]
504
+ if ($expectValue) { $expectValue = $false; continue }
505
+ if ($word.StartsWith('-')) {
506
+ if ($activeOptions -notcontains $word) { $foreignOption = $true; continue }
507
+ $seenOptions += $word
508
+ if ($valueOptions.ContainsKey($command) -and $valueOptions[$command] -contains $word) { $expectValue = $true }
509
+ } else {
510
+ $positionCount += 1
511
+ }
512
+ }
513
+ $integerKey = "${'$'}{command}:${'$'}previous"
514
+ if ($previousOwned -and $integerRanges.ContainsKey($integerKey)) {
515
+ $range = $integerRanges[$integerKey]
516
+ if ($conditionalIntegerRanges.ContainsKey($integerKey)) {
517
+ $condition = $conditionalIntegerRanges[$integerKey]
518
+ if ($seenOptions -contains $condition[0]) { $range = @($condition[1], $condition[2]) }
519
+ }
520
+ $matched = $false
521
+ foreach ($number in $range[0]..$range[1]) {
522
+ $candidate = [string]$number
523
+ if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
524
+ [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
525
+ $matched = $true
526
+ }
527
+ }
528
+ if (-not $matched) { & $noMatch }
529
+ return
530
+ }
531
+ if ($previousOwned -and $values.ContainsKey($previous)) {
532
+ $matched = $false
533
+ foreach ($candidate in $values[$previous]) {
534
+ if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
535
+ [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
536
+ $matched = $true
537
+ }
538
+ }
539
+ if (-not $matched) { & $noMatch }
540
+ return
541
+ }
542
+ $positionKey = "${'$'}{command}:${'$'}positionCount"
543
+ if ($positionValues.ContainsKey($positionKey) -and -not $foreignOption -and -not $expectValue -and -not $wordToComplete.StartsWith('-')) {
544
+ $matched = $false
545
+ foreach ($candidate in $positionValues[$positionKey]) {
546
+ if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
547
+ [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
548
+ $matched = $true
549
+ }
550
+ }
551
+ if (-not $matched) { & $noMatch }
552
+ return
553
+ }
554
+ if ($fileLimits.ContainsKey($command) -and $fileLimits[$command] -gt $positionCount -and -not $foreignOption -and -not $expectValue -and -not $wordToComplete.StartsWith('-')) {
555
+ $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
556
+ $files | ForEach-Object {
557
+ [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
558
+ }
559
+ if ($files.Count -eq 0) { & $noMatch }
560
+ return
561
+ }
562
+ $candidates = if ($rootCompletion -and -not $directLookup) {
563
+ @($commands) + @(${ROOT_META_ALIASES.map((alias) => `'${alias}'`).join(', ')})
564
+ } elseif ($commandKnown) {
565
+ $activeOptions
295
566
  } else {
296
- @('--help')
567
+ @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')})
297
568
  }
569
+ $matched = $false
298
570
  foreach ($candidate in $candidates) {
299
571
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
300
572
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
573
+ $matched = $true
301
574
  }
302
575
  }
576
+ if (-not $matched) { & $noMatch }
303
577
  }
304
578
  `;
305
579
  }
306
580
  function buildShellCompletion(shell) {
307
- if (shell === 'bash')
308
- return bashCompletion();
309
- if (shell === 'zsh')
310
- return zshCompletion();
311
- if (shell === 'fish')
312
- return fishCompletion();
313
- return powershellCompletion();
581
+ const script = shell === 'bash'
582
+ ? bashCompletion()
583
+ : shell === 'zsh'
584
+ ? zshCompletion()
585
+ : shell === 'fish'
586
+ ? fishCompletion()
587
+ : powershellCompletion();
588
+ if (Buffer.byteLength(script, 'utf8') > MAX_CLI_COMPLETION_BYTES) {
589
+ throw new RangeError(`Generated completion is limited to ${MAX_CLI_COMPLETION_BYTES} UTF-8 bytes.`);
590
+ }
591
+ return script;
314
592
  }
315
- export { buildShellCompletion, };
593
+ export { MAX_CLI_COMPLETION_BYTES, buildShellCompletion, };