politty 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/completion/index.cjs +4 -3
  2. package/dist/completion/index.cjs.map +1 -1
  3. package/dist/completion/index.d.cts +2 -2
  4. package/dist/completion/index.d.cts.map +1 -1
  5. package/dist/completion/index.d.ts +2 -2
  6. package/dist/completion/index.d.ts.map +1 -1
  7. package/dist/completion/index.js +3 -3
  8. package/dist/completion/index.js.map +1 -1
  9. package/dist/docs/index.cjs +2 -2
  10. package/dist/docs/index.js +2 -2
  11. package/dist/{extractor-JfoYSoMk.js → extractor-DO-FDKkW.js} +397 -295
  12. package/dist/extractor-DO-FDKkW.js.map +1 -0
  13. package/dist/{extractor-CqfDnGKd.cjs → extractor-cjruDqQ2.cjs} +404 -296
  14. package/dist/extractor-cjruDqQ2.cjs.map +1 -0
  15. package/dist/index.cjs +3 -3
  16. package/dist/index.d.cts +1 -1
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +3 -3
  19. package/dist/{runner-9dLE13Dv.cjs → runner-0yr2HFay.cjs} +2 -2
  20. package/dist/{runner-9dLE13Dv.cjs.map → runner-0yr2HFay.cjs.map} +1 -1
  21. package/dist/{runner-LJRI4haB.js → runner-BoZpJtIR.js} +2 -2
  22. package/dist/{runner-LJRI4haB.js.map → runner-BoZpJtIR.js.map} +1 -1
  23. package/dist/schema-extractor-CHiBRT39.d.ts.map +1 -1
  24. package/dist/{schema-extractor-CP3ar0Wi.js → schema-extractor-DAkmmrOy.js} +5 -1
  25. package/dist/schema-extractor-DAkmmrOy.js.map +1 -0
  26. package/dist/schema-extractor-DyfK21m_.d.cts.map +1 -1
  27. package/dist/{schema-extractor-Cv7ipqLS.cjs → schema-extractor-Mk1MHBkQ.cjs} +5 -1
  28. package/dist/schema-extractor-Mk1MHBkQ.cjs.map +1 -0
  29. package/dist/{extractor-DsJ6hYqQ.d.cts → value-completion-resolver-0xf8_07p.d.cts} +56 -11
  30. package/dist/value-completion-resolver-0xf8_07p.d.cts.map +1 -0
  31. package/dist/{extractor-CCi4rjSI.d.ts → value-completion-resolver-CUKbibx-.d.ts} +56 -11
  32. package/dist/value-completion-resolver-CUKbibx-.d.ts.map +1 -0
  33. package/package.json +6 -5
  34. package/dist/extractor-CCi4rjSI.d.ts.map +0 -1
  35. package/dist/extractor-CqfDnGKd.cjs.map +0 -1
  36. package/dist/extractor-DsJ6hYqQ.d.cts.map +0 -1
  37. package/dist/extractor-JfoYSoMk.js.map +0 -1
  38. package/dist/schema-extractor-CP3ar0Wi.js.map +0 -1
  39. package/dist/schema-extractor-Cv7ipqLS.cjs.map +0 -1
@@ -1,5 +1,6 @@
1
- import { a as arg, t as extractFields } from "./schema-extractor-CP3ar0Wi.js";
1
+ import { a as arg, t as extractFields } from "./schema-extractor-DAkmmrOy.js";
2
2
  import { z } from "zod";
3
+ import { execSync } from "node:child_process";
3
4
 
4
5
  //#region src/core/command.ts
