@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,5 +1,6 @@
1
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;
2
+ // Emergency bound for one static script; input and execution bounds are independent.
3
+ const MAX_CLI_COMPLETION_BYTES = 1024 * 1024;
3
4
  const HELP_META_ACTION = metaActionDefinition('help');
4
5
  const VERSION_META_ACTION = metaActionDefinition('version');
5
6
  const COMMAND_META_ALIASES = Object.freeze([...HELP_META_ACTION.aliases]);
@@ -92,10 +93,83 @@ function commandOptionPatterns(kinds) {
92
93
  return CLI_COMMANDS.flatMap((command) => optionNamesByKind(command, kinds)
93
94
  .map((option) => `${command}:${option}`)).join('|');
94
95
  }
96
+ function completionExclusions() {
97
+ const exclusions = new Map();
98
+ let changed = false;
99
+ const add = (command, selected, excluded) => {
100
+ const key = `${command}:${selected}`;
101
+ const values = exclusions.get(key) ?? new Set();
102
+ if (!values.has(excluded)) {
103
+ values.add(excluded);
104
+ changed = true;
105
+ }
106
+ exclusions.set(key, values);
107
+ };
108
+ for (const definition of CLI_COMMAND_REGISTRY) {
109
+ for (const constraint of definition.grammar.constraints) {
110
+ if (constraint.kind === 'mutually_exclusive') {
111
+ for (const selected of constraint.options) {
112
+ for (const excluded of constraint.options) {
113
+ if (selected !== excluded)
114
+ add(definition.command, selected, excluded);
115
+ }
116
+ }
117
+ }
118
+ else if (constraint.kind === 'excludes_all' || constraint.kind === 'value_excludes') {
119
+ const selected = constraint.kind === 'value_excludes'
120
+ ? `${constraint.option}=${constraint.value}` : constraint.option;
121
+ for (const excluded of constraint.excludedOptions) {
122
+ add(definition.command, selected, excluded);
123
+ add(definition.command, excluded, selected);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ do {
129
+ changed = false;
130
+ for (const definition of CLI_COMMAND_REGISTRY) {
131
+ for (const constraint of definition.grammar.constraints) {
132
+ if (constraint.kind !== 'requires_all' && constraint.kind !== 'requires_any')
133
+ continue;
134
+ const required = constraint.requiredOptions;
135
+ for (const [key, blocked] of exclusions) {
136
+ if (!key.startsWith(`${definition.command}:`))
137
+ continue;
138
+ const unavailable = required.map((option) => blocked.has(option));
139
+ if (constraint.kind === 'requires_all' ? unavailable.some(Boolean) : unavailable.every(Boolean)) {
140
+ add(definition.command, key.slice(definition.command.length + 1), constraint.option);
141
+ }
142
+ }
143
+ const requiredExclusions = required.map((option) => exclusions.get(`${definition.command}:${option}`) ?? new Set());
144
+ for (const blocked of new Set(requiredExclusions.flatMap((values) => [...values]))) {
145
+ if (constraint.kind === 'requires_all' || requiredExclusions.every((values) => values.has(blocked))) {
146
+ add(definition.command, constraint.option, blocked);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ } while (changed);
152
+ return new Map([...exclusions].map(([key, values]) => [key, [...values]]));
153
+ }
154
+ const COMPLETION_EXCLUSIONS = completionExclusions();
155
+ function groupedCompletionExclusions() {
156
+ const groups = new Map();
157
+ for (const [key, values] of COMPLETION_EXCLUSIONS) {
158
+ const excluded = [...values].sort().join(' ');
159
+ const keys = groups.get(excluded) ?? [];
160
+ keys.push(key);
161
+ groups.set(excluded, keys);
162
+ }
163
+ return [...groups].map(([excluded, keys]) => ({ keys, excluded }));
164
+ }
165
+ function repeatableOptions(command) {
166
+ return commandDefinition(command).grammar.options
167
+ .filter((option) => option.occurrence === 'repeatable').map((option) => option.option);
168
+ }
95
169
  function bashCompletion() {
96
- 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');
97
- const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) COMPREPLY=( $(compgen -W "${group.values.join(' ')}" -- "\${current}") ); return ;;`).join('\n');
98
- const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) COMPREPLY=( $(compgen -W "${values.join(' ')}" -- "\${current}") ); return ;;`).join('\n');
170
+ const cases = CLI_COMMANDS.map((command) => ` ${command}) options="${commandOptions(command)}"; value_options="${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}"; repeat_options="${repeatableOptions(command).join(' ')}"; file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
171
+ const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) _whoisleuth_complete_values "${group.values.join(' ')}"; return ;;`).join('\n');
172
+ const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) _whoisleuth_complete_values "${values.join(' ')}"; return ;;`).join('\n');
99
173
  const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
100
174
  .filter((option) => option.valueKind === 'integer')
101
175
  .map((option) => {
@@ -120,8 +194,28 @@ _whoisleuth_direct_lookup_target() {
120
194
  case " ${CLI_COMMANDS.join(' ')} " in *" \${candidate} "*) return 1 ;; esac
121
195
  "\${COMP_WORDS[0]}" "\${candidate}" --plan --json >/dev/null 2>&1
122
196
  }
197
+ _whoisleuth_complete_files() {
198
+ local candidate
199
+ COMPREPLY=()
200
+ # Readline handles quoting and escapes; preserve each path as one candidate.
201
+ while IFS= read -r candidate; do
202
+ COMPREPLY+=("\${candidate}")
203
+ done < <(compgen -o default -- "\${1}")
204
+ }
205
+ _whoisleuth_exclusions() {
206
+ case "\${1}:\${2}" in
207
+ ${groupedCompletionExclusions().map((group) => ` ${group.keys.join('|')}) printf '%s' '${group.excluded}' ;;`).join('\n')}
208
+ esac
209
+ }
210
+ _whoisleuth_complete_values() {
211
+ local candidate
212
+ COMPREPLY=()
213
+ for candidate in $(compgen -W "\${1}" -- "\${current}"); do
214
+ [[ " \${blocked_options} " == *" \${previous}=\${candidate} "* ]] || COMPREPLY+=("\${candidate}")
215
+ done
216
+ }
123
217
  _whoisleuth_completion() {
124
- local current previous command options value_options file_limit positional_count expect_value foreign_option word integer_values seen_options i
218
+ local current previous command options value_options repeat_options file_limit positional_count expect_value foreign_option word integer_values seen_options i ended blocked_options value_option candidate available
125
219
  current="\${COMP_WORDS[COMP_CWORD]}"
126
220
  previous="\${COMP_WORDS[COMP_CWORD-1]}"
127
221
  command="\${COMP_WORDS[1]}"
@@ -140,18 +234,28 @@ ${cases}
140
234
  expect_value=0
141
235
  foreign_option=0
142
236
  seen_options=""
237
+ repeat_options="\${repeat_options:-}"
238
+ blocked_options=""
239
+ ended=0
240
+ value_option=""
143
241
  for ((i=2; i<COMP_CWORD; i++)); do
144
242
  word="\${COMP_WORDS[i]}"
145
- if (( expect_value )); then expect_value=0; continue; fi
243
+ if (( expect_value )); then
244
+ blocked_options="\${blocked_options} $(_whoisleuth_exclusions "\${command}" "\${value_option}=\${word}")"
245
+ expect_value=0; continue
246
+ fi
247
+ if (( ended )); then ((positional_count+=1)); continue; fi
248
+ if [[ "\${word}" == -- ]]; then ended=1; continue; fi
146
249
  if [[ "\${word}" == -* ]]; then
147
250
  if [[ " \${options} " != *" \${word} "* ]]; then foreign_option=1; continue; fi
148
251
  seen_options="\${seen_options} \${word}"
149
- [[ " \${value_options} " == *" \${word} "* ]] && expect_value=1
252
+ blocked_options="\${blocked_options} $(_whoisleuth_exclusions "\${command}" "\${word}")"
253
+ if [[ " \${value_options} " == *" \${word} "* ]]; then expect_value=1; value_option="\${word}"; fi
150
254
  else
151
255
  ((positional_count+=1))
152
256
  fi
153
257
  done
154
- if [[ " \${options} " == *" \${previous} "* ]]; then
258
+ if (( ! ended && expect_value )) && [[ " \${options} " == *" \${previous} "* ]]; then
155
259
  case "\${command}:\${previous}" in
156
260
  ${commandValueCases}
157
261
  esac
@@ -159,7 +263,7 @@ ${commandValueCases}
159
263
  ${valueCases}
160
264
  esac
161
265
  case "\${command}:\${previous}" in
162
- ${commandOptionPatterns(['file'])}) COMPREPLY=( $(compgen -f -- "\${current}") ); return ;;
266
+ ${commandOptionPatterns(['file'])}) _whoisleuth_complete_files "\${current}"; return ;;
163
267
  ${integerCases}
164
268
  ${commandOptionPatterns(['text'])}) COMPREPLY=(); return ;;
165
269
  esac
@@ -167,20 +271,29 @@ ${integerCases}
167
271
  case "\${command}:\${positional_count}" in
168
272
  ${positionalValueCases}
169
273
  esac
170
- if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && [[ "\${current}" != -* ]]; then
171
- COMPREPLY=( $(compgen -f -- "\${current}") )
274
+ if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && { (( ended )) || [[ "\${current}" != -* ]]; }; then
275
+ _whoisleuth_complete_files "\${current}"
172
276
  return
173
277
  fi
174
- COMPREPLY=( $(compgen -W "\${options}" -- "\${current}") )
278
+ COMPREPLY=()
279
+ (( ended || foreign_option || expect_value )) && return
280
+ available=""
281
+ for candidate in \${options}; do
282
+ [[ " \${blocked_options} " == *" \${candidate} "* ]] && continue
283
+ if [[ " \${seen_options} " == *" \${candidate} "* && " \${repeat_options} " != *" \${candidate} "* ]]; then continue; fi
284
+ if (( COMP_CWORD > 2 )) && [[ " ${COMMAND_META_ALIASES.join(' ')} " == *" \${candidate} "* ]]; then continue; fi
285
+ available="\${available} \${candidate}"
286
+ done
287
+ COMPREPLY=( $(compgen -W "\${available}" -- "\${current}") )
175
288
  }
176
- complete -F _whoisleuth_completion whoisleuth
289
+ complete -o filenames -F _whoisleuth_completion whoisleuth
177
290
  `;
178
291
  }
179
292
  function zshCompletion() {
180
293
  const commandEntries = CLI_COMMANDS.map((command) => `'${command}:${COMMAND_DESCRIPTIONS[command] || command}'`).join(' ');
181
- 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');
182
- const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) compadd -- ${group.values.join(' ')}; return ;;`).join('\n');
183
- const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) compadd -- ${values.join(' ')}; return ;;`).join('\n');
294
+ const cases = CLI_COMMANDS.map((command) => ` ${command}) options=(${commandOptions(command)}); value_options=(${optionNamesByKind(command, ['enum', 'file', 'integer', 'policy_list', 'text']).join(' ')}); repeat_options=(${repeatableOptions(command).join(' ')}); file_limit=${maximumFilePositionals(command)} ;;`).join('\n');
295
+ const commandValueCases = COMMAND_VALUE_OPTION_GROUPS.map((group) => ` ${group.commands.map((command) => `${command}:${group.option}`).join('|')}) _whoisleuth_values ${group.values.join(' ')}; return ;;`).join('\n');
296
+ const valueCases = Object.entries(VALUE_OPTIONS).map(([option, values]) => ` ${option}) _whoisleuth_values ${values.join(' ')}; return ;;`).join('\n');
184
297
  const integerCases = CLI_COMMAND_REGISTRY.flatMap((definition) => definition.grammar.options
185
298
  .filter((option) => option.valueKind === 'integer')
186
299
  .map((option) => {
@@ -204,10 +317,23 @@ _whoisleuth_direct_lookup_target() {
204
317
  [[ " ${CLI_COMMANDS.join(' ')} " == *" \${candidate} "* ]] && return 1
205
318
  "\${words[1]}" "\${candidate}" --plan --json >/dev/null 2>&1
206
319
  }
320
+ _whoisleuth_exclusions() {
321
+ case "\${1}:\${2}" in
322
+ ${groupedCompletionExclusions().map((group) => ` ${group.keys.join('|')}) print -r -- '${group.excluded}' ;;`).join('\n')}
323
+ esac
324
+ }
325
+ _whoisleuth_values() {
326
+ local candidate
327
+ local -a values
328
+ for candidate in "$@"; do
329
+ [[ " \${blocked_options} " == *" \${previous}=\${candidate} "* ]] || values+=("\${candidate}")
330
+ done
331
+ (( \${#values} )) && compadd -- \${values[@]}
332
+ }
207
333
  _whoisleuth() {
208
- local command previous word
209
- local -a options commands value_options seen_options
210
- integer file_limit=0 positional_count=0 expect_value=0 foreign_option=0 i
334
+ local command previous word blocked_options="" value_option="" candidate
335
+ local -a options commands value_options seen_options repeat_options available
336
+ integer file_limit=0 positional_count=0 expect_value=0 foreign_option=0 ended=0 i
211
337
  commands=(${commandEntries} ${[
212
338
  ...HELP_META_ACTION.aliases.map((alias) => `'${alias}:Show help'`),
213
339
  ...VERSION_META_ACTION.aliases.map((alias) => `'${alias}:Show version'`),
@@ -227,16 +353,22 @@ ${cases}
227
353
  esac
228
354
  for ((i=3; i<CURRENT; i++)); do
229
355
  word="\${words[i]}"
230
- if (( expect_value )); then expect_value=0; continue; fi
356
+ if (( expect_value )); then
357
+ blocked_options="\${blocked_options} $(_whoisleuth_exclusions "\${command}" "\${value_option}=\${word}")"
358
+ expect_value=0; continue
359
+ fi
360
+ if (( ended )); then ((positional_count+=1)); continue; fi
361
+ if [[ "\${word}" == -- ]]; then ended=1; continue; fi
231
362
  if [[ "\${word}" == -* ]]; then
232
363
  if [[ " \${options[*]} " != *" \${word} "* ]]; then foreign_option=1; continue; fi
233
364
  seen_options+=("\${word}")
234
- [[ " \${value_options[*]} " == *" \${word} "* ]] && expect_value=1
365
+ blocked_options="\${blocked_options} $(_whoisleuth_exclusions "\${command}" "\${word}")"
366
+ if [[ " \${value_options[*]} " == *" \${word} "* ]]; then expect_value=1; value_option="\${word}"; fi
235
367
  else
236
368
  ((positional_count+=1))
237
369
  fi
238
370
  done
239
- if [[ " \${options[*]} " == *" \${previous} "* ]]; then
371
+ if (( ! ended && expect_value )) && [[ " \${options[*]} " == *" \${previous} "* ]]; then
240
372
  case "\${command}:\${previous}" in
241
373
  ${commandValueCases}
242
374
  esac
@@ -252,11 +384,18 @@ ${integerCases}
252
384
  case "\${command}:\${positional_count}" in
253
385
  ${positionalValueCases}
254
386
  esac
255
- if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && [[ "\${words[CURRENT]}" != -* ]]; then
387
+ if (( file_limit > positional_count && foreign_option == 0 && expect_value == 0 )) && { (( ended )) || [[ "\${words[CURRENT]}" != -* ]]; }; then
256
388
  _files
257
389
  return
258
390
  fi
259
- compadd -- \${options[@]}
391
+ (( ended || foreign_option || expect_value )) && return
392
+ for candidate in \${options[@]}; do
393
+ [[ " \${blocked_options} " == *" \${candidate} "* ]] && continue
394
+ if [[ " \${seen_options[*]} " == *" \${candidate} "* && " \${repeat_options[*]} " != *" \${candidate} "* ]]; then continue; fi
395
+ if (( CURRENT > 3 )) && [[ " ${COMMAND_META_ALIASES.join(' ')} " == *" \${candidate} "* ]]; then continue; fi
396
+ available+=("\${candidate}")
397
+ done
398
+ (( \${#available} )) && compadd -- \${available[@]}
260
399
  }
261
400
  if [[ "\${funcstack[1]}" == "_whoisleuth" ]]; then
262
401
  _whoisleuth "\$@"
@@ -266,10 +405,16 @@ fi
266
405
  `;
267
406
  }
268
407
  function fishCompletion() {
269
- const commandLines = CLI_COMMANDS.map((command) => (`complete -c whoisleuth -n '__fish_use_subcommand' -a '${command}' -d '${COMMAND_DESCRIPTIONS[command]}'`));
408
+ const commandLines = CLI_COMMANDS.map((command) => (`__whoisleuth_add_command '${command}' '${COMMAND_DESCRIPTIONS[command]}'`));
270
409
  const optionGroups = new Map();
271
410
  for (const definition of CLI_COMMAND_REGISTRY) {
272
- for (const option of definition.grammar.options) {
411
+ const options = [
412
+ ...definition.grammar.options,
413
+ ...definition.grammar.metaActions.map((id) => ({
414
+ option: longMetaAlias(metaActionDefinition(id)), arity: 0, valueKind: 'flag',
415
+ })),
416
+ ];
417
+ for (const option of options) {
273
418
  const key = `${option.option}\u0000${option.arity}\u0000${option.valueKind === 'file'}`;
274
419
  const group = optionGroups.get(key) ?? {
275
420
  option: option.option,
@@ -284,14 +429,14 @@ function fishCompletion() {
284
429
  const optionLines = [...optionGroups.values()].map((group) => {
285
430
  const condition = `__whoisleuth_command_is ${group.commands.join(' ')}`
286
431
  + (group.commands.includes('lookup') ? '; or __whoisleuth_direct_lookup_target' : '');
287
- 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' : ''}`;
432
+ return `complete -c whoisleuth -n 'begin; ${condition}; end; and __whoisleuth_option_available ${group.option}'${group.option === HELP_LONG_ALIAS ? ` -s ${HELP_SHORT_ALIAS.slice(1)}` : ''} -l ${group.option.slice(2)}${group.arity === 1 ? group.file ? ' -r -F' : ' -r -f' : ''}`;
288
433
  });
289
434
  const commandValueLines = COMMAND_VALUE_OPTION_GROUPS.flatMap((group) => {
290
435
  const lines = [
291
- `complete -c whoisleuth -n '__whoisleuth_command_is ${group.commands.join(' ')}; and __fish_prev_arg_in ${group.option}' -a '${group.values.join(' ')}'`,
436
+ `complete -c whoisleuth -n '__whoisleuth_command_is ${group.commands.join(' ')}; and __fish_prev_arg_in ${group.option}' -l ${group.option.slice(2)} -r -f -a '(__whoisleuth_values ${group.option} ${group.values.join(' ')})'`,
292
437
  ];
293
438
  if (group.commands.includes('lookup')) {
294
- lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${group.option}' -a '${group.values.join(' ')}'`);
439
+ lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${group.option}' -l ${group.option.slice(2)} -r -f -a '(__whoisleuth_values ${group.option} ${group.values.join(' ')})'`);
295
440
  }
296
441
  return lines;
297
442
  });
@@ -301,10 +446,10 @@ function fishCompletion() {
301
446
  ? `__fish_prev_arg_in ${option}`
302
447
  : `__whoisleuth_command_is ${commands.join(' ')}; and __fish_prev_arg_in ${option}`;
303
448
  const lines = commands.length > 0
304
- ? [`complete -c whoisleuth -n '${namedCondition}' -a '${values.join(' ')}'`]
449
+ ? [`complete -c whoisleuth -n '${namedCondition}' -l ${option.slice(2)} -r -f -a '(__whoisleuth_values ${option} ${values.join(' ')})'`]
305
450
  : [];
306
451
  if (commands.includes('lookup')) {
307
- lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${option}' -a '${values.join(' ')}'`);
452
+ lines.push(`complete -c whoisleuth -n '__whoisleuth_direct_lookup_target; and __fish_prev_arg_in ${option}' -l ${option.slice(2)} -r -f -a '(__whoisleuth_values ${option} ${values.join(' ')})'`);
308
453
  }
309
454
  return lines;
310
455
  });
@@ -313,6 +458,7 @@ function fishCompletion() {
313
458
  .flatMap((option) => option.integerRanges.map((range) => {
314
459
  const qualifiers = [
315
460
  `__fish_prev_arg_in ${option.option}`,
461
+ `and __whoisleuth_value_expected ${option.option}`,
316
462
  `and __whoisleuth_command_is ${definition.command}`,
317
463
  ...(range.whenOptionPresent
318
464
  ? [`and __whoisleuth_seen ${definition.command} ${range.whenOptionPresent}`]
@@ -320,24 +466,14 @@ function fishCompletion() {
320
466
  ? [`and not __whoisleuth_seen ${definition.command} ${option.integerRanges.find((candidate) => candidate.whenOptionPresent)?.whenOptionPresent}`]
321
467
  : []),
322
468
  ].join('; ');
323
- return `complete -c whoisleuth -n '${qualifiers}' -a '(__whoisleuth_integer_values ${range.minimum} ${range.maximum})'`;
469
+ return `complete -c whoisleuth -n '${qualifiers}' -l ${option.option.slice(2)} -r -f -a '(__whoisleuth_integer_values ${range.minimum} ${range.maximum})'`;
324
470
  })));
325
- const positionalCases = [...new Set([
326
- ...FILE_POSITIONAL_COMMANDS,
327
- ...CLI_COMMANDS.filter((command) => positionalEnumValues(command).length > 0),
328
- ...CLI_COMMAND_REGISTRY.filter((definition) => definition.grammar.options
329
- .some((option) => option.integerRanges.some((range) => range.whenOptionPresent !== null)))
330
- .map((definition) => definition.command),
331
- ])].map((command) => {
471
+ const positionalCases = CLI_COMMANDS.map((command) => {
332
472
  const definition = commandDefinition(command);
333
473
  const commandValueOptions = definition.grammar.options
334
- .filter((option) => option.scope === 'command' && option.arity === 1)
474
+ .filter((option) => option.arity === 1)
335
475
  .map((option) => option.option);
336
- const positioning = FILE_POSITIONAL_COMMANDS.includes(command)
337
- || positionalEnumValues(command).length > 0;
338
- return ` case ${command}\n${positioning
339
- ? ` set options $options ${definition.completion.options.join(' ')}\n`
340
- : ' set strict 0\n'} set value_options $value_options ${commandValueOptions.join(' ')}\n set limit ${maximumFilePositionals(command)}`;
476
+ return ` case ${command}\n set options ${commandOptions(command)}\n set value_options ${commandValueOptions.join(' ')}\n set repeat_options ${repeatableOptions(command).join(' ')}\n set limit ${maximumFilePositionals(command)}`;
341
477
  }).join('\n');
342
478
  const positionalEnumLines = CLI_COMMANDS.flatMap((command) => {
343
479
  const values = positionalEnumValues(command);
@@ -373,6 +509,9 @@ function __whoisleuth_command_is
373
509
  test (count \$words) -ge 2; or return 1
374
510
  contains -- \$words[2] $argv
375
511
  end
512
+ function __whoisleuth_add_command
513
+ complete -c whoisleuth -n '__fish_use_subcommand' -a \$argv[1] -d \$argv[2]
514
+ end
376
515
  function __whoisleuth_integer_values
377
516
  set -l value $argv[1]
378
517
  while test $value -le $argv[2]
@@ -380,60 +519,118 @@ function __whoisleuth_integer_values
380
519
  set value (math $value + 1)
381
520
  end
382
521
  end
522
+ function __whoisleuth_exclusions
523
+ switch "$argv[1]:$argv[2]"
524
+ ${groupedCompletionExclusions().map((group) => ` case ${group.keys.map((key) => `'${key}'`).join(' ')}\n printf '%s\\n' ${group.excluded}`).join('\n')}
525
+ end
526
+ end
383
527
  function __whoisleuth_position_state
384
528
  set -l expected_command $argv[1]
385
529
  set -l words (commandline -opc)
386
530
  test (count $words) -ge 2; or return 1
387
531
  set -l command $words[2]
388
- test "$command" = "$expected_command"; or return 1
389
- set -l options ${COMMAND_META_ALIASES.join(' ')} --output --force --config --profile --palette
390
- set -l value_options --output --config --profile --palette
532
+ if not contains -- $command ${CLI_COMMANDS.join(' ')}
533
+ __whoisleuth_direct_lookup_target; or return 1
534
+ set command lookup
535
+ end
536
+ if test -n "$expected_command"
537
+ test "$command" = "$expected_command"; or return 1
538
+ end
539
+ set -l options
540
+ set -l value_options
541
+ set -l repeat_options
391
542
  set -l limit 0
392
- set -l strict 1
393
543
  switch $command
394
544
  ${positionalCases}
395
545
  case '*'
396
546
  return 1
397
547
  end
398
548
  set -l expect_value 0
549
+ set -l value_option @none
550
+ set -l ended 0
399
551
  set -l positional_count 0
400
552
  set -l seen
553
+ set -l blocked
401
554
  for word in $words[3..-1]
402
555
  if test $expect_value -eq 1
556
+ set -a blocked (__whoisleuth_exclusions $command "$value_option=$word")
403
557
  set expect_value 0
558
+ set value_option @none
559
+ continue
560
+ end
561
+ if test $ended -eq 1
562
+ set positional_count (math $positional_count + 1)
563
+ continue
564
+ end
565
+ if test "$word" = --
566
+ set ended 1
404
567
  continue
405
568
  end
406
569
  if string match -q -- '-*' $word
407
- if test $strict -eq 1
408
- contains -- $word $options; or return 1
409
- end
570
+ contains -- $word $options; or return 1
410
571
  set -a seen $word
411
- contains -- $word $value_options; and set expect_value 1
572
+ set -a blocked (__whoisleuth_exclusions $command $word)
573
+ if contains -- $word $value_options
574
+ set expect_value 1
575
+ set value_option $word
576
+ end
412
577
  else
413
578
  set positional_count (math $positional_count + 1)
414
579
  end
415
580
  end
416
- test $expect_value -eq 0; or return 1
417
- echo $positional_count $limit $seen
581
+ # Rows: position, file limit, pending option, separator, argument count,
582
+ # supplied options, excluded options/values, and repeatable options.
583
+ printf '%s\\n' $positional_count $limit $value_option $ended (math (count $words) - 2) "seen $seen" "blocked $blocked" "repeat $repeat_options"
418
584
  end
419
585
  function __whoisleuth_position_is
420
586
  set -l state (__whoisleuth_position_state $argv[2]); or return 1
587
+ test "$state[3]" = @none; or return 1
421
588
  test $state[1] -eq $argv[1]
422
589
  end
423
590
  function __whoisleuth_file_position
424
591
  set -l words (commandline -opc)
425
592
  test (count $words) -ge 2; or return 1
426
- set -l state (__whoisleuth_position_state $words[2]); or return 1
593
+ set -l state (__whoisleuth_position_state); or return 1
427
594
  test $state[2] -gt $state[1]; or return 1
595
+ test "$state[3]" = @none; or return 1
596
+ test $state[4] -eq 1; and return 0
428
597
  not string match -qr -- ^- (commandline -ct)
429
598
  end
430
599
  function __whoisleuth_seen
431
600
  set -l s (__whoisleuth_position_state $argv[1]); or return 1
432
- contains -- $argv[2] $s[3..-1]
601
+ contains -- $argv[2] (string split ' ' -- $s[6])
602
+ end
603
+ function __whoisleuth_option_available
604
+ set -l state (__whoisleuth_position_state); or return 1
605
+ test $state[4] -eq 0; or return 1
606
+ if test "$state[3]" != @none
607
+ test "$state[3]" = "$argv[1]"
608
+ return $status
609
+ end
610
+ contains -- $argv[1] (string split ' ' -- $state[7]); and return 1
611
+ if contains -- $argv[1] (string split ' ' -- $state[6])
612
+ contains -- $argv[1] (string split ' ' -- $state[8]); or return 1
613
+ end
614
+ if test $state[5] -gt 0
615
+ contains -- $argv[1] ${COMMAND_META_ALIASES.join(' ')}; and return 1
616
+ end
617
+ return 0
618
+ end
619
+ function __whoisleuth_value_expected
620
+ set -l state (__whoisleuth_position_state); or return 1
621
+ test $state[4] -eq 0; and test "$state[3]" = "$argv[1]"
622
+ end
623
+ function __whoisleuth_values
624
+ set -l state (__whoisleuth_position_state); or return 1
625
+ test $state[4] -eq 0; and test "$state[3]" = "$argv[1]"; or return 1
626
+ for value in $argv[2..-1]
627
+ contains -- "$argv[1]=$value" (string split ' ' -- $state[7]); or printf '%s\\n' $value
628
+ end
433
629
  end
434
630
  complete -c whoisleuth -f
435
631
  ${[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')}
436
632
  ${commandLines.join('\n')}
633
+ functions -e __whoisleuth_add_command
437
634
  ${positionalEnumLines.join('\n')}
438
635
  complete -c whoisleuth -n '__whoisleuth_file_position' -F
439
636
  ${optionLines.join('\n')}
@@ -475,9 +672,27 @@ function powershellCompletion() {
475
672
  Register-ArgumentCompleter -Native -CommandName whoisleuth -ScriptBlock {
476
673
  param($wordToComplete, $commandAst, $cursorPosition)
477
674
  $noMatch = { [System.Management.Automation.CompletionResult]::new(' ', 'no matches', 'ParameterValue', 'No completion available') }
675
+ $completeFiles = {
676
+ param($prefix)
677
+ $files = @([System.Management.Automation.CompletionCompleters]::CompleteFilename($prefix))
678
+ foreach ($file in $files) {
679
+ # Native arguments need literal paths, not provider wildcard escapes.
680
+ $path = $file.ToolTip
681
+ if ($file.ResultType -eq 'ProviderContainer') { $path += [IO.Path]::DirectorySeparatorChar }
682
+ $literal = "'" + [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($path) + "'"
683
+ [System.Management.Automation.CompletionResult]::new($literal, $file.ListItemText, $file.ResultType, $file.ToolTip)
684
+ }
685
+ if ($files.Count -eq 0) { & $noMatch }
686
+ }
478
687
  $commands = @(${CLI_COMMANDS.map((command) => `'${command}'`).join(', ')})
479
688
  $options = @{
480
689
  ${Object.entries(commandOptions).map(([command, options]) => ` '${command}' = @(${options.map((option) => `'${option}'`).join(', ')})`).join('\n')}
690
+ }
691
+ $repeatOptions = @{
692
+ ${CLI_COMMANDS.filter((command) => repeatableOptions(command).length > 0).map((command) => ` '${command}' = @(${repeatableOptions(command).map((option) => `'${option}'`).join(', ')})`).join('\n')}
693
+ }
694
+ $exclusions = @{
695
+ ${[...COMPLETION_EXCLUSIONS].map(([key, excluded]) => ` '${key}' = @(${excluded.map((option) => `'${option}'`).join(', ')})`).join('\n')}
481
696
  }
482
697
  $values = @{
483
698
  ${[
@@ -524,36 +739,43 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
524
739
  if ($directLookup) { $command = 'lookup' }
525
740
  $commandKnown = $options.ContainsKey($command)
526
741
  $activeOptions = if ($commandKnown) { $options[$command] } else { @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')}) }
527
- $previousOwned = $activeOptions -contains $previous
528
- if ($previousOwned -and $fileOptions.ContainsKey($command) -and $fileOptions[$command] -contains $previous) {
529
- $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
530
- $files | ForEach-Object {
531
- [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
532
- }
533
- if ($files.Count -eq 0) { & $noMatch }
534
- return
535
- }
536
- if ($previousOwned -and $textOptions.ContainsKey($command) -and $textOptions[$command] -contains $previous) {
537
- $completionText = if ([string]::IsNullOrEmpty($wordToComplete)) { ' ' } else { [string]$wordToComplete }
538
- $listItemText = if ([string]::IsNullOrEmpty($wordToComplete)) { 'value' } else { [string]$wordToComplete }
539
- [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterValue', 'Enter a value')
540
- return
541
- }
542
742
  $positionCount = 0
543
743
  $expectValue = $false
544
744
  $foreignOption = $false
745
+ $ended = $false
746
+ $valueOption = ''
545
747
  $seenOptions = @()
748
+ $blockedOptions = @()
546
749
  for ($index = 2; $index -le $previousIndex; $index += 1) {
547
750
  $word = $elements[$index]
548
- if ($expectValue) { $expectValue = $false; continue }
751
+ if ($expectValue) {
752
+ $key = "${'$'}{command}:${'$'}valueOption=$word"
753
+ if ($exclusions.ContainsKey($key)) { $blockedOptions += $exclusions[$key] }
754
+ $expectValue = $false; continue
755
+ }
756
+ if ($ended) { $positionCount += 1; continue }
757
+ if ($word -eq '--') { $ended = $true; continue }
549
758
  if ($word.StartsWith('-')) {
550
759
  if ($activeOptions -notcontains $word) { $foreignOption = $true; continue }
551
760
  $seenOptions += $word
552
- if ($valueOptions.ContainsKey($command) -and $valueOptions[$command] -contains $word) { $expectValue = $true }
761
+ $key = "${'$'}{command}:$word"
762
+ if ($exclusions.ContainsKey($key)) { $blockedOptions += $exclusions[$key] }
763
+ if ($valueOptions.ContainsKey($command) -and $valueOptions[$command] -contains $word) { $expectValue = $true; $valueOption = $word }
553
764
  } else {
554
765
  $positionCount += 1
555
766
  }
556
767
  }
768
+ $previousOwned = -not $ended -and $expectValue -and $activeOptions -contains $previous
769
+ if ($previousOwned -and $fileOptions.ContainsKey($command) -and $fileOptions[$command] -contains $previous) {
770
+ & $completeFiles $wordToComplete
771
+ return
772
+ }
773
+ if ($previousOwned -and $textOptions.ContainsKey($command) -and $textOptions[$command] -contains $previous) {
774
+ $completionText = if ([string]::IsNullOrEmpty($wordToComplete)) { ' ' } else { [string]$wordToComplete }
775
+ $listItemText = if ([string]::IsNullOrEmpty($wordToComplete)) { 'value' } else { [string]$wordToComplete }
776
+ [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterValue', 'Enter a value')
777
+ return
778
+ }
557
779
  $integerKey = "${'$'}{command}:${'$'}previous"
558
780
  if ($previousOwned -and $integerRanges.ContainsKey($integerKey)) {
559
781
  $range = $integerRanges[$integerKey]
@@ -577,6 +799,7 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
577
799
  if ($previousOwned -and $values.ContainsKey($valueKey)) {
578
800
  $matched = $false
579
801
  foreach ($candidate in $values[$valueKey]) {
802
+ if ($blockedOptions -contains "$previous=$candidate") { continue }
580
803
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
581
804
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
582
805
  $matched = $true
@@ -597,14 +820,11 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
597
820
  if (-not $matched) { & $noMatch }
598
821
  return
599
822
  }
600
- if ($fileLimits.ContainsKey($command) -and $fileLimits[$command] -gt $positionCount -and -not $foreignOption -and -not $expectValue -and -not $wordToComplete.StartsWith('-')) {
601
- $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
602
- $files | ForEach-Object {
603
- [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
604
- }
605
- if ($files.Count -eq 0) { & $noMatch }
823
+ if ($fileLimits.ContainsKey($command) -and $fileLimits[$command] -gt $positionCount -and -not $foreignOption -and -not $expectValue -and ($ended -or -not $wordToComplete.StartsWith('-'))) {
824
+ & $completeFiles $wordToComplete
606
825
  return
607
826
  }
827
+ if ($ended -or $foreignOption -or $expectValue) { & $noMatch; return }
608
828
  $candidates = if ($rootCompletion -and -not $directLookup) {
609
829
  @($commands) + @(${ROOT_META_ALIASES.map((alias) => `'${alias}'`).join(', ')})
610
830
  } elseif ($commandKnown) {
@@ -614,6 +834,9 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
614
834
  }
615
835
  $matched = $false
616
836
  foreach ($candidate in $candidates) {
837
+ if ($blockedOptions -contains $candidate) { continue }
838
+ if ($seenOptions -contains $candidate -and (-not $repeatOptions.ContainsKey($command) -or $repeatOptions[$command] -notcontains $candidate)) { continue }
839
+ if ($previousIndex -ge 2 -and @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')}) -contains $candidate) { continue }
617
840
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
618
841
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
619
842
  $matched = $true