politty 0.4.14 → 0.4.16

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 (80) hide show
  1. package/README.md +7 -1
  2. package/dist/{arg-registry-CkPDokIu.d.ts → arg-registry-Cd6xnjHa.d.ts} +118 -4
  3. package/dist/arg-registry-Cd6xnjHa.d.ts.map +1 -0
  4. package/dist/{arg-registry-r5wYN6qd.d.cts → arg-registry-MVWOAcvw.d.cts} +118 -4
  5. package/dist/arg-registry-MVWOAcvw.d.cts.map +1 -0
  6. package/dist/augment.d.cts +1 -1
  7. package/dist/augment.d.cts.map +1 -1
  8. package/dist/augment.d.ts +1 -1
  9. package/dist/augment.d.ts.map +1 -1
  10. package/dist/completion/index.cjs +2 -1
  11. package/dist/completion/index.d.cts +2 -2
  12. package/dist/completion/index.d.ts +2 -2
  13. package/dist/completion/index.js +2 -2
  14. package/dist/{completion-yHz8Pdr7.js → completion-B04iiki9.js} +580 -31
  15. package/dist/completion-B04iiki9.js.map +1 -0
  16. package/dist/{completion-CAekGYS4.cjs → completion-BlZxMSeU.cjs} +598 -43
  17. package/dist/completion-BlZxMSeU.cjs.map +1 -0
  18. package/dist/docs/index.cjs +121 -65
  19. package/dist/docs/index.cjs.map +1 -1
  20. package/dist/docs/index.d.cts +5 -1
  21. package/dist/docs/index.d.cts.map +1 -1
  22. package/dist/docs/index.d.ts +5 -1
  23. package/dist/docs/index.d.ts.map +1 -1
  24. package/dist/docs/index.js +124 -67
  25. package/dist/docs/index.js.map +1 -1
  26. package/dist/{index-DPswv0Vt.d.cts → index-CPebddth.d.cts} +58 -4
  27. package/dist/index-CPebddth.d.cts.map +1 -0
  28. package/dist/{index-BLySW_2k.d.ts → index-DR9HLxIP.d.ts} +58 -4
  29. package/dist/index-DR9HLxIP.d.ts.map +1 -0
  30. package/dist/index.cjs +12 -10
  31. package/dist/index.d.cts +39 -4
  32. package/dist/index.d.cts.map +1 -1
  33. package/dist/index.d.ts +39 -4
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -4
  36. package/dist/{subcommand-router-C9ONv6Nq.cjs → log-collector-Cd2_mv87.cjs} +1 -59
  37. package/dist/log-collector-Cd2_mv87.cjs.map +1 -0
  38. package/dist/{subcommand-router--EUt6ftA.js → log-collector-Cu6MCtAx.js} +2 -43
  39. package/dist/log-collector-Cu6MCtAx.js.map +1 -0
  40. package/dist/prompt/clack/index.cjs +1 -1
  41. package/dist/prompt/clack/index.cjs.map +1 -1
  42. package/dist/prompt/clack/index.d.cts +1 -1
  43. package/dist/prompt/clack/index.d.cts.map +1 -1
  44. package/dist/prompt/clack/index.d.ts +1 -1
  45. package/dist/prompt/clack/index.d.ts.map +1 -1
  46. package/dist/prompt/clack/index.js.map +1 -1
  47. package/dist/prompt/index.d.cts +1 -1
  48. package/dist/prompt/index.d.cts.map +1 -1
  49. package/dist/prompt/index.d.ts +1 -1
  50. package/dist/prompt/index.d.ts.map +1 -1
  51. package/dist/prompt/inquirer/index.cjs +1 -1
  52. package/dist/prompt/inquirer/index.cjs.map +1 -1
  53. package/dist/prompt/inquirer/index.d.cts +1 -1
  54. package/dist/prompt/inquirer/index.d.cts.map +1 -1
  55. package/dist/prompt/inquirer/index.d.ts +1 -1
  56. package/dist/prompt/inquirer/index.d.ts.map +1 -1
  57. package/dist/prompt/inquirer/index.js.map +1 -1
  58. package/dist/prompt-BKHqGrFw.js.map +1 -1
  59. package/dist/prompt-aXfSf27y.cjs.map +1 -1
  60. package/dist/{runner-DSZw1AsW.js → runner-BHeCMEa5.js} +383 -57
  61. package/dist/runner-BHeCMEa5.js.map +1 -0
  62. package/dist/{runner-CY5fOsSh.cjs → runner-BcyR6Z8r.cjs} +434 -96
  63. package/dist/runner-BcyR6Z8r.cjs.map +1 -0
  64. package/dist/{lazy-AGV9Pkt5.cjs → subcommand-router-DQy0KZU-.cjs} +148 -4
  65. package/dist/subcommand-router-DQy0KZU-.cjs.map +1 -0
  66. package/dist/{lazy-DiMJSDMB.js → subcommand-router-XZBWe8HN.js} +118 -4
  67. package/dist/subcommand-router-XZBWe8HN.js.map +1 -0
  68. package/package.json +16 -16
  69. package/dist/arg-registry-CkPDokIu.d.ts.map +0 -1
  70. package/dist/arg-registry-r5wYN6qd.d.cts.map +0 -1
  71. package/dist/completion-CAekGYS4.cjs.map +0 -1
  72. package/dist/completion-yHz8Pdr7.js.map +0 -1
  73. package/dist/index-BLySW_2k.d.ts.map +0 -1
  74. package/dist/index-DPswv0Vt.d.cts.map +0 -1
  75. package/dist/lazy-AGV9Pkt5.cjs.map +0 -1
  76. package/dist/lazy-DiMJSDMB.js.map +0 -1
  77. package/dist/runner-CY5fOsSh.cjs.map +0 -1
  78. package/dist/runner-DSZw1AsW.js.map +0 -1
  79. package/dist/subcommand-router--EUt6ftA.js.map +0 -1
  80. package/dist/subcommand-router-C9ONv6Nq.cjs.map +0 -1
