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