politty 0.3.0 → 0.3.2

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 (62) hide show
  1. package/README.md +84 -2
  2. package/dist/{arg-registry-w5mMKJkZ.d.ts → arg-registry-B4a4Fj7f.d.cts} +51 -2
  3. package/dist/arg-registry-B4a4Fj7f.d.cts.map +1 -0
  4. package/dist/{arg-registry-i6SA4l-E.d.cts → arg-registry-C3GP-5C9.d.ts} +51 -2
  5. package/dist/arg-registry-C3GP-5C9.d.ts.map +1 -0
  6. package/dist/augment.d.cts +1 -1
  7. package/dist/augment.d.ts +1 -1
  8. package/dist/completion/index.cjs +23 -462
  9. package/dist/completion/index.cjs.map +1 -1
  10. package/dist/completion/index.d.cts +13 -91
  11. package/dist/completion/index.d.cts.map +1 -1
  12. package/dist/completion/index.d.ts +13 -91
  13. package/dist/completion/index.d.ts.map +1 -1
  14. package/dist/completion/index.js +12 -457
  15. package/dist/completion/index.js.map +1 -1
  16. package/dist/docs/index.cjs +718 -280
  17. package/dist/docs/index.cjs.map +1 -1
  18. package/dist/docs/index.d.cts +102 -47
  19. package/dist/docs/index.d.cts.map +1 -1
  20. package/dist/docs/index.d.ts +102 -47
  21. package/dist/docs/index.d.ts.map +1 -1
  22. package/dist/docs/index.js +713 -281
  23. package/dist/docs/index.js.map +1 -1
  24. package/dist/extractor-B7bOwpvP.cjs +970 -0
  25. package/dist/extractor-B7bOwpvP.cjs.map +1 -0
  26. package/dist/extractor-CCi4rjSI.d.ts +238 -0
  27. package/dist/extractor-CCi4rjSI.d.ts.map +1 -0
  28. package/dist/extractor-DsJ6hYqQ.d.cts +238 -0
  29. package/dist/extractor-DsJ6hYqQ.d.cts.map +1 -0
  30. package/dist/extractor-JfoYSoMk.js +898 -0
  31. package/dist/extractor-JfoYSoMk.js.map +1 -0
  32. package/dist/index.cjs +8 -5
  33. package/dist/index.d.cts +5 -3
  34. package/dist/index.d.cts.map +1 -1
  35. package/dist/index.d.ts +5 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +5 -4
  38. package/dist/{runner-B38UBqhv.cjs → runner-BFKk3Arg.cjs} +25 -13
  39. package/dist/runner-BFKk3Arg.cjs.map +1 -0
  40. package/dist/{runner-CUN50BqK.js → runner-ElQbBn1U.js} +25 -13
  41. package/dist/runner-ElQbBn1U.js.map +1 -0
  42. package/dist/{schema-extractor-B9D3Rf22.cjs → schema-extractor-BU705qpC.cjs} +43 -4
  43. package/dist/schema-extractor-BU705qpC.cjs.map +1 -0
  44. package/dist/{schema-extractor-CXeUTW_Z.d.cts → schema-extractor-CHiBRT39.d.ts} +6 -1
  45. package/dist/schema-extractor-CHiBRT39.d.ts.map +1 -0
  46. package/dist/{schema-extractor-1YXqFSDT.js → schema-extractor-CP3ar0Wi.js} +43 -4
  47. package/dist/schema-extractor-CP3ar0Wi.js.map +1 -0
  48. package/dist/{schema-extractor-Cl_D04BX.d.ts → schema-extractor-DyfK21m_.d.cts} +6 -1
  49. package/dist/schema-extractor-DyfK21m_.d.cts.map +1 -0
  50. package/package.json +6 -6
  51. package/dist/arg-registry-i6SA4l-E.d.cts.map +0 -1
  52. package/dist/arg-registry-w5mMKJkZ.d.ts.map +0 -1
  53. package/dist/command-Bgd-yIwv.cjs +0 -25
  54. package/dist/command-Bgd-yIwv.cjs.map +0 -1
  55. package/dist/command-D-P2Pc3J.js +0 -20
  56. package/dist/command-D-P2Pc3J.js.map +0 -1
  57. package/dist/runner-B38UBqhv.cjs.map +0 -1
  58. package/dist/runner-CUN50BqK.js.map +0 -1
  59. package/dist/schema-extractor-1YXqFSDT.js.map +0 -1
  60. package/dist/schema-extractor-B9D3Rf22.cjs.map +0 -1
  61. package/dist/schema-extractor-CXeUTW_Z.d.cts.map +0 -1
  62. package/dist/schema-extractor-Cl_D04BX.d.ts.map +0 -1
