politty 0.4.16 → 0.5.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 (52) hide show
  1. package/dist/{arg-registry-MVWOAcvw.d.cts → arg-registry--NRaNFJM.d.cts} +123 -3
  2. package/dist/arg-registry--NRaNFJM.d.cts.map +1 -0
  3. package/dist/{arg-registry-Cd6xnjHa.d.ts → arg-registry-6E0WHOh_.d.ts} +123 -3
  4. package/dist/arg-registry-6E0WHOh_.d.ts.map +1 -0
  5. package/dist/augment.d.cts +1 -1
  6. package/dist/augment.d.ts +1 -1
  7. package/dist/completion/index.cjs +1 -1
  8. package/dist/completion/index.d.cts +3 -2
  9. package/dist/completion/index.d.ts +3 -2
  10. package/dist/completion/index.js +1 -1
  11. package/dist/completion-BFOAOg95.cjs +4294 -0
  12. package/dist/completion-BFOAOg95.cjs.map +1 -0
  13. package/dist/completion-K5LGh1hO.js +4192 -0
  14. package/dist/completion-K5LGh1hO.js.map +1 -0
  15. package/dist/docs/index.cjs +9 -9
  16. package/dist/docs/index.cjs.map +1 -1
  17. package/dist/docs/index.d.cts +1 -1
  18. package/dist/docs/index.d.ts +1 -1
  19. package/dist/docs/index.js +2 -2
  20. package/dist/{index-DR9HLxIP.d.ts → index-Cg8qstsT.d.cts} +136 -15
  21. package/dist/index-Cg8qstsT.d.cts.map +1 -0
  22. package/dist/{index-CPebddth.d.cts → index-O3yn97Ed.d.ts} +136 -15
  23. package/dist/index-O3yn97Ed.d.ts.map +1 -0
  24. package/dist/index.cjs +10 -10
  25. package/dist/index.d.cts +3 -3
  26. package/dist/index.d.ts +3 -3
  27. package/dist/index.js +3 -3
  28. package/dist/prompt/clack/index.d.cts +1 -1
  29. package/dist/prompt/clack/index.d.ts +1 -1
  30. package/dist/prompt/index.d.cts +1 -1
  31. package/dist/prompt/index.d.ts +1 -1
  32. package/dist/prompt/inquirer/index.d.cts +1 -1
  33. package/dist/prompt/inquirer/index.d.ts +1 -1
  34. package/dist/{runner-BHeCMEa5.js → runner-BmSEiD9A.js} +12 -9
  35. package/dist/{runner-BHeCMEa5.js.map → runner-BmSEiD9A.js.map} +1 -1
  36. package/dist/{runner-BcyR6Z8r.cjs → runner-CRZ_7Y9i.cjs} +56 -53
  37. package/dist/{runner-BcyR6Z8r.cjs.map → runner-CRZ_7Y9i.cjs.map} +1 -1
  38. package/dist/{subcommand-router-XZBWe8HN.js → schema-extractor-C50R-1re.js} +135 -135
  39. package/dist/schema-extractor-C50R-1re.js.map +1 -0
  40. package/dist/{subcommand-router-DQy0KZU-.cjs → schema-extractor-SLPgBNgZ.cjs} +134 -134
  41. package/dist/schema-extractor-SLPgBNgZ.cjs.map +1 -0
  42. package/package.json +8 -8
  43. package/dist/arg-registry-Cd6xnjHa.d.ts.map +0 -1
  44. package/dist/arg-registry-MVWOAcvw.d.cts.map +0 -1
  45. package/dist/completion-B04iiki9.js +0 -2338
  46. package/dist/completion-B04iiki9.js.map +0 -1
  47. package/dist/completion-BlZxMSeU.cjs +0 -2440
  48. package/dist/completion-BlZxMSeU.cjs.map +0 -1
  49. package/dist/index-CPebddth.d.cts.map +0 -1
  50. package/dist/index-DR9HLxIP.d.ts.map +0 -1
  51. package/dist/subcommand-router-DQy0KZU-.cjs.map +0 -1
  52. package/dist/subcommand-router-XZBWe8HN.js.map +0 -1