5
6
  function defineCommand(config) {
@@ -21,7 +22,12 @@ function defineCommand(config) {
21
22
  /**
22
23
  * Generate bash completion script for a command
23
24
  *
24
- * Generates a dynamic script that calls the CLI's __complete command at runtime.
25
+ * Generates a minimal script that delegates all logic to the CLI's __complete command.
26
+ * The shell script only handles:
27
+ * - Getting current command line tokens
28
+ * - Calling __complete with --shell bash
29
+ * - Setting COMPREPLY from the output
30
+ * - Falling back to native file/directory completion when directed
25
31
  */
26
32
  function generateBashCompletion(_command, options) {
27
33
  const programName = options.programName;
@@ -30,99 +36,126 @@ function generateBashCompletion(_command, options) {
30
36
  # Generated by politty
31
37
 
32
38
  _${programName}_completions() {
33
- local cur="\${COMP_WORDS[COMP_CWORD]}"
34
- local args=("\${COMP_WORDS[@]:1:COMP_CWORD}")
35
- local completion_prefix=""
36
- local completion_cur="$cur"
39
+ COMPREPLY=()
40
+ local IFS=$'\\n'
41
+
42
+ # Rejoin words split by '=' in COMP_WORDBREAKS (e.g. --opt=value)
43
+ local -a _words=()
44
+ local _i=1
45
+ while (( _i <= COMP_CWORD )); do
46
+ if [[ "\${COMP_WORDS[_i]}" == "=" && \${#_words[@]} -gt 0 ]]; then
47
+ _words[\${#_words[@]}-1]+="=\${COMP_WORDS[_i+1]:-}"
48
+ (( _i += 2 ))
49
+ else
50
+ _words+=("\${COMP_WORDS[_i]}")
51
+ (( _i++ ))
52
+ fi
53
+ done
37
54
 
38
- # Handle inline option-value completion for long options (e.g. --format=js)
39
- if [[ "$cur" == --*=* ]]; then
40
- completion_prefix="\${cur%%=*}="
41
- completion_cur="\${cur#*=}"
42
- fi
55
+ local lines prev_opts="$-"
56
+ set -f
57
+ lines=($(${programName} __complete --shell bash -- "\${_words[@]}" 2>/dev/null))
58
+ [[ "$prev_opts" != *f* ]] && set +f
43
59
 
44
- # Call the CLI to get completions
45
- local output
46
- if ! output=$(${programName} __complete -- "\${args[@]}" 2>/dev/null); then
47
- # Backward compatibility for CLIs exposing only completion
48
- output=$(${programName} completion __complete -- "\${args[@]}" 2>/dev/null)
60
+ local count=\${#lines[@]}
61
+ if (( count == 0 )); then
62
+ return 0
49
63
  fi
50
64
 
51
- local candidates=()
65
+ local last="\${lines[count-1]}"
52
66
  local directive=0
53
- local command_completion=""
54
- local file_extensions=""
55
-
56
- # Parse output: value\\tdescription lines, ending with :directive
57
- while IFS=$'\\t' read -r name desc; do
58
- if [[ "$name" == :* ]]; then
59
- directive="\${name:1}"
60
- elif [[ "$name" == __command:* ]]; then
61
- command_completion="\${name#__command:}"
62
- elif [[ "$name" == __extensions:* ]]; then
63
- file_extensions="\${name#__extensions:}"
64
- elif [[ -n "$name" ]]; then
65
- candidates+=("$name")
66
- fi
67
- done <<< "$output"
68
-
69
- # Execute shellCommand completion if requested by __complete
70
- if [[ -n "$command_completion" ]]; then
71
- while IFS= read -r command_candidate; do
72
- if [[ -n "$command_candidate" ]]; then
73
- candidates+=("$command_candidate")
74
- fi
75
- done < <(eval "$command_completion" 2>/dev/null)
67
+ if [[ "$last" == :* ]]; then
68
+ directive="\${last:1}"
69
+ unset 'lines[count-1]'
70
+ (( count-- ))
76
71
  fi
77
72
 
78
- # Handle directives
79
- # 16 = FileCompletion, 32 = DirectoryCompletion
80
- if (( directive & 16 )); then
81
- COMPREPLY=($(compgen -f -- "$completion_cur"))
73
+ # Parse @ext: metadata (extension filter for native file completion)
74
+ local extensions=""
75
+ if (( count > 0 )); then
76
+ local maybe_ext="\${lines[count-1]}"
77
+ if [[ "$maybe_ext" == @ext:* ]]; then
78
+ extensions="\${maybe_ext:5}"
79
+ unset 'lines[count-1]'
80
+ fi
81
+ fi
82
82
 
83
- if [[ -n "$file_extensions" ]]; then
84
- local -a filtered=()
85
- local -a extension_list=()
86
- local file_candidate ext
83
+ local cur=""
84
+ (( \${#_words[@]} > 0 )) && cur="\${_words[\${#_words[@]}-1]}"
87
85
 
88
- IFS=',' read -r -a extension_list <<< "$file_extensions"
86
+ # Strip --opt= prefix for native file/directory completion
87
+ local inline_prefix=""
88
+ if [[ "$cur" == --*=* ]]; then
89
+ inline_prefix="\${cur%%=*}="
90
+ cur="\${cur#*=}"
91
+ fi
89
92
 
90
- for file_candidate in "\${COMPREPLY[@]}"; do
91
- if [[ -d "$file_candidate" ]]; then
92
- filtered+=("$file_candidate")
93
- continue
94
- fi
93
+ # 16 = FileCompletion: delegate entirely to native file completion
94
+ if (( directive & 16 )); then
95
+ local -a entries=($(compgen -f -- "$cur"))
96
+ if [[ -n "$inline_prefix" ]]; then
97
+ local i
98
+ for (( i=0; i<\${#entries[@]}; i++ )); do
99
+ entries[$i]="\${inline_prefix}\${entries[$i]}"
100
+ done
101
+ fi
102
+ COMPREPLY=("\${entries[@]}")
103
+ compopt -o filenames
104
+ return 0
105
+ fi
95
106
 
96
- for ext in "\${extension_list[@]}"; do
97
- if [[ "$file_candidate" == *."$ext" ]]; then
98
- filtered+=("$file_candidate")
107
+ # Extension-filtered file completion: keep matching files + directories
108
+ if [[ -n "$extensions" ]]; then
109
+ local -a all_entries=($(compgen -f -- "$cur"))
110
+ local IFS=','
111
+ local -a ext_arr=($extensions)
112
+ IFS=$'\\n'
113
+ for f in "\${all_entries[@]}"; do
114
+ if [[ -d "$f" ]]; then
115
+ COMPREPLY+=("\${inline_prefix}$f")
116
+ else
117
+ for ext in "\${ext_arr[@]}"; do
118
+ if [[ "$f" == *".$ext" ]]; then
119
+ COMPREPLY+=("\${inline_prefix}$f")
99
120
  break
100
121
  fi
101
122
  done
102
- done
123
+ fi
124
+ done
125
+ compopt -o filenames
126
+ compopt +o default 2>/dev/null
127
+ return 0
128
+ fi
103
129
 
104
- COMPREPLY=("\${filtered[@]}")
130
+ # Start with JS candidates
131
+ if (( \${#lines[@]} > 0 )); then
132
+ COMPREPLY=("\${lines[@]}")
133
+ fi
134
+
135
+ # 32 = DirectoryCompletion: merge native directory matches
136
+ if (( directive & 32 )); then
137
+ local -a dirs=($(compgen -d -- "$cur"))
138
+ if [[ -n "$inline_prefix" ]]; then
139
+ local i
140
+ for (( i=0; i<\${#dirs[@]}; i++ )); do
141
+ dirs[$i]="\${inline_prefix}\${dirs[$i]}"
142
+ done
105
143
  fi
106
- elif (( directive & 32 )); then
107
- COMPREPLY=($(compgen -d -- "$completion_cur"))
108
- elif [[ \${#candidates[@]} -gt 0 ]]; then
109
- COMPREPLY=($(compgen -W "\${candidates[*]}" -- "$completion_cur"))
144
+ COMPREPLY+=("\${dirs[@]}")
145
+ compopt -o filenames
110
146
  fi
111
147
 
112
- if [[ -n "$completion_prefix" && \${#COMPREPLY[@]} -gt 0 ]]; then
113
- local -a prefixed=()
114
- local candidate
115
- for candidate in "\${COMPREPLY[@]}"; do
116
- prefixed+=("$completion_prefix$candidate")
117
- done
118
- COMPREPLY=("\${prefixed[@]}")
148
+ # 2 = NoFileCompletion, 32 = DirectoryCompletion:
149
+ # suppress -o default file fallback when completions are restricted
150
+ if (( directive & 2 )) || (( directive & 32 )); then
151
+ compopt +o default 2>/dev/null
119
152
  fi
120
153
 
121
154
  return 0
122
155
  }
123
156
 
124
157
  # Register the completion function
125
- complete -F _${programName}_completions ${programName}
158
+ complete -o default -F _${programName}_completions ${programName}
126
159
  `,
127
160
  shell: "bash",
128
161
  installInstructions: `# To enable completions, add the following to your ~/.bashrc:
@@ -141,6 +174,9 @@ source ~/.bashrc`
141
174
  //#endregion
142
175
  //#region src/completion/dynamic/candidate-generator.ts
143
176
  /**
177
+ * Generate completion candidates based on context
178
+ */
179
+ /**
144
180
  * Completion directive flags (bitwise)
145
181
  */
146
182
  const CompletionDirective = {
@@ -170,14 +206,61 @@ function generateCandidates(context) {
170
206
  };
171
207
  }
172
208
  }
173
- function addFileExtensionMetadata(candidates, extensions) {
174
- if (!extensions || extensions.length === 0) return;
175
- const normalized = Array.from(new Set(extensions.map((ext) => ext.trim().replace(/^\./, "")).filter((ext) => ext.length > 0)));
176
- if (normalized.length === 0) return;
177
- candidates.push({
178
- value: `__extensions:${normalized.join(",")}`,
179
- type: "value"
180
- });
209
+ /**
210
+ * Execute a shell command and return results as candidates
211
+ */
212
+ function executeShellCommand(command) {
213
+ try {
214
+ return execSync(command, {
215
+ encoding: "utf-8",
216
+ timeout: 5e3
217
+ }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => ({
218
+ value: line,
219
+ type: "value"
220
+ }));
221
+ } catch {
222
+ return [];
223
+ }
224
+ }
225
+ /**
226
+ * Resolve value completion, executing shell commands and file lookups in JS
227
+ */
228
+ function resolveValueCandidates(vc, candidates, _currentWord, description) {
229
+ let directive = CompletionDirective.FilterPrefix;
230
+ let fileExtensions;
231
+ switch (vc.type) {
232
+ case "choices":
233
+ if (vc.choices) for (const choice of vc.choices) candidates.push({
234
+ value: choice,
235
+ description,
236
+ type: "value"
237
+ });
238
+ directive |= CompletionDirective.NoFileCompletion;
239
+ break;
240
+ case "file":
241
+ if (vc.extensions && vc.extensions.length > 0) {
242
+ fileExtensions = Array.from(new Set(vc.extensions.map((ext) => ext.trim().replace(/^\./, "")).filter((ext) => ext.length > 0)));
243
+ if (fileExtensions.length === 0) {
244
+ fileExtensions = void 0;
245
+ directive |= CompletionDirective.FileCompletion;
246
+ }
247
+ } else directive |= CompletionDirective.FileCompletion;
248
+ break;
249
+ case "directory":
250
+ directive |= CompletionDirective.DirectoryCompletion;
251
+ break;
252
+ case "command":
253
+ if (vc.shellCommand) candidates.push(...executeShellCommand(vc.shellCommand));
254
+ directive |= CompletionDirective.NoFileCompletion;
255
+ break;
256
+ case "none":
257
+ directive |= CompletionDirective.NoFileCompletion;
258
+ break;
259
+ }
260
+ return {
261
+ directive,
262
+ fileExtensions
263
+ };
181
264
  }
182
265
  /**
183
266
  * Generate subcommand candidates
@@ -236,43 +319,20 @@ function generateOptionNameCandidates(context) {
236
319
  */
237
320
  function generateOptionValueCandidates(context) {
238
321
  const candidates = [];
239
- let directive = CompletionDirective.FilterPrefix;
240
322
  if (!context.targetOption) return {
241
323
  candidates,
242
- directive
324
+ directive: CompletionDirective.FilterPrefix
243
325
  };
244
326
  const vc = context.targetOption.valueCompletion;
245
327
  if (!vc) return {
246
328
  candidates,
247
- directive
329
+ directive: CompletionDirective.FilterPrefix
248
330
  };
249
- switch (vc.type) {
250
- case "choices":
251
- if (vc.choices) for (const choice of vc.choices) candidates.push({
252
- value: choice,
253
- type: "value"
254
- });
255
- break;
256
- case "file":
257
- directive |= CompletionDirective.FileCompletion;
258
- addFileExtensionMetadata(candidates, vc.extensions);
259
- break;
260
- case "directory":
261
- directive |= CompletionDirective.DirectoryCompletion;
262
- break;
263
- case "command":
264
- if (vc.shellCommand) candidates.push({
265
- value: `__command:${vc.shellCommand}`,
266
- type: "value"
267
- });
268
- break;
269
- case "none":
270
- directive |= CompletionDirective.NoFileCompletion;
271
- break;
272
- }
331
+ const { directive, fileExtensions } = resolveValueCandidates(vc, candidates, context.currentWord);
273
332
  return {
274
333
  candidates,
275
- directive
334
+ directive,
335
+ fileExtensions
276
336
  };
277
337
  }
278
338
  /**
@@ -280,71 +340,36 @@ function generateOptionValueCandidates(context) {
280
340
  */
281
341
  function generatePositionalCandidates(context) {
282
342
  const candidates = [];
283
- let directive = CompletionDirective.FilterPrefix;
284
343
  const positionalIndex = context.positionalIndex ?? 0;
285
- const positional = context.positionals[positionalIndex];
344
+ const positional = context.positionals[positionalIndex] ?? (context.positionals.at(-1)?.variadic ? context.positionals.at(-1) : void 0);
286
345
  if (!positional) return {
287
346
  candidates,
288
- directive
347
+ directive: CompletionDirective.FilterPrefix
289
348
  };
290
349
  const vc = positional.valueCompletion;
291
350
  if (!vc) return {
292
351
  candidates,
293
- directive
352
+ directive: CompletionDirective.FilterPrefix
294
353
  };
295
- switch (vc.type) {
296
- case "choices":
297
- if (vc.choices) for (const choice of vc.choices) candidates.push({
298
- value: choice,
299
- description: positional.description,
300
- type: "value"
301
- });
302
- break;
303
- case "file":
304
- directive |= CompletionDirective.FileCompletion;
305
- addFileExtensionMetadata(candidates, vc.extensions);
306
- break;
307
- case "directory":
308
- directive |= CompletionDirective.DirectoryCompletion;
309
- break;
310
- case "command":
311
- if (vc.shellCommand) candidates.push({
312
- value: `__command:${vc.shellCommand}`,
313
- type: "value"
314
- });
315
- break;
316
- case "none":
317
- directive |= CompletionDirective.NoFileCompletion;
318
- break;
319
- }
354
+ const { directive, fileExtensions } = resolveValueCandidates(vc, candidates, context.currentWord, positional.description);
320
355
  return {
321
356
  candidates,
322
- directive
357
+ directive,
358
+ fileExtensions
323
359
  };
324
360
  }
325
- /**
326
- * Format candidates as shell completion output
327
- *
328
- * Format: value\tdescription (tab-separated)
329
- * Last line: :directive_code
330
- */
331
- function formatOutput(result) {
332
- const lines = [];
333
- for (const candidate of result.candidates) if (candidate.description) lines.push(`${candidate.value}\t${candidate.description}`);
334
- else lines.push(candidate.value);
335
- lines.push(`:${result.directive}`);
336
- return lines.join("\n");
337
- }
338
361
 
339
362
  //#endregion
340
- //#region src/completion/dynamic/context-parser.ts
341
- /**
342
- * Parse completion context from partial command line
343
- */
363
+ //#region src/completion/value-completion-resolver.ts
344
364
  /**
345
365
  * Resolve value completion from field metadata
366
+ *
367
+ * Priority:
368
+ * 1. Explicit custom completion (choices or shellCommand)
369
+ * 2. Explicit completion type (file, directory, none)
370
+ * 3. Auto-detected enum values from schema
346
371
  */
347
- function resolveValueCompletion$1(field) {
372
+ function resolveValueCompletion(field) {
348
373
  const meta = field.completion;
349
374
  if (meta?.custom) {
350
375
  if (meta.custom.choices && meta.custom.choices.length > 0) return {
@@ -369,6 +394,12 @@ function resolveValueCompletion$1(field) {
369
394
  choices: field.enumValues
370
395
  };
371
396
  }
397
+
398
+ //#endregion
399
+ //#region src/completion/dynamic/context-parser.ts
400
+ /**
401
+ * Parse completion context from partial command line
402
+ */
372
403
  /**
373
404
  * Extract options from a command
374
405
  */
@@ -382,7 +413,7 @@ function extractOptions$1(command) {
382
413
  takesValue: field.type !== "boolean",
383
414
  valueType: field.type,
384
415
  required: field.required,
385
- valueCompletion: resolveValueCompletion$1(field)
416
+ valueCompletion: resolveValueCompletion(field)
386
417
  }));
387
418
  }
388
419
  /**
@@ -396,7 +427,8 @@ function extractPositionalsForContext(command) {
396
427
  position: index,
397
428
  description: field.description,
398
429
  required: field.required,
399
- valueCompletion: resolveValueCompletion$1(field)
430
+ variadic: field.type === "array",
431
+ valueCompletion: resolveValueCompletion(field)
400
432
  }));
401
433
  }
402
434
  /**
@@ -505,7 +537,11 @@ function parseCompletionContext(argv, rootCommand) {
505
537
  if (opt && opt.takesValue) {
506
538
  completionType = "option-value";
507
539
  targetOption = opt;
508
- } else completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
540
+ } else if (currentWord.startsWith("-")) completionType = "option-name";
541
+ else {
542
+ completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
543
+ if (completionType === "positional") positionalIndex = positionalCount;
544
+ }
509
545
  } else if (!afterDoubleDash && currentWord.startsWith("--") && hasInlineValue(currentWord)) {
510
546
  const optName = parseOptionName(currentWord);
511
547
  const opt = findOption(options, optName);
@@ -515,7 +551,7 @@ function parseCompletionContext(argv, rootCommand) {
515
551
  } else completionType = "option-name";
516
552
  } else if (!afterDoubleDash && currentWord.startsWith("-")) completionType = "option-name";
517
553
  else {
518
- completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
554
+ completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount, afterDoubleDash);
519
555
  if (completionType === "positional") positionalIndex = positionalCount;
520
556
  }
521
557
  return {
@@ -536,15 +572,99 @@ function parseCompletionContext(argv, rootCommand) {
536
572
  /**
537
573
  * Determine default completion type when not completing an option
538
574
  */
539
- function determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount) {
575
+ function determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount, afterDoubleDash) {
576
+ if (afterDoubleDash) return "positional";
540
577
  if (subcommands.length > 0) {
541
578
  if (subcommands.filter((s) => s.startsWith(currentWord)).length > 0 || currentWord === "") return "subcommand";
542
579
  }
543
580
  if (positionalCount < positionals.length) return "positional";
544
- if (positionals.length > 0) return "positional";
581
+ if (positionals.length > 0 && positionals[positionals.length - 1].variadic) return "positional";
545
582
  return "subcommand";
546
583
  }
547
584
 
585
+ //#endregion
586
+ //#region src/completion/dynamic/shell-formatter.ts
587
+ /**
588
+ * Format completion candidates for the specified shell
589
+ *
590
+ * @returns Shell-ready output string (lines separated by newline, last line is :directive)
591
+ */
592
+ function formatForShell(result, options) {
593
+ switch (options.shell) {
594
+ case "bash": return formatForBash(result, options);
595
+ case "zsh": return formatForZsh(result, options);
596
+ case "fish": return formatForFish(result, options);
597
+ }
598
+ }
599
+ /**
600
+ * Check if the FilterPrefix directive is set
601
+ */
602
+ function shouldFilterPrefix(directive) {
603
+ return (directive & CompletionDirective.FilterPrefix) !== 0;
604
+ }
605
+ /**
606
+ * Filter candidates by prefix
607
+ */
608
+ function filterByPrefix(candidates, prefix) {
609
+ if (!prefix) return candidates;
610
+ return candidates.filter((c) => c.value.startsWith(prefix));
611
+ }
612
+ /**
613
+ * Append extension metadata and directive to output lines
614
+ */
615
+ function appendMetadata(lines, result) {
616
+ if (result.fileExtensions && result.fileExtensions.length > 0) lines.push(`@ext:${result.fileExtensions.join(",")}`);
617
+ lines.push(`:${result.directive}`);
618
+ }
619
+ /**
620
+ * Format for bash
621
+ *
622
+ * - Pre-filters candidates by currentWord prefix (replaces compgen -W)
623
+ * - Handles --opt=value inline values by prepending prefix
624
+ * - Outputs plain values only (no descriptions - bash COMPREPLY doesn't support them)
625
+ * - Last line: :directive
626
+ */
627
+ function formatForBash(result, options) {
628
+ let { candidates } = result;
629
+ if (shouldFilterPrefix(result.directive)) candidates = filterByPrefix(candidates, options.currentWord);
630
+ const lines = candidates.map((c) => {
631
+ if (options.inlinePrefix) return `${options.inlinePrefix}${c.value}`;
632
+ return c.value;
633
+ });
634
+ appendMetadata(lines, result);
635
+ return lines.join("\n");
636
+ }
637
+ /**
638
+ * Format for zsh
639
+ *
640
+ * - Outputs value:description pairs for _describe
641
+ * - Colons in values/descriptions are escaped with backslash
642
+ * - Last line: :directive
643
+ */
644
+ function formatForZsh(result, _options) {
645
+ const lines = result.candidates.map((c) => {
646
+ const escapedValue = c.value.replace(/:/g, "\\:");
647
+ if (c.description) return `${escapedValue}:${c.description.replace(/:/g, "\\:")}`;
648
+ return escapedValue;
649
+ });
650
+ appendMetadata(lines, result);
651
+ return lines.join("\n");
652
+ }
653
+ /**
654
+ * Format for fish
655
+ *
656
+ * - Outputs value\tdescription pairs
657
+ * - Last line: :directive
658
+ */
659
+ function formatForFish(result, _options) {
660
+ const lines = result.candidates.map((c) => {
661
+ if (c.description) return `${c.value}\t${c.description}`;
662
+ return c.value;
663
+ });
664
+ appendMetadata(lines, result);
665
+ return lines.join("\n");
666
+ }
667
+
548
668
  //#endregion
549
669
  //#region src/completion/dynamic/complete-command.ts
550
670
  /**
@@ -553,24 +673,35 @@ function determineDefaultCompletionType(currentWord, subcommands, positionals, p
553
673
  * This creates a hidden `__complete` command that outputs completion candidates
554
674
  * for shell scripts to consume. Usage:
555
675
  *
556
- * mycli __complete -- build --fo
557
- * mycli __complete -- plugin add
676
+ * mycli __complete --shell bash -- build --fo
677
+ * mycli __complete --shell zsh -- plugin add
558
678
  *
559
- * Output format:
560
- * value\tdescription
561
- * ...
562
- * :directive_code
679
+ * Output format depends on the target shell:
680
+ * bash: plain values (pre-filtered by prefix), last line :directive
681
+ * zsh: value:description pairs, last line :directive
682
+ * fish: value\tdescription pairs, last line :directive
683
+ */
684
+ /**
685
+ * Detect inline option-value prefix (e.g., "--format=" from "--format=json")
563
686
  */
687
+ function detectInlinePrefix(currentWord) {
688
+ if (currentWord.startsWith("--") && currentWord.includes("=")) return currentWord.slice(0, currentWord.indexOf("=") + 1);
689
+ }
564
690
  /**
565
691
  * Schema for the __complete command
566
- *
567
- * Arguments after -- are collected as the completion arguments
568
692
  */
569
- const completeArgsSchema = z.object({ args: arg(z.array(z.string()).default([]), {
570
- positional: true,
571
- description: "Arguments to complete",
572
- variadic: true
573
- }) });
693
+ const completeArgsSchema = z.object({
694
+ shell: arg(z.enum([
695
+ "bash",
696
+ "zsh",
697
+ "fish"
698
+ ]), { description: "Target shell for output formatting" }),
699
+ args: arg(z.array(z.string()).default([]), {
700
+ positional: true,
701
+ description: "Arguments to complete",
702
+ variadic: true
703
+ })
704
+ });
574
705
  /**
575
706
  * Create the dynamic completion command
576
707
  *
@@ -583,8 +714,15 @@ function createDynamicCompleteCommand(rootCommand, _programName) {
583
714
  name: "__complete",
584
715
  args: completeArgsSchema,
585
716
  run(args) {
586
- const result = generateCandidates(parseCompletionContext(args.args, rootCommand));
587
- console.log(formatOutput(result));
717
+ const context = parseCompletionContext(args.args, rootCommand);
718
+ const result = generateCandidates(context);
719
+ const inlinePrefix = detectInlinePrefix(context.currentWord);
720
+ const output = formatForShell(result, {
721
+ shell: args.shell,
722
+ currentWord: inlinePrefix ? context.currentWord.slice(inlinePrefix.length) : context.currentWord,
723
+ inlinePrefix
724
+ });
725
+ console.log(output);
588
726
  }
589
727
  });
590
728
  }
@@ -600,81 +738,75 @@ function hasCompleteCommand(command) {
600
738
  /**
601
739
  * Generate fish completion script for a command
602
740
  *
603
- * Generates a dynamic script that calls the CLI's __complete command at runtime.
741
+ * Generates a minimal script that delegates all logic to the CLI's __complete command.
742
+ * The shell script only handles:
743
+ * - Getting current command line tokens
744
+ * - Calling __complete with --shell fish
745
+ * - Echoing output as completions
746
+ * - Falling back to native file/directory completion when directed
604
747
  */
605
748
  function generateFishCompletion(_command, options) {
606
749
  const programName = options.programName;
607
750
  return {
608
751
  script: `# Fish completion for ${programName}
609
752
  # Generated by politty
610
- # This script calls the CLI to generate completions dynamically
611
753
 
612
754
  function __fish_${programName}_complete
613
- # Get current command line arguments
614
755
  set -l args (commandline -opc)
615
- # Remove the program name
616
756
  set -e args[1]
617
-
618
- # Call the CLI to get completions
619
757
  set -l directive 0
620
- set -l command_completion
621
- set -l file_extensions
758
+ set -l extensions ""
759
+
760
+ # commandline -opc excludes the current token; always include it
761
+ set -l ct (commandline -ct)
762
+ if test (count $ct) -eq 0
763
+ set -a args ""
764
+ else
765
+ set -a args $ct
766
+ end
622
767
 
623
- for line in (${programName} __complete -- $args 2>/dev/null)
768
+ for line in (${programName} __complete --shell fish -- $args 2>/dev/null)
624
769
  if string match -q ':*' -- $line
625
- # Parse directive
626
770
  set directive (string sub -s 2 -- $line)
627
- else if string match -q '__command:*' -- $line
628
- # Parse shell command completion request
629
- set command_completion (string sub -s 11 -- $line)
630
- else if string match -q '__extensions:*' -- $line
631
- # Parse optional file extension metadata
632
- set file_extensions (string sub -s 14 -- $line)
771
+ else if string match -q '@ext:*' -- $line
772
+ set extensions (string sub -s 6 -- $line)
633
773
  else if test -n "$line"
634
- # Parse completion: value\\tdescription
635
- set -l parts (string split \\t -- $line)
636
- if test (count $parts) -ge 2
637
- echo $parts[1]\\t$parts[2]
638
- else
639
- echo $parts[1]
640
- end
774
+ echo $line
641
775
  end
642
776
  end
643
777
 
644
- # Execute shellCommand completion if requested by __complete
645
- if test -n "$command_completion"
646
- for command_candidate in (eval "$command_completion" 2>/dev/null)
647
- if test -n "$command_candidate"
648
- echo $command_candidate
778
+ # 16 = FileCompletion: delegate entirely to native file completion
779
+ if test (math "bitand($directive, 16)") -ne 0
780
+ __fish_complete_path
781
+ return
782
+ end
783
+
784
+ # Extension-filtered file completion: keep matching files + directories
785
+ if test -n "$extensions"
786
+ set -l cur (commandline -ct)
787
+ test (count $cur) -eq 0; and set cur ""
788
+ __fish_complete_directories "$cur"
789
+ for ext in (string split "," -- $extensions)
790
+ for f in "$cur"*.$ext
791
+ if test -f "$f"
792
+ echo $f
793
+ end
649
794
  end
650
795
  end
796
+ return
651
797
  end
652
798
 
653
- # Handle directives by returning special values
654
- # The main completion function will check for these
655
- if test (math "$directive & 16") -ne 0
656
- echo "__directive:file"
657
- else if test (math "$directive & 32") -ne 0
658
- echo "__directive:directory"
799
+ # 32 = DirectoryCompletion: add native directory matches
800
+ if test (math "bitand($directive, 32)") -ne 0
801
+ __fish_complete_directories
659
802
  end
660
803
  end
661
804
 
662
805
  # Clear existing completions
663
806
  complete -e -c ${programName}
664
807
 
665
- # Main completion
666
- complete -c ${programName} -f -a '(
667
- set -l completions (__fish_${programName}_complete)
668
- for c in $completions
669
- if string match -q "__directive:file" -- $c
670
- __fish_complete_path
671
- else if string match -q "__directive:directory" -- $c
672
- __fish_complete_directories
673
- else
674
- echo $c
675
- end
676
- end
677
- )'
808
+ # Register completion
809
+ complete -c ${programName} -f -a '(__fish_${programName}_complete)'
678
810
  `,
679
811
  shell: "fish",
680
812
  installInstructions: `# To enable completions, run one of the following:
@@ -696,7 +828,12 @@ source ~/.config/fish/completions/${programName}.fish`
696
828
  /**
697
829
  * Generate zsh completion script for a command
698
830
  *
699
- * Generates a dynamic script that calls the CLI's __complete command at runtime.
831
+ * Generates a minimal script that delegates all logic to the CLI's __complete command.
832
+ * The shell script only handles:
833
+ * - Getting current command line tokens
834
+ * - Calling __complete with --shell zsh
835
+ * - Passing output directly to _describe
836
+ * - Falling back to native file/directory completion when directed
700
837
  */
701
838
  function generateZshCompletion(_command, options) {
702
839
  const programName = options.programName;
@@ -708,57 +845,55 @@ function generateZshCompletion(_command, options) {
708
845
 
709
846
  _${programName}() {
710
847
  local -a candidates
711
- local output line directive=0
712
- local command_completion=""
713
- local file_extensions=""
714
-
715
- # Get the current words being completed
716
- local -a args
717
- args=("\${words[@]:1}")
718
-
719
- # Call the CLI to get completions
720
- output=("\${(@f)$(${programName} __complete -- "\${args[@]}" 2>/dev/null)}")
848
+ local line directive=0
849
+ local -a args=("\${words[@]:1}")
850
+ local -a output=("\${(@f)$(${programName} __complete --shell zsh -- "\${args[@]}" 2>/dev/null)}")
721
851
 
722
- # Parse output
852
+ local extensions=""
723
853
  for line in "\${output[@]}"; do
724
854
  if [[ "$line" == :* ]]; then
725
855
  directive="\${line:1}"
726
- elif [[ "$line" == __command:* ]]; then
727
- command_completion="\${line#__command:}"
728
- elif [[ "$line" == __extensions:* ]]; then
729
- file_extensions="\${line#__extensions:}"
856
+ elif [[ "$line" == @ext:* ]]; then
857
+ extensions="\${line:5}"
730
858
  elif [[ -n "$line" ]]; then
731
- local name="\${line%%$'\\t'*}"
732
- local desc="\${line#*$'\\t'}"
733
- if [[ "$name" == "$desc" ]]; then
734
- candidates+=("$name")
735
- else
736
- candidates+=("$name:$desc")
737
- fi
859
+ candidates+=("$line")
738
860
  fi
739
861
  done
740
862
 
741
- # Execute shellCommand completion if requested by __complete
742
- if [[ -n "$command_completion" ]]; then
743
- local command_candidate
744
- for command_candidate in "\${(@f)$(eval "$command_completion" 2>/dev/null)}"; do
745
- if [[ -n "$command_candidate" ]]; then
746
- candidates+=("$command_candidate")
747
- fi
863
+ # 16 = FileCompletion: delegate entirely to native file completion
864
+ if (( directive & 16 )); then
865
+ _files
866
+ return 0
867
+ fi
868
+
869
+ # Extension-filtered file completion: call _files -g per extension
870
+ if [[ -n "$extensions" ]]; then
871
+ local ext
872
+ for ext in \${(s:,:)extensions}; do
873
+ _files -g "*.$ext"
748
874
  done
875
+ return 0
749
876
  fi
750
877
 
751
- # Handle directives
752
- # 16 = FileCompletion, 32 = DirectoryCompletion
753
- if (( directive & 16 )); then
754
- _files
755
- elif (( directive & 32 )); then
756
- _files -/
757
- elif (( \${#candidates[@]} > 0 )); then
878
+ if (( \${#candidates[@]} > 0 )); then
758
879
  _describe 'completions' candidates
759
880
  fi
881
+
882
+ # 32 = DirectoryCompletion: add native directory matches
883
+ if (( directive & 32 )); then
884
+ _files -/
885
+ fi
886
+
887
+ # 2 = NoFileCompletion, 32 = DirectoryCompletion:
888
+ # prevent fallback to default completers (e.g. file completion)
889
+ if (( directive & 2 )) || (( directive & 32 )); then
890
+ return 0
891
+ fi
760
892
  }
761
893
 
894
+ # Prevent _files -g from falling back to showing all files when no pattern matches
895
+ zstyle ':completion:*:*:${programName}:*' file-patterns '%p:globbed-files *(-/):directories'
896
+
762
897
  compdef _${programName} ${programName}
763
898
  `,
764
899
  shell: "zsh",
@@ -785,39 +920,6 @@ source ~/.zshrc`
785
920
  * Extract completion data from commands
786
921
  */
787
922
  /**
788
- * Resolve value completion from field metadata
789
- *
790
- * Priority:
791
- * 1. Explicit custom completion (choices or shellCommand)
792
- * 2. Explicit completion type (file, directory, none)
793
- * 3. Auto-detected enum values from schema
794
- */
795
- function resolveValueCompletion(field) {
796
- const meta = field.completion;
797
- if (meta?.custom) {
798
- if (meta.custom.choices && meta.custom.choices.length > 0) return {
799
- type: "choices",
800
- choices: meta.custom.choices
801
- };
802
- if (meta.custom.shellCommand) return {
803
- type: "command",
804
- shellCommand: meta.custom.shellCommand
805
- };
806
- }
807
- if (meta?.type) {
808
- if (meta.type === "file") return meta.extensions ? {
809
- type: "file",
810
- extensions: meta.extensions
811
- } : { type: "file" };
812
- if (meta.type === "directory") return { type: "directory" };
813
- if (meta.type === "none") return { type: "none" };
814
- }
815
- if (field.enumValues && field.enumValues.length > 0) return {
816
- type: "choices",
817
- choices: field.enumValues
818
- };
819
- }
820
- /**
821
923
  * Convert a resolved field to a completable option
822
924
  */
823
925
  function fieldToOption(field) {
@@ -894,5 +996,5 @@ function extractCompletionData(command, programName) {
894
996
  }
895
997
 
896
998
  //#endregion
897
- export { createDynamicCompleteCommand as a, CompletionDirective as c, generateBashCompletion as d, defineCommand as f, generateFishCompletion as i, formatOutput as l, extractPositionals as n, hasCompleteCommand as o, generateZshCompletion as r, parseCompletionContext as s, extractCompletionData as t, generateCandidates as u };
898
- //# sourceMappingURL=extractor-JfoYSoMk.js.map
999
+ export { createDynamicCompleteCommand as a, parseCompletionContext as c, generateCandidates as d, generateBashCompletion as f, generateFishCompletion as i, resolveValueCompletion as l, extractPositionals as n, hasCompleteCommand as o, defineCommand as p, generateZshCompletion as r, formatForShell as s, extractCompletionData as t, CompletionDirective as u };
1000
+ //# sourceMappingURL=extractor-DO-FDKkW.js.map