@@ -0,0 +1,898 @@
1
+ import { a as arg, t as extractFields } from "./schema-extractor-CP3ar0Wi.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/core/command.ts
5
+ function defineCommand(config) {
6
+ return {
7
+ name: config.name,
8
+ description: config.description,
9
+ args: config.args,
10
+ subCommands: config.subCommands,
11
+ setup: config.setup,
12
+ run: config.run,
13
+ cleanup: config.cleanup,
14
+ notes: config.notes,
15
+ examples: config.examples
16
+ };
17
+ }
18
+
19
+ //#endregion
20
+ //#region src/completion/bash.ts
21
+ /**
22
+ * Generate bash completion script for a command
23
+ *
24
+ * Generates a dynamic script that calls the CLI's __complete command at runtime.
25
+ */
26
+ function generateBashCompletion(_command, options) {
27
+ const programName = options.programName;
28
+ return {
29
+ script: `# Bash completion for ${programName}
30
+ # Generated by politty
31
+
32
+ _${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"
37
+
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
43
+
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)
49
+ fi
50
+
51
+ local candidates=()
52
+ 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)
76
+ fi
77
+
78
+ # Handle directives
79
+ # 16 = FileCompletion, 32 = DirectoryCompletion
80
+ if (( directive & 16 )); then
81
+ COMPREPLY=($(compgen -f -- "$completion_cur"))
82
+
83
+ if [[ -n "$file_extensions" ]]; then
84
+ local -a filtered=()
85
+ local -a extension_list=()
86
+ local file_candidate ext
87
+
88
+ IFS=',' read -r -a extension_list <<< "$file_extensions"
89
+
90
+ for file_candidate in "\${COMPREPLY[@]}"; do
91
+ if [[ -d "$file_candidate" ]]; then
92
+ filtered+=("$file_candidate")
93
+ continue
94
+ fi
95
+
96
+ for ext in "\${extension_list[@]}"; do
97
+ if [[ "$file_candidate" == *."$ext" ]]; then
98
+ filtered+=("$file_candidate")
99
+ break
100
+ fi
101
+ done
102
+ done
103
+
104
+ COMPREPLY=("\${filtered[@]}")
105
+ 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"))
110
+ fi
111
+
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[@]}")
119
+ fi
120
+
121
+ return 0
122
+ }
123
+
124
+ # Register the completion function
125
+ complete -F _${programName}_completions ${programName}
126
+ `,
127
+ shell: "bash",
128
+ installInstructions: `# To enable completions, add the following to your ~/.bashrc:
129
+
130
+ # Option 1: Source directly
131
+ eval "$(${programName} completion bash)"
132
+
133
+ # Option 2: Save to a file
134
+ ${programName} completion bash > ~/.local/share/bash-completion/completions/${programName}
135
+
136
+ # Then reload your shell or run:
137
+ source ~/.bashrc`
138
+ };
139
+ }
140
+
141
+ //#endregion
142
+ //#region src/completion/dynamic/candidate-generator.ts
143
+ /**
144
+ * Completion directive flags (bitwise)
145
+ */
146
+ const CompletionDirective = {
147
+ Default: 0,
148
+ NoSpace: 1,
149
+ NoFileCompletion: 2,
150
+ FilterPrefix: 4,
151
+ KeepOrder: 8,
152
+ FileCompletion: 16,
153
+ DirectoryCompletion: 32,
154
+ Error: 64
155
+ };
156
+ /**
157
+ * Generate completion candidates based on context
158
+ */
159
+ function generateCandidates(context) {
160
+ const candidates = [];
161
+ let directive = CompletionDirective.Default;
162
+ switch (context.completionType) {
163
+ case "subcommand": return generateSubcommandCandidates(context);
164
+ case "option-name": return generateOptionNameCandidates(context);
165
+ case "option-value": return generateOptionValueCandidates(context);
166
+ case "positional": return generatePositionalCandidates(context);
167
+ default: return {
168
+ candidates,
169
+ directive
170
+ };
171
+ }
172
+ }
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
+ });
181
+ }
182
+ /**
183
+ * Generate subcommand candidates
184
+ */
185
+ function generateSubcommandCandidates(context) {
186
+ const candidates = [];
187
+ let directive = CompletionDirective.FilterPrefix;
188
+ for (const name of context.subcommands) {
189
+ let description;
190
+ if (context.currentCommand.subCommands) {
191
+ const sub = context.currentCommand.subCommands[name];
192
+ if (sub && typeof sub !== "function") description = sub.description;
193
+ }
194
+ candidates.push({
195
+ value: name,
196
+ description,
197
+ type: "subcommand"
198
+ });
199
+ }
200
+ if (candidates.length === 0 || context.currentWord.startsWith("-")) {
201
+ const optionResult = generateOptionNameCandidates(context);
202
+ candidates.push(...optionResult.candidates);
203
+ }
204
+ return {
205
+ candidates,
206
+ directive
207
+ };
208
+ }
209
+ /**
210
+ * Generate option name candidates
211
+ */
212
+ function generateOptionNameCandidates(context) {
213
+ const candidates = [];
214
+ const directive = CompletionDirective.FilterPrefix;
215
+ const availableOptions = context.options.filter((opt) => {
216
+ if (opt.valueType === "array") return true;
217
+ return !context.usedOptions.has(opt.cliName) && !context.usedOptions.has(opt.alias || "");
218
+ });
219
+ for (const opt of availableOptions) candidates.push({
220
+ value: `--${opt.cliName}`,
221
+ description: opt.description,
222
+ type: "option"
223
+ });
224
+ if (!context.usedOptions.has("help")) candidates.push({
225
+ value: "--help",
226
+ description: "Show help information",
227
+ type: "option"
228
+ });
229
+ return {
230
+ candidates,
231
+ directive
232
+ };
233
+ }
234
+ /**
235
+ * Generate option value candidates
236
+ */
237
+ function generateOptionValueCandidates(context) {
238
+ const candidates = [];
239
+ let directive = CompletionDirective.FilterPrefix;
240
+ if (!context.targetOption) return {
241
+ candidates,
242
+ directive
243
+ };
244
+ const vc = context.targetOption.valueCompletion;
245
+ if (!vc) return {
246
+ candidates,
247
+ directive
248
+ };
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
+ }
273
+ return {
274
+ candidates,
275
+ directive
276
+ };
277
+ }
278
+ /**
279
+ * Generate positional argument candidates
280
+ */
281
+ function generatePositionalCandidates(context) {
282
+ const candidates = [];
283
+ let directive = CompletionDirective.FilterPrefix;
284
+ const positionalIndex = context.positionalIndex ?? 0;
285
+ const positional = context.positionals[positionalIndex];
286
+ if (!positional) return {
287
+ candidates,
288
+ directive
289
+ };
290
+ const vc = positional.valueCompletion;
291
+ if (!vc) return {
292
+ candidates,
293
+ directive
294
+ };
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
+ }
320
+ return {
321
+ candidates,
322
+ directive
323
+ };
324
+ }
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
+
339
+ //#endregion
340
+ //#region src/completion/dynamic/context-parser.ts
341
+ /**
342
+ * Parse completion context from partial command line
343
+ */
344
+ /**
345
+ * Resolve value completion from field metadata
346
+ */
347
+ function resolveValueCompletion$1(field) {
348
+ const meta = field.completion;
349
+ if (meta?.custom) {
350
+ if (meta.custom.choices && meta.custom.choices.length > 0) return {
351
+ type: "choices",
352
+ choices: meta.custom.choices
353
+ };
354
+ if (meta.custom.shellCommand) return {
355
+ type: "command",
356
+ shellCommand: meta.custom.shellCommand
357
+ };
358
+ }
359
+ if (meta?.type) {
360
+ if (meta.type === "file") return meta.extensions ? {
361
+ type: "file",
362
+ extensions: meta.extensions
363
+ } : { type: "file" };
364
+ if (meta.type === "directory") return { type: "directory" };
365
+ if (meta.type === "none") return { type: "none" };
366
+ }
367
+ if (field.enumValues && field.enumValues.length > 0) return {
368
+ type: "choices",
369
+ choices: field.enumValues
370
+ };
371
+ }
372
+ /**
373
+ * Extract options from a command
374
+ */
375
+ function extractOptions$1(command) {
376
+ if (!command.args) return [];
377
+ return extractFields(command.args).fields.filter((field) => !field.positional).map((field) => ({
378
+ name: field.name,
379
+ cliName: field.cliName,
380
+ alias: field.alias,
381
+ description: field.description,
382
+ takesValue: field.type !== "boolean",
383
+ valueType: field.type,
384
+ required: field.required,
385
+ valueCompletion: resolveValueCompletion$1(field)
386
+ }));
387
+ }
388
+ /**
389
+ * Extract positionals from a command
390
+ */
391
+ function extractPositionalsForContext(command) {
392
+ if (!command.args) return [];
393
+ return extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
394
+ name: field.name,
395
+ cliName: field.cliName,
396
+ position: index,
397
+ description: field.description,
398
+ required: field.required,
399
+ valueCompletion: resolveValueCompletion$1(field)
400
+ }));
401
+ }
402
+ /**
403
+ * Get subcommand names from a command
404
+ */
405
+ function getSubcommandNames(command) {
406
+ if (!command.subCommands) return [];
407
+ return Object.keys(command.subCommands).filter((name) => !name.startsWith("__"));
408
+ }
409
+ /**
410
+ * Resolve subcommand by name
411
+ */
412
+ function resolveSubcommand(command, name) {
413
+ if (!command.subCommands) return null;
414
+ const sub = command.subCommands[name];
415
+ if (!sub) return null;
416
+ if (typeof sub === "function") return null;
417
+ return sub;
418
+ }
419
+ /**
420
+ * Check if a word is an option (starts with - or --)
421
+ */
422
+ function isOption(word) {
423
+ return word.startsWith("-");
424
+ }
425
+ /**
426
+ * Parse option name from word (e.g., "--foo=bar" -> "foo", "-v" -> "v")
427
+ */
428
+ function parseOptionName(word) {
429
+ if (word.startsWith("--")) {
430
+ const withoutPrefix = word.slice(2);
431
+ const eqIndex = withoutPrefix.indexOf("=");
432
+ return eqIndex >= 0 ? withoutPrefix.slice(0, eqIndex) : withoutPrefix;
433
+ }
434
+ if (word.startsWith("-")) return word.slice(1, 2);
435
+ return word;
436
+ }
437
+ /**
438
+ * Check if option has inline value (e.g., "--foo=bar")
439
+ */
440
+ function hasInlineValue(word) {
441
+ return word.includes("=");
442
+ }
443
+ /**
444
+ * Find option by name or alias
445
+ */
446
+ function findOption(options, nameOrAlias) {
447
+ return options.find((opt) => opt.cliName === nameOrAlias || opt.alias === nameOrAlias);
448
+ }
449
+ /**
450
+ * Parse completion context from command line arguments
451
+ *
452
+ * @param argv - Arguments after the program name (e.g., ["build", "--fo"])
453
+ * @param rootCommand - The root command
454
+ * @returns Completion context
455
+ */
456
+ function parseCompletionContext(argv, rootCommand) {
457
+ let currentCommand = rootCommand;
458
+ const subcommandPath = [];
459
+ const usedOptions = /* @__PURE__ */ new Set();
460
+ let positionalCount = 0;
461
+ let i = 0;
462
+ let options = extractOptions$1(currentCommand);
463
+ let afterDoubleDash = false;
464
+ while (i < argv.length - 1) {
465
+ const word = argv[i];
466
+ if (!afterDoubleDash && word === "--") {
467
+ afterDoubleDash = true;
468
+ i++;
469
+ continue;
470
+ }
471
+ if (!afterDoubleDash && isOption(word)) {
472
+ const optName = parseOptionName(word);
473
+ const opt = findOption(options, optName);
474
+ if (opt) {
475
+ usedOptions.add(opt.cliName);
476
+ if (opt.alias) usedOptions.add(opt.alias);
477
+ if (opt.takesValue && !hasInlineValue(word)) i++;
478
+ }
479
+ i++;
480
+ continue;
481
+ }
482
+ const subcommand = afterDoubleDash ? null : resolveSubcommand(currentCommand, word);
483
+ if (subcommand) {
484
+ subcommandPath.push(word);
485
+ currentCommand = subcommand;
486
+ options = extractOptions$1(currentCommand);
487
+ usedOptions.clear();
488
+ positionalCount = 0;
489
+ i++;
490
+ continue;
491
+ }
492
+ positionalCount++;
493
+ i++;
494
+ }
495
+ const currentWord = argv[argv.length - 1] ?? "";
496
+ const previousWord = argv[argv.length - 2] ?? "";
497
+ const positionals = extractPositionalsForContext(currentCommand);
498
+ const subcommands = getSubcommandNames(currentCommand);
499
+ let completionType;
500
+ let targetOption;
501
+ let positionalIndex;
502
+ if (!afterDoubleDash && previousWord && isOption(previousWord) && !hasInlineValue(previousWord)) {
503
+ const optName = parseOptionName(previousWord);
504
+ const opt = findOption(options, optName);
505
+ if (opt && opt.takesValue) {
506
+ completionType = "option-value";
507
+ targetOption = opt;
508
+ } else completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
509
+ } else if (!afterDoubleDash && currentWord.startsWith("--") && hasInlineValue(currentWord)) {
510
+ const optName = parseOptionName(currentWord);
511
+ const opt = findOption(options, optName);
512
+ if (opt && opt.takesValue) {
513
+ completionType = "option-value";
514
+ targetOption = opt;
515
+ } else completionType = "option-name";
516
+ } else if (!afterDoubleDash && currentWord.startsWith("-")) completionType = "option-name";
517
+ else {
518
+ completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
519
+ if (completionType === "positional") positionalIndex = positionalCount;
520
+ }
521
+ return {
522
+ subcommandPath,
523
+ currentCommand,
524
+ currentWord,
525
+ previousWord,
526
+ completionType,
527
+ targetOption,
528
+ positionalIndex,
529
+ options,
530
+ subcommands,
531
+ positionals,
532
+ usedOptions,
533
+ providedPositionalCount: positionalCount
534
+ };
535
+ }
536
+ /**
537
+ * Determine default completion type when not completing an option
538
+ */
539
+ function determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount) {
540
+ if (subcommands.length > 0) {
541
+ if (subcommands.filter((s) => s.startsWith(currentWord)).length > 0 || currentWord === "") return "subcommand";
542
+ }
543
+ if (positionalCount < positionals.length) return "positional";
544
+ if (positionals.length > 0) return "positional";
545
+ return "subcommand";
546
+ }
547
+
548
+ //#endregion
549
+ //#region src/completion/dynamic/complete-command.ts
550
+ /**
551
+ * Dynamic completion command implementation
552
+ *
553
+ * This creates a hidden `__complete` command that outputs completion candidates
554
+ * for shell scripts to consume. Usage:
555
+ *
556
+ * mycli __complete -- build --fo
557
+ * mycli __complete -- plugin add
558
+ *
559
+ * Output format:
560
+ * value\tdescription
561
+ * ...
562
+ * :directive_code
563
+ */
564
+ /**
565
+ * Schema for the __complete command
566
+ *
567
+ * Arguments after -- are collected as the completion arguments
568
+ */
569
+ const completeArgsSchema = z.object({ args: arg(z.array(z.string()).default([]), {
570
+ positional: true,
571
+ description: "Arguments to complete",
572
+ variadic: true
573
+ }) });
574
+ /**
575
+ * Create the dynamic completion command
576
+ *
577
+ * @param rootCommand - The root command to generate completions for
578
+ * @param programName - The program name (optional, defaults to rootCommand.name)
579
+ * @returns A command that outputs completion candidates
580
+ */
581
+ function createDynamicCompleteCommand(rootCommand, _programName) {
582
+ return defineCommand({
583
+ name: "__complete",
584
+ args: completeArgsSchema,
585
+ run(args) {
586
+ const result = generateCandidates(parseCompletionContext(args.args, rootCommand));
587
+ console.log(formatOutput(result));
588
+ }
589
+ });
590
+ }
591
+ /**
592
+ * Check if a command tree contains the __complete command
593
+ */
594
+ function hasCompleteCommand(command) {
595
+ return Boolean(command.subCommands?.["__complete"]);
596
+ }
597
+
598
+ //#endregion
599
+ //#region src/completion/fish.ts
600
+ /**
601
+ * Generate fish completion script for a command
602
+ *
603
+ * Generates a dynamic script that calls the CLI's __complete command at runtime.
604
+ */
605
+ function generateFishCompletion(_command, options) {
606
+ const programName = options.programName;
607
+ return {
608
+ script: `# Fish completion for ${programName}
609
+ # Generated by politty
610
+ # This script calls the CLI to generate completions dynamically
611
+
612
+ function __fish_${programName}_complete
613
+ # Get current command line arguments
614
+ set -l args (commandline -opc)
615
+ # Remove the program name
616
+ set -e args[1]
617
+
618
+ # Call the CLI to get completions
619
+ set -l directive 0
620
+ set -l command_completion
621
+ set -l file_extensions
622
+
623
+ for line in (${programName} __complete -- $args 2>/dev/null)
624
+ if string match -q ':*' -- $line
625
+ # Parse directive
626
+ 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)
633
+ 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
641
+ end
642
+ end
643
+
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
649
+ end
650
+ end
651
+ end
652
+
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"
659
+ end
660
+ end
661
+
662
+ # Clear existing completions
663
+ complete -e -c ${programName}
664
+
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
+ )'
678
+ `,
679
+ shell: "fish",
680
+ installInstructions: `# To enable completions, run one of the following:
681
+
682
+ # Option 1: Source directly
683
+ ${programName} completion fish | source
684
+
685
+ # Option 2: Save to the fish completions directory
686
+ ${programName} completion fish > ~/.config/fish/completions/${programName}.fish
687
+
688
+ # The completion will be available immediately in new shell sessions.
689
+ # To use in the current session, run:
690
+ source ~/.config/fish/completions/${programName}.fish`
691
+ };
692
+ }
693
+
694
+ //#endregion
695
+ //#region src/completion/zsh.ts
696
+ /**
697
+ * Generate zsh completion script for a command
698
+ *
699
+ * Generates a dynamic script that calls the CLI's __complete command at runtime.
700
+ */
701
+ function generateZshCompletion(_command, options) {
702
+ const programName = options.programName;
703
+ return {
704
+ script: `#compdef ${programName}
705
+
706
+ # Zsh completion for ${programName}
707
+ # Generated by politty
708
+
709
+ _${programName}() {
710
+ 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)}")
721
+
722
+ # Parse output
723
+ for line in "\${output[@]}"; do
724
+ if [[ "$line" == :* ]]; then
725
+ directive="\${line:1}"
726
+ elif [[ "$line" == __command:* ]]; then
727
+ command_completion="\${line#__command:}"
728
+ elif [[ "$line" == __extensions:* ]]; then
729
+ file_extensions="\${line#__extensions:}"
730
+ 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
738
+ fi
739
+ done
740
+
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
748
+ done
749
+ fi
750
+
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
758
+ _describe 'completions' candidates
759
+ fi
760
+ }
761
+
762
+ compdef _${programName} ${programName}
763
+ `,
764
+ shell: "zsh",
765
+ installInstructions: `# To enable completions, add the following to your ~/.zshrc:
766
+
767
+ # Option 1: Source directly (add before compinit)
768
+ eval "$(${programName} completion zsh)"
769
+
770
+ # Option 2: Save to a file in your fpath
771
+ ${programName} completion zsh > ~/.zsh/completions/_${programName}
772
+
773
+ # Make sure your fpath includes the completions directory:
774
+ # fpath=(~/.zsh/completions $fpath)
775
+ # autoload -Uz compinit && compinit
776
+
777
+ # Then reload your shell or run:
778
+ source ~/.zshrc`
779
+ };
780
+ }
781
+
782
+ //#endregion
783
+ //#region src/completion/extractor.ts
784
+ /**
785
+ * Extract completion data from commands
786
+ */
787
+ /**
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
+ * Convert a resolved field to a completable option
822
+ */
823
+ function fieldToOption(field) {
824
+ return {
825
+ name: field.name,
826
+ cliName: field.cliName,
827
+ alias: field.alias,
828
+ description: field.description,
829
+ takesValue: field.type !== "boolean",
830
+ valueType: field.type,
831
+ required: field.required,
832
+ valueCompletion: resolveValueCompletion(field)
833
+ };
834
+ }
835
+ /**
836
+ * Extract options from a command's args schema
837
+ */
838
+ function extractOptions(command) {
839
+ if (!command.args) return [];
840
+ return extractFields(command.args).fields.filter((field) => !field.positional).map(fieldToOption);
841
+ }
842
+ /**
843
+ * Extract positional arguments from a command
844
+ */
845
+ function extractPositionals(command) {
846
+ if (!command.args) return [];
847
+ return extractFields(command.args).fields.filter((field) => field.positional);
848
+ }
849
+ /**
850
+ * Extract completable positional arguments from a command
851
+ */
852
+ function extractCompletablePositionals(command) {
853
+ if (!command.args) return [];
854
+ return extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
855
+ name: field.name,
856
+ cliName: field.cliName,
857
+ position: index,
858
+ description: field.description,
859
+ required: field.required,
860
+ valueCompletion: resolveValueCompletion(field)
861
+ }));
862
+ }
863
+ /**
864
+ * Extract a completable subcommand from a command
865
+ */
866
+ function extractSubcommand(name, command) {
867
+ const subcommands = [];
868
+ if (command.subCommands) for (const [subName, subCommand] of Object.entries(command.subCommands)) if (typeof subCommand === "function") subcommands.push({
869
+ name: subName,
870
+ description: "(lazy loaded)",
871
+ subcommands: [],
872
+ options: [],
873
+ positionals: []
874
+ });
875
+ else subcommands.push(extractSubcommand(subName, subCommand));
876
+ return {
877
+ name,
878
+ description: command.description,
879
+ subcommands,
880
+ options: extractOptions(command),
881
+ positionals: extractCompletablePositionals(command)
882
+ };
883
+ }
884
+ /**
885
+ * Extract completion data from a command tree
886
+ */
887
+ function extractCompletionData(command, programName) {
888
+ const rootSubcommand = extractSubcommand(programName, command);
889
+ return {
890
+ command: rootSubcommand,
891
+ programName,
892
+ globalOptions: rootSubcommand.options
893
+ };
894
+ }
895
+
896
+ //#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