@@ -1,2440 +0,0 @@
1
- const require_log_collector = require('./log-collector-Cd2_mv87.cjs');
2
- const require_subcommand_router = require('./subcommand-router-DQy0KZU-.cjs');
3
- let zod = require("zod");
4
- let node_fs = require("node:fs");
5
- let node_path = require("node:path");
6
- let node_child_process = require("node:child_process");
7
-
8
- //#region src/core/command.ts
9
- function defineCommand(config) {
10
- return {
11
- name: config.name,
12
- description: config.description,
13
- aliases: config.aliases,
14
- args: config.args,
15
- subCommands: config.subCommands,
16
- setup: config.setup,
17
- run: config.run,
18
- cleanup: config.cleanup,
19
- notes: config.notes,
20
- examples: config.examples
21
- };
22
- }
23
- /**
24
- * Create a typed defineCommand factory with pre-bound global args type.
25
- * This is the recommended pattern for type-safe global options.
26
- *
27
- * @example
28
- * ```ts
29
- * // global-args.ts
30
- * type GlobalArgsType = { verbose: boolean; config?: string };
31
- * export const defineAppCommand = createDefineCommand<GlobalArgsType>();
32
- *
33
- * // commands/build.ts
34
- * export const buildCommand = defineAppCommand({
35
- * name: "build",
36
- * args: z.object({ output: arg(z.string().default("dist")) }),
37
- * run: (args) => {
38
- * args.verbose; // typed via GlobalArgsType
39
- * args.output; // typed via local args
40
- * },
41
- * });
42
- * ```
43
- */
44
- function createDefineCommand() {
45
- return defineCommand;
46
- }
47
-
48
- //#endregion
49
- //#region src/completion/value-completion-resolver.ts
50
- /**
51
- * Resolve value completion from field metadata
52
- *
53
- * Priority:
54
- * 1. Explicit custom completion (choices or shellCommand)
55
- * 2. Explicit completion type (file, directory, none)
56
- * 3. Auto-detected enum values from schema
57
- */
58
- function resolveValueCompletion(field) {
59
- const meta = field.completion;
60
- if (meta?.custom) {
61
- if (meta.custom.choices && meta.custom.choices.length > 0) return {
62
- type: "choices",
63
- choices: meta.custom.choices
64
- };
65
- if (meta.custom.shellCommand) return {
66
- type: "command",
67
- shellCommand: meta.custom.shellCommand
68
- };
69
- }
70
- if (meta?.type) {
71
- if (meta.type === "file") {
72
- if (meta.matcher) return {
73
- type: "file",
74
- matcher: meta.matcher
75
- };
76
- if (meta.extensions) return {
77
- type: "file",
78
- extensions: meta.extensions
79
- };
80
- return { type: "file" };
81
- }
82
- if (meta.type === "directory") return { type: "directory" };
83
- if (meta.type === "none") return { type: "none" };
84
- }
85
- if (field.enumValues && field.enumValues.length > 0) return {
86
- type: "choices",
87
- choices: field.enumValues
88
- };
89
- }
90
-
91
- //#endregion
92
- //#region src/completion/extractor.ts
93
- /**
94
- * Extract completion data from commands
95
- */
96
- /**
97
- * Sanitize a name for use as a shell function/variable identifier.
98
- * Replaces any character that is not alphanumeric or underscore with underscore.
99
- *
100
- * Note: This is not injective -- distinct names may produce the same output
101
- * (e.g., "foo-bar" and "foo_bar" both become "foo_bar"). When used for nested
102
- * path encoding (`path.map(sanitize).join("_")`), cross-level collisions are
103
- * theoretically possible (e.g., "foo-bar:baz" vs "foo:bar-baz") but extremely
104
- * unlikely in real CLI designs. If collision-safety is needed, sanitize must be
105
- * replaced with an injective encoding.
106
- */
107
- function sanitize(name) {
108
- return name.replace(/[^a-zA-Z0-9_]/g, "_");
109
- }
110
- /**
111
- * Filter subcommands to only visible (non-internal) ones.
112
- * Internal subcommands start with "__" and are hidden from completion/help.
113
- */
114
- function getVisibleSubs(subs) {
115
- return subs.filter((s) => !s.name.startsWith("__"));
116
- }
117
- /**
118
- * Get all completable subcommand names including aliases.
119
- * Returns an array of { name, description } for all visible subcommands
120
- * and their aliases.
121
- */
122
- function getSubNamesWithAliases(subs) {
123
- const result = [];
124
- for (const sub of getVisibleSubs(subs)) {
125
- result.push({
126
- name: sub.name,
127
- description: sub.description
128
- });
129
- if (sub.aliases) for (const alias of sub.aliases) result.push({
130
- name: alias,
131
- description: sub.description
132
- });
133
- }
134
- return result;
135
- }
136
- /**
137
- * Convert a resolved field to a completable option
138
- */
139
- function fieldToOption(field) {
140
- return {
141
- name: field.name,
142
- cliName: field.cliName,
143
- alias: field.alias,
144
- negation: field.negationDisplay,
145
- negationDescription: field.negationDescription,
146
- description: field.description,
147
- takesValue: field.type !== "boolean",
148
- valueType: field.type,
149
- required: field.required,
150
- valueCompletion: resolveValueCompletion(field)
151
- };
152
- }
153
- /**
154
- * Extract options from a command's args schema
155
- */
156
- function extractOptions$1(command) {
157
- if (!command.args) return [];
158
- return require_subcommand_router.extractFields(command.args).fields.filter((field) => !field.positional).map(fieldToOption);
159
- }
160
- /**
161
- * Extract positional arguments from a command
162
- */
163
- function extractPositionals(command) {
164
- if (!command.args) return [];
165
- return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional);
166
- }
167
- /**
168
- * Extract completable positional arguments from a command
169
- */
170
- function extractCompletablePositionals(command) {
171
- if (!command.args) return [];
172
- return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
173
- name: field.name,
174
- cliName: field.cliName,
175
- position: index,
176
- description: field.description,
177
- required: field.required,
178
- variadic: field.type === "array",
179
- valueCompletion: resolveValueCompletion(field)
180
- }));
181
- }
182
- /**
183
- * Extract a completable subcommand from a command
184
- */
185
- function extractSubcommand(name, command) {
186
- const subcommands = [];
187
- if (command.subCommands) for (const [subName, subCommand] of Object.entries(command.subCommands)) {
188
- const resolved = require_subcommand_router.resolveSubCommandMeta(subCommand);
189
- if (resolved) subcommands.push(extractSubcommand(subName, resolved));
190
- else subcommands.push({
191
- name: subName,
192
- description: "(lazy loaded)",
193
- subcommands: [],
194
- options: [],
195
- positionals: []
196
- });
197
- }
198
- return {
199
- name,
200
- description: command.description,
201
- aliases: command.aliases,
202
- subcommands,
203
- options: extractOptions$1(command),
204
- positionals: extractCompletablePositionals(command)
205
- };
206
- }
207
- /** Join parent and child with a separator, omitting separator when parent is empty. */
208
- function joinPrefix(parent, child, sep) {
209
- return parent ? `${parent}${sep}${child}` : child;
210
- }
211
- /**
212
- * Collect opt-takes-value case entries for a subcommand tree.
213
- * Used by bash and zsh generators (identical case syntax: `path:--opt) return 0 ;;`).
214
- * parentPath is a colon-delimited path (e.g., "" for root, "workspace:user" for nested).
215
- */
216
- function optTakesValueEntries(sub, parentPath) {
217
- const lines = [];
218
- for (const opt of sub.options) if (opt.takesValue) {
219
- const patterns = [`${parentPath}:--${opt.cliName}`];
220
- if (opt.alias) for (const a of opt.alias) patterns.push(`${parentPath}:${a.length === 1 ? `-${a}` : `--${a}`}`);
221
- lines.push(` ${patterns.join("|")}) return 0 ;;`);
222
- }
223
- for (const child of getVisibleSubs(sub.subcommands)) {
224
- lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, child.name, ":")));
225
- if (child.aliases) for (const alias of child.aliases) lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, alias, ":")));
226
- }
227
- return lines;
228
- }
229
- /**
230
- * Recursively collect all subcommand route entries.
231
- * Returns entries used by all shell generators for both dispatch routing
232
- * and subcommand lookup (is_subcmd) tables.
233
- * Aliases are mapped to the same handler as the canonical name.
234
- */
235
- function collectRouteEntries(sub, parentPath = "", parentFunc = "") {
236
- const entries = [];
237
- for (const child of getVisibleSubs(sub.subcommands)) {
238
- const pathStr = joinPrefix(parentPath, child.name, ":");
239
- const funcSuffix = joinPrefix(parentFunc, sanitize(child.name), "_");
240
- entries.push(...collectRouteEntries(child, pathStr, funcSuffix));
241
- entries.push({
242
- pathStr,
243
- funcSuffix,
244
- lookupPattern: `${parentPath}:${child.name}`
245
- });
246
- if (child.aliases) for (const alias of child.aliases) {
247
- const aliasPathStr = joinPrefix(parentPath, alias, ":");
248
- entries.push(...collectRouteEntries(child, aliasPathStr, funcSuffix));
249
- entries.push({
250
- pathStr: aliasPathStr,
251
- funcSuffix,
252
- lookupPattern: `${parentPath}:${alias}`
253
- });
254
- }
255
- }
256
- return entries;
257
- }
258
- /**
259
- * Generate is_subcmd case/switch body lines (bash/zsh case syntax).
260
- * Returns lines for the case statement body only (caller wraps in function).
261
- */
262
- function isSubcmdCaseLines(routeEntries) {
263
- return routeEntries.map((r) => ` ${r.lookupPattern}) return 0 ;;`);
264
- }
265
- /**
266
- * Recursively merge global options into a subcommand and all its descendants.
267
- * Avoids duplicates by checking existing option names.
268
- */
269
- function propagateGlobalOptions(sub, globalOptions) {
270
- const existingNames = new Set(sub.options.map((o) => o.name));
271
- const newOpts = globalOptions.filter((o) => !existingNames.has(o.name));
272
- sub.options = [...sub.options, ...newOpts];
273
- for (const child of sub.subcommands) propagateGlobalOptions(child, globalOptions);
274
- }
275
- /**
276
- * Extract completion data from a command tree
277
- *
278
- * @param command - The root command
279
- * @param programName - Program name for completion scripts
280
- * @param globalArgsSchema - Optional global args schema. When provided, global options
281
- * are derived from this schema instead of the root command's options.
282
- */
283
- function extractCompletionData(command, programName, globalArgsSchema) {
284
- const rootSubcommand = extractSubcommand(programName, command);
285
- let globalOptions;
286
- if (globalArgsSchema) {
287
- globalOptions = require_subcommand_router.extractFields(globalArgsSchema).fields.filter((field) => !field.positional).map(fieldToOption);
288
- propagateGlobalOptions(rootSubcommand, globalOptions);
289
- } else globalOptions = rootSubcommand.options;
290
- return {
291
- command: rootSubcommand,
292
- programName,
293
- globalOptions
294
- };
295
- }
296
-
297
- //#endregion
298
- //#region src/completion/header.ts
299
- /**
300
- * Static-script header utilities.
301
- *
302
- * Every completion script generated by politty starts with a small
303
- * machine-readable header. The rc loader and the runMain background
304
- * refresh path use the `# politty-bin-sig:` line to detect when the
305
- * cached script is stale relative to the binary on disk.
306
- */
307
- /** Schema version of the header itself. Bump when the header layout changes. */
308
- const COMPLETION_VERSION = 1;
309
- /**
310
- * Read the binary's mtime in whole seconds (matches POSIX `stat -c %Y` /
311
- * BSD `stat -f %m`). Returns `"0"` on failure so the header is always
312
- * well-formed.
313
- */
314
- function computeBinSig(binPath) {
315
- try {
316
- return Math.floor((0, node_fs.statSync)(binPath).mtimeMs / 1e3).toString();
317
- } catch {
318
- return "0";
319
- }
320
- }
321
- /**
322
- * Walk `$PATH` looking for an executable named `programName`. Returns
323
- * the first match's full path, or `null` when not found. We mirror the
324
- * shell's `command -v <prog>` here so the sig embedded in the header
325
- * (computed by Node) lines up with what the rc loader stat-checks at
326
- * runtime — including pnpm/npm bin shims that wrap the real entrypoint.
327
- * Without this alignment, shimmed installs would never match the
328
- * embedded sig and the cache would regenerate on every shell startup.
329
- */
330
- function findOnPath(programName) {
331
- if (!programName || /[/\\\0]/.test(programName)) return null;
332
- const path = process.env.PATH ?? "";
333
- for (const dir of path.split(":")) {
334
- if (!dir) continue;
335
- const candidate = (0, node_path.join)(dir, programName);
336
- try {
337
- if ((0, node_fs.statSync)(candidate).isFile()) return candidate;
338
- } catch {}
339
- }
340
- return null;
341
- }
342
- /**
343
- * Resolve the binary path used for sig computation and stat checks.
344
- *
345
- * Order: explicit override → `$PATH` lookup of `programName` → `process.argv[1]`.
346
- * The `$PATH` lookup keeps Node-side and shell-side stats pointed at the
347
- * same shim file when the CLI is invoked through a package-manager bin shim.
348
- */
349
- function resolveBinPath(programName, override) {
350
- if (override) return override;
351
- return findOnPath(programName) ?? process.argv[1] ?? "";
352
- }
353
- /**
354
- * Build the header lines (no trailing blank line). Returned without a
355
- * leading `#!` so each generator can prepend its own shebang/compdef
356
- * marker.
357
- */
358
- function buildHeaderLines(opts) {
359
- const sig = computeBinSig(resolveBinPath(opts.programName, opts.binPath));
360
- const lines = [
361
- `# politty-completion-version: ${1}`,
362
- `# politty-bin-sig: ${sig}`,
363
- `# program: ${opts.programName}`
364
- ];
365
- if (opts.programVersion) lines.push(`# program-version: ${opts.programVersion}`);
366
- lines.push(`# shell: ${opts.shell}`);
367
- return lines;
368
- }
369
-
370
- //#endregion
371
- //#region src/completion/bash.ts
372
- /** Escape a string for use inside bash double-quotes */
373
- function escapeBashDQ(s) {
374
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
375
- }
376
- /**
377
- * Generate bash value completion code for a ValueCompletion spec.
378
- * Returns an array of bash lines.
379
- */
380
- function bashValueLines(vc, inline) {
381
- if (!vc) return [];
382
- switch (vc.type) {
383
- case "choices": {
384
- const items = vc.choices.map((c) => `"${escapeBashDQ(c)}"`).join(" ");
385
- if (inline) return [
386
- `local -a _choices=(${items})`,
387
- `COMPREPLY=()`,
388
- `local _c; for _c in "\${_choices[@]}"; do [[ "$_c" == "$_cur"* ]] && COMPREPLY+=("\${_inline_prefix}\${_c}"); done`,
389
- `compopt -o nospace`,
390
- `compopt +o default 2>/dev/null`
391
- ];
392
- return [
393
- `local -a _choices=(${items})`,
394
- `COMPREPLY=()`,
395
- `local _c; for _c in "\${_choices[@]}"; do [[ "$_c" == "$_cur"* ]] && COMPREPLY+=("$_c"); done`,
396
- `compopt +o default 2>/dev/null`
397
- ];
398
- }
399
- case "file":
400
- if (vc.matcher?.length) return bashFileFilter(vc.matcher.map((p) => `[[ "\${_f##*/}" == ${p} ]]`).join(" || "), inline);
401
- if (vc.extensions?.length) return bashFileFilter(vc.extensions.map((ext) => `[[ "$_f" == *".${ext}" ]]`).join(" || "), inline);
402
- if (inline) return [
403
- `local -a _entries=($(compgen -f -- "$_cur"))`,
404
- `COMPREPLY=("\${_entries[@]/#/$_inline_prefix}")`,
405
- `compopt -o filenames`
406
- ];
407
- return [`COMPREPLY=($(compgen -f -- "$_cur"))`, `compopt -o filenames`];
408
- case "directory":
409
- if (inline) return [
410
- `local -a _dirs=($(compgen -d -- "$_cur"))`,
411
- `COMPREPLY=("\${_dirs[@]/#/$_inline_prefix}")`,
412
- `compopt -o filenames`
413
- ];
414
- return [`COMPREPLY=($(compgen -d -- "$_cur"))`, `compopt -o filenames`];
415
- case "command": {
416
- const cmd = vc.shellCommand;
417
- if (inline) return [`COMPREPLY=($(compgen -P "$_inline_prefix" -W "$(${cmd})" -- "$_cur"))`];
418
- return [`COMPREPLY=($(compgen -W "$(${cmd})" -- "$_cur"))`];
419
- }
420
- case "none": return [`compopt +o default 2>/dev/null`];
421
- }
422
- }
423
- function bashFileFilter(checks, inline) {
424
- const prefix = inline ? `"\${_inline_prefix}$_f"` : `"$_f"`;
425
- return [
426
- `local -a _all_entries=($(compgen -f -- "$_cur"))`,
427
- `for _f in "\${_all_entries[@]}"; do`,
428
- ` if [[ -d "$_f" ]]; then`,
429
- ` COMPREPLY+=(${prefix})`,
430
- ` elif ${checks}; then`,
431
- ` COMPREPLY+=(${prefix})`,
432
- ` fi`,
433
- `done`,
434
- `compopt -o filenames`,
435
- `compopt +o default 2>/dev/null`
436
- ];
437
- }
438
- /** Collect value-taking option patterns for case matching */
439
- function optionValueCases$2(options, inline) {
440
- const lines = [];
441
- for (const opt of options) {
442
- if (!opt.takesValue || !opt.valueCompletion) continue;
443
- const valLines = bashValueLines(opt.valueCompletion, inline);
444
- if (valLines.length === 0) continue;
445
- const patterns = [`--${opt.cliName}`];
446
- if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `-${a}` : `--${a}`);
447
- const patternStr = patterns.join("|");
448
- lines.push(` ${patternStr})`);
449
- for (const vl of valLines) lines.push(` ${vl}`);
450
- lines.push(` return ;;`);
451
- }
452
- return lines;
453
- }
454
- /** Generate positional completion block */
455
- function positionalBlock$2(positionals) {
456
- if (positionals.length === 0) return [];
457
- const lines = [];
458
- lines.push(` case "$_pos_count" in`);
459
- for (const pos of positionals) {
460
- if (pos.variadic) lines.push(` ${pos.position}|*)`);
461
- else lines.push(` ${pos.position})`);
462
- for (const vl of bashValueLines(pos.valueCompletion, false)) lines.push(` ${vl}`);
463
- lines.push(` ;;`);
464
- }
465
- lines.push(` esac`);
466
- return lines;
467
- }
468
- /** Generate prev/inline value completion blocks for options */
469
- function valueCompletionBlocks(options) {
470
- if (!options.some((o) => o.takesValue && o.valueCompletion)) return [];
471
- const lines = [];
472
- const prevCases = optionValueCases$2(options, false);
473
- if (prevCases.length > 0) {
474
- lines.push(` if [[ -z "$_inline_prefix" ]]; then`);
475
- lines.push(` case "$_prev" in`);
476
- lines.push(...prevCases);
477
- lines.push(` esac`);
478
- lines.push(` fi`);
479
- }
480
- const inlineCases = optionValueCases$2(options, true);
481
- if (inlineCases.length > 0) {
482
- lines.push(` if [[ -n "$_inline_prefix" ]]; then`);
483
- lines.push(` case "\${_inline_prefix%=}" in`);
484
- lines.push(...inlineCases);
485
- lines.push(` esac`);
486
- lines.push(` fi`);
487
- }
488
- return lines;
489
- }
490
- /** Generate available-options list lines */
491
- function availableOptionLines$2(options, fn) {
492
- const lines = [];
493
- for (const opt of options) if (opt.valueType === "array") lines.push(` _avail+=(--${opt.cliName})`);
494
- else {
495
- const patterns = [`"--${opt.cliName}"`];
496
- if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
497
- if (opt.negation) patterns.push(`"--${opt.negation}"`);
498
- lines.push(` __${fn}_not_used ${patterns.join(" ")} && _avail+=(--${opt.cliName})`);
499
- if (opt.negation) lines.push(` __${fn}_not_used ${patterns.join(" ")} && _avail+=(--${opt.negation})`);
500
- }
501
- lines.push(` __${fn}_not_used "--help" && _avail+=(--help)`);
502
- return lines;
503
- }
504
- /**
505
- * Generate a per-subcommand completion function.
506
- * Recursively generates functions for nested subcommands.
507
- */
508
- function generateSubHandler$2(sub, fn, path) {
509
- const fullPath = [...path, sub.name];
510
- const funcName = `__${fn}_complete_${fullPath.map(sanitize).join("_")}`;
511
- const visibleSubs = getVisibleSubs(sub.subcommands);
512
- const lines = [];
513
- for (const child of visibleSubs) lines.push(...generateSubHandler$2(child, fn, fullPath));
514
- lines.push(`${funcName}() {`);
515
- lines.push(...valueCompletionBlocks(sub.options));
516
- const fullPathStr = fullPath.join(":");
517
- lines.push(` if [[ -z "$_inline_prefix" ]] && __${fn}_opt_takes_value "${fullPathStr}" "$_prev"; then return; fi`);
518
- lines.push(` if [[ -n "$_inline_prefix" ]] && __${fn}_opt_takes_value "${fullPathStr}" "\${_inline_prefix%=}"; then return; fi`);
519
- if (sub.positionals.length > 0) {
520
- lines.push(` if (( _after_dd )); then`);
521
- lines.push(...positionalBlock$2(sub.positionals).map((l) => ` ${l}`));
522
- lines.push(` return`);
523
- lines.push(` fi`);
524
- } else lines.push(` if (( _after_dd )); then return; fi`);
525
- lines.push(` if [[ "$_cur" == -* ]]; then`);
526
- lines.push(` local -a _avail=()`);
527
- lines.push(...availableOptionLines$2(sub.options, fn));
528
- lines.push(` COMPREPLY=($(compgen -W "\${_avail[*]}" -- "$_cur"))`);
529
- lines.push(` compopt +o default 2>/dev/null`);
530
- lines.push(` return`);
531
- lines.push(` fi`);
532
- if (visibleSubs.length > 0) {
533
- const subNames = getSubNamesWithAliases(sub.subcommands).map((s) => s.name).join(" ");
534
- lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
535
- lines.push(` compopt +o default 2>/dev/null`);
536
- } else if (sub.positionals.length > 0) lines.push(...positionalBlock$2(sub.positionals));
537
- lines.push(`}`);
538
- lines.push(``);
539
- return lines;
540
- }
541
- function generateBashCompletion(command, options) {
542
- const { programName } = options;
543
- const data = extractCompletionData(command, programName, options.globalArgsSchema);
544
- const fn = sanitize(programName);
545
- const root = data.command;
546
- const visibleSubs = getVisibleSubs(root.subcommands);
547
- const lines = [];
548
- lines.push(...buildHeaderLines({
549
- programName,
550
- shell: "bash",
551
- binPath: options.binPath,
552
- programVersion: options.programVersion
553
- }));
554
- lines.push(`# Generated by politty`);
555
- lines.push(``);
556
- lines.push(`__${fn}_not_used() {`);
557
- lines.push(` for _u in "\${_used_opts[@]}"; do`);
558
- lines.push(` for _chk in "$@"; do`);
559
- lines.push(` [[ "$_u" == "$_chk" ]] && return 1`);
560
- lines.push(` done`);
561
- lines.push(` done`);
562
- lines.push(` return 0`);
563
- lines.push(`}`);
564
- lines.push(``);
565
- lines.push(`__${fn}_opt_takes_value() {`);
566
- lines.push(` case "$1:$2" in`);
567
- lines.push(...optTakesValueEntries(root, ""));
568
- lines.push(` esac`);
569
- lines.push(` return 1`);
570
- lines.push(`}`);
571
- lines.push(``);
572
- const routeEntries = collectRouteEntries(root);
573
- if (routeEntries.length > 0) {
574
- lines.push(`__${fn}_is_subcmd() {`);
575
- lines.push(` case "$1:$2" in`);
576
- lines.push(...isSubcmdCaseLines(routeEntries));
577
- lines.push(` esac`);
578
- lines.push(` return 1`);
579
- lines.push(`}`);
580
- lines.push(``);
581
- }
582
- for (const sub of visibleSubs) lines.push(...generateSubHandler$2(sub, fn, []));
583
- lines.push(`__${fn}_complete_root() {`);
584
- lines.push(...valueCompletionBlocks(root.options));
585
- lines.push(` if [[ -z "$_inline_prefix" ]] && __${fn}_opt_takes_value "" "$_prev"; then return; fi`);
586
- lines.push(` if [[ -n "$_inline_prefix" ]] && __${fn}_opt_takes_value "" "\${_inline_prefix%=}"; then return; fi`);
587
- if (root.positionals.length > 0) {
588
- lines.push(` if (( _after_dd )); then`);
589
- lines.push(...positionalBlock$2(root.positionals).map((l) => ` ${l}`));
590
- lines.push(` return`);
591
- lines.push(` fi`);
592
- } else lines.push(` if (( _after_dd )); then return; fi`);
593
- lines.push(` if [[ "$_cur" == -* ]]; then`);
594
- lines.push(` local -a _avail=()`);
595
- lines.push(...availableOptionLines$2(root.options, fn));
596
- lines.push(` COMPREPLY=($(compgen -W "\${_avail[*]}" -- "$_cur"))`);
597
- lines.push(` compopt +o default 2>/dev/null`);
598
- if (visibleSubs.length > 0) {
599
- lines.push(` else`);
600
- const subNames = getSubNamesWithAliases(root.subcommands).map((s) => s.name).join(" ");
601
- lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
602
- lines.push(` compopt +o default 2>/dev/null`);
603
- } else if (root.positionals.length > 0) {
604
- lines.push(` else`);
605
- lines.push(...positionalBlock$2(root.positionals).map((l) => ` ${l}`));
606
- }
607
- lines.push(` fi`);
608
- lines.push(`}`);
609
- lines.push(``);
610
- const subRouting = routeEntries.map((r) => ` ${r.pathStr}) __${fn}_complete_${r.funcSuffix} ;;`).join("\n");
611
- lines.push(`_${fn}_completions() {`);
612
- lines.push(` COMPREPLY=()`);
613
- lines.push(``);
614
- lines.push(` # Rejoin words split by '=' in COMP_WORDBREAKS`);
615
- lines.push(` local -a _words=()`);
616
- lines.push(` local _i=1`);
617
- lines.push(` while (( _i <= COMP_CWORD )); do`);
618
- lines.push(` if [[ "\${COMP_WORDS[_i]}" == "=" && \${#_words[@]} -gt 0 ]]; then`);
619
- lines.push(` _words[\${#_words[@]}-1]+="=\${COMP_WORDS[_i+1]:-}"`);
620
- lines.push(` (( _i += 2 ))`);
621
- lines.push(` else`);
622
- lines.push(` _words+=("\${COMP_WORDS[_i]}")`);
623
- lines.push(` (( _i++ ))`);
624
- lines.push(` fi`);
625
- lines.push(` done`);
626
- lines.push(``);
627
- lines.push(` local _cur=""`);
628
- lines.push(` (( \${#_words[@]} > 0 )) && _cur="\${_words[\${#_words[@]}-1]}"`);
629
- lines.push(``);
630
- lines.push(` local _inline_prefix=""`);
631
- lines.push(` if [[ "$_cur" == --*=* ]]; then`);
632
- lines.push(` _inline_prefix="\${_cur%%=*}="`);
633
- lines.push(` _cur="\${_cur#*=}"`);
634
- lines.push(` fi`);
635
- lines.push(``);
636
- lines.push(` local _prev=""`);
637
- lines.push(` (( \${#_words[@]} > 1 )) && _prev="\${_words[\${#_words[@]}-2]}"`);
638
- lines.push(``);
639
- lines.push(` local _subcmd="" _after_dd=0 _pos_count=0 _skip_next=0`);
640
- lines.push(` local -a _used_opts=()`);
641
- lines.push(``);
642
- lines.push(` local _j=0`);
643
- lines.push(` while (( _j < \${#_words[@]} - 1 )); do`);
644
- lines.push(` local _w="\${_words[_j]}"`);
645
- lines.push(` if (( _skip_next )); then _skip_next=0; (( _j++ )); continue; fi`);
646
- lines.push(` if [[ "$_w" == "--" ]]; then _after_dd=1; (( _j++ )); continue; fi`);
647
- lines.push(` if (( _after_dd )); then (( _pos_count++ )); (( _j++ )); continue; fi`);
648
- lines.push(` if [[ "$_w" == --*=* ]]; then _used_opts+=("\${_w%%=*}"); (( _j++ )); continue; fi`);
649
- lines.push(` if [[ "$_w" == -* ]]; then`);
650
- lines.push(` _used_opts+=("$_w")`);
651
- lines.push(` __${fn}_opt_takes_value "$_subcmd" "$_w" && _skip_next=1`);
652
- lines.push(` (( _j++ )); continue`);
653
- lines.push(` fi`);
654
- if (routeEntries.length > 0) lines.push(` if __${fn}_is_subcmd "$_subcmd" "$_w"; then _subcmd="\${_subcmd:+\${_subcmd}:}$_w"; _used_opts=(); _pos_count=0; else (( _pos_count++ )); fi`);
655
- else lines.push(` (( _pos_count++ ))`);
656
- lines.push(` (( _j++ ))`);
657
- lines.push(` done`);
658
- lines.push(``);
659
- lines.push(` case "$_subcmd" in`);
660
- lines.push(subRouting);
661
- lines.push(` *) __${fn}_complete_root ;;`);
662
- lines.push(` esac`);
663
- lines.push(`}`);
664
- lines.push(``);
665
- lines.push(`complete -o default -F _${fn}_completions ${programName}`);
666
- lines.push(``);
667
- return {
668
- script: lines.join("\n"),
669
- shell: "bash",
670
- installInstructions: `# To enable completions, add the following to your ~/.bashrc:
671
-
672
- # Option 1: Source directly
673
- eval "$(${programName} completion bash)"
674
-
675
- # Option 2: Save to a file
676
- ${programName} completion bash > ~/.local/share/bash-completion/completions/${programName}
677
-
678
- # Then reload your shell or run:
679
- source ~/.bashrc`
680
- };
681
- }
682
-
683
- //#endregion
684
- //#region src/completion/dynamic/candidate-generator.ts
685
- /**
686
- * Generate completion candidates based on context
687
- */
688
- /**
689
- * Completion directive flags (bitwise)
690
- */
691
- const CompletionDirective = {
692
- /** Default completion behavior */
693
- Default: 0,
694
- /** Don't add space after completion */
695
- NoSpace: 1,
696
- /** Don't offer file completion (even if no other completions) */
697
- NoFileCompletion: 2,
698
- /** Filter completions using current word as prefix */
699
- FilterPrefix: 4,
700
- /** Keep the order of completions */
701
- KeepOrder: 8,
702
- /** Trigger file completion */
703
- FileCompletion: 16,
704
- /** Trigger directory completion */
705
- DirectoryCompletion: 32,
706
- /** Error occurred during completion */
707
- Error: 64
708
- };
709
- /**
710
- * Generate completion candidates based on context
711
- */
712
- function generateCandidates(context) {
713
- const candidates = [];
714
- let directive = CompletionDirective.Default;
715
- switch (context.completionType) {
716
- case "subcommand": return generateSubcommandCandidates(context);
717
- case "option-name": return generateOptionNameCandidates(context);
718
- case "option-value": return generateOptionValueCandidates(context);
719
- case "positional": return generatePositionalCandidates(context);
720
- default: return {
721
- candidates,
722
- directive
723
- };
724
- }
725
- }
726
- /**
727
- * Execute a shell command and return results as candidates
728
- */
729
- function executeShellCommand(command) {
730
- try {
731
- return (0, node_child_process.execSync)(command, {
732
- encoding: "utf-8",
733
- timeout: 5e3
734
- }).split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => ({
735
- value: line,
736
- type: "value"
737
- }));
738
- } catch {
739
- return [];
740
- }
741
- }
742
- /**
743
- * Resolve value completion, executing shell commands and file lookups in JS
744
- */
745
- function resolveValueCandidates(vc, candidates, _currentWord, description) {
746
- let directive = CompletionDirective.FilterPrefix;
747
- let fileExtensions;
748
- let fileMatchers;
749
- switch (vc.type) {
750
- case "choices":
751
- if (vc.choices) for (const choice of vc.choices) candidates.push({
752
- value: choice,
753
- description,
754
- type: "value"
755
- });
756
- directive |= CompletionDirective.NoFileCompletion;
757
- break;
758
- case "file":
759
- if (vc.matcher && vc.matcher.length > 0) {
760
- fileMatchers = vc.matcher.filter((m) => m.trim().length > 0);
761
- if (fileMatchers.length === 0) {
762
- fileMatchers = void 0;
763
- directive |= CompletionDirective.FileCompletion;
764
- }
765
- } else if (vc.extensions && vc.extensions.length > 0) {
766
- fileExtensions = Array.from(new Set(vc.extensions.map((ext) => ext.trim().replace(/^\./, "")).filter((ext) => ext.length > 0)));
767
- if (fileExtensions.length === 0) {
768
- fileExtensions = void 0;
769
- directive |= CompletionDirective.FileCompletion;
770
- }
771
- } else directive |= CompletionDirective.FileCompletion;
772
- break;
773
- case "directory":
774
- directive |= CompletionDirective.DirectoryCompletion;
775
- break;
776
- case "command":
777
- if (vc.shellCommand) candidates.push(...executeShellCommand(vc.shellCommand));
778
- directive |= CompletionDirective.NoFileCompletion;
779
- break;
780
- case "none":
781
- directive |= CompletionDirective.NoFileCompletion;
782
- break;
783
- }
784
- return {
785
- directive,
786
- fileExtensions,
787
- fileMatchers
788
- };
789
- }
790
- /**
791
- * Generate subcommand candidates
792
- */
793
- function generateSubcommandCandidates(context) {
794
- const candidates = [];
795
- let directive = CompletionDirective.FilterPrefix;
796
- for (const name of context.subcommands) {
797
- let description;
798
- const sub = context.currentCommand.subCommands?.[name];
799
- if (sub) description = require_subcommand_router.resolveSubCommandMeta(sub)?.description;
800
- else {
801
- const canonical = require_subcommand_router.resolveSubCommandAlias(context.currentCommand, name);
802
- if (canonical) {
803
- const resolved = context.currentCommand.subCommands?.[canonical];
804
- if (resolved) description = require_subcommand_router.resolveSubCommandMeta(resolved)?.description;
805
- }
806
- }
807
- candidates.push({
808
- value: name,
809
- description,
810
- type: "subcommand"
811
- });
812
- }
813
- if (candidates.length === 0 || context.currentWord.startsWith("-")) {
814
- const optionResult = generateOptionNameCandidates(context);
815
- candidates.push(...optionResult.candidates);
816
- }
817
- return {
818
- candidates,
819
- directive
820
- };
821
- }
822
- /**
823
- * Generate option name candidates
824
- */
825
- function generateOptionNameCandidates(context) {
826
- const candidates = [];
827
- const directive = CompletionDirective.FilterPrefix;
828
- const availableOptions = context.options.filter((opt) => {
829
- if (opt.valueType === "array") return true;
830
- if (context.usedOptions.has(opt.cliName)) return false;
831
- if (opt.alias && opt.alias.some((a) => context.usedOptions.has(a))) return false;
832
- if (opt.negation && context.usedOptions.has(opt.negation)) return false;
833
- return true;
834
- });
835
- for (const opt of availableOptions) {
836
- candidates.push({
837
- value: `--${opt.cliName}`,
838
- description: opt.description,
839
- type: "option"
840
- });
841
- if (opt.negation) candidates.push({
842
- value: `--${opt.negation}`,
843
- description: opt.negationDescription ?? opt.description,
844
- type: "option"
845
- });
846
- }
847
- if (!context.usedOptions.has("help")) candidates.push({
848
- value: "--help",
849
- description: "Show help information",
850
- type: "option"
851
- });
852
- return {
853
- candidates,
854
- directive
855
- };
856
- }
857
- /**
858
- * Generate option value candidates
859
- */
860
- function generateOptionValueCandidates(context) {
861
- const candidates = [];
862
- if (!context.targetOption) return {
863
- candidates,
864
- directive: CompletionDirective.FilterPrefix
865
- };
866
- const vc = context.targetOption.valueCompletion;
867
- if (!vc) return {
868
- candidates,
869
- directive: CompletionDirective.FilterPrefix
870
- };
871
- return {
872
- candidates,
873
- ...resolveValueCandidates(vc, candidates, context.currentWord)
874
- };
875
- }
876
- /**
877
- * Generate positional argument candidates
878
- */
879
- function generatePositionalCandidates(context) {
880
- const candidates = [];
881
- const positionalIndex = context.positionalIndex ?? 0;
882
- const positional = context.positionals[positionalIndex] ?? (context.positionals.at(-1)?.variadic ? context.positionals.at(-1) : void 0);
883
- if (!positional) return {
884
- candidates,
885
- directive: CompletionDirective.FilterPrefix
886
- };
887
- const vc = positional.valueCompletion;
888
- if (!vc) return {
889
- candidates,
890
- directive: CompletionDirective.FilterPrefix
891
- };
892
- return {
893
- candidates,
894
- ...resolveValueCandidates(vc, candidates, context.currentWord, positional.description)
895
- };
896
- }
897
-
898
- //#endregion
899
- //#region src/completion/dynamic/context-parser.ts
900
- /**
901
- * Parse completion context from partial command line
902
- */
903
- /**
904
- * Extract options from a command
905
- */
906
- function extractOptions(command) {
907
- if (!command.args) return [];
908
- return require_subcommand_router.extractFields(command.args).fields.filter((field) => !field.positional).map((field) => ({
909
- name: field.name,
910
- cliName: field.cliName,
911
- alias: field.alias,
912
- negation: field.negationDisplay,
913
- negationDescription: field.negationDescription,
914
- description: field.description,
915
- takesValue: field.type !== "boolean",
916
- valueType: field.type,
917
- required: field.required,
918
- valueCompletion: resolveValueCompletion(field)
919
- }));
920
- }
921
- /**
922
- * Extract positionals from a command
923
- */
924
- function extractPositionalsForContext(command) {
925
- if (!command.args) return [];
926
- return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
927
- name: field.name,
928
- cliName: field.cliName,
929
- position: index,
930
- description: field.description,
931
- required: field.required,
932
- variadic: field.type === "array",
933
- valueCompletion: resolveValueCompletion(field)
934
- }));
935
- }
936
- /**
937
- * Get subcommand names from a command (including aliases)
938
- */
939
- function getSubcommandNames(command) {
940
- if (!command.subCommands) return [];
941
- const names = [];
942
- for (const [name, subCmd] of Object.entries(command.subCommands)) {
943
- if (name.startsWith("__")) continue;
944
- names.push(name);
945
- const meta = require_subcommand_router.resolveSubCommandMeta(subCmd);
946
- if (meta?.aliases) names.push(...meta.aliases);
947
- }
948
- return names;
949
- }
950
- /**
951
- * Resolve subcommand by name (including alias lookup)
952
- */
953
- function resolveSubcommand(command, name) {
954
- if (!command.subCommands) return null;
955
- const sub = command.subCommands[name];
956
- if (sub) return require_subcommand_router.resolveSubCommandMeta(sub);
957
- const canonical = require_subcommand_router.resolveSubCommandAlias(command, name);
958
- if (canonical) return require_subcommand_router.resolveSubCommandMeta(command.subCommands[canonical]);
959
- return null;
960
- }
961
- /**
962
- * Check if a word is an option (starts with - or --)
963
- */
964
- function isOption(word) {
965
- return word.startsWith("-");
966
- }
967
- /**
968
- * Parse option name from word (e.g., "--foo=bar" -> "foo", "-v" -> "v")
969
- */
970
- function parseOptionName(word) {
971
- if (word.startsWith("--")) {
972
- const withoutPrefix = word.slice(2);
973
- const eqIndex = withoutPrefix.indexOf("=");
974
- return eqIndex >= 0 ? withoutPrefix.slice(0, eqIndex) : withoutPrefix;
975
- }
976
- if (word.startsWith("-")) return word.slice(1, 2);
977
- return word;
978
- }
979
- /**
980
- * Check if option has inline value (e.g., "--foo=bar")
981
- */
982
- function hasInlineValue(word) {
983
- return word.includes("=");
984
- }
985
- /**
986
- * Find option by name or alias
987
- */
988
- function findOption(options, nameOrAlias) {
989
- return options.find((opt) => {
990
- if (opt.cliName === nameOrAlias) return true;
991
- if (opt.alias?.includes(nameOrAlias)) return true;
992
- if (nameOrAlias.length > 1) {
993
- if (opt.cliName.includes("-") && require_subcommand_router.toCamelCase(opt.cliName) === nameOrAlias) return true;
994
- if (opt.alias?.some((a) => a.includes("-") && require_subcommand_router.toCamelCase(a) === nameOrAlias)) return true;
995
- if (opt.negation) {
996
- if (opt.negation === nameOrAlias) return true;
997
- if (opt.negation.includes("-") && require_subcommand_router.toCamelCase(opt.negation) === nameOrAlias) return true;
998
- }
999
- }
1000
- return false;
1001
- });
1002
- }
1003
- /**
1004
- * Parse completion context from command line arguments
1005
- *
1006
- * @param argv - Arguments after the program name (e.g., ["build", "--fo"])
1007
- * @param rootCommand - The root command
1008
- * @returns Completion context
1009
- */
1010
- function parseCompletionContext(argv, rootCommand) {
1011
- let currentCommand = rootCommand;
1012
- const subcommandPath = [];
1013
- const usedOptions = /* @__PURE__ */ new Set();
1014
- let positionalCount = 0;
1015
- let i = 0;
1016
- let options = extractOptions(currentCommand);
1017
- let afterDoubleDash = false;
1018
- while (i < argv.length - 1) {
1019
- const word = argv[i];
1020
- if (!afterDoubleDash && word === "--") {
1021
- afterDoubleDash = true;
1022
- i++;
1023
- continue;
1024
- }
1025
- if (!afterDoubleDash && isOption(word)) {
1026
- const optName = parseOptionName(word);
1027
- const opt = findOption(options, optName);
1028
- if (opt) {
1029
- usedOptions.add(opt.cliName);
1030
- if (opt.alias) for (const a of opt.alias) usedOptions.add(a);
1031
- if (opt.negation) usedOptions.add(opt.negation);
1032
- if (opt.takesValue && !hasInlineValue(word)) i++;
1033
- }
1034
- i++;
1035
- continue;
1036
- }
1037
- const subcommand = afterDoubleDash ? null : resolveSubcommand(currentCommand, word);
1038
- if (subcommand) {
1039
- subcommandPath.push(word);
1040
- currentCommand = subcommand;
1041
- options = extractOptions(currentCommand);
1042
- usedOptions.clear();
1043
- positionalCount = 0;
1044
- i++;
1045
- continue;
1046
- }
1047
- positionalCount++;
1048
- i++;
1049
- }
1050
- const currentWord = argv[argv.length - 1] ?? "";
1051
- const previousWord = argv[argv.length - 2] ?? "";
1052
- const positionals = extractPositionalsForContext(currentCommand);
1053
- const subcommands = getSubcommandNames(currentCommand);
1054
- let completionType;
1055
- let targetOption;
1056
- let positionalIndex;
1057
- if (!afterDoubleDash && previousWord && isOption(previousWord) && !hasInlineValue(previousWord)) {
1058
- const optName = parseOptionName(previousWord);
1059
- const opt = findOption(options, optName);
1060
- if (opt && opt.takesValue) {
1061
- completionType = "option-value";
1062
- targetOption = opt;
1063
- } else if (currentWord.startsWith("-")) completionType = "option-name";
1064
- else {
1065
- completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount);
1066
- if (completionType === "positional") positionalIndex = positionalCount;
1067
- }
1068
- } else if (!afterDoubleDash && currentWord.startsWith("--") && hasInlineValue(currentWord)) {
1069
- const optName = parseOptionName(currentWord);
1070
- const opt = findOption(options, optName);
1071
- if (opt && opt.takesValue) {
1072
- completionType = "option-value";
1073
- targetOption = opt;
1074
- } else completionType = "option-name";
1075
- } else if (!afterDoubleDash && currentWord.startsWith("-")) completionType = "option-name";
1076
- else {
1077
- completionType = determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount, afterDoubleDash);
1078
- if (completionType === "positional") positionalIndex = positionalCount;
1079
- }
1080
- return {
1081
- subcommandPath,
1082
- currentCommand,
1083
- currentWord,
1084
- previousWord,
1085
- completionType,
1086
- targetOption,
1087
- positionalIndex,
1088
- options,
1089
- subcommands,
1090
- positionals,
1091
- usedOptions,
1092
- providedPositionalCount: positionalCount
1093
- };
1094
- }
1095
- /**
1096
- * Determine default completion type when not completing an option
1097
- */
1098
- function determineDefaultCompletionType(currentWord, subcommands, positionals, positionalCount, afterDoubleDash) {
1099
- if (afterDoubleDash) return "positional";
1100
- if (subcommands.length > 0) {
1101
- if (subcommands.filter((s) => s.startsWith(currentWord)).length > 0 || currentWord === "") return "subcommand";
1102
- }
1103
- if (positionalCount < positionals.length) return "positional";
1104
- if (positionals.length > 0 && positionals[positionals.length - 1].variadic) return "positional";
1105
- return "subcommand";
1106
- }
1107
-
1108
- //#endregion
1109
- //#region src/completion/dynamic/shell-formatter.ts
1110
- /**
1111
- * Format completion candidates for the specified shell
1112
- *
1113
- * @returns Shell-ready output string (lines separated by newline, last line is :directive)
1114
- */
1115
- function formatForShell(result, options) {
1116
- switch (options.shell) {
1117
- case "bash": return formatForBash(result, options);
1118
- case "zsh": return formatForZsh(result, options);
1119
- case "fish": return formatForFish(result, options);
1120
- }
1121
- }
1122
- /**
1123
- * Check if the FilterPrefix directive is set
1124
- */
1125
- function shouldFilterPrefix(directive) {
1126
- return (directive & CompletionDirective.FilterPrefix) !== 0;
1127
- }
1128
- /**
1129
- * Filter candidates by prefix
1130
- */
1131
- function filterByPrefix(candidates, prefix) {
1132
- if (!prefix) return candidates;
1133
- return candidates.filter((c) => c.value.startsWith(prefix));
1134
- }
1135
- /**
1136
- * Append extension metadata and directive to output lines
1137
- */
1138
- function appendMetadata(lines, result) {
1139
- if (result.fileExtensions && result.fileExtensions.length > 0) lines.push(`@ext:${result.fileExtensions.join(",")}`);
1140
- if (result.fileMatchers && result.fileMatchers.length > 0) lines.push(`@matcher:${result.fileMatchers.join(",")}`);
1141
- lines.push(`:${result.directive}`);
1142
- }
1143
- /**
1144
- * Format for bash
1145
- *
1146
- * - Pre-filters candidates by currentWord prefix (replaces compgen -W)
1147
- * - Handles --opt=value inline values by prepending prefix
1148
- * - Outputs plain values only (no descriptions - bash COMPREPLY doesn't support them)
1149
- * - Last line: :directive
1150
- */
1151
- function formatForBash(result, options) {
1152
- let { candidates } = result;
1153
- if (shouldFilterPrefix(result.directive)) candidates = filterByPrefix(candidates, options.currentWord);
1154
- const lines = candidates.map((c) => {
1155
- if (options.inlinePrefix) return `${options.inlinePrefix}${c.value}`;
1156
- return c.value;
1157
- });
1158
- appendMetadata(lines, result);
1159
- return lines.join("\n");
1160
- }
1161
- /**
1162
- * Format for zsh
1163
- *
1164
- * - Outputs value:description pairs for _describe
1165
- * - Colons in values/descriptions are escaped with backslash
1166
- * - Last line: :directive
1167
- */
1168
- function formatForZsh(result, _options) {
1169
- const lines = result.candidates.map((c) => {
1170
- const escapedValue = c.value.replace(/:/g, "\\:");
1171
- if (c.description) return `${escapedValue}:${c.description.replace(/:/g, "\\:")}`;
1172
- return escapedValue;
1173
- });
1174
- appendMetadata(lines, result);
1175
- return lines.join("\n");
1176
- }
1177
- /**
1178
- * Format for fish
1179
- *
1180
- * - Outputs value\tdescription pairs
1181
- * - Last line: :directive
1182
- */
1183
- function formatForFish(result, _options) {
1184
- const lines = result.candidates.map((c) => {
1185
- if (c.description) return `${c.value}\t${c.description}`;
1186
- return c.value;
1187
- });
1188
- appendMetadata(lines, result);
1189
- return lines.join("\n");
1190
- }
1191
-
1192
- //#endregion
1193
- //#region src/completion/dynamic/complete-command.ts
1194
- /**
1195
- * Dynamic completion command implementation
1196
- *
1197
- * This creates a hidden `__complete` command that outputs completion candidates
1198
- * for shell scripts to consume. Usage:
1199
- *
1200
- * mycli __complete --shell bash -- build --fo
1201
- * mycli __complete --shell zsh -- plugin add
1202
- *
1203
- * Output format depends on the target shell:
1204
- * bash: plain values (pre-filtered by prefix), last line :directive
1205
- * zsh: value:description pairs, last line :directive
1206
- * fish: value\tdescription pairs, last line :directive
1207
- */
1208
- /**
1209
- * Detect inline option-value prefix (e.g., "--format=" from "--format=json")
1210
- */
1211
- function detectInlinePrefix(currentWord) {
1212
- if (currentWord.startsWith("--") && currentWord.includes("=")) return currentWord.slice(0, currentWord.indexOf("=") + 1);
1213
- }
1214
- /**
1215
- * Schema for the __complete command
1216
- */
1217
- const completeArgsSchema = zod.z.object({
1218
- shell: require_subcommand_router.arg(zod.z.enum([
1219
- "bash",
1220
- "zsh",
1221
- "fish"
1222
- ]), { description: "Target shell for output formatting" }),
1223
- args: require_subcommand_router.arg(zod.z.array(zod.z.string()).default([]), {
1224
- positional: true,
1225
- description: "Arguments to complete",
1226
- variadic: true
1227
- })
1228
- });
1229
- /**
1230
- * Create the dynamic completion command
1231
- *
1232
- * @param rootCommand - The root command to generate completions for
1233
- * @param programName - The program name (optional, defaults to rootCommand.name)
1234
- * @returns A command that outputs completion candidates
1235
- */
1236
- function createDynamicCompleteCommand(rootCommand, _programName) {
1237
- return defineCommand({
1238
- name: "__complete",
1239
- args: completeArgsSchema,
1240
- run(args) {
1241
- const context = parseCompletionContext(args.args, rootCommand);
1242
- const result = generateCandidates(context);
1243
- const inlinePrefix = detectInlinePrefix(context.currentWord);
1244
- const output = formatForShell(result, {
1245
- shell: args.shell,
1246
- currentWord: inlinePrefix ? context.currentWord.slice(inlinePrefix.length) : context.currentWord,
1247
- inlinePrefix
1248
- });
1249
- console.log(output);
1250
- }
1251
- });
1252
- }
1253
- /**
1254
- * Check if a command tree contains the __complete command
1255
- */
1256
- function hasCompleteCommand(command) {
1257
- return Boolean(command.subCommands?.["__complete"]);
1258
- }
1259
-
1260
- //#endregion
1261
- //#region src/completion/fish.ts
1262
- /** Escape shell-special characters for fish double-quoted strings */
1263
- function escapeDesc$1(s) {
1264
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
1265
- }
1266
- /**
1267
- * Generate fish value completion lines for a ValueCompletion spec.
1268
- * Each line outputs candidates via echo (tab-separated value\tdescription).
1269
- */
1270
- function fishValueLines(vc) {
1271
- if (!vc) return [];
1272
- switch (vc.type) {
1273
- case "choices": return vc.choices.map((c) => `echo "${escapeDesc$1(c)}"`);
1274
- case "file":
1275
- if (vc.matcher?.length) return fishMatcherLines(vc.matcher);
1276
- if (vc.extensions?.length) return fishExtensionLines(vc.extensions);
1277
- return [`__fish_complete_path "$_cur"`];
1278
- case "directory": return [`__fish_complete_directories "$_cur"`];
1279
- case "command": return [
1280
- `for _v in (${vc.shellCommand})`,
1281
- ` echo "$_v"`,
1282
- `end`
1283
- ];
1284
- case "none": return [];
1285
- }
1286
- }
1287
- /** Generate fish matcher-filtered file completion */
1288
- function fishMatcherLines(patterns) {
1289
- return [
1290
- `__fish_complete_directories "$_cur"`,
1291
- `set -l _dir ""`,
1292
- `if string match -q '*/*' "$_cur"`,
1293
- ` set _dir (string replace -r '[^/]*$' '' "$_cur")`,
1294
- `end`,
1295
- ...patterns.flatMap((p) => [
1296
- `for _f in "$_dir"${p}`,
1297
- ` test -f "$_f"; and string match -q "$_cur*" "$_f"; and echo "$_f"`,
1298
- `end`
1299
- ])
1300
- ];
1301
- }
1302
- /** Generate fish extension-filtered file completion */
1303
- function fishExtensionLines(extensions) {
1304
- const lines = [];
1305
- lines.push(`__fish_complete_directories "$_cur"`);
1306
- for (const ext of extensions) {
1307
- lines.push(`for _f in "$_cur"*.${ext}`);
1308
- lines.push(` test -f "$_f"; and echo "$_f"`);
1309
- lines.push(`end`);
1310
- }
1311
- return lines;
1312
- }
1313
- /** Generate option-value switch cases for fish */
1314
- function optionValueCases$1(options) {
1315
- const lines = [];
1316
- for (const opt of options) {
1317
- if (!opt.takesValue || !opt.valueCompletion) continue;
1318
- const valLines = fishValueLines(opt.valueCompletion);
1319
- if (valLines.length === 0) continue;
1320
- const conditions = [`test "$_prev" = "--${opt.cliName}"`];
1321
- if (opt.alias) for (const a of opt.alias) conditions.push(`test "$_prev" = "${a.length === 1 ? `-${a}` : `--${a}`}"`);
1322
- const cond = conditions.join("; or ");
1323
- lines.push(` if ${cond}`);
1324
- for (const vl of valLines) lines.push(` ${vl}`);
1325
- lines.push(` return`);
1326
- lines.push(` end`);
1327
- }
1328
- return lines;
1329
- }
1330
- /** Generate positional completion block for fish */
1331
- function positionalBlock$1(positionals) {
1332
- if (positionals.length === 0) return [];
1333
- const lines = [];
1334
- for (const pos of positionals) {
1335
- const valLines = fishValueLines(pos.valueCompletion);
1336
- if (valLines.length === 0) continue;
1337
- if (pos.variadic) lines.push(` if test $_pos_count -ge ${pos.position}`);
1338
- else lines.push(` if test $_pos_count -eq ${pos.position}`);
1339
- for (const vl of valLines) lines.push(` ${vl}`);
1340
- lines.push(` return`);
1341
- lines.push(` end`);
1342
- }
1343
- return lines;
1344
- }
1345
- /** Generate available-option echo lines for fish */
1346
- function availableOptionLines$1(options, fn) {
1347
- const lines = [];
1348
- for (const opt of options) {
1349
- const desc = escapeDesc$1(opt.description ?? "");
1350
- const negDesc = opt.negationDescription ? escapeDesc$1(opt.negationDescription) : desc;
1351
- if (opt.valueType === "array") lines.push(` echo "--${opt.cliName}\t${desc}"`);
1352
- else {
1353
- const checks = [`"--${opt.cliName}"`];
1354
- if (opt.alias) for (const a of opt.alias) checks.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
1355
- if (opt.negation) checks.push(`"--${opt.negation}"`);
1356
- lines.push(` __${fn}_not_used ${checks.join(" ")}; and echo "--${opt.cliName}\t${desc}"`);
1357
- if (opt.negation) lines.push(` __${fn}_not_used ${checks.join(" ")}; and echo "--${opt.negation}\t${negDesc}"`);
1358
- }
1359
- }
1360
- lines.push(` __${fn}_not_used "--help"; and echo "--help\tShow help"`);
1361
- return lines;
1362
- }
1363
- /** Generate value-option completion block if any value-taking options exist */
1364
- function valueCompletionBlock$1(options) {
1365
- if (!options.some((o) => o.takesValue && o.valueCompletion)) return [];
1366
- return optionValueCases$1(options);
1367
- }
1368
- /**
1369
- * Generate a per-subcommand completion function for fish.
1370
- * Recursively generates functions for nested subcommands.
1371
- */
1372
- function generateSubHandler$1(sub, fn, path) {
1373
- const fullPath = [...path, sub.name];
1374
- const funcName = `__${fn}_complete_${fullPath.map(sanitize).join("_")}`;
1375
- const visibleSubs = getVisibleSubs(sub.subcommands);
1376
- const lines = [];
1377
- for (const child of visibleSubs) lines.push(...generateSubHandler$1(child, fn, fullPath));
1378
- lines.push(`function ${funcName} --no-scope-shadowing`);
1379
- lines.push(...valueCompletionBlock$1(sub.options));
1380
- const fullPathStr = fullPath.join(":");
1381
- lines.push(` if __${fn}_opt_takes_value "${fullPathStr}" "$_prev"; return; end`);
1382
- if (sub.positionals.length > 0) {
1383
- lines.push(` if test $_after_dd -eq 1`);
1384
- lines.push(...positionalBlock$1(sub.positionals).map((l) => ` ${l}`));
1385
- lines.push(` return`);
1386
- lines.push(` end`);
1387
- } else lines.push(` if test $_after_dd -eq 1; return; end`);
1388
- lines.push(` if string match -q -- '-*' "$_cur"`);
1389
- lines.push(...availableOptionLines$1(sub.options, fn));
1390
- lines.push(` return`);
1391
- lines.push(` end`);
1392
- if (visibleSubs.length > 0) for (const s of getSubNamesWithAliases(sub.subcommands)) {
1393
- const desc = escapeDesc$1(s.description ?? "");
1394
- lines.push(` echo "${s.name}\t${desc}"`);
1395
- }
1396
- else if (sub.positionals.length > 0) lines.push(...positionalBlock$1(sub.positionals));
1397
- lines.push(`end`);
1398
- lines.push(``);
1399
- return lines;
1400
- }
1401
- /** Generate opt-takes-value entries for fish switch cases */
1402
- function optTakesValueCases(sub, parentPath) {
1403
- const lines = [];
1404
- for (const opt of sub.options) if (opt.takesValue) {
1405
- const patterns = [`"${parentPath}:--${opt.cliName}"`];
1406
- if (opt.alias) for (const a of opt.alias) patterns.push(`"${parentPath}:${a.length === 1 ? `-${a}` : `--${a}`}"`);
1407
- lines.push(` case ${patterns.join(" ")}`);
1408
- lines.push(` return 0`);
1409
- }
1410
- for (const child of getVisibleSubs(sub.subcommands)) {
1411
- const childPath = parentPath ? `${parentPath}:${child.name}` : child.name;
1412
- lines.push(...optTakesValueCases(child, childPath));
1413
- if (child.aliases) for (const alias of child.aliases) {
1414
- const aliasPath = parentPath ? `${parentPath}:${alias}` : alias;
1415
- lines.push(...optTakesValueCases(child, aliasPath));
1416
- }
1417
- }
1418
- return lines;
1419
- }
1420
- function generateFishCompletion(command, options) {
1421
- const { programName } = options;
1422
- const data = extractCompletionData(command, programName, options.globalArgsSchema);
1423
- const fn = sanitize(programName);
1424
- const root = data.command;
1425
- const visibleSubs = getVisibleSubs(root.subcommands);
1426
- const lines = [];
1427
- lines.push(...buildHeaderLines({
1428
- programName,
1429
- shell: "fish",
1430
- binPath: options.binPath,
1431
- programVersion: options.programVersion
1432
- }));
1433
- lines.push(`# Generated by politty`);
1434
- lines.push(``);
1435
- const sig = computeBinSig(resolveBinPath(programName, options.binPath));
1436
- const refreshFn = `__${fn}_refresh_completion`;
1437
- lines.push(`function ${refreshFn} --no-scope-shadowing`);
1438
- lines.push(` set -l _bin (command -v ${programName})`);
1439
- lines.push(` test -z "$_bin"; and return 1`);
1440
- lines.push(` set -l _sig (stat -L -c '%Y' "$_bin" 2>/dev/null; or stat -L -f '%m' "$_bin" 2>/dev/null)`);
1441
- lines.push(` test "$_sig" = "${sig}"; and return 1`);
1442
- lines.push(` set -l _target "$__fish_config_dir/completions/${programName}.fish"`);
1443
- lines.push(` "$_bin" __refresh-completion fish 2>/dev/null`);
1444
- lines.push(` and source "$_target" 2>/dev/null`);
1445
- lines.push(` and return 0`);
1446
- lines.push(` return 1`);
1447
- lines.push(`end`);
1448
- lines.push(`${refreshFn}`);
1449
- lines.push(`set -l _politty_refreshed $status`);
1450
- lines.push(`functions -e ${refreshFn}`);
1451
- lines.push(`test $_politty_refreshed -eq 0; and return`);
1452
- lines.push(``);
1453
- lines.push(`function __${fn}_not_used --no-scope-shadowing`);
1454
- lines.push(` for _chk in $argv`);
1455
- lines.push(` if contains -- "$_chk" $_used_opts`);
1456
- lines.push(` return 1`);
1457
- lines.push(` end`);
1458
- lines.push(` end`);
1459
- lines.push(` return 0`);
1460
- lines.push(`end`);
1461
- lines.push(``);
1462
- lines.push(`function __${fn}_opt_takes_value`);
1463
- lines.push(` switch "$argv[1]:$argv[2]"`);
1464
- lines.push(...optTakesValueCases(root, ""));
1465
- lines.push(` end`);
1466
- lines.push(` return 1`);
1467
- lines.push(`end`);
1468
- lines.push(``);
1469
- const routeEntries = collectRouteEntries(root);
1470
- if (routeEntries.length > 0) {
1471
- lines.push(`function __${fn}_is_subcmd`);
1472
- lines.push(` switch "$argv[1]:$argv[2]"`);
1473
- for (const r of routeEntries) {
1474
- lines.push(` case "${r.lookupPattern}"`);
1475
- lines.push(` return 0`);
1476
- }
1477
- lines.push(` end`);
1478
- lines.push(` return 1`);
1479
- lines.push(`end`);
1480
- lines.push(``);
1481
- }
1482
- for (const sub of visibleSubs) lines.push(...generateSubHandler$1(sub, fn, []));
1483
- lines.push(`function __${fn}_complete_root --no-scope-shadowing`);
1484
- lines.push(...valueCompletionBlock$1(root.options));
1485
- lines.push(` if __${fn}_opt_takes_value "" "$_prev"; return; end`);
1486
- if (root.positionals.length > 0) {
1487
- lines.push(` if test $_after_dd -eq 1`);
1488
- lines.push(...positionalBlock$1(root.positionals).map((l) => ` ${l}`));
1489
- lines.push(` return`);
1490
- lines.push(` end`);
1491
- } else lines.push(` if test $_after_dd -eq 1; return; end`);
1492
- lines.push(` if string match -q -- '-*' "$_cur"`);
1493
- lines.push(...availableOptionLines$1(root.options, fn));
1494
- if (visibleSubs.length > 0) {
1495
- lines.push(` else`);
1496
- for (const s of getSubNamesWithAliases(root.subcommands)) {
1497
- const desc = escapeDesc$1(s.description ?? "");
1498
- lines.push(` echo "${s.name}\t${desc}"`);
1499
- }
1500
- } else if (root.positionals.length > 0) {
1501
- lines.push(` else`);
1502
- lines.push(...positionalBlock$1(root.positionals));
1503
- }
1504
- lines.push(` end`);
1505
- lines.push(`end`);
1506
- lines.push(``);
1507
- lines.push(`function __fish_${fn}_complete`);
1508
- lines.push(` set -l _args (commandline -opc)`);
1509
- lines.push(` set -e _args[1]`);
1510
- lines.push(``);
1511
- lines.push(` set -l _ct (commandline -ct)`);
1512
- lines.push(` if test (count $_ct) -eq 0`);
1513
- lines.push(` set -a _args ""`);
1514
- lines.push(` else`);
1515
- lines.push(` set -a _args $_ct`);
1516
- lines.push(` end`);
1517
- lines.push(``);
1518
- lines.push(` set -l _cur ""`);
1519
- lines.push(` if test (count $_args) -gt 0`);
1520
- lines.push(` set _cur "$_args[-1]"`);
1521
- lines.push(` end`);
1522
- lines.push(``);
1523
- lines.push(` set -l _prev ""`);
1524
- lines.push(` if test (count $_args) -gt 1`);
1525
- lines.push(` set _prev "$_args[-2]"`);
1526
- lines.push(` end`);
1527
- lines.push(``);
1528
- lines.push(` set -l _subcmd "" ; set -l _after_dd 0 ; set -l _pos_count 0 ; set -l _skip_next 0`);
1529
- lines.push(` set -l _used_opts`);
1530
- lines.push(``);
1531
- lines.push(` set -l _j 1`);
1532
- lines.push(` set -l _limit (math (count $_args) - 1)`);
1533
- lines.push(` while test $_j -le $_limit`);
1534
- lines.push(` set -l _w "$_args[$_j]"`);
1535
- lines.push(` if test $_skip_next -eq 1; set _skip_next 0; set _j (math $_j + 1); continue; end`);
1536
- lines.push(` if test "$_w" = "--"; set _after_dd 1; set _j (math $_j + 1); continue; end`);
1537
- lines.push(` if test $_after_dd -eq 1; set _pos_count (math $_pos_count + 1); set _j (math $_j + 1); continue; end`);
1538
- lines.push(` if string match -q -- '--*=*' "$_w"; set -a _used_opts (string replace -r '=.*' '' -- "$_w"); set _j (math $_j + 1); continue; end`);
1539
- lines.push(` if string match -q -- '-*' "$_w"`);
1540
- lines.push(` set -a _used_opts "$_w"`);
1541
- lines.push(` __${fn}_opt_takes_value "$_subcmd" "$_w"; and set _skip_next 1`);
1542
- lines.push(` set _j (math $_j + 1); continue`);
1543
- lines.push(` end`);
1544
- if (routeEntries.length > 0) lines.push(` if __${fn}_is_subcmd "$_subcmd" "$_w"; test -n "$_subcmd"; and set _subcmd "$_subcmd:$_w"; or set _subcmd "$_w"; set _used_opts; set _pos_count 0; else; set _pos_count (math $_pos_count + 1); end`);
1545
- else lines.push(` set _pos_count (math $_pos_count + 1)`);
1546
- lines.push(` set _j (math $_j + 1)`);
1547
- lines.push(` end`);
1548
- lines.push(``);
1549
- lines.push(` switch "$_subcmd"`);
1550
- for (const r of routeEntries) lines.push(` case "${r.pathStr}"; __${fn}_complete_${r.funcSuffix}`);
1551
- lines.push(` case '*'; __${fn}_complete_root`);
1552
- lines.push(` end`);
1553
- lines.push(`end`);
1554
- lines.push(``);
1555
- lines.push(`# Clear existing completions`);
1556
- lines.push(`complete -e -c ${programName}`);
1557
- lines.push(``);
1558
- lines.push(`# Register completion`);
1559
- lines.push(`complete -c ${programName} -f -a '(__fish_${fn}_complete)'`);
1560
- lines.push(``);
1561
- return {
1562
- script: lines.join("\n"),
1563
- shell: "fish",
1564
- installInstructions: `# To enable completions, run one of the following:
1565
-
1566
- # Option 1: Source directly
1567
- ${programName} completion fish | source
1568
-
1569
- # Option 2: Save to the fish completions directory
1570
- ${programName} completion fish > ~/.config/fish/completions/${programName}.fish
1571
-
1572
- # The completion will be available immediately in new shell sessions.
1573
- # To use in the current session, run:
1574
- source ~/.config/fish/completions/${programName}.fish`
1575
- };
1576
- }
1577
-
1578
- //#endregion
1579
- //#region src/completion/loader.ts
1580
- /**
1581
- * Rc-loader generators (bash / zsh).
1582
- *
1583
- * These produce the small snippet a user adds once to `~/.bashrc` or
1584
- * `~/.zshrc`. The snippet:
1585
- *
1586
- * 1. Looks up the binary on $PATH.
1587
- * 2. Reads its mtime.
1588
- * 3. If the on-disk completion cache is missing or its
1589
- * `# politty-bin-sig:` header differs, regenerates the cache by
1590
- * spawning the binary once.
1591
- * 4. Sources the cache.
1592
- *
1593
- * All failure modes are silent no-ops so a broken / missing CLI never
1594
- * blocks shell startup.
1595
- */
1596
- /**
1597
- * Single-quote escape: `'` -> `'\''`. Inside single quotes the shell
1598
- * performs no expansion at all, so `$`, backticks, and `$(...)` are
1599
- * inert. Used for hardcoded paths because callers may sources them
1600
- * from env / config — we must not let metachars in the path execute as
1601
- * commands when the rc snippet is sourced.
1602
- */
1603
- function shSingleQuote(s) {
1604
- return `'${s.replace(/'/g, "'\\''")}'`;
1605
- }
1606
- function bashCachePathExpr(programName, cacheDir, shell) {
1607
- if (cacheDir) return shSingleQuote(`${cacheDir}/completion.${shell}`);
1608
- return `"\${XDG_CACHE_HOME:-$HOME/.cache}/${programName}/completion.${shell}"`;
1609
- }
1610
- function generateBashLoader(opts) {
1611
- const fn = sanitize(opts.programName);
1612
- const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "bash");
1613
- return `__${fn}_load_completion() {
1614
- local _bin _cache _sig _hdr
1615
- _bin=$(type -P ${opts.programName} 2>/dev/null)
1616
- [[ -n "$_bin" ]] || return 0
1617
- _cache=${cache}
1618
- _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
1619
- _hdr="# politty-bin-sig: $_sig"
1620
- if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
1621
- # Use the hidden __refresh-completion subcommand instead of
1622
- # \`$_bin completion bash\`: the foreground completion command
1623
- # is subject to user setup/cleanup/prompt and required
1624
- # globalArgs validation, which can silently fail or block when
1625
- # invoked from rc; runMain bypasses those for __-prefixed
1626
- # internal subcommands.
1627
- "$_bin" __refresh-completion bash 2>/dev/null
1628
- fi
1629
- # If regen failed but a stale cache survived from a previous run,
1630
- # source it anyway — a stale completion is preferable to no
1631
- # completion at all.
1632
- [[ -f "$_cache" ]] || return 0
1633
- # shellcheck disable=SC1090
1634
- source "$_cache"
1635
- }
1636
- __${fn}_load_completion
1637
- unset -f __${fn}_load_completion
1638
- `;
1639
- }
1640
- function generateZshLoader(opts) {
1641
- const fn = sanitize(opts.programName);
1642
- const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "zsh");
1643
- return `__${fn}_load_completion() {
1644
- emulate -L zsh
1645
- setopt local_options no_aliases
1646
- local _bin _cache _sig _hdr
1647
- _bin=$(whence -p ${opts.programName} 2>/dev/null)
1648
- [[ -n "$_bin" ]] || return 0
1649
- _cache=${cache}
1650
- _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
1651
- _hdr="# politty-bin-sig: $_sig"
1652
- if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
1653
- # See bash loader for why we use __refresh-completion instead
1654
- # of \`$_bin completion zsh\`.
1655
- "$_bin" __refresh-completion zsh 2>/dev/null
1656
- fi
1657
- # See bash loader: keep stale completion over no completion.
1658
- [[ -f "$_cache" ]] || return 0
1659
- source "$_cache"
1660
- }
1661
- __${fn}_load_completion
1662
- unfunction __${fn}_load_completion
1663
- `;
1664
- }
1665
- /**
1666
- * Build the rc-loader snippet for bash or zsh. Fish doesn't have an
1667
- * rc-loader; instead, `<program> completion fish --install` writes a
1668
- * self-rewriting autoload file.
1669
- */
1670
- function generateLoader(opts) {
1671
- switch (opts.shell) {
1672
- case "bash": return generateBashLoader(opts);
1673
- case "zsh": return generateZshLoader(opts);
1674
- case "fish": throw new Error("fish does not use an rc loader. Run `<program> completion fish --install` to write the self-refreshing autoload file instead.");
1675
- }
1676
- }
1677
- /**
1678
- * Default cache file path (used by `completion <bash|zsh> --install`
1679
- * and the `__refresh-completion` subcommand). For fish, the install
1680
- * path is `$__fish_config_dir/completions/<program>.fish` and is
1681
- * computed inside `installPath()` instead.
1682
- */
1683
- function defaultCacheDir(programName) {
1684
- return `${process.env.XDG_CACHE_HOME ?? `${process.env.HOME ?? ""}/.cache`}/${programName}`;
1685
- }
1686
-
1687
- //#endregion
1688
- //#region src/completion/install.ts
1689
- /**
1690
- * On-disk install + refresh helpers.
1691
- *
1692
- * `install` writes the generated script to its canonical cache /
1693
- * autoload path. `refresh` is the body of the `__refresh-completion`
1694
- * hidden subcommand and the runMain background hook — it regenerates
1695
- * the cache only when the binary's mtime no longer matches the
1696
- * embedded `# politty-bin-sig:` header.
1697
- *
1698
- * All file I/O is best-effort: failures fall through silently. A stale
1699
- * (or missing) cache is preferable to crashing the user's shell.
1700
- */
1701
- /**
1702
- * Resolve where a script for the given shell should live on disk.
1703
- *
1704
- * - bash/zsh: `<cacheDir>/completion.<shell>` — sourced by the rc loader.
1705
- * - fish: `$__fish_config_dir/completions/<program>.fish` — autoloaded
1706
- * by fish on TAB. We approximate `$__fish_config_dir` from
1707
- * `$XDG_CONFIG_HOME` / `$HOME`.
1708
- */
1709
- function installPath(programName, shell, cacheDir) {
1710
- if (shell === "fish") return (0, node_path.join)(process.env.XDG_CONFIG_HOME ?? `${process.env.HOME ?? ""}/.config`, "fish", "completions", `${programName}.fish`);
1711
- return (0, node_path.join)(cacheDir ?? defaultCacheDir(programName), `completion.${shell}`);
1712
- }
1713
- /** Atomic write: tmp file in the same dir, then rename. */
1714
- function writeAtomic(path, content) {
1715
- (0, node_fs.mkdirSync)((0, node_path.dirname)(path), { recursive: true });
1716
- const tmp = `${path}.tmp.${process.pid}`;
1717
- (0, node_fs.writeFileSync)(tmp, content);
1718
- (0, node_fs.renameSync)(tmp, path);
1719
- }
1720
- function generateScript(ctx, shell) {
1721
- return generateCompletion(ctx.rootCommand, {
1722
- shell,
1723
- programName: ctx.programName,
1724
- includeDescriptions: true,
1725
- ...ctx.programVersion !== void 0 && { programVersion: ctx.programVersion },
1726
- ...ctx.binPath !== void 0 && { binPath: ctx.binPath },
1727
- ...ctx.cacheDir !== void 0 && { cacheDir: ctx.cacheDir },
1728
- ...ctx.globalArgsSchema !== void 0 && { globalArgsSchema: ctx.globalArgsSchema }
1729
- }).script;
1730
- }
1731
- /** Write the script for `shell` to its install path. Returns the path. */
1732
- function install(ctx, shell) {
1733
- const target = installPath(ctx.programName, shell, ctx.cacheDir);
1734
- writeAtomic(target, generateScript(ctx, shell));
1735
- return target;
1736
- }
1737
- /**
1738
- * Read the first ~5 lines of an existing cache file and return its
1739
- * embedded bin-sig. Returns `null` when the file is missing, unreadable,
1740
- * or doesn't have a sig header.
1741
- */
1742
- function readCachedSig(path) {
1743
- try {
1744
- if (!(0, node_fs.existsSync)(path)) return null;
1745
- const m = (0, node_fs.readFileSync)(path, "utf8").split("\n", 6).join("\n").match(/^# politty-bin-sig: (\S+)/m);
1746
- return m ? m[1] : null;
1747
- } catch {
1748
- return null;
1749
- }
1750
- }
1751
- /**
1752
- * Rewrite the cache only when stale. Used by:
1753
- * - `<program> __refresh-completion <shell>` (the hidden subcommand
1754
- * spawned both by the rc loader and by the runMain background hook)
1755
- *
1756
- * Caller is responsible for gating: the runMain hook (`maybeSpawnRefresh`)
1757
- * checks `hasManagedCache` before spawning so we don't silently create
1758
- * a fish autoload the user never opted into. The rc loader / fish
1759
- * autoload only run after the user has installed completion in the
1760
- * first place, so they're allowed to refresh unconditionally.
1761
- *
1762
- * Must never throw — a stale completion is fine, a crash isn't.
1763
- */
1764
- function refreshIfStale(ctx, shell) {
1765
- try {
1766
- const target = installPath(ctx.programName, shell, ctx.cacheDir);
1767
- const binPath = resolveBinPath(ctx.programName, ctx.binPath);
1768
- if (!binPath) return;
1769
- let currentSig;
1770
- try {
1771
- currentSig = Math.floor((0, node_fs.statSync)(binPath).mtimeMs / 1e3).toString();
1772
- } catch {
1773
- return;
1774
- }
1775
- if (readCachedSig(target) === currentSig) return;
1776
- writeAtomic(target, generateScript(ctx, shell));
1777
- } catch {}
1778
- }
1779
- /**
1780
- * Returns true when a politty-managed cache file already exists on disk
1781
- * for the given shell — i.e. the user has installed completion via
1782
- * `<program> completion <shell> --install` or the rc loader has already
1783
- * sourced one. Used by the runMain background hook to avoid spawning
1784
- * the refresher (and thereby silently creating files) on plain CLI runs
1785
- * the user never opted into.
1786
- */
1787
- function hasManagedCache(ctx, shell) {
1788
- return readCachedSig(installPath(ctx.programName, shell, ctx.cacheDir)) !== null;
1789
- }
1790
- /**
1791
- * Spawn a detached child process that runs `<program> __refresh-completion <shell>`.
1792
- * The child is fully decoupled (`stdio: "ignore"` + `unref()`), so it
1793
- * outlives the parent without holding any handles.
1794
- *
1795
- * Caller is expected to gate this on the right conditions (interactive
1796
- * shell, not running inside `__complete` itself, etc.).
1797
- *
1798
- * Returns `void` and never throws — even spawn failures are absorbed.
1799
- */
1800
- function spawnBackgroundRefresh(programArgv0, shell) {
1801
- try {
1802
- (0, node_child_process.spawn)(process.execPath, [
1803
- programArgv0,
1804
- "__refresh-completion",
1805
- shell
1806
- ], {
1807
- detached: true,
1808
- stdio: "ignore"
1809
- }).unref();
1810
- } catch {}
1811
- }
1812
-
1813
- //#endregion
1814
- //#region src/completion/zsh.ts
1815
- function escapeDesc(s) {
1816
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`").replace(/:/g, "\\:");
1817
- }
1818
- /**
1819
- * Generate zsh value completion lines for a ValueCompletion spec.
1820
- * Uses `_vals` array (must be declared in the calling function scope).
1821
- */
1822
- function zshValueLines(vc, fn) {
1823
- if (!vc) return [];
1824
- switch (vc.type) {
1825
- case "choices": return [`_vals=(${vc.choices.map((c) => `"${escapeDesc(c)}"`).join(" ")})`, `__${fn}_cdescribe 'completions' _vals`];
1826
- case "file":
1827
- if (vc.matcher?.length) return vc.matcher.map((p) => `_files -g "${p}"`);
1828
- if (vc.extensions?.length) return vc.extensions.map((ext) => `_files -g "*.${ext}"`);
1829
- return [`_files`];
1830
- case "directory": return [`_files -/`];
1831
- case "command": return [`_vals=("\${(@f)$(${vc.shellCommand})}")`, `__${fn}_cdescribe 'completions' _vals`];
1832
- case "none": return [];
1833
- }
1834
- }
1835
- /** Generate option-value case branches */
1836
- function optionValueCases(options, fn) {
1837
- const lines = [];
1838
- for (const opt of options) {
1839
- if (!opt.takesValue || !opt.valueCompletion) continue;
1840
- const valLines = zshValueLines(opt.valueCompletion, fn);
1841
- if (valLines.length === 0) continue;
1842
- const patterns = [`--${opt.cliName}`];
1843
- if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `-${a}` : `--${a}`);
1844
- lines.push(` ${patterns.join("|")})`);
1845
- for (const vl of valLines) lines.push(` ${vl}`);
1846
- lines.push(` return 0 ;;`);
1847
- }
1848
- return lines;
1849
- }
1850
- /** Generate positional completion block */
1851
- function positionalBlock(positionals, fn) {
1852
- if (positionals.length === 0) return [];
1853
- const lines = [];
1854
- lines.push(` case "$_pos_count" in`);
1855
- for (const pos of positionals) {
1856
- if (pos.variadic) lines.push(` ${pos.position}|*)`);
1857
- else lines.push(` ${pos.position})`);
1858
- const valLines = zshValueLines(pos.valueCompletion, fn);
1859
- for (const vl of valLines) lines.push(` ${vl}`);
1860
- lines.push(` ;;`);
1861
- }
1862
- lines.push(` esac`);
1863
- return lines;
1864
- }
1865
- /** Generate prev-word value completion case block */
1866
- function valueCompletionBlock(options, fn) {
1867
- if (!options.some((o) => o.takesValue && o.valueCompletion)) return [];
1868
- const prevCases = optionValueCases(options, fn);
1869
- if (prevCases.length === 0) return [];
1870
- return [
1871
- ` case "\${words[CURRENT-1]}" in`,
1872
- ...prevCases,
1873
- ` esac`
1874
- ];
1875
- }
1876
- /** Generate available-options list lines */
1877
- function availableOptionLines(options, fn) {
1878
- const lines = [];
1879
- for (const opt of options) {
1880
- const desc = opt.description ? `:${escapeDesc(opt.description)}` : "";
1881
- const negDesc = opt.negationDescription ? `:${escapeDesc(opt.negationDescription)}` : desc;
1882
- if (opt.valueType === "array") lines.push(` _opts+=("--${opt.cliName}${desc}")`);
1883
- else {
1884
- const patterns = [`"--${opt.cliName}"`];
1885
- if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
1886
- if (opt.negation) patterns.push(`"--${opt.negation}"`);
1887
- lines.push(` __${fn}_not_used ${patterns.join(" ")} && _opts+=("--${opt.cliName}${desc}")`);
1888
- if (opt.negation) lines.push(` __${fn}_not_used ${patterns.join(" ")} && _opts+=("--${opt.negation}${negDesc}")`);
1889
- }
1890
- }
1891
- lines.push(` __${fn}_not_used "--help" && _opts+=("--help:Show help")`);
1892
- return lines;
1893
- }
1894
- /**
1895
- * Generate a per-subcommand completion function.
1896
- * Recursively generates functions for nested subcommands.
1897
- */
1898
- function generateSubHandler(sub, fn, path) {
1899
- const fullPath = [...path, sub.name];
1900
- const funcName = `__${fn}_complete_${fullPath.map(sanitize).join("_")}`;
1901
- const visibleSubs = getVisibleSubs(sub.subcommands);
1902
- const lines = [];
1903
- for (const child of visibleSubs) lines.push(...generateSubHandler(child, fn, fullPath));
1904
- lines.push(`${funcName}() {`);
1905
- lines.push(` local -a _vals=()`);
1906
- lines.push(...valueCompletionBlock(sub.options, fn));
1907
- const fullPathStr = fullPath.join(":");
1908
- lines.push(` if __${fn}_opt_takes_value "${fullPathStr}" "\${words[CURRENT-1]}"; then return 0; fi`);
1909
- if (sub.positionals.length > 0) {
1910
- lines.push(` if (( _after_dd )); then`);
1911
- lines.push(...positionalBlock(sub.positionals, fn).map((l) => ` ${l}`));
1912
- lines.push(` return 0`);
1913
- lines.push(` fi`);
1914
- } else lines.push(` if (( _after_dd )); then return 0; fi`);
1915
- lines.push(` if [[ "\${words[CURRENT]}" == -* ]]; then`);
1916
- lines.push(` local -a _opts=()`);
1917
- lines.push(...availableOptionLines(sub.options, fn));
1918
- lines.push(` __${fn}_cdescribe 'options' _opts`);
1919
- lines.push(` return 0`);
1920
- lines.push(` fi`);
1921
- if (visibleSubs.length > 0) {
1922
- const subItems = getSubNamesWithAliases(sub.subcommands).map((s) => {
1923
- const desc = s.description ? `:${escapeDesc(s.description)}` : "";
1924
- return `"${s.name}${desc}"`;
1925
- }).join(" ");
1926
- lines.push(` local -a _subs=(${subItems})`);
1927
- lines.push(` __${fn}_cdescribe 'subcommands' _subs`);
1928
- } else if (sub.positionals.length > 0) lines.push(...positionalBlock(sub.positionals, fn));
1929
- lines.push(`}`);
1930
- lines.push(``);
1931
- return lines;
1932
- }
1933
- function generateZshCompletion(command, options) {
1934
- const { programName } = options;
1935
- const data = extractCompletionData(command, programName, options.globalArgsSchema);
1936
- const fn = sanitize(programName);
1937
- const root = data.command;
1938
- const visibleSubs = getVisibleSubs(root.subcommands);
1939
- const lines = [];
1940
- lines.push(`#compdef ${programName}`);
1941
- lines.push(``);
1942
- lines.push(...buildHeaderLines({
1943
- programName,
1944
- shell: "zsh",
1945
- binPath: options.binPath,
1946
- programVersion: options.programVersion
1947
- }));
1948
- lines.push(`# Generated by politty`);
1949
- lines.push(``);
1950
- lines.push(`__${fn}_not_used() {`);
1951
- lines.push(` local _u _chk`);
1952
- lines.push(` for _u in "\${_used_opts[@]}"; do`);
1953
- lines.push(` for _chk in "$@"; do`);
1954
- lines.push(` [[ "$_u" == "$_chk" ]] && return 1`);
1955
- lines.push(` done`);
1956
- lines.push(` done`);
1957
- lines.push(` return 0`);
1958
- lines.push(`}`);
1959
- lines.push(``);
1960
- lines.push(`__${fn}_cdescribe() {`);
1961
- lines.push(` _describe "$@" 2>/dev/null && return 0`);
1962
- lines.push(` shift`);
1963
- lines.push(` local -a _cd_vals=("\${(@)\${(P)1}%%:*}")`);
1964
- lines.push(` compadd -a _cd_vals 2>/dev/null`);
1965
- lines.push(` return 0`);
1966
- lines.push(`}`);
1967
- lines.push(``);
1968
- lines.push(`__${fn}_opt_takes_value() {`);
1969
- lines.push(` case "$1:$2" in`);
1970
- lines.push(...optTakesValueEntries(root, ""));
1971
- lines.push(` esac`);
1972
- lines.push(` return 1`);
1973
- lines.push(`}`);
1974
- lines.push(``);
1975
- const routeEntries = collectRouteEntries(root);
1976
- if (routeEntries.length > 0) {
1977
- lines.push(`__${fn}_is_subcmd() {`);
1978
- lines.push(` case "$1:$2" in`);
1979
- lines.push(...isSubcmdCaseLines(routeEntries));
1980
- lines.push(` esac`);
1981
- lines.push(` return 1`);
1982
- lines.push(`}`);
1983
- lines.push(``);
1984
- }
1985
- for (const sub of visibleSubs) lines.push(...generateSubHandler(sub, fn, []));
1986
- lines.push(`__${fn}_complete_root() {`);
1987
- lines.push(` local -a _vals=()`);
1988
- lines.push(...valueCompletionBlock(root.options, fn));
1989
- lines.push(` if __${fn}_opt_takes_value "" "\${words[CURRENT-1]}"; then return 0; fi`);
1990
- if (root.positionals.length > 0) {
1991
- lines.push(` if (( _after_dd )); then`);
1992
- lines.push(...positionalBlock(root.positionals, fn).map((l) => ` ${l}`));
1993
- lines.push(` return 0`);
1994
- lines.push(` fi`);
1995
- } else lines.push(` if (( _after_dd )); then return 0; fi`);
1996
- lines.push(` if [[ "\${words[CURRENT]}" == -* ]]; then`);
1997
- lines.push(` local -a _opts=()`);
1998
- lines.push(...availableOptionLines(root.options, fn));
1999
- lines.push(` __${fn}_cdescribe 'options' _opts`);
2000
- if (visibleSubs.length > 0) {
2001
- lines.push(` else`);
2002
- const subItems = getSubNamesWithAliases(root.subcommands).map((s) => {
2003
- const desc = s.description ? `:${escapeDesc(s.description)}` : "";
2004
- return `"${s.name}${desc}"`;
2005
- }).join(" ");
2006
- lines.push(` local -a _subs=(${subItems})`);
2007
- lines.push(` __${fn}_cdescribe 'subcommands' _subs`);
2008
- } else if (root.positionals.length > 0) {
2009
- lines.push(` else`);
2010
- lines.push(...positionalBlock(root.positionals, fn).map((l) => ` ${l}`));
2011
- }
2012
- lines.push(` fi`);
2013
- lines.push(`}`);
2014
- lines.push(``);
2015
- const subRouting = routeEntries.map((r) => ` ${r.pathStr}) __${fn}_complete_${r.funcSuffix} ;;`).join("\n");
2016
- lines.push(`_${fn}() {`);
2017
- lines.push(` (( CURRENT )) || CURRENT=\${#words}`);
2018
- lines.push(``);
2019
- lines.push(` local _subcmd="" _after_dd=0 _pos_count=0 _skip_next=0`);
2020
- lines.push(` local -a _used_opts=()`);
2021
- lines.push(``);
2022
- lines.push(` local _j=2`);
2023
- lines.push(` while (( _j < CURRENT )); do`);
2024
- lines.push(` local _w="\${words[_j]}"`);
2025
- lines.push(` if (( _skip_next )); then _skip_next=0; (( _j++ )); continue; fi`);
2026
- lines.push(` if [[ "$_w" == "--" ]]; then _after_dd=1; (( _j++ )); continue; fi`);
2027
- lines.push(` if (( _after_dd )); then (( _pos_count++ )); (( _j++ )); continue; fi`);
2028
- lines.push(` if [[ "$_w" == --*=* ]]; then _used_opts+=("\${_w%%=*}"); (( _j++ )); continue; fi`);
2029
- lines.push(` if [[ "$_w" == -* ]]; then`);
2030
- lines.push(` _used_opts+=("$_w")`);
2031
- lines.push(` __${fn}_opt_takes_value "$_subcmd" "$_w" && _skip_next=1`);
2032
- lines.push(` (( _j++ )); continue`);
2033
- lines.push(` fi`);
2034
- if (routeEntries.length > 0) lines.push(` if __${fn}_is_subcmd "$_subcmd" "$_w"; then _subcmd="\${_subcmd:+\${_subcmd}:}$_w"; _used_opts=(); _pos_count=0; else (( _pos_count++ )); fi`);
2035
- else lines.push(` (( _pos_count++ ))`);
2036
- lines.push(` (( _j++ ))`);
2037
- lines.push(` done`);
2038
- lines.push(``);
2039
- lines.push(` case "$_subcmd" in`);
2040
- lines.push(subRouting);
2041
- lines.push(` *) __${fn}_complete_root ;;`);
2042
- lines.push(` esac`);
2043
- lines.push(`}`);
2044
- lines.push(``);
2045
- lines.push(`zstyle ':completion:*:*:${programName}:*' file-patterns '%p:globbed-files *(-/):directories'`);
2046
- lines.push(``);
2047
- lines.push(`compdef _${fn} ${programName}`);
2048
- lines.push(``);
2049
- return {
2050
- script: lines.join("\n"),
2051
- shell: "zsh",
2052
- installInstructions: `# To enable completions, add the following to your ~/.zshrc:
2053
-
2054
- # Option 1: Source directly (add before compinit)
2055
- eval "$(${programName} completion zsh)"
2056
-
2057
- # Option 2: Save to a file in your fpath
2058
- ${programName} completion zsh > ~/.zsh/completions/_${programName}
2059
-
2060
- # Make sure your fpath includes the completions directory:
2061
- # fpath=(~/.zsh/completions $fpath)
2062
- # autoload -Uz compinit && compinit
2063
-
2064
- # Then reload your shell or run:
2065
- source ~/.zshrc`
2066
- };
2067
- }
2068
-
2069
- //#endregion
2070
- //#region src/completion/index.ts
2071
- /**
2072
- * Shell completion generation module
2073
- *
2074
- * Provides utilities to generate shell completion scripts for bash, zsh, and fish.
2075
- *
2076
- * @example
2077
- * ```typescript
2078
- * import { generateCompletion, createCompletionCommand } from "politty/completion";
2079
- *
2080
- * // Generate completion script directly
2081
- * const result = generateCompletion(myCommand, {
2082
- * shell: "bash",
2083
- * programName: "mycli"
2084
- * });
2085
- * console.log(result.script);
2086
- *
2087
- * // Or add a completion subcommand to your CLI
2088
- * const mainCommand = withCompletionCommand(
2089
- * defineCommand({
2090
- * name: "mycli",
2091
- * subCommands: { ... },
2092
- * }),
2093
- * );
2094
- * ```
2095
- */
2096
- /**
2097
- * Generate completion script for the specified shell
2098
- */
2099
- function generateCompletion(command, options) {
2100
- switch (options.shell) {
2101
- case "bash": return generateBashCompletion(command, options);
2102
- case "zsh": return generateZshCompletion(command, options);
2103
- case "fish": return generateFishCompletion(command, options);
2104
- default: throw new Error(`Unsupported shell: ${options.shell}`);
2105
- }
2106
- }
2107
- /**
2108
- * Get the list of supported shells
2109
- */
2110
- function getSupportedShells() {
2111
- return [
2112
- "bash",
2113
- "zsh",
2114
- "fish"
2115
- ];
2116
- }
2117
- /**
2118
- * Detect the current shell from environment
2119
- */
2120
- function detectShell() {
2121
- const shellName = (process.env.SHELL || "").split("/").pop()?.toLowerCase() || "";
2122
- if (shellName.includes("bash")) return "bash";
2123
- if (shellName.includes("zsh")) return "zsh";
2124
- if (shellName.includes("fish")) return "fish";
2125
- return null;
2126
- }
2127
- /**
2128
- * Schema for the completion command arguments
2129
- */
2130
- const completionArgsSchema = zod.z.object({
2131
- shell: require_subcommand_router.arg(zod.z.enum([
2132
- "bash",
2133
- "zsh",
2134
- "fish"
2135
- ]).optional().describe("Shell type (auto-detected if not specified)"), {
2136
- positional: true,
2137
- description: "Shell type (bash, zsh, or fish)",
2138
- placeholder: "SHELL"
2139
- }),
2140
- instructions: require_subcommand_router.arg(zod.z.boolean().default(false), {
2141
- alias: "i",
2142
- description: "Show installation instructions"
2143
- }),
2144
- loader: require_subcommand_router.arg(zod.z.boolean().default(false), { description: "Print just the rc loader snippet (bash/zsh). Add it to ~/.bashrc or ~/.zshrc; it auto-regenerates the cache when the binary changes." }),
2145
- install: require_subcommand_router.arg(zod.z.boolean().default(false), { description: "Write the completion script to its on-disk cache (bash/zsh) or autoload location (fish) instead of printing it." })
2146
- });
2147
- const refreshArgsSchema = zod.z.object({ shell: require_subcommand_router.arg(zod.z.enum([
2148
- "bash",
2149
- "zsh",
2150
- "fish"
2151
- ]), {
2152
- positional: true,
2153
- description: "Shell to refresh",
2154
- placeholder: "SHELL"
2155
- }) });
2156
- /**
2157
- * Create a completion subcommand for your CLI
2158
- *
2159
- * This creates a ready-to-use subcommand that generates completion scripts.
2160
- *
2161
- * @example
2162
- * ```typescript
2163
- * const mainCommand = defineCommand({
2164
- * name: "mycli",
2165
- * subCommands: {
2166
- * completion: createCompletionCommand(mainCommand)
2167
- * }
2168
- * });
2169
- * ```
2170
- */
2171
- function createCompletionCommand(rootCommand, programName, globalArgsSchema, extra = {}) {
2172
- const resolvedProgramName = programName ?? rootCommand.name;
2173
- const { cacheDir, programVersion } = extra;
2174
- const refreshExtra = {
2175
- ...cacheDir !== void 0 && { cacheDir },
2176
- ...programVersion !== void 0 && { programVersion },
2177
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2178
- };
2179
- const installCtxBase = {
2180
- programName: resolvedProgramName,
2181
- ...refreshExtra
2182
- };
2183
- const loaderOptsBase = {
2184
- programName: resolvedProgramName,
2185
- ...cacheDir !== void 0 && { cacheDir }
2186
- };
2187
- if (!rootCommand.subCommands?.__complete) rootCommand.subCommands = {
2188
- ...rootCommand.subCommands,
2189
- __complete: createDynamicCompleteCommand(rootCommand, resolvedProgramName)
2190
- };
2191
- if (!rootCommand.subCommands?.["__refresh-completion"]) rootCommand.subCommands = {
2192
- ...rootCommand.subCommands,
2193
- "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
2194
- };
2195
- return defineCommand({
2196
- name: "completion",
2197
- description: "Generate shell completion script",
2198
- args: completionArgsSchema,
2199
- run(args) {
2200
- const shellType = args.shell || detectShell();
2201
- if (!shellType) {
2202
- console.error("Could not detect shell type. Please specify one of: bash, zsh, fish");
2203
- process.exitCode = 1;
2204
- return;
2205
- }
2206
- if (args.install) {
2207
- let target;
2208
- try {
2209
- target = install({
2210
- rootCommand,
2211
- ...installCtxBase
2212
- }, shellType);
2213
- } catch (e) {
2214
- throw new Error(`install failed: ${e instanceof Error ? e.message : String(e)}`);
2215
- }
2216
- console.error(`installed: ${target}`);
2217
- if (shellType !== "fish") {
2218
- console.error("");
2219
- console.error(`Add to your ~/.${shellType}rc:`);
2220
- console.error("");
2221
- console.error(generateLoader({
2222
- ...loaderOptsBase,
2223
- shell: shellType
2224
- }).trim().replace(/^/gm, " "));
2225
- }
2226
- return;
2227
- }
2228
- if (args.loader) {
2229
- if (shellType === "fish") throw new Error("fish does not use an rc loader. Run `<program> completion fish --install` to write the self-refreshing autoload file instead.");
2230
- process.stdout.write(generateLoader({
2231
- ...loaderOptsBase,
2232
- shell: shellType
2233
- }));
2234
- return;
2235
- }
2236
- const result = generateCompletion(rootCommand, {
2237
- shell: shellType,
2238
- programName: resolvedProgramName,
2239
- includeDescriptions: true,
2240
- ...globalArgsSchema !== void 0 && { globalArgsSchema },
2241
- ...programVersion !== void 0 && { programVersion },
2242
- ...cacheDir !== void 0 && { cacheDir }
2243
- });
2244
- if (args.instructions) console.log(result.installInstructions);
2245
- else console.log(result.script);
2246
- }
2247
- });
2248
- }
2249
- /**
2250
- * Hidden subcommand that the runMain background hook spawns. It does
2251
- * the same stat-compare + atomic rewrite as the rc loader, but in a
2252
- * detached child process so it's invisible to the user.
2253
- */
2254
- function createRefreshCompletionCommand(rootCommand, programName, extra = {}) {
2255
- return defineCommand({
2256
- name: "__refresh-completion",
2257
- description: "(internal) Refresh the on-disk completion cache if stale.",
2258
- args: refreshArgsSchema,
2259
- run(args) {
2260
- refreshIfStale({
2261
- rootCommand,
2262
- programName,
2263
- ...extra
2264
- }, args.shell);
2265
- }
2266
- });
2267
- }
2268
- /**
2269
- * Wrap a command with a completion subcommand
2270
- *
2271
- * This avoids circular references that occur when a command references itself
2272
- * in its subCommands (e.g., for completion generation).
2273
- *
2274
- * @param command - The command to wrap
2275
- * @param options - Options including programName
2276
- * @returns A new command with the completion subcommand added
2277
- *
2278
- * @example
2279
- * ```typescript
2280
- * const mainCommand = withCompletionCommand(
2281
- * defineCommand({
2282
- * name: "mycli",
2283
- * subCommands: { ... },
2284
- * }),
2285
- * );
2286
- * ```
2287
- */
2288
- function withCompletionCommand(command, options) {
2289
- const { programName, globalArgsSchema, cacheDir, programVersion } = typeof options === "string" ? { programName: options } : options ?? {};
2290
- const resolvedProgramName = programName ?? command.name;
2291
- const extra = {
2292
- ...cacheDir !== void 0 && { cacheDir },
2293
- ...programVersion !== void 0 && { programVersion },
2294
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2295
- };
2296
- const wrappedCommand = { ...command };
2297
- wrappedCommand.subCommands = {
2298
- ...command.subCommands,
2299
- completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema, extra),
2300
- __complete: createDynamicCompleteCommand(wrappedCommand, programName),
2301
- "__refresh-completion": createRefreshCompletionCommand(wrappedCommand, resolvedProgramName, extra)
2302
- };
2303
- wrappedCommand.runMainHook = (argv) => {
2304
- maybeSpawnRefresh(argv, {
2305
- programName: resolvedProgramName,
2306
- ...cacheDir !== void 0 && { cacheDir }
2307
- });
2308
- };
2309
- return wrappedCommand;
2310
- }
2311
- /**
2312
- * Background-refresh trigger fired from `runMain` via `runMainHook`.
2313
- *
2314
- * Skipped when:
2315
- * - the user is invoking `__complete` / `__refresh-completion` /
2316
- * `completion` themselves (avoids loops and double work)
2317
- * - $SHELL doesn't resolve to a known shell
2318
- * - the user opted out via $POLITTY_NO_COMPLETION_REFRESH
2319
- * - process.argv[1] is missing (shouldn't happen for normal CLIs)
2320
- * - no politty-managed cache exists yet — i.e. the user hasn't
2321
- * installed completion. Without this gate the detached child would
2322
- * create a fish autoload (or any cache file) on every CLI run,
2323
- * even though the user never opted in via `--install` or the rc loader.
2324
- */
2325
- function maybeSpawnRefresh(argv, ctx) {
2326
- if (process.env.POLITTY_NO_COMPLETION_REFRESH) return;
2327
- const firstPositional = argv.find((a) => !a.startsWith("-"));
2328
- if (firstPositional === "__complete" || firstPositional === "__refresh-completion" || firstPositional === "completion") return;
2329
- const shell = detectShell();
2330
- if (!shell) return;
2331
- const argv0 = process.argv[1];
2332
- if (!argv0) return;
2333
- if (!hasManagedCache(ctx, shell)) return;
2334
- spawnBackgroundRefresh(argv0, shell);
2335
- }
2336
-
2337
- //#endregion
2338
- Object.defineProperty(exports, 'CompletionDirective', {
2339
- enumerable: true,
2340
- get: function () {
2341
- return CompletionDirective;
2342
- }
2343
- });
2344
- Object.defineProperty(exports, 'createCompletionCommand', {
2345
- enumerable: true,
2346
- get: function () {
2347
- return createCompletionCommand;
2348
- }
2349
- });
2350
- Object.defineProperty(exports, 'createDefineCommand', {
2351
- enumerable: true,
2352
- get: function () {
2353
- return createDefineCommand;
2354
- }
2355
- });
2356
- Object.defineProperty(exports, 'createDynamicCompleteCommand', {
2357
- enumerable: true,
2358
- get: function () {
2359
- return createDynamicCompleteCommand;
2360
- }
2361
- });
2362
- Object.defineProperty(exports, 'createRefreshCompletionCommand', {
2363
- enumerable: true,
2364
- get: function () {
2365
- return createRefreshCompletionCommand;
2366
- }
2367
- });
2368
- Object.defineProperty(exports, 'defineCommand', {
2369
- enumerable: true,
2370
- get: function () {
2371
- return defineCommand;
2372
- }
2373
- });
2374
- Object.defineProperty(exports, 'detectShell', {
2375
- enumerable: true,
2376
- get: function () {
2377
- return detectShell;
2378
- }
2379
- });
2380
- Object.defineProperty(exports, 'extractCompletionData', {
2381
- enumerable: true,
2382
- get: function () {
2383
- return extractCompletionData;
2384
- }
2385
- });
2386
- Object.defineProperty(exports, 'extractPositionals', {
2387
- enumerable: true,
2388
- get: function () {
2389
- return extractPositionals;
2390
- }
2391
- });
2392
- Object.defineProperty(exports, 'formatForShell', {
2393
- enumerable: true,
2394
- get: function () {
2395
- return formatForShell;
2396
- }
2397
- });
2398
- Object.defineProperty(exports, 'generateCandidates', {
2399
- enumerable: true,
2400
- get: function () {
2401
- return generateCandidates;
2402
- }
2403
- });
2404
- Object.defineProperty(exports, 'generateCompletion', {
2405
- enumerable: true,
2406
- get: function () {
2407
- return generateCompletion;
2408
- }
2409
- });
2410
- Object.defineProperty(exports, 'getSupportedShells', {
2411
- enumerable: true,
2412
- get: function () {
2413
- return getSupportedShells;
2414
- }
2415
- });
2416
- Object.defineProperty(exports, 'hasCompleteCommand', {
2417
- enumerable: true,
2418
- get: function () {
2419
- return hasCompleteCommand;
2420
- }
2421
- });
2422
- Object.defineProperty(exports, 'parseCompletionContext', {
2423
- enumerable: true,
2424
- get: function () {
2425
- return parseCompletionContext;
2426
- }
2427
- });
2428
- Object.defineProperty(exports, 'resolveValueCompletion', {
2429
- enumerable: true,
2430
- get: function () {
2431
- return resolveValueCompletion;
2432
- }
2433
- });
2434
- Object.defineProperty(exports, 'withCompletionCommand', {
2435
- enumerable: true,
2436
- get: function () {
2437
- return withCompletionCommand;
2438
- }
2439
- });
2440
- //# sourceMappingURL=completion-BlZxMSeU.cjs.map