@slicedearth/whoisleuth-cli 1.45.0 → 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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,178 +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', '--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
- 'registry-support': ['--json', '--quiet', '--no-color'],
18
- 'registry-doctor': ['--json', '--quiet', '--no-color'],
19
- 'registry-cohort': ['--json', '--quiet', '--no-color'],
20
- 'registry-scaffold': ['--profile', '--suffix', '--scenario'],
21
- 'risk-calibrate': ['--json', '--quiet', '--no-color'],
22
- 'lookalike-calibrate': ['--json', '--quiet', '--no-color'],
23
- 'verify-artifact': ['--passphrase-file', '--json', '--quiet', '--no-color'],
24
- 'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
25
- 'sign-artifact': ['--private-key-file'],
26
- 'verify-signature': ['--public-key-file', '--json', '--quiet', '--no-color'],
27
- 'source-report': ['--json', '--quiet', '--no-color'],
28
- compare: ['--json', '--quiet', '--no-color'],
29
- 'page-compare': ['--json', '--quiet', '--no-color'],
30
- 'mail-review': ['--json', '--quiet', '--no-color'],
31
- 'review-evidence': ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
32
- brief: ['--json', '--quiet', '--no-color'],
33
- 'case-pack': ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
34
- 'domain-control': ['--json', '--quiet', '--no-color'],
35
- 'monitor-once': ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
36
- assurance: ['--json', '--quiet', '--no-color'],
37
- 'change-packet': ['--json', '--quiet', '--no-color'],
38
- 'sharing-review': ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
39
- 'workflow-plan': ['--json', '--quiet', '--no-color'],
40
- 'workflow-run': ['--approve-network', '--resume', '--json', '--quiet', '--no-color'],
41
- diff: ['--json', '--quiet', '--no-color'],
42
- reconcile: ['--json', '--quiet', '--no-color'],
43
- timeline: ['--json', '--quiet', '--no-color'],
44
- export: ['--markdown', '--html', '--compact'],
45
- completion: [],
46
- commands: ['--json', '--quiet', '--no-color'],
47
- doctor: ['--network', '--json', '--quiet', '--no-color'],
48
- manual: [],
49
- });
50
- const COMMAND_DESCRIPTIONS = Object.freeze({
51
- manifest: 'Build an evidence manifest offline',
52
- 'map-observations': 'Normalise passive DNS observations offline',
53
- 'oam-export': 'Export an observation-archive map offline',
54
- lookup: 'Collect one domain, IP, or ASN',
55
- bulk: 'Run bounded multi-target collection',
56
- 'ct-search': 'Search certificate observations',
57
- 'ct-intake': 'Normalise certificate observations offline',
58
- discover: 'Generate lookalike candidates offline',
59
- 'discover-scan': 'Collect a supervised candidate review queue',
60
- posture: 'Review DNS and mail posture',
61
- http: 'Inspect one homepage request',
62
- tls: 'Inspect one TLS connection',
63
- 'registry-support': 'Explain local registry coverage',
64
- 'registry-doctor': 'Diagnose saved registry collection',
65
- 'registry-cohort': 'Aggregate target-free registry quality cohorts',
66
- 'registry-scaffold': 'Create a sanitised registry fixture scaffold',
67
- 'risk-calibrate': 'Replay reviewed Risk labels offline',
68
- 'lookalike-calibrate': 'Summarise reviewed lookalike yield offline',
69
- 'verify-artifact': 'Validate saved evidence offline',
70
- 'inspect-archive': 'Inspect an archive locally',
71
- 'sign-artifact': 'Sign a reviewed artefact locally',
72
- 'verify-signature': 'Verify a signed evidence package',
73
- 'source-report': 'Build a target-free source report',
74
- compare: 'Compare registry publications in one lookup',
75
- 'page-compare': 'Compare saved static page evidence',
76
- 'mail-review': 'Review saved passive mail evidence',
77
- 'review-evidence': 'Review supplied evidence offline',
78
- brief: 'Build a decision brief from a saved lookup',
79
- 'case-pack': 'Build a reviewed case package',
80
- 'domain-control': 'Build or review a domain control manifest',
81
- 'monitor-once': 'Run one bounded domain control review',
82
- assurance: 'Review domain change, recovery, or retirement plans',
83
- 'change-packet': 'Build a reviewed change packet offline',
84
- 'sharing-review': 'Lint an artefact before deliberate sharing',
85
- 'workflow-plan': 'Plan a fixed investigation recipe',
86
- 'workflow-run': 'Execute approved fixed-recipe steps',
87
- diff: 'Compare two saved domain lookups',
88
- reconcile: 'Reconcile independently labelled observations',
89
- timeline: 'Build same-domain history from saved lookups',
90
- export: 'Convert a lookup to an evidence report',
91
- completion: 'Print shell completion',
92
- commands: 'List installed command contracts',
93
- doctor: 'Check the local CLI runtime',
94
- manual: 'Print the generated manual page',
95
- });
96
- const VALUE_OPTIONS = Object.freeze({
97
- '--preset': ['common', 'impersonation', 'all'],
98
- '--keyboard': ['qwerty', 'azerty', 'qwertz', 'all'],
99
- '--mail-profile': ['standard', 'defensive-no-mail', 'parked'],
100
- '--marking': ['clear', 'green', 'amber', 'amber-strict', 'red'],
101
- '--recipient-scope': ['public', 'community', 'organization', 'named-recipients'],
102
- '--audience': ['internal', 'trusted', 'public'],
103
- '--concurrency': ['1', '2', '3', '4', '5', '6', '7', '8'],
104
- });
105
- const FILE_OPTIONS = Object.freeze([
106
- '--checkpoint',
107
- '--config',
108
- '--allowlist',
109
- '--dictionary',
110
- '--output',
111
- '--passphrase-file',
112
- '--private-key-file',
113
- '--public-key-file',
114
- '--snapshot',
115
- '--observation-snapshot',
116
- '--previous',
117
- '--resume',
118
- ]);
119
- const FILE_POSITIONAL_COMMANDS = new Set([
120
- 'manifest',
121
- 'map-observations',
122
- 'oam-export',
123
- 'ct-intake',
124
- 'change-packet',
125
- ]);
126
- const TEXT_OPTIONS = Object.freeze([
127
- '--families',
128
- '--resolver',
129
- '--fail-on',
130
- '--expect-content-digest',
131
- '--profile',
132
- '--purpose',
133
- '--observer',
134
- '--retired-selectors',
135
- '--search',
136
- '--selectors',
137
- '--tlds',
138
- '--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,
139
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
+ }
140
65
  function commandOptions(command) {
141
- 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('|');
142
71
  }
143
72
  function bashCompletion() {
144
- 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');
145
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');
146
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
+ }
147
99
  _whoisleuth_completion() {
148
- 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
149
101
  current="\${COMP_WORDS[COMP_CWORD]}"
150
102
  previous="\${COMP_WORDS[COMP_CWORD-1]}"
151
103
  command="\${COMP_WORDS[1]}"
152
104
  if [[ \${COMP_CWORD} -eq 1 ]]; then
153
- COMPREPLY=( $(compgen -W "${CLI_COMMANDS.join(' ')} --help --version" -- "\${current}") )
154
- return
155
- fi
156
- if [[ "\${command}" == "completion" && \${COMP_CWORD} -eq 2 ]]; then
157
- COMPREPLY=( $(compgen -W "bash zsh fish powershell" -- "\${current}") )
105
+ COMPREPLY=( $(compgen -W "${CLI_COMMANDS.join(' ')} ${ROOT_META_ALIASES.join(' ')}" -- "\${current}") )
158
106
  return
159
107
  fi
160
- if [[ "\${command}" == "workflow-plan" && \${COMP_CWORD} -eq 2 ]]; then
161
- COMPREPLY=( $(compgen -W "${INVESTIGATION_PLAN_RECIPES.join(' ')}" -- "\${current}") )
162
- return
108
+ if _whoisleuth_direct_lookup_target "\${command}"; then
109
+ command="lookup"
163
110
  fi
164
- case "\${previous}" in
165
- ${valueCases}
166
- ${FILE_OPTIONS.join('|')}) COMPREPLY=( $(compgen -f -- "\${current}") ); return ;;
167
- ${TEXT_OPTIONS.join('|')}) COMPREPLY=(); return ;;
168
- esac
169
111
  case "\${command}" in
