@slicedearth/whoisleuth-cli 2.3.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 (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -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 +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  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 +83 -61
  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-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. 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 "\$@"
@@ -269,7 +408,13 @@ function fishCompletion() {
269
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);
@@ -383,56 +519,113 @@ function __whoisleuth_integer_values
383
519
  set value (math $value + 1)
384
520
  end
385
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
386
527
  function __whoisleuth_position_state
387
528
  set -l expected_command $argv[1]
388
529
  set -l words (commandline -opc)
389
530
  test (count $words) -ge 2; or return 1
390
531
  set -l command $words[2]
391
- test "$command" = "$expected_command"; or return 1
392
- set -l options ${COMMAND_META_ALIASES.join(' ')} --output --force --config --profile --palette
393
- 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
394
542
  set -l limit 0
395
- set -l strict 1
396
543
  switch $command
397
544
  ${positionalCases}
398
545
  case '*'
399
546
  return 1
400
547
  end
401
548
  set -l expect_value 0
549
+ set -l value_option @none
550
+ set -l ended 0
402
551
  set -l positional_count 0
403
552
  set -l seen
553
+ set -l blocked
404
554
  for word in $words[3..-1]
405
555
  if test $expect_value -eq 1
556
+ set -a blocked (__whoisleuth_exclusions $command "$value_option=$word")
406
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
407
567
  continue
408
568
  end
409
569
  if string match -q -- '-*' $word
410
- if test $strict -eq 1
411
- contains -- $word $options; or return 1
412
- end
570
+ contains -- $word $options; or return 1
413
571
  set -a seen $word
414
- 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
415
577
  else
416
578
  set positional_count (math $positional_count + 1)
417
579
  end
418
580
  end
419
- test $expect_value -eq 0; or return 1
420
- 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"
421
584
  end
422
585
  function __whoisleuth_position_is
423
586
  set -l state (__whoisleuth_position_state $argv[2]); or return 1
587
+ test "$state[3]" = @none; or return 1
424
588
  test $state[1] -eq $argv[1]
425
589
  end
426
590
  function __whoisleuth_file_position
427
591
  set -l words (commandline -opc)
428
592
  test (count $words) -ge 2; or return 1
429
- set -l state (__whoisleuth_position_state $words[2]); or return 1
593
+ set -l state (__whoisleuth_position_state); or return 1
430
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
431
597
  not string match -qr -- ^- (commandline -ct)
432
598
  end
433
599
  function __whoisleuth_seen
434
600
  set -l s (__whoisleuth_position_state $argv[1]); or return 1
435
- 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
436
629
  end
437
630
  complete -c whoisleuth -f
438
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')}
@@ -479,9 +672,27 @@ function powershellCompletion() {
479
672
  Register-ArgumentCompleter -Native -CommandName whoisleuth -ScriptBlock {
480
673
  param($wordToComplete, $commandAst, $cursorPosition)
481
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
+ }
482
687
  $commands = @(${CLI_COMMANDS.map((command) => `'${command}'`).join(', ')})
483
688
  $options = @{
484
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')}
485
696
  }
486
697
  $values = @{
487
698
  ${[
@@ -528,36 +739,43 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
528
739
  if ($directLookup) { $command = 'lookup' }
529
740
  $commandKnown = $options.ContainsKey($command)
530
741
  $activeOptions = if ($commandKnown) { $options[$command] } else { @(${COMMAND_META_ALIASES.map((alias) => `'${alias}'`).join(', ')}) }
531
- $previousOwned = $activeOptions -contains $previous
532
- if ($previousOwned -and $fileOptions.ContainsKey($command) -and $fileOptions[$command] -contains $previous) {
533
- $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
534
- $files | ForEach-Object {
535
- [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
536
- }
537
- if ($files.Count -eq 0) { & $noMatch }
538
- return
539
- }
540
- if ($previousOwned -and $textOptions.ContainsKey($command) -and $textOptions[$command] -contains $previous) {
541
- $completionText = if ([string]::IsNullOrEmpty($wordToComplete)) { ' ' } else { [string]$wordToComplete }
542
- $listItemText = if ([string]::IsNullOrEmpty($wordToComplete)) { 'value' } else { [string]$wordToComplete }
543
- [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterValue', 'Enter a value')
544
- return
545
- }
546
742
  $positionCount = 0
547
743
  $expectValue = $false
548
744
  $foreignOption = $false
745
+ $ended = $false
746
+ $valueOption = ''
549
747
  $seenOptions = @()
748
+ $blockedOptions = @()
550
749
  for ($index = 2; $index -le $previousIndex; $index += 1) {
551
750
  $word = $elements[$index]
552
- 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 }
553
758
  if ($word.StartsWith('-')) {
554
759
  if ($activeOptions -notcontains $word) { $foreignOption = $true; continue }
555
760
  $seenOptions += $word
556
- 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 }
557
764
  } else {
558
765
  $positionCount += 1
559
766
  }
560
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
+ }
561
779
  $integerKey = "${'$'}{command}:${'$'}previous"
562
780
  if ($previousOwned -and $integerRanges.ContainsKey($integerKey)) {
563
781
  $range = $integerRanges[$integerKey]
@@ -581,6 +799,7 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
581
799
  if ($previousOwned -and $values.ContainsKey($valueKey)) {
582
800
  $matched = $false
583
801
  foreach ($candidate in $values[$valueKey]) {
802
+ if ($blockedOptions -contains "$previous=$candidate") { continue }
584
803
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
585
804
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
586
805
  $matched = $true
@@ -601,14 +820,11 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
601
820
  if (-not $matched) { & $noMatch }
602
821
  return
603
822
  }
604
- if ($fileLimits.ContainsKey($command) -and $fileLimits[$command] -gt $positionCount -and -not $foreignOption -and -not $expectValue -and -not $wordToComplete.StartsWith('-')) {
605
- $files = @(Get-ChildItem -Path "${'$'}wordToComplete*" -File -ErrorAction SilentlyContinue)
606
- $files | ForEach-Object {
607
- [System.Management.Automation.CompletionResult]::new($_.FullName, $_.Name, 'ProviderItem', $_.FullName)
608
- }
609
- 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
610
825
  return
611
826
  }
827
+ if ($ended -or $foreignOption -or $expectValue) { & $noMatch; return }
612
828
  $candidates = if ($rootCompletion -and -not $directLookup) {
613
829
  @($commands) + @(${ROOT_META_ALIASES.map((alias) => `'${alias}'`).join(', ')})
614
830
  } elseif ($commandKnown) {
@@ -618,6 +834,9 @@ ${Object.entries(conditionalIntegerRanges).map(([key, range]) => ` '${key}' =
618
834
  }
619
835
  $matched = $false
620
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 }
621
840
  if ($candidate.StartsWith($wordToComplete, [System.StringComparison]::OrdinalIgnoreCase)) {
622
841
  [System.Management.Automation.CompletionResult]::new($candidate, $candidate, 'ParameterValue', $candidate)
623
842
  $matched = $true
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { CliUsageError } from "./errors.mjs";
5
- import { cliMetaActionForInvocation } from "./command-reference.mjs";
5
+ import { cliInvocationOptionIndices, cliMetaActionForInvocation } from "./command-reference.mjs";
6
6
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
7
7
  import { scanBoundedJson } from "../lib/bounded-json.mjs";
8
8
  export const CLI_CONFIG_SCHEMA = 'whoisleuth.cli.config';
@@ -107,7 +107,8 @@ async function readConfigFile(path) {
107
107
  }
108
108
  function suppliedGroups(argumentsList) {
109
109
  const groups = new Set();
110
- for (const argument of argumentsList) {
110
+ for (const index of cliInvocationOptionIndices(argumentsList)) {
111
+ const argument = argumentsList[index];
111
112
  const group = OPTION_GROUPS[argument];
112
113
  if (group)
113
114
  groups.add(group);
@@ -119,10 +120,11 @@ function suppliedGroups(argumentsList) {
119
120
  export async function resolveCliProfileArguments(argv, options = {}) {
120
121
  if (cliMetaActionForInvocation(argv))
121
122
  return [...argv];
123
+ const optionIndices = cliInvocationOptionIndices(argv);
122
124
  // registry-scaffold owns --profile as the capability template to generate.
123
125
  // Do not reinterpret it as a global CLI-default profile.
124
126
  if (argv[0] === 'registry-scaffold') {
125
- if (argv.includes('--config')) {
127
+ if (argv.some((argument, index) => argument === '--config' && optionIndices.has(index))) {
126
128
  throw new CliUsageError('registry-scaffold does not accept global CLI configuration options.');
127
129
  }
128
130
  return [...argv];
@@ -134,7 +136,7 @@ export async function resolveCliProfileArguments(argv, options = {}) {
134
136
  const argument = argv[index];
135
137
  if (argument === undefined)
136
138
  break;
137
- if (argument === '--config' || argument === '--profile') {
139
+ if (optionIndices.has(index) && (argument === '--config' || argument === '--profile')) {
138
140
  const next = argv[++index];
139
141
  if (!next || next.startsWith('-'))
140
142
  throw new CliUsageError(`${argument} requires one value.`);
@@ -177,7 +179,7 @@ export async function resolveCliProfileArguments(argv, options = {}) {
177
179
  throw new CliUsageError(`CLI profile "${selectedName}" was not found.`);
178
180
  if (!retained.length)
179
181
  throw new CliUsageError('A CLI profile must accompany a command.');
180
- const explicitGroups = suppliedGroups(retained.slice(1));
182
+ const explicitGroups = suppliedGroups(retained);
181
183
  const defaults = [];
182
184
  for (let index = 0; index < selected.arguments.length; index += 1) {
183
185
  const argument = selected.arguments[index];