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,12 +1,15 @@
1
- import { c as toCamelCase, i as extractFields, r as resolveSubCommandMeta, u as arg } from "./lazy-DiMJSDMB.js";
1
+ import { c as resolveSubCommandMeta, h as arg, i as resolveSubCommandAlias, l as extractFields, p as toCamelCase } from "./subcommand-router-XZBWe8HN.js";
2
2
  import { z } from "zod";
3
- import { execSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { execSync, spawn } from "node:child_process";
4
6
 
5
7
  //#region src/core/command.ts
6
8
  function defineCommand(config) {
7
9
  return {
8
10
  name: config.name,
9
11
  description: config.description,
12
+ aliases: config.aliases,
10
13
  args: config.args,
11
14
  subCommands: config.subCommands,
12
15
  setup: config.setup,
@@ -111,6 +114,25 @@ function getVisibleSubs(subs) {
111
114
  return subs.filter((s) => !s.name.startsWith("__"));
112
115
  }
113
116
  /**
117
+ * Get all completable subcommand names including aliases.
118
+ * Returns an array of { name, description } for all visible subcommands
119
+ * and their aliases.
120
+ */
121
+ function getSubNamesWithAliases(subs) {
122
+ const result = [];
123
+ for (const sub of getVisibleSubs(subs)) {
124
+ result.push({
125
+ name: sub.name,
126
+ description: sub.description
127
+ });
128
+ if (sub.aliases) for (const alias of sub.aliases) result.push({
129
+ name: alias,
130
+ description: sub.description
131
+ });
132
+ }
133
+ return result;
134
+ }
135
+ /**
114
136
  * Convert a resolved field to a completable option
115
137
  */
116
138
  function fieldToOption(field) {
@@ -118,6 +140,8 @@ function fieldToOption(field) {
118
140
  name: field.name,
119
141
  cliName: field.cliName,
120
142
  alias: field.alias,
143
+ negation: field.negationDisplay,
144
+ negationDescription: field.negationDescription,
121
145
  description: field.description,
122
146
  takesValue: field.type !== "boolean",
123
147
  valueType: field.type,
@@ -173,6 +197,7 @@ function extractSubcommand(name, command) {
173
197
  return {
174
198
  name,
175
199
  description: command.description,
200
+ aliases: command.aliases,
176
201
  subcommands,
177
202
  options: extractOptions$1(command),
178
203
  positionals: extractCompletablePositionals(command)
@@ -194,13 +219,17 @@ function optTakesValueEntries(sub, parentPath) {
194
219
  if (opt.alias) for (const a of opt.alias) patterns.push(`${parentPath}:${a.length === 1 ? `-${a}` : `--${a}`}`);
195
220
  lines.push(` ${patterns.join("|")}) return 0 ;;`);
196
221
  }
197
- for (const child of getVisibleSubs(sub.subcommands)) lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, child.name, ":")));
222
+ for (const child of getVisibleSubs(sub.subcommands)) {
223
+ lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, child.name, ":")));
224
+ if (child.aliases) for (const alias of child.aliases) lines.push(...optTakesValueEntries(child, joinPrefix(parentPath, alias, ":")));
225
+ }
198
226
  return lines;
199
227
  }
200
228
  /**
201
229
  * Recursively collect all subcommand route entries.
202
230
  * Returns entries used by all shell generators for both dispatch routing
203
231
  * and subcommand lookup (is_subcmd) tables.
232
+ * Aliases are mapped to the same handler as the canonical name.
204
233
  */
205
234
  function collectRouteEntries(sub, parentPath = "", parentFunc = "") {
206
235
  const entries = [];
@@ -213,6 +242,15 @@ function collectRouteEntries(sub, parentPath = "", parentFunc = "") {
213
242
  funcSuffix,
214
243
  lookupPattern: `${parentPath}:${child.name}`
215
244
  });
245
+ if (child.aliases) for (const alias of child.aliases) {
246
+ const aliasPathStr = joinPrefix(parentPath, alias, ":");
247
+ entries.push(...collectRouteEntries(child, aliasPathStr, funcSuffix));
248
+ entries.push({
249
+ pathStr: aliasPathStr,
250
+ funcSuffix,
251
+ lookupPattern: `${parentPath}:${alias}`
252
+ });
253
+ }
216
254
  }
217
255
  return entries;
218
256
  }
@@ -255,6 +293,79 @@ function extractCompletionData(command, programName, globalArgsSchema) {
255
293
  };
256
294
  }
257
295
 