170
112
  ${cases}
171
- *) options="--help" ;;
113
+ *) options="${COMMAND_META_ALIASES.join(' ')}"; value_options=""; file_limit=0 ;;
172
114
  esac
173
- case " ${[...FILE_POSITIONAL_COMMANDS].join(' ')} " in
174
- *" \${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}
175
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
176
147
  COMPREPLY=( $(compgen -W "\${options}" -- "\${current}") )
177
148
  }
178
149
  complete -F _whoisleuth_completion whoisleuth
@@ -180,37 +151,77 @@ complete -F _whoisleuth_completion whoisleuth
180
151
  }
181
152
  function zshCompletion() {
182
153
  const commandEntries = CLI_COMMANDS.map((command) => `'${command}:${COMMAND_DESCRIPTIONS[command] || command}'`).join(' ');
183
- 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');
184
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');
185
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
+ }
186
179
  _whoisleuth() {
187
- local command previous
188
- local -a options commands
189
- 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(' ')})
190
187
  if (( CURRENT == 2 )); then
191
188
  _describe 'command' commands
192
189
  return
193
190
  fi
194
191
  command="\${words[2]}"
195
192
  previous="\${words[CURRENT-1]}"
196
- if [[ "\${command}" == "completion" && CURRENT -eq 3 ]]; then
197
- compadd -- bash zsh fish powershell
198
- return
199
- fi
200
- if [[ "\${command}" == "workflow-plan" && CURRENT -eq 3 ]]; then
201
- compadd -- ${INVESTIGATION_PLAN_RECIPES.join(' ')}
202
- return
193
+ if _whoisleuth_direct_lookup_target "\${command}"; then
194
+ command="lookup"
203
195
  fi