@@ -1,6 +1,8 @@
1
- const require_subcommand_router = require('./subcommand-router-C9ONv6Nq.cjs');
2
- const require_lazy = require('./lazy-AGV9Pkt5.cjs');
1
+ const require_log_collector = require('./log-collector-Cd2_mv87.cjs');
2
+ const require_subcommand_router = require('./subcommand-router-DQy0KZU-.cjs');
3
3
  let zod = require("zod");
4
+ let node_fs = require("node:fs");
5
+ let node_path = require("node:path");
4
6
  let node_child_process = require("node:child_process");
5
7
 
6
8
  //#region src/core/command.ts
@@ -8,6 +10,7 @@ function defineCommand(config) {
8
10
  return {
9
11
  name: config.name,
10
12
  description: config.description,
13
+ aliases: config.aliases,
11
14
  args: config.args,
12
15
  subCommands: config.subCommands,
13
16
  setup: config.setup,
@@ -112,6 +115,25 @@ function getVisibleSubs(subs) {
112
115
  return subs.filter((s) => !s.name.startsWith("__"));
113
116
  }
114
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
+ /**
115
137
  * Convert a resolved field to a completable option
116
138
  */
117
139
  function fieldToOption(field) {
@@ -119,6 +141,8 @@ function fieldToOption(field) {
119
141
  name: field.name,
120
142
  cliName: field.cliName,
121
143
  alias: field.alias,
144
+ negation: field.negationDisplay,
145
+ negationDescription: field.negationDescription,
122
146
  description: field.description,
123
147
  takesValue: field.type !== "boolean",
124
148
  valueType: field.type,
@@ -131,21 +155,21 @@ function fieldToOption(field) {
131
155
  */
132
156
  function extractOptions$1(command) {
133
157
  if (!command.args) return [];
134
- return require_lazy.extractFields(command.args).fields.filter((field) => !field.positional).map(fieldToOption);
158
+ return require_subcommand_router.extractFields(command.args).fields.filter((field) => !field.positional).map(fieldToOption);
135
159
  }
136
160
  /**
137
161
  * Extract positional arguments from a command
138
162
  */
139
163
  function extractPositionals(command) {
140
164
  if (!command.args) return [];
141
- return require_lazy.extractFields(command.args).fields.filter((field) => field.positional);
165
+ return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional);
142
166
  }
143
167
  /**
144
168
  * Extract completable positional arguments from a command
145
169
  */
146
170
  function extractCompletablePositionals(command) {
147
171
  if (!command.args) return [];
148
- return require_lazy.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
172
+ return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
149
173
  name: field.name,
150
174
  cliName: field.cliName,
151
175
  position: index,
@@ -161,7 +185,7 @@ function extractCompletablePositionals(command) {
161
185
  function extractSubcommand(name, command) {
162
186
  const subcommands = [];
163
187
  if (command.subCommands) for (const [subName, subCommand] of Object.entries(command.subCommands)) {
164
- const resolved = require_lazy.resolveSubCommandMeta(subCommand);
188
+ const resolved = require_subcommand_router.resolveSubCommandMeta(subCommand);
165
189
  if (resolved) subcommands.push(extractSubcommand(subName, resolved));
166
190
  else subcommands.push({
167
191
  name: subName,
@@ -174,6 +198,7 @@ function extractSubcommand(name, command) {
174
198
  return {
175
199
  name,
176
200
  description: command.description,
201
+ aliases: command.aliases,
177
202
  subcommands,
178
203
  options: extractOptions$1(command),
179
204
  positionals: extractCompletablePositionals(command)
@@ -195,13 +220,17 @@ function optTakesValueEntries(sub, parentPath) {
195
220
  if (opt.alias) for (const a of opt.alias) patterns.push(`${parentPath}:${a.length === 1 ? `-${a}` : `--${a}`}`);
196
221
  lines.push(` ${patterns.join("|")}) return 0 ;;`);
197
222
  }
198
- for (const child of getVisibleSubs(sub.subcommands)) lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, child.name, ":")));
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
+ }
199
227
  return lines;
200
228
  }
201
229
  /**
202
230
  * Recursively collect all subcommand route entries.
203
231
  * Returns entries used by all shell generators for both dispatch routing
204
232
  * and subcommand lookup (is_subcmd) tables.
233
+ * Aliases are mapped to the same handler as the canonical name.
205
234
  */
206
235
  function collectRouteEntries(sub, parentPath = "", parentFunc = "") {
207
236
  const entries = [];
@@ -214,6 +243,15 @@ function collectRouteEntries(sub, parentPath = "", parentFunc = "") {
214
243
  funcSuffix,
215
244
  lookupPattern: `${parentPath}:${child.name}`
216
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
+ }
217
255
  }
218
256
  return entries;
219
257
  }
@@ -246,7 +284,7 @@ function extractCompletionData(command, programName, globalArgsSchema) {
246
284
  const rootSubcommand = extractSubcommand(programName, command);
247
285
  let globalOptions;
248
286
  if (globalArgsSchema) {
249
- globalOptions = require_lazy.extractFields(globalArgsSchema).fields.filter((field) => !field.positional).map(fieldToOption);
287
+ globalOptions = require_subcommand_router.extractFields(globalArgsSchema).fields.filter((field) => !field.positional).map(fieldToOption);
250
288
  propagateGlobalOptions(rootSubcommand, globalOptions);
251
289
  } else globalOptions = rootSubcommand.options;
252
290
  return {
@@ -256,6 +294,79 @@ function extractCompletionData(command, programName, globalArgsSchema) {
256
294
  };
257
295
  }
258
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
+
259
370
  //#endregion
260
371
  //#region src/completion/bash.ts
261
372
  /** Escape a string for use inside bash double-quotes */
@@ -383,7 +494,9 @@ function availableOptionLines$2(options, fn) {
383
494
  else {
384
495
  const patterns = [`"--${opt.cliName}"`];
385
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}"`);
386
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})`);
387
500
  }
388
501
  lines.push(` __${fn}_not_used "--help" && _avail+=(--help)`);
389
502
  return lines;
@@ -417,7 +530,7 @@ function generateSubHandler$2(sub, fn, path) {
417
530
  lines.push(` return`);
418
531
  lines.push(` fi`);
419
532
  if (visibleSubs.length > 0) {
420
- const subNames = visibleSubs.map((s) => s.name).join(" ");
533
+ const subNames = getSubNamesWithAliases(sub.subcommands).map((s) => s.name).join(" ");
421
534
  lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
422
535
  lines.push(` compopt +o default 2>/dev/null`);
423
536
  } else if (sub.positionals.length > 0) lines.push(...positionalBlock$2(sub.positionals));
@@ -432,7 +545,12 @@ function generateBashCompletion(command, options) {
432
545
  const root = data.command;
433
546
  const visibleSubs = getVisibleSubs(root.subcommands);
434
547
  const lines = [];
435
- lines.push(`# Bash completion for ${programName}`);
548
+ lines.push(...buildHeaderLines({
549
+ programName,
550
+ shell: "bash",
551
+ binPath: options.binPath,
552
+ programVersion: options.programVersion
553
+ }));
436
554
  lines.push(`# Generated by politty`);
437
555
  lines.push(``);
438
556
  lines.push(`__${fn}_not_used() {`);
@@ -479,7 +597,7 @@ function generateBashCompletion(command, options) {
479
597
  lines.push(` compopt +o default 2>/dev/null`);
480
598
  if (visibleSubs.length > 0) {
481
599
  lines.push(` else`);
482
- const subNames = visibleSubs.map((s) => s.name).join(" ");
600
+ const subNames = getSubNamesWithAliases(root.subcommands).map((s) => s.name).join(" ");
483
601
  lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
484
602
  lines.push(` compopt +o default 2>/dev/null`);
485
603
  } else if (root.positionals.length > 0) {
@@ -571,13 +689,21 @@ source ~/.bashrc`
571
689
  * Completion directive flags (bitwise)
572
690
  */
573
691
  const CompletionDirective = {
692
+ /** Default completion behavior */
574
693
  Default: 0,
694
+ /** Don't add space after completion */
575
695
  NoSpace: 1,
696
+ /** Don't offer file completion (even if no other completions) */
576
697
  NoFileCompletion: 2,
698
+ /** Filter completions using current word as prefix */
577
699
  FilterPrefix: 4,
700
+ /** Keep the order of completions */
578
701
  KeepOrder: 8,
702
+ /** Trigger file completion */
579
703
  FileCompletion: 16,
704
+ /** Trigger directory completion */
580
705
  DirectoryCompletion: 32,
706
+ /** Error occurred during completion */
581
707
  Error: 64
582
708
  };
583
709
  /**
@@ -668,8 +794,16 @@ function generateSubcommandCandidates(context) {
668
794
  const candidates = [];
669
795
  let directive = CompletionDirective.FilterPrefix;
670
796
  for (const name of context.subcommands) {
797
+ let description;
671
798
  const sub = context.currentCommand.subCommands?.[name];
672
- const description = sub ? require_lazy.resolveSubCommandMeta(sub)?.description : void 0;
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
+ }
673
807
  candidates.push({
674
808
  value: name,
675
809
  description,
@@ -695,13 +829,21 @@ function generateOptionNameCandidates(context) {
695
829
  if (opt.valueType === "array") return true;
696
830
  if (context.usedOptions.has(opt.cliName)) return false;
697
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;
698
833
  return true;
699
834
  });
700
- for (const opt of availableOptions) candidates.push({
701
- value: `--${opt.cliName}`,
702
- description: opt.description,
703
- type: "option"
704
- });
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
+ }
705
847
  if (!context.usedOptions.has("help")) candidates.push({
706
848
  value: "--help",
707
849
  description: "Show help information",
@@ -763,10 +905,12 @@ function generatePositionalCandidates(context) {
763
905
  */
764
906
  function extractOptions(command) {
765
907
  if (!command.args) return [];
766
- return require_lazy.extractFields(command.args).fields.filter((field) => !field.positional).map((field) => ({
908
+ return require_subcommand_router.extractFields(command.args).fields.filter((field) => !field.positional).map((field) => ({
767
909
  name: field.name,
768
910
  cliName: field.cliName,
769
911
  alias: field.alias,
912
+ negation: field.negationDisplay,
913
+ negationDescription: field.negationDescription,
770
914
  description: field.description,
771
915
  takesValue: field.type !== "boolean",
772
916
  valueType: field.type,
@@ -779,7 +923,7 @@ function extractOptions(command) {
779
923
  */
780
924
  function extractPositionalsForContext(command) {
781
925
  if (!command.args) return [];
782
- return require_lazy.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
926
+ return require_subcommand_router.extractFields(command.args).fields.filter((field) => field.positional).map((field, index) => ({
783
927
  name: field.name,
784
928
  cliName: field.cliName,
785
929
  position: index,
@@ -790,20 +934,29 @@ function extractPositionalsForContext(command) {
790
934
  }));
791
935
  }
792
936
  /**
793
- * Get subcommand names from a command
937
+ * Get subcommand names from a command (including aliases)
794
938
  */
795
939
  function getSubcommandNames(command) {
796
940
  if (!command.subCommands) return [];
797
- return Object.keys(command.subCommands).filter((name) => !name.startsWith("__"));
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;
798
949
  }
799
950
  /**
800
- * Resolve subcommand by name
951
+ * Resolve subcommand by name (including alias lookup)
801
952
  */
802
953
  function resolveSubcommand(command, name) {
803
954
  if (!command.subCommands) return null;
804
955
  const sub = command.subCommands[name];
805
- if (!sub) return null;
806
- return require_lazy.resolveSubCommandMeta(sub);
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;
807
960
  }
808
961
  /**
809
962
  * Check if a word is an option (starts with - or --)
@@ -837,8 +990,12 @@ function findOption(options, nameOrAlias) {
837
990
  if (opt.cliName === nameOrAlias) return true;
838
991
  if (opt.alias?.includes(nameOrAlias)) return true;
839
992
  if (nameOrAlias.length > 1) {
840
- if (opt.cliName.includes("-") && require_lazy.toCamelCase(opt.cliName) === nameOrAlias) return true;
841
- if (opt.alias?.some((a) => a.includes("-") && require_lazy.toCamelCase(a) === nameOrAlias)) return true;
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
+ }
842
999
  }
843
1000
  return false;
844
1001
  });
@@ -871,6 +1028,7 @@ function parseCompletionContext(argv, rootCommand) {
871
1028
  if (opt) {
872
1029
  usedOptions.add(opt.cliName);
873
1030
  if (opt.alias) for (const a of opt.alias) usedOptions.add(a);
1031
+ if (opt.negation) usedOptions.add(opt.negation);
874
1032
  if (opt.takesValue && !hasInlineValue(word)) i++;
875
1033
  }
876
1034
  i++;
@@ -1057,12 +1215,12 @@ function detectInlinePrefix(currentWord) {
1057
1215
  * Schema for the __complete command
1058
1216
  */
1059
1217
  const completeArgsSchema = zod.z.object({
1060
- shell: require_lazy.arg(zod.z.enum([
1218
+ shell: require_subcommand_router.arg(zod.z.enum([
1061
1219
  "bash",
1062
1220
  "zsh",
1063
1221
  "fish"
1064
1222
  ]), { description: "Target shell for output formatting" }),
1065
- args: require_lazy.arg(zod.z.array(zod.z.string()).default([]), {
1223
+ args: require_subcommand_router.arg(zod.z.array(zod.z.string()).default([]), {
1066
1224
  positional: true,
1067
1225
  description: "Arguments to complete",
1068
1226
  variadic: true
@@ -1189,11 +1347,14 @@ function availableOptionLines$1(options, fn) {
1189
1347
  const lines = [];
1190
1348
  for (const opt of options) {
1191
1349
  const desc = escapeDesc$1(opt.description ?? "");
1350
+ const negDesc = opt.negationDescription ? escapeDesc$1(opt.negationDescription) : desc;
1192
1351
  if (opt.valueType === "array") lines.push(` echo "--${opt.cliName}\t${desc}"`);
1193
1352
  else {
1194
1353
  const checks = [`"--${opt.cliName}"`];
1195
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}"`);
1196
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}"`);
1197
1358
  }
1198
1359
  }
1199
1360
  lines.push(` __${fn}_not_used "--help"; and echo "--help\tShow help"`);
@@ -1228,7 +1389,7 @@ function generateSubHandler$1(sub, fn, path) {
1228
1389
  lines.push(...availableOptionLines$1(sub.options, fn));
1229
1390
  lines.push(` return`);
1230
1391
  lines.push(` end`);
1231
- if (visibleSubs.length > 0) for (const s of visibleSubs) {
1392
+ if (visibleSubs.length > 0) for (const s of getSubNamesWithAliases(sub.subcommands)) {
1232
1393
  const desc = escapeDesc$1(s.description ?? "");
1233
1394
  lines.push(` echo "${s.name}\t${desc}"`);
1234
1395
  }
@@ -1249,6 +1410,10 @@ function optTakesValueCases(sub, parentPath) {
1249
1410
  for (const child of getVisibleSubs(sub.subcommands)) {
1250
1411
  const childPath = parentPath ? `${parentPath}:${child.name}` : child.name;
1251
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
+ }
1252
1417
  }
1253
1418
  return lines;
1254
1419
  }
@@ -1259,9 +1424,32 @@ function generateFishCompletion(command, options) {
1259
1424
  const root = data.command;
1260
1425
  const visibleSubs = getVisibleSubs(root.subcommands);
1261
1426
  const lines = [];
1262
- lines.push(`# Fish completion for ${programName}`);
1427
+ lines.push(...buildHeaderLines({
1428
+ programName,
1429
+ shell: "fish",
1430
+ binPath: options.binPath,
1431
+ programVersion: options.programVersion
1432
+ }));
1263
1433
  lines.push(`# Generated by politty`);
1264
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(``);
1265
1453
  lines.push(`function __${fn}_not_used --no-scope-shadowing`);
1266
1454
  lines.push(` for _chk in $argv`);
1267
1455
  lines.push(` if contains -- "$_chk" $_used_opts`);
@@ -1305,7 +1493,7 @@ function generateFishCompletion(command, options) {
1305
1493
  lines.push(...availableOptionLines$1(root.options, fn));
1306
1494
  if (visibleSubs.length > 0) {
1307
1495
  lines.push(` else`);
1308
- for (const s of visibleSubs) {
1496
+ for (const s of getSubNamesWithAliases(root.subcommands)) {
1309
1497
  const desc = escapeDesc$1(s.description ?? "");
1310
1498
  lines.push(` echo "${s.name}\t${desc}"`);
1311
1499
  }
@@ -1387,6 +1575,241 @@ source ~/.config/fish/completions/${programName}.fish`
1387
1575
  };
1388
1576
  }
1389
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
+
1390
1813
  //#endregion
1391
1814
  //#region src/completion/zsh.ts
1392
1815
  function escapeDesc(s) {
@@ -1455,11 +1878,14 @@ function availableOptionLines(options, fn) {
1455
1878
  const lines = [];
1456
1879
  for (const opt of options) {
1457
1880
  const desc = opt.description ? `:${escapeDesc(opt.description)}` : "";
1881
+ const negDesc = opt.negationDescription ? `:${escapeDesc(opt.negationDescription)}` : desc;
1458
1882
  if (opt.valueType === "array") lines.push(` _opts+=("--${opt.cliName}${desc}")`);
1459
1883
  else {
1460
1884
  const patterns = [`"--${opt.cliName}"`];
1461
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}"`);
1462
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}")`);
1463
1889
  }
1464
1890
  }
1465
1891
  lines.push(` __${fn}_not_used "--help" && _opts+=("--help:Show help")`);
@@ -1493,7 +1919,7 @@ function generateSubHandler(sub, fn, path) {
1493
1919
  lines.push(` return 0`);
1494
1920
  lines.push(` fi`);
1495
1921
  if (visibleSubs.length > 0) {
1496
- const subItems = visibleSubs.map((s) => {
1922
+ const subItems = getSubNamesWithAliases(sub.subcommands).map((s) => {
1497
1923
  const desc = s.description ? `:${escapeDesc(s.description)}` : "";
1498
1924
  return `"${s.name}${desc}"`;
1499
1925
  }).join(" ");
@@ -1513,7 +1939,12 @@ function generateZshCompletion(command, options) {
1513
1939
  const lines = [];
1514
1940
  lines.push(`#compdef ${programName}`);
1515
1941
  lines.push(``);
1516
- lines.push(`# Zsh completion for ${programName}`);
1942
+ lines.push(...buildHeaderLines({
1943
+ programName,
1944
+ shell: "zsh",
1945
+ binPath: options.binPath,
1946
+ programVersion: options.programVersion
1947
+ }));
1517
1948
  lines.push(`# Generated by politty`);
1518
1949
  lines.push(``);
1519
1950
  lines.push(`__${fn}_not_used() {`);
@@ -1568,7 +1999,7 @@ function generateZshCompletion(command, options) {
1568
1999
  lines.push(` __${fn}_cdescribe 'options' _opts`);
1569
2000
  if (visibleSubs.length > 0) {
1570
2001
  lines.push(` else`);
1571
- const subItems = visibleSubs.map((s) => {
2002
+ const subItems = getSubNamesWithAliases(root.subcommands).map((s) => {
1572
2003
  const desc = s.description ? `:${escapeDesc(s.description)}` : "";
1573
2004
  return `"${s.name}${desc}"`;
1574
2005
  }).join(" ");
@@ -1697,7 +2128,7 @@ function detectShell() {
1697
2128
  * Schema for the completion command arguments
1698
2129
  */
1699
2130
  const completionArgsSchema = zod.z.object({
1700
- shell: require_lazy.arg(zod.z.enum([
2131
+ shell: require_subcommand_router.arg(zod.z.enum([
1701
2132
  "bash",
1702
2133
  "zsh",
1703
2134
  "fish"
@@ -1706,11 +2137,22 @@ const completionArgsSchema = zod.z.object({
1706
2137
  description: "Shell type (bash, zsh, or fish)",
1707
2138
  placeholder: "SHELL"
1708
2139
  }),
1709
- instructions: require_lazy.arg(zod.z.boolean().default(false), {
2140
+ instructions: require_subcommand_router.arg(zod.z.boolean().default(false), {
1710
2141
  alias: "i",
1711
2142
  description: "Show installation instructions"
1712
- })
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." })
1713
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
+ }) });
1714
2156
  /**
1715
2157
  * Create a completion subcommand for your CLI
1716
2158
  *
@@ -1726,12 +2168,30 @@ const completionArgsSchema = zod.z.object({
1726
2168
  * });
1727
2169
  * ```
1728
2170
  */
1729
- function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
2171
+ function createCompletionCommand(rootCommand, programName, globalArgsSchema, extra = {}) {
1730
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
+ };
1731
2187
  if (!rootCommand.subCommands?.__complete) rootCommand.subCommands = {
1732
2188
  ...rootCommand.subCommands,
1733
2189
  __complete: createDynamicCompleteCommand(rootCommand, resolvedProgramName)
1734
2190
  };
2191
+ if (!rootCommand.subCommands?.["__refresh-completion"]) rootCommand.subCommands = {
2192
+ ...rootCommand.subCommands,
2193
+ "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
2194
+ };
1735
2195
  return defineCommand({
1736
2196
  name: "completion",
1737
2197
  description: "Generate shell completion script",
@@ -1743,11 +2203,43 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1743
2203
  process.exitCode = 1;
1744
2204
  return;
1745
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
+ }
1746
2236
  const result = generateCompletion(rootCommand, {
1747
2237
  shell: shellType,
1748
2238
  programName: resolvedProgramName,
1749
2239
  includeDescriptions: true,
1750
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2240
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
2241
+ ...programVersion !== void 0 && { programVersion },
2242
+ ...cacheDir !== void 0 && { cacheDir }
1751
2243
  });
1752
2244
  if (args.instructions) console.log(result.installInstructions);
1753
2245
  else console.log(result.script);
@@ -1755,6 +2247,25 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1755
2247
  });
1756
2248
  }
1757
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
+ /**
1758
2269
  * Wrap a command with a completion subcommand
1759
2270
  *
1760
2271
  * This avoids circular references that occur when a command references itself
@@ -1775,15 +2286,53 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1775
2286
  * ```
1776
2287
  */
1777
2288
  function withCompletionCommand(command, options) {
1778
- const { programName, globalArgsSchema } = typeof options === "string" ? { programName: options } : 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
+ };
1779
2296
  const wrappedCommand = { ...command };
1780
2297
  wrappedCommand.subCommands = {
1781
2298
  ...command.subCommands,
1782
- completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema),
1783
- __complete: createDynamicCompleteCommand(wrappedCommand, programName)
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
+ });
1784
2308
  };
1785
2309
  return wrappedCommand;
1786
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
+ }
1787
2336
 
1788
2337
  //#endregion
1789
2338
  Object.defineProperty(exports, 'CompletionDirective', {
@@ -1810,6 +2359,12 @@ Object.defineProperty(exports, 'createDynamicCompleteCommand', {
1810
2359
  return createDynamicCompleteCommand;
1811
2360
  }
1812
2361
  });
2362
+ Object.defineProperty(exports, 'createRefreshCompletionCommand', {
2363
+ enumerable: true,
2364
+ get: function () {
2365
+ return createRefreshCompletionCommand;
2366
+ }
2367
+ });
1813
2368
  Object.defineProperty(exports, 'defineCommand', {
1814
2369
  enumerable: true,
1815
2370
  get: function () {
@@ -1882,4 +2437,4 @@ Object.defineProperty(exports, 'withCompletionCommand', {
1882
2437
  return withCompletionCommand;
1883
2438
  }
1884
2439
  });
1885
- //# sourceMappingURL=completion-CAekGYS4.cjs.map
2440
+ //# sourceMappingURL=completion-BlZxMSeU.cjs.map