296
+ //#endregion
297
+ //#region src/completion/header.ts
298
+ /**
299
+ * Static-script header utilities.
300
+ *
301
+ * Every completion script generated by politty starts with a small
302
+ * machine-readable header. The rc loader and the runMain background
303
+ * refresh path use the `# politty-bin-sig:` line to detect when the
304
+ * cached script is stale relative to the binary on disk.
305
+ */
306
+ /** Schema version of the header itself. Bump when the header layout changes. */
307
+ const COMPLETION_VERSION = 1;
308
+ /**
309
+ * Read the binary's mtime in whole seconds (matches POSIX `stat -c %Y` /
310
+ * BSD `stat -f %m`). Returns `"0"` on failure so the header is always
311
+ * well-formed.
312
+ */
313
+ function computeBinSig(binPath) {
314
+ try {
315
+ return Math.floor(statSync(binPath).mtimeMs / 1e3).toString();
316
+ } catch {
317
+ return "0";
318
+ }
319
+ }
320
+ /**
321
+ * Walk `$PATH` looking for an executable named `programName`. Returns
322
+ * the first match's full path, or `null` when not found. We mirror the
323
+ * shell's `command -v <prog>` here so the sig embedded in the header
324
+ * (computed by Node) lines up with what the rc loader stat-checks at
325
+ * runtime — including pnpm/npm bin shims that wrap the real entrypoint.
326
+ * Without this alignment, shimmed installs would never match the
327
+ * embedded sig and the cache would regenerate on every shell startup.
328
+ */
329
+ function findOnPath(programName) {
330
+ if (!programName || /[/\\\0]/.test(programName)) return null;
331
+ const path = process.env.PATH ?? "";
332
+ for (const dir of path.split(":")) {
333
+ if (!dir) continue;
334
+ const candidate = join(dir, programName);
335
+ try {
336
+ if (statSync(candidate).isFile()) return candidate;
337
+ } catch {}
338
+ }
339
+ return null;
340
+ }
341
+ /**
342
+ * Resolve the binary path used for sig computation and stat checks.
343
+ *
344
+ * Order: explicit override → `$PATH` lookup of `programName` → `process.argv[1]`.
345
+ * The `$PATH` lookup keeps Node-side and shell-side stats pointed at the
346
+ * same shim file when the CLI is invoked through a package-manager bin shim.
347
+ */
348
+ function resolveBinPath(programName, override) {
349
+ if (override) return override;
350
+ return findOnPath(programName) ?? process.argv[1] ?? "";
351
+ }
352
+ /**
353
+ * Build the header lines (no trailing blank line). Returned without a
354
+ * leading `#!` so each generator can prepend its own shebang/compdef
355
+ * marker.
356
+ */
357
+ function buildHeaderLines(opts) {
358
+ const sig = computeBinSig(resolveBinPath(opts.programName, opts.binPath));
359
+ const lines = [
360
+ `# politty-completion-version: ${1}`,
361
+ `# politty-bin-sig: ${sig}`,
362
+ `# program: ${opts.programName}`
363
+ ];
364
+ if (opts.programVersion) lines.push(`# program-version: ${opts.programVersion}`);
365
+ lines.push(`# shell: ${opts.shell}`);
366
+ return lines;
367
+ }
368
+
258
369
  //#endregion
259
370
  //#region src/completion/bash.ts
260
371
  /** Escape a string for use inside bash double-quotes */
@@ -382,7 +493,9 @@ function availableOptionLines$2(options, fn) {
382
493
  else {
383
494
  const patterns = [`"--${opt.cliName}"`];
384
495
  if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
496
+ if (opt.negation) patterns.push(`"--${opt.negation}"`);
385
497
  lines.push(` __${fn}_not_used ${patterns.join(" ")} && _avail+=(--${opt.cliName})`);
498
+ if (opt.negation) lines.push(` __${fn}_not_used ${patterns.join(" ")} && _avail+=(--${opt.negation})`);
386
499
  }
387
500
  lines.push(` __${fn}_not_used "--help" && _avail+=(--help)`);
388
501
  return lines;
@@ -416,7 +529,7 @@ function generateSubHandler$2(sub, fn, path) {
416
529
  lines.push(` return`);
417
530
  lines.push(` fi`);
418
531
  if (visibleSubs.length > 0) {
419
- const subNames = visibleSubs.map((s) => s.name).join(" ");
532
+ const subNames = getSubNamesWithAliases(sub.subcommands).map((s) => s.name).join(" ");
420
533
  lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
421
534
  lines.push(` compopt +o default 2>/dev/null`);
422
535
  } else if (sub.positionals.length > 0) lines.push(...positionalBlock$2(sub.positionals));
@@ -431,7 +544,12 @@ function generateBashCompletion(command, options) {
431
544
  const root = data.command;
432
545
  const visibleSubs = getVisibleSubs(root.subcommands);
433
546
  const lines = [];
434
- lines.push(`# Bash completion for ${programName}`);
547
+ lines.push(...buildHeaderLines({
548
+ programName,
549
+ shell: "bash",
550
+ binPath: options.binPath,
551
+ programVersion: options.programVersion
552
+ }));
435
553
  lines.push(`# Generated by politty`);
436
554
  lines.push(``);
437
555
  lines.push(`__${fn}_not_used() {`);
@@ -478,7 +596,7 @@ function generateBashCompletion(command, options) {
478
596
  lines.push(` compopt +o default 2>/dev/null`);
479
597
  if (visibleSubs.length > 0) {
480
598
  lines.push(` else`);
481
- const subNames = visibleSubs.map((s) => s.name).join(" ");
599
+ const subNames = getSubNamesWithAliases(root.subcommands).map((s) => s.name).join(" ");
482
600
  lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
483
601
  lines.push(` compopt +o default 2>/dev/null`);
484
602
  } else if (root.positionals.length > 0) {
@@ -570,13 +688,21 @@ source ~/.bashrc`
570
688
  * Completion directive flags (bitwise)
571
689
  */
572
690
  const CompletionDirective = {
691
+ /** Default completion behavior */
573
692
  Default: 0,
693
+ /** Don't add space after completion */
574
694
  NoSpace: 1,
695
+ /** Don't offer file completion (even if no other completions) */
575
696
  NoFileCompletion: 2,
697
+ /** Filter completions using current word as prefix */
576
698
  FilterPrefix: 4,
699
+ /** Keep the order of completions */
577
700
  KeepOrder: 8,
701
+ /** Trigger file completion */
578
702
  FileCompletion: 16,
703
+ /** Trigger directory completion */
579
704
  DirectoryCompletion: 32,
705
+ /** Error occurred during completion */
580
706
  Error: 64
581
707
  };
582
708
  /**
@@ -667,8 +793,16 @@ function generateSubcommandCandidates(context) {
667
793
  const candidates = [];
668
794
  let directive = CompletionDirective.FilterPrefix;
669
795
  for (const name of context.subcommands) {
796
+ let description;
670
797
  const sub = context.currentCommand.subCommands?.[name];
671
- const description = sub ? resolveSubCommandMeta(sub)?.description : void 0;
798
+ if (sub) description = resolveSubCommandMeta(sub)?.description;
799
+ else {
800
+ const canonical = resolveSubCommandAlias(context.currentCommand, name);
801
+ if (canonical) {
802
+ const resolved = context.currentCommand.subCommands?.[canonical];
803
+ if (resolved) description = resolveSubCommandMeta(resolved)?.description;
804
+ }
805
+ }
672
806
  candidates.push({
673
807
  value: name,
674
808
  description,
@@ -694,13 +828,21 @@ function generateOptionNameCandidates(context) {
694
828
  if (opt.valueType === "array") return true;
695
829
  if (context.usedOptions.has(opt.cliName)) return false;
696
830
  if (opt.alias && opt.alias.some((a) => context.usedOptions.has(a))) return false;
831
+ if (opt.negation && context.usedOptions.has(opt.negation)) return false;
697
832
  return true;
698
833
  });
699
- for (const opt of availableOptions) candidates.push({
700
- value: `--${opt.cliName}`,
701
- description: opt.description,
702
- type: "option"
703
- });
834
+ for (const opt of availableOptions) {
835
+ candidates.push({
836
+ value: `--${opt.cliName}`,
837
+ description: opt.description,
838
+ type: "option"
839
+ });
840
+ if (opt.negation) candidates.push({
841
+ value: `--${opt.negation}`,
842
+ description: opt.negationDescription ?? opt.description,
843
+ type: "option"
844
+ });
845
+ }
704
846
  if (!context.usedOptions.has("help")) candidates.push({
705
847
  value: "--help",
706
848
  description: "Show help information",
@@ -766,6 +908,8 @@ function extractOptions(command) {
766
908
  name: field.name,
767
909
  cliName: field.cliName,
768
910
  alias: field.alias,
911
+ negation: field.negationDisplay,
912
+ negationDescription: field.negationDescription,
769
913
  description: field.description,
770
914
  takesValue: field.type !== "boolean",
771
915
  valueType: field.type,
@@ -789,20 +933,29 @@ function extractPositionalsForContext(command) {
789
933
  }));
790
934
  }
791
935
  /**
792
- * Get subcommand names from a command
936
+ * Get subcommand names from a command (including aliases)
793
937
  */
794
938
  function getSubcommandNames(command) {
795
939
  if (!command.subCommands) return [];
796
- return Object.keys(command.subCommands).filter((name) => !name.startsWith("__"));
940
+ const names = [];
941
+ for (const [name, subCmd] of Object.entries(command.subCommands)) {
942
+ if (name.startsWith("__")) continue;
943
+ names.push(name);
944
+ const meta = resolveSubCommandMeta(subCmd);
945
+ if (meta?.aliases) names.push(...meta.aliases);
946
+ }
947
+ return names;
797
948
  }
798
949
  /**
799
- * Resolve subcommand by name
950
+ * Resolve subcommand by name (including alias lookup)
800
951
  */
801
952
  function resolveSubcommand(command, name) {
802
953
  if (!command.subCommands) return null;
803
954
  const sub = command.subCommands[name];
804
- if (!sub) return null;
805
- return resolveSubCommandMeta(sub);
955
+ if (sub) return resolveSubCommandMeta(sub);
956
+ const canonical = resolveSubCommandAlias(command, name);
957
+ if (canonical) return resolveSubCommandMeta(command.subCommands[canonical]);
958
+ return null;
806
959
  }
807
960
  /**
808
961
  * Check if a word is an option (starts with - or --)
@@ -838,6 +991,10 @@ function findOption(options, nameOrAlias) {
838
991
  if (nameOrAlias.length > 1) {
839
992
  if (opt.cliName.includes("-") && toCamelCase(opt.cliName) === nameOrAlias) return true;
840
993
  if (opt.alias?.some((a) => a.includes("-") && toCamelCase(a) === nameOrAlias)) return true;
994
+ if (opt.negation) {
995
+ if (opt.negation === nameOrAlias) return true;
996
+ if (opt.negation.includes("-") && toCamelCase(opt.negation) === nameOrAlias) return true;
997
+ }
841
998
  }
842
999
  return false;
843
1000
  });
@@ -870,6 +1027,7 @@ function parseCompletionContext(argv, rootCommand) {
870
1027
  if (opt) {
871
1028
  usedOptions.add(opt.cliName);
872
1029
  if (opt.alias) for (const a of opt.alias) usedOptions.add(a);
1030
+ if (opt.negation) usedOptions.add(opt.negation);
873
1031
  if (opt.takesValue && !hasInlineValue(word)) i++;
874
1032
  }
875
1033
  i++;
@@ -1188,11 +1346,14 @@ function availableOptionLines$1(options, fn) {
1188
1346
  const lines = [];
1189
1347
  for (const opt of options) {
1190
1348
  const desc = escapeDesc$1(opt.description ?? "");
1349
+ const negDesc = opt.negationDescription ? escapeDesc$1(opt.negationDescription) : desc;
1191
1350
  if (opt.valueType === "array") lines.push(` echo "--${opt.cliName}\t${desc}"`);
1192
1351
  else {
1193
1352
  const checks = [`"--${opt.cliName}"`];
1194
1353
  if (opt.alias) for (const a of opt.alias) checks.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
1354
+ if (opt.negation) checks.push(`"--${opt.negation}"`);
1195
1355
  lines.push(` __${fn}_not_used ${checks.join(" ")}; and echo "--${opt.cliName}\t${desc}"`);
1356
+ if (opt.negation) lines.push(` __${fn}_not_used ${checks.join(" ")}; and echo "--${opt.negation}\t${negDesc}"`);
1196
1357
  }
1197
1358
  }
1198
1359
  lines.push(` __${fn}_not_used "--help"; and echo "--help\tShow help"`);
@@ -1227,7 +1388,7 @@ function generateSubHandler$1(sub, fn, path) {
1227
1388
  lines.push(...availableOptionLines$1(sub.options, fn));
1228
1389
  lines.push(` return`);
1229
1390
  lines.push(` end`);
1230
- if (visibleSubs.length > 0) for (const s of visibleSubs) {
1391
+ if (visibleSubs.length > 0) for (const s of getSubNamesWithAliases(sub.subcommands)) {
1231
1392
  const desc = escapeDesc$1(s.description ?? "");
1232
1393
  lines.push(` echo "${s.name}\t${desc}"`);
1233
1394
  }
@@ -1248,6 +1409,10 @@ function optTakesValueCases(sub, parentPath) {
1248
1409
  for (const child of getVisibleSubs(sub.subcommands)) {
1249
1410
  const childPath = parentPath ? `${parentPath}:${child.name}` : child.name;
1250
1411
  lines.push(...optTakesValueCases(child, childPath));
1412
+ if (child.aliases) for (const alias of child.aliases) {
1413
+ const aliasPath = parentPath ? `${parentPath}:${alias}` : alias;
1414
+ lines.push(...optTakesValueCases(child, aliasPath));
1415
+ }
1251
1416
  }
1252
1417
  return lines;
1253
1418
  }
@@ -1258,9 +1423,32 @@ function generateFishCompletion(command, options) {
1258
1423
  const root = data.command;
1259
1424
  const visibleSubs = getVisibleSubs(root.subcommands);
1260
1425
  const lines = [];
1261
- lines.push(`# Fish completion for ${programName}`);
1426
+ lines.push(...buildHeaderLines({
1427
+ programName,
1428
+ shell: "fish",
1429
+ binPath: options.binPath,
1430
+ programVersion: options.programVersion
1431
+ }));
1262
1432
  lines.push(`# Generated by politty`);
1263
1433
  lines.push(``);
1434
+ const sig = computeBinSig(resolveBinPath(programName, options.binPath));
1435
+ const refreshFn = `__${fn}_refresh_completion`;
1436
+ lines.push(`function ${refreshFn} --no-scope-shadowing`);
1437
+ lines.push(` set -l _bin (command -v ${programName})`);
1438
+ lines.push(` test -z "$_bin"; and return 1`);
1439
+ lines.push(` set -l _sig (stat -L -c '%Y' "$_bin" 2>/dev/null; or stat -L -f '%m' "$_bin" 2>/dev/null)`);
1440
+ lines.push(` test "$_sig" = "${sig}"; and return 1`);
1441
+ lines.push(` set -l _target "$__fish_config_dir/completions/${programName}.fish"`);
1442
+ lines.push(` "$_bin" __refresh-completion fish 2>/dev/null`);
1443
+ lines.push(` and source "$_target" 2>/dev/null`);
1444
+ lines.push(` and return 0`);
1445
+ lines.push(` return 1`);
1446
+ lines.push(`end`);
1447
+ lines.push(`${refreshFn}`);
1448
+ lines.push(`set -l _politty_refreshed $status`);
1449
+ lines.push(`functions -e ${refreshFn}`);
1450
+ lines.push(`test $_politty_refreshed -eq 0; and return`);
1451
+ lines.push(``);
1264
1452
  lines.push(`function __${fn}_not_used --no-scope-shadowing`);
1265
1453
  lines.push(` for _chk in $argv`);
1266
1454
  lines.push(` if contains -- "$_chk" $_used_opts`);
@@ -1304,7 +1492,7 @@ function generateFishCompletion(command, options) {
1304
1492
  lines.push(...availableOptionLines$1(root.options, fn));
1305
1493
  if (visibleSubs.length > 0) {
1306
1494
  lines.push(` else`);
1307
- for (const s of visibleSubs) {
1495
+ for (const s of getSubNamesWithAliases(root.subcommands)) {
1308
1496
  const desc = escapeDesc$1(s.description ?? "");
1309
1497
  lines.push(` echo "${s.name}\t${desc}"`);
1310
1498
  }
@@ -1386,6 +1574,241 @@ source ~/.config/fish/completions/${programName}.fish`
1386
1574
  };
1387
1575
  }
1388
1576
 
1577
+ //#endregion
1578
+ //#region src/completion/loader.ts
1579
+ /**
1580
+ * Rc-loader generators (bash / zsh).
1581
+ *
1582
+ * These produce the small snippet a user adds once to `~/.bashrc` or
1583
+ * `~/.zshrc`. The snippet:
1584
+ *
1585
+ * 1. Looks up the binary on $PATH.
1586
+ * 2. Reads its mtime.
1587
+ * 3. If the on-disk completion cache is missing or its
1588
+ * `# politty-bin-sig:` header differs, regenerates the cache by
1589
+ * spawning the binary once.
1590
+ * 4. Sources the cache.
1591
+ *
1592
+ * All failure modes are silent no-ops so a broken / missing CLI never
1593
+ * blocks shell startup.
1594
+ */
1595
+ /**
1596
+ * Single-quote escape: `'` -> `'\''`. Inside single quotes the shell
1597
+ * performs no expansion at all, so `$`, backticks, and `$(...)` are
1598
+ * inert. Used for hardcoded paths because callers may sources them
1599
+ * from env / config — we must not let metachars in the path execute as
1600
+ * commands when the rc snippet is sourced.
1601
+ */
1602
+ function shSingleQuote(s) {
1603
+ return `'${s.replace(/'/g, "'\\''")}'`;
1604
+ }
1605
+ function bashCachePathExpr(programName, cacheDir, shell) {
1606
+ if (cacheDir) return shSingleQuote(`${cacheDir}/completion.${shell}`);
1607
+ return `"\${XDG_CACHE_HOME:-$HOME/.cache}/${programName}/completion.${shell}"`;
1608
+ }
1609
+ function generateBashLoader(opts) {
1610
+ const fn = sanitize(opts.programName);
1611
+ const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "bash");
1612
+ return `__${fn}_load_completion() {
1613
+ local _bin _cache _sig _hdr
1614
+ _bin=$(type -P ${opts.programName} 2>/dev/null)
1615
+ [[ -n "$_bin" ]] || return 0
1616
+ _cache=${cache}
1617
+ _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
1618
+ _hdr="# politty-bin-sig: $_sig"
1619
+ if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
1620
+ # Use the hidden __refresh-completion subcommand instead of
1621
+ # \`$_bin completion bash\`: the foreground completion command
1622
+ # is subject to user setup/cleanup/prompt and required
1623
+ # globalArgs validation, which can silently fail or block when
1624
+ # invoked from rc; runMain bypasses those for __-prefixed
1625
+ # internal subcommands.
1626
+ "$_bin" __refresh-completion bash 2>/dev/null
1627
+ fi
1628
+ # If regen failed but a stale cache survived from a previous run,
1629
+ # source it anyway — a stale completion is preferable to no
1630
+ # completion at all.
1631
+ [[ -f "$_cache" ]] || return 0
1632
+ # shellcheck disable=SC1090
1633
+ source "$_cache"
1634
+ }
1635
+ __${fn}_load_completion
1636
+ unset -f __${fn}_load_completion
1637
+ `;
1638
+ }
1639
+ function generateZshLoader(opts) {
1640
+ const fn = sanitize(opts.programName);
1641
+ const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "zsh");
1642
+ return `__${fn}_load_completion() {
1643
+ emulate -L zsh
1644
+ setopt local_options no_aliases
1645
+ local _bin _cache _sig _hdr
1646
+ _bin=$(whence -p ${opts.programName} 2>/dev/null)
1647
+ [[ -n "$_bin" ]] || return 0
1648
+ _cache=${cache}
1649
+ _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
1650
+ _hdr="# politty-bin-sig: $_sig"
1651
+ if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
1652
+ # See bash loader for why we use __refresh-completion instead
1653
+ # of \`$_bin completion zsh\`.
1654
+ "$_bin" __refresh-completion zsh 2>/dev/null
1655
+ fi
1656
+ # See bash loader: keep stale completion over no completion.
1657
+ [[ -f "$_cache" ]] || return 0
1658
+ source "$_cache"
1659
+ }
1660
+ __${fn}_load_completion
1661
+ unfunction __${fn}_load_completion
1662
+ `;
1663
+ }
1664
+ /**
1665
+ * Build the rc-loader snippet for bash or zsh. Fish doesn't have an
1666
+ * rc-loader; instead, `<program> completion fish --install` writes a
1667
+ * self-rewriting autoload file.
1668
+ */
1669
+ function generateLoader(opts) {
1670
+ switch (opts.shell) {
1671
+ case "bash": return generateBashLoader(opts);
1672
+ case "zsh": return generateZshLoader(opts);
1673
+ 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.");
1674
+ }
1675
+ }
1676
+ /**
1677
+ * Default cache file path (used by `completion <bash|zsh> --install`
1678
+ * and the `__refresh-completion` subcommand). For fish, the install
1679
+ * path is `$__fish_config_dir/completions/<program>.fish` and is
1680
+ * computed inside `installPath()` instead.
1681
+ */
1682
+ function defaultCacheDir(programName) {
1683
+ return `${process.env.XDG_CACHE_HOME ?? `${process.env.HOME ?? ""}/.cache`}/${programName}`;
1684
+ }
1685
+
1686
+ //#endregion
1687
+ //#region src/completion/install.ts
1688
+ /**
1689
+ * On-disk install + refresh helpers.
1690
+ *
1691
+ * `install` writes the generated script to its canonical cache /
1692
+ * autoload path. `refresh` is the body of the `__refresh-completion`
1693
+ * hidden subcommand and the runMain background hook — it regenerates
1694
+ * the cache only when the binary's mtime no longer matches the
1695
+ * embedded `# politty-bin-sig:` header.
1696
+ *
1697
+ * All file I/O is best-effort: failures fall through silently. A stale
1698
+ * (or missing) cache is preferable to crashing the user's shell.
1699
+ */
1700
+ /**
1701
+ * Resolve where a script for the given shell should live on disk.
1702
+ *
1703
+ * - bash/zsh: `<cacheDir>/completion.<shell>` — sourced by the rc loader.
1704
+ * - fish: `$__fish_config_dir/completions/<program>.fish` — autoloaded
1705
+ * by fish on TAB. We approximate `$__fish_config_dir` from
1706
+ * `$XDG_CONFIG_HOME` / `$HOME`.
1707
+ */
1708
+ function installPath(programName, shell, cacheDir) {
1709
+ if (shell === "fish") return join(process.env.XDG_CONFIG_HOME ?? `${process.env.HOME ?? ""}/.config`, "fish", "completions", `${programName}.fish`);
1710
+ return join(cacheDir ?? defaultCacheDir(programName), `completion.${shell}`);
1711
+ }
1712
+ /** Atomic write: tmp file in the same dir, then rename. */
1713
+ function writeAtomic(path, content) {
1714
+ mkdirSync(dirname(path), { recursive: true });
1715
+ const tmp = `${path}.tmp.${process.pid}`;
1716
+ writeFileSync(tmp, content);
1717
+ renameSync(tmp, path);
1718
+ }
1719
+ function generateScript(ctx, shell) {
1720
+ return generateCompletion(ctx.rootCommand, {
1721
+ shell,
1722
+ programName: ctx.programName,
1723
+ includeDescriptions: true,
1724
+ ...ctx.programVersion !== void 0 && { programVersion: ctx.programVersion },
1725
+ ...ctx.binPath !== void 0 && { binPath: ctx.binPath },
1726
+ ...ctx.cacheDir !== void 0 && { cacheDir: ctx.cacheDir },
1727
+ ...ctx.globalArgsSchema !== void 0 && { globalArgsSchema: ctx.globalArgsSchema }
1728
+ }).script;
1729
+ }
1730
+ /** Write the script for `shell` to its install path. Returns the path. */
1731
+ function install(ctx, shell) {
1732
+ const target = installPath(ctx.programName, shell, ctx.cacheDir);
1733
+ writeAtomic(target, generateScript(ctx, shell));
1734
+ return target;
1735
+ }
1736
+ /**
1737
+ * Read the first ~5 lines of an existing cache file and return its
1738
+ * embedded bin-sig. Returns `null` when the file is missing, unreadable,
1739
+ * or doesn't have a sig header.
1740
+ */
1741
+ function readCachedSig(path) {
1742
+ try {
1743
+ if (!existsSync(path)) return null;
1744
+ const m = readFileSync(path, "utf8").split("\n", 6).join("\n").match(/^# politty-bin-sig: (\S+)/m);
1745
+ return m ? m[1] : null;
1746
+ } catch {
1747
+ return null;
1748
+ }
1749
+ }
1750
+ /**
1751
+ * Rewrite the cache only when stale. Used by:
1752
+ * - `<program> __refresh-completion <shell>` (the hidden subcommand
1753
+ * spawned both by the rc loader and by the runMain background hook)
1754
+ *
1755
+ * Caller is responsible for gating: the runMain hook (`maybeSpawnRefresh`)
1756
+ * checks `hasManagedCache` before spawning so we don't silently create
1757
+ * a fish autoload the user never opted into. The rc loader / fish
1758
+ * autoload only run after the user has installed completion in the
1759
+ * first place, so they're allowed to refresh unconditionally.
1760
+ *
1761
+ * Must never throw — a stale completion is fine, a crash isn't.
1762
+ */
1763
+ function refreshIfStale(ctx, shell) {
1764
+ try {
1765
+ const target = installPath(ctx.programName, shell, ctx.cacheDir);
1766
+ const binPath = resolveBinPath(ctx.programName, ctx.binPath);
1767
+ if (!binPath) return;
1768
+ let currentSig;
1769
+ try {
1770
+ currentSig = Math.floor(statSync(binPath).mtimeMs / 1e3).toString();
1771
+ } catch {
1772
+ return;
1773
+ }
1774
+ if (readCachedSig(target) === currentSig) return;
1775
+ writeAtomic(target, generateScript(ctx, shell));
1776
+ } catch {}
1777
+ }
1778
+ /**
1779
+ * Returns true when a politty-managed cache file already exists on disk
1780
+ * for the given shell — i.e. the user has installed completion via
1781
+ * `<program> completion <shell> --install` or the rc loader has already
1782
+ * sourced one. Used by the runMain background hook to avoid spawning
1783
+ * the refresher (and thereby silently creating files) on plain CLI runs
1784
+ * the user never opted into.
1785
+ */
1786
+ function hasManagedCache(ctx, shell) {
1787
+ return readCachedSig(installPath(ctx.programName, shell, ctx.cacheDir)) !== null;
1788
+ }
1789
+ /**
1790
+ * Spawn a detached child process that runs `<program> __refresh-completion <shell>`.
1791
+ * The child is fully decoupled (`stdio: "ignore"` + `unref()`), so it
1792
+ * outlives the parent without holding any handles.
1793
+ *
1794
+ * Caller is expected to gate this on the right conditions (interactive
1795
+ * shell, not running inside `__complete` itself, etc.).
1796
+ *
1797
+ * Returns `void` and never throws — even spawn failures are absorbed.
1798
+ */
1799
+ function spawnBackgroundRefresh(programArgv0, shell) {
1800
+ try {
1801
+ spawn(process.execPath, [
1802
+ programArgv0,
1803
+ "__refresh-completion",
1804
+ shell
1805
+ ], {
1806
+ detached: true,
1807
+ stdio: "ignore"
1808
+ }).unref();
1809
+ } catch {}
1810
+ }
1811
+
1389
1812
  //#endregion
1390
1813
  //#region src/completion/zsh.ts
1391
1814
  function escapeDesc(s) {
@@ -1454,11 +1877,14 @@ function availableOptionLines(options, fn) {
1454
1877
  const lines = [];
1455
1878
  for (const opt of options) {
1456
1879
  const desc = opt.description ? `:${escapeDesc(opt.description)}` : "";
1880
+ const negDesc = opt.negationDescription ? `:${escapeDesc(opt.negationDescription)}` : desc;
1457
1881
  if (opt.valueType === "array") lines.push(` _opts+=("--${opt.cliName}${desc}")`);
1458
1882
  else {
1459
1883
  const patterns = [`"--${opt.cliName}"`];
1460
1884
  if (opt.alias) for (const a of opt.alias) patterns.push(a.length === 1 ? `"-${a}"` : `"--${a}"`);
1885
+ if (opt.negation) patterns.push(`"--${opt.negation}"`);
1461
1886
  lines.push(` __${fn}_not_used ${patterns.join(" ")} && _opts+=("--${opt.cliName}${desc}")`);
1887
+ if (opt.negation) lines.push(` __${fn}_not_used ${patterns.join(" ")} && _opts+=("--${opt.negation}${negDesc}")`);
1462
1888
  }
1463
1889
  }
1464
1890
  lines.push(` __${fn}_not_used "--help" && _opts+=("--help:Show help")`);
@@ -1492,7 +1918,7 @@ function generateSubHandler(sub, fn, path) {
1492
1918
  lines.push(` return 0`);
1493
1919
  lines.push(` fi`);
1494
1920
  if (visibleSubs.length > 0) {
1495
- const subItems = visibleSubs.map((s) => {
1921
+ const subItems = getSubNamesWithAliases(sub.subcommands).map((s) => {
1496
1922
  const desc = s.description ? `:${escapeDesc(s.description)}` : "";
1497
1923
  return `"${s.name}${desc}"`;
1498
1924
  }).join(" ");
@@ -1512,7 +1938,12 @@ function generateZshCompletion(command, options) {
1512
1938
  const lines = [];
1513
1939
  lines.push(`#compdef ${programName}`);
1514
1940
  lines.push(``);
1515
- lines.push(`# Zsh completion for ${programName}`);
1941
+ lines.push(...buildHeaderLines({
1942
+ programName,
1943
+ shell: "zsh",
1944
+ binPath: options.binPath,
1945
+ programVersion: options.programVersion
1946
+ }));
1516
1947
  lines.push(`# Generated by politty`);
1517
1948
  lines.push(``);
1518
1949
  lines.push(`__${fn}_not_used() {`);
@@ -1567,7 +1998,7 @@ function generateZshCompletion(command, options) {
1567
1998
  lines.push(` __${fn}_cdescribe 'options' _opts`);
1568
1999
  if (visibleSubs.length > 0) {
1569
2000
  lines.push(` else`);
1570
- const subItems = visibleSubs.map((s) => {
2001
+ const subItems = getSubNamesWithAliases(root.subcommands).map((s) => {
1571
2002
  const desc = s.description ? `:${escapeDesc(s.description)}` : "";
1572
2003
  return `"${s.name}${desc}"`;
1573
2004
  }).join(" ");
@@ -1708,8 +2139,19 @@ const completionArgsSchema = z.object({
1708
2139
  instructions: arg(z.boolean().default(false), {
1709
2140
  alias: "i",
1710
2141
  description: "Show installation instructions"
1711
- })
2142
+ }),
2143
+ loader: arg(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." }),
2144
+ install: arg(z.boolean().default(false), { description: "Write the completion script to its on-disk cache (bash/zsh) or autoload location (fish) instead of printing it." })
1712
2145
  });
2146
+ const refreshArgsSchema = z.object({ shell: arg(z.enum([
2147
+ "bash",
2148
+ "zsh",
2149
+ "fish"
2150
+ ]), {
2151
+ positional: true,
2152
+ description: "Shell to refresh",
2153
+ placeholder: "SHELL"
2154
+ }) });
1713
2155
  /**
1714
2156
  * Create a completion subcommand for your CLI
1715
2157
  *
@@ -1725,12 +2167,30 @@ const completionArgsSchema = z.object({
1725
2167
  * });
1726
2168
  * ```
1727
2169
  */
1728
- function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
2170
+ function createCompletionCommand(rootCommand, programName, globalArgsSchema, extra = {}) {
1729
2171
  const resolvedProgramName = programName ?? rootCommand.name;
2172
+ const { cacheDir, programVersion } = extra;
2173
+ const refreshExtra = {
2174
+ ...cacheDir !== void 0 && { cacheDir },
2175
+ ...programVersion !== void 0 && { programVersion },
2176
+ ...globalArgsSchema !== void 0 && { globalArgsSchema }
2177
+ };
2178
+ const installCtxBase = {
2179
+ programName: resolvedProgramName,
2180
+ ...refreshExtra
2181
+ };
2182
+ const loaderOptsBase = {
2183
+ programName: resolvedProgramName,
2184
+ ...cacheDir !== void 0 && { cacheDir }
2185
+ };
1730
2186
  if (!rootCommand.subCommands?.__complete) rootCommand.subCommands = {
1731
2187
  ...rootCommand.subCommands,
1732
2188
  __complete: createDynamicCompleteCommand(rootCommand, resolvedProgramName)
1733
2189
  };
2190
+ if (!rootCommand.subCommands?.["__refresh-completion"]) rootCommand.subCommands = {
2191
+ ...rootCommand.subCommands,
2192
+ "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
2193
+ };
1734
2194
  return defineCommand({
1735
2195
  name: "completion",
1736
2196
  description: "Generate shell completion script",
@@ -1742,11 +2202,43 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1742
2202
  process.exitCode = 1;
1743
2203
  return;
1744
2204
  }
2205
+ if (args.install) {
2206
+ let target;
2207
+ try {
2208
+ target = install({
2209
+ rootCommand,
2210
+ ...installCtxBase
2211
+ }, shellType);
2212
+ } catch (e) {
2213
+ throw new Error(`install failed: ${e instanceof Error ? e.message : String(e)}`);
2214
+ }
2215
+ console.error(`installed: ${target}`);
2216
+ if (shellType !== "fish") {
2217
+ console.error("");
2218
+ console.error(`Add to your ~/.${shellType}rc:`);
2219
+ console.error("");
2220
+ console.error(generateLoader({
2221
+ ...loaderOptsBase,
2222
+ shell: shellType
2223
+ }).trim().replace(/^/gm, " "));
2224
+ }
2225
+ return;
2226
+ }
2227
+ if (args.loader) {
2228
+ 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.");
2229
+ process.stdout.write(generateLoader({
2230
+ ...loaderOptsBase,
2231
+ shell: shellType
2232
+ }));
2233
+ return;
2234
+ }
1745
2235
  const result = generateCompletion(rootCommand, {
1746
2236
  shell: shellType,
1747
2237
  programName: resolvedProgramName,
1748
2238
  includeDescriptions: true,
1749
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2239
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
2240
+ ...programVersion !== void 0 && { programVersion },
2241
+ ...cacheDir !== void 0 && { cacheDir }
1750
2242
  });
1751
2243
  if (args.instructions) console.log(result.installInstructions);
1752
2244
  else console.log(result.script);
@@ -1754,6 +2246,25 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1754
2246
  });
1755
2247
  }
1756
2248
  /**
2249
+ * Hidden subcommand that the runMain background hook spawns. It does
2250
+ * the same stat-compare + atomic rewrite as the rc loader, but in a
2251
+ * detached child process so it's invisible to the user.
2252
+ */
2253
+ function createRefreshCompletionCommand(rootCommand, programName, extra = {}) {
2254
+ return defineCommand({
2255
+ name: "__refresh-completion",
2256
+ description: "(internal) Refresh the on-disk completion cache if stale.",
2257
+ args: refreshArgsSchema,
2258
+ run(args) {
2259
+ refreshIfStale({
2260
+ rootCommand,
2261
+ programName,
2262
+ ...extra
2263
+ }, args.shell);
2264
+ }
2265
+ });
2266
+ }
2267
+ /**
1757
2268
  * Wrap a command with a completion subcommand
1758
2269
  *
1759
2270
  * This avoids circular references that occur when a command references itself
@@ -1774,16 +2285,54 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1774
2285
  * ```
1775
2286
  */
1776
2287
  function withCompletionCommand(command, options) {
1777
- const { programName, globalArgsSchema } = typeof options === "string" ? { programName: options } : options ?? {};
2288
+ const { programName, globalArgsSchema, cacheDir, programVersion } = typeof options === "string" ? { programName: options } : options ?? {};
2289
+ const resolvedProgramName = programName ?? command.name;
2290
+ const extra = {
2291
+ ...cacheDir !== void 0 && { cacheDir },
2292
+ ...programVersion !== void 0 && { programVersion },
2293
+ ...globalArgsSchema !== void 0 && { globalArgsSchema }
2294
+ };
1778
2295
  const wrappedCommand = { ...command };
1779
2296
  wrappedCommand.subCommands = {
1780
2297
  ...command.subCommands,
1781
- completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema),
1782
- __complete: createDynamicCompleteCommand(wrappedCommand, programName)
2298
+ completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema, extra),
2299
+ __complete: createDynamicCompleteCommand(wrappedCommand, programName),
2300
+ "__refresh-completion": createRefreshCompletionCommand(wrappedCommand, resolvedProgramName, extra)
2301
+ };
2302
+ wrappedCommand.runMainHook = (argv) => {
2303
+ maybeSpawnRefresh(argv, {
2304
+ programName: resolvedProgramName,
2305
+ ...cacheDir !== void 0 && { cacheDir }
2306
+ });
1783
2307
  };
1784
2308
  return wrappedCommand;
1785
2309
  }
2310
+ /**
2311
+ * Background-refresh trigger fired from `runMain` via `runMainHook`.
2312
+ *
2313
+ * Skipped when:
2314
+ * - the user is invoking `__complete` / `__refresh-completion` /
2315
+ * `completion` themselves (avoids loops and double work)
2316
+ * - $SHELL doesn't resolve to a known shell
2317
+ * - the user opted out via $POLITTY_NO_COMPLETION_REFRESH
2318
+ * - process.argv[1] is missing (shouldn't happen for normal CLIs)
2319
+ * - no politty-managed cache exists yet — i.e. the user hasn't
2320
+ * installed completion. Without this gate the detached child would
2321
+ * create a fish autoload (or any cache file) on every CLI run,
2322
+ * even though the user never opted in via `--install` or the rc loader.
2323
+ */
2324
+ function maybeSpawnRefresh(argv, ctx) {
2325
+ if (process.env.POLITTY_NO_COMPLETION_REFRESH) return;
2326
+ const firstPositional = argv.find((a) => !a.startsWith("-"));
2327
+ if (firstPositional === "__complete" || firstPositional === "__refresh-completion" || firstPositional === "completion") return;
2328
+ const shell = detectShell();
2329
+ if (!shell) return;
2330
+ const argv0 = process.argv[1];
2331
+ if (!argv0) return;
2332
+ if (!hasManagedCache(ctx, shell)) return;
2333
+ spawnBackgroundRefresh(argv0, shell);
2334
+ }
1786
2335
 
1787
2336
  //#endregion
1788
- export { withCompletionCommand as a, formatForShell as c, generateCandidates as d, extractCompletionData as f, defineCommand as g, createDefineCommand as h, getSupportedShells as i, parseCompletionContext as l, resolveValueCompletion as m, detectShell as n, createDynamicCompleteCommand as o, extractPositionals as p, generateCompletion as r, hasCompleteCommand as s, createCompletionCommand as t, CompletionDirective as u };
1789
- //# sourceMappingURL=completion-yHz8Pdr7.js.map
2337
+ export { defineCommand as _, getSupportedShells as a, hasCompleteCommand as c, CompletionDirective as d, generateCandidates as f, createDefineCommand as g, resolveValueCompletion as h, generateCompletion as i, formatForShell as l, extractPositionals as m, createRefreshCompletionCommand as n, withCompletionCommand as o, extractCompletionData as p, detectShell as r, createDynamicCompleteCommand as s, createCompletionCommand as t, parseCompletionContext as u };
2338
+ //# sourceMappingURL=completion-B04iiki9.js.map