204
- case "\${previous}" in
205
- ${valueCases}
206
- ${FILE_OPTIONS.join('|')}) _files; return ;;
207
- ${TEXT_OPTIONS.join('|')}) _message 'value'; return ;;
208
- esac
209
196
  case "\${command}" in
210
197
  ${cases}
211
- *) options=(--help) ;;
198
+ *) options=(${COMMAND_META_ALIASES.join(' ')}); value_options=(); file_limit=0 ;;
212
199
  esac
213
- 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
214
225
  _files
215
226
  return
216
227
  fi
@@ -224,84 +235,359 @@ fi
224
235
  `;
225
236
  }
226
237
  function fishCompletion() {
227
- 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' : ''}`));
228
- const optionLine = (condition, option) => {
229
- const name = option.replace(/^--/u, '');
230
- const fileValue = FILE_OPTIONS.includes(option);
231
- const requiresValue = fileValue || [...Object.keys(VALUE_OPTIONS), ...TEXT_OPTIONS].includes(option);
232
- return `complete -c whoisleuth -n '${condition}' -l ${name}${requiresValue ? ' -r' : ''}${fileValue ? ' -F' : ''}`;
233
- };
234
- const commonCondition = `__fish_seen_subcommand_from ${CLI_COMMANDS.join(' ')}`;
235
- const commonOptionLines = COMMON_OPTIONS.map((option) => optionLine(commonCondition, option));
236
- const optionLines = Object.entries(OPTIONS_BY_COMMAND).flatMap(([command, options]) => (options.map((option) => optionLine(`__fish_seen_subcommand_from ${command}`, option))));
237
- 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
+ });
238
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
239
394
  complete -c whoisleuth -f
240
- complete -c whoisleuth -n '__fish_use_subcommand' -l help
241
- 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')}
242
396
  ${commandLines.join('\n')}
243
- complete -c whoisleuth -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish powershell'
244
- complete -c whoisleuth -n '__fish_seen_subcommand_from workflow-plan' -a '${INVESTIGATION_PLAN_RECIPES.join(' ')}'
245
- ${commonOptionLines.join('\n')}
397
+ ${positionalEnumLines.join('\n')}
398
+ complete -c whoisleuth -n '__whoisleuth_file_position' -F
246
399
  ${optionLines.join('\n')}
247
400
  ${valueLines.join('\n')}
401
+ ${integerLines.join('\n')}
248
402
  `;
249
403
  }
250
404
  function powershellCompletion() {
251
405
  const commandOptions = Object.fromEntries(CLI_COMMANDS.map((command) => [
252
406
  command,
253
- [...COMMON_OPTIONS, ...OPTIONS_BY_COMMAND[command]],
407
+ optionNames(command),
254
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
+ })]))));
255
433
  return `# WHOISleuth PowerShell completion
256
434
  Register-ArgumentCompleter -Native -CommandName whoisleuth -ScriptBlock {
257
435
  param($wordToComplete, $commandAst, $cursorPosition)
436
+ $noMatch = { [System.Management.Automation.CompletionResult]::new(' ', 'no matches', 'ParameterValue', 'No completion available') }
258
437
  $commands = @(${CLI_COMMANDS.map((command) => `'${command}'`).join(', ')})
259
438
  $options = @{
260
439
  ${Object.entries(commandOptions).map(([command, options]) => ` '${command}' = @(${options.map((option) => `'${option}'`).join(', ')})`).join('\n')}
261
440
  }
262
441
  $values = @{
263
442
  ${Object.entries(VALUE_OPTIONS).map(([option, values]) => ` '${option}' = @(${values.map((value) => `'${value}'`).join(', ')})`).join('\n')}
264
- 'completion' = @('bash', 'zsh', 'fish', 'powershell')
265
- 'workflow-plan' = @(${INVESTIGATION_PLAN_RECIPES.map((value) => `'${value}'`).join(', ')})
266
443
  }
267
- $fileCommands = @(${[...FILE_POSITIONAL_COMMANDS].map((command) => `'${command}'`).join(', ')})
268
- $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
+ })
269
469
  $command = if ($elements.Count -gt 1) { $elements[1] } else { '' }
270
- $previous = if ($elements.Count -gt 1) { $elements[$elements.Count - 1] } else { '' }
271
- if ($fileCommands -contains $command -and -not $wordToComplete.StartsWith('-')) {
272
- 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 {
273
487
  [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
274
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')
275
496
  return
276
497
  }
277
- $candidates = if ($elements.Count -le 2) {
278
- @($commands) + @('--help', '--version')
279
- } elseif ($command -eq 'completion' -and $elements.Count -le 3) {
280
- $values['completion']
281
- } elseif ($command -eq 'workflow-plan' -and $elements.Count -le 3) {
282
- $values['workflow-plan']
283
- } elseif ($values.ContainsKey($previous)) {
284
- $values[$previous]
285
- } elseif ($options.ContainsKey($command)) {
286
- $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
287
566
  } else {
288
- @('--help')
567
+ @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')})
289
568
  }
569
+ $matched = $false
290
570
  foreach ($candidate in $candidates) {
291
571
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
292
572
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
573
+ $matched = $true
293
574
  }
294
575
  }
576
+ if (-not $matched) { & $noMatch }
295
577
  }
296
578
  `;
297
579
  }
298
580
  function buildShellCompletion(shell) {
299
- if (shell === 'bash')
300
- return bashCompletion();
301
- if (shell === 'zsh')
302
- return zshCompletion();
303
- if (shell === 'fish')
304
- return fishCompletion();
305
- 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;
306
592
  }
307
- export { buildShellCompletion, };
593
+ export { MAX_CLI_COMPLETION_BYTES, buildShellCompletion, };