politty 0.4.15 → 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 (73) hide show
  1. package/dist/{arg-registry-CB5gGtzp.d.cts → arg-registry-Cd6xnjHa.d.ts} +116 -4
  2. package/dist/arg-registry-Cd6xnjHa.d.ts.map +1 -0
  3. package/dist/{arg-registry-Dw0f11Zc.d.ts → arg-registry-MVWOAcvw.d.cts} +116 -4
  4. package/dist/arg-registry-MVWOAcvw.d.cts.map +1 -0
  5. package/dist/augment.d.cts +1 -1
  6. package/dist/augment.d.cts.map +1 -1
  7. package/dist/augment.d.ts +1 -1
  8. package/dist/augment.d.ts.map +1 -1
  9. package/dist/completion/index.cjs +2 -1
  10. package/dist/completion/index.d.cts +2 -2
  11. package/dist/completion/index.d.ts +2 -2
  12. package/dist/completion/index.js +2 -2
  13. package/dist/{completion-Ca5ESJlG.js → completion-B04iiki9.js} +512 -18
  14. package/dist/completion-B04iiki9.js.map +1 -0
  15. package/dist/{completion-B5fgnUGm.cjs → completion-BlZxMSeU.cjs} +516 -16
  16. package/dist/completion-BlZxMSeU.cjs.map +1 -0
  17. package/dist/docs/index.cjs +82 -22
  18. package/dist/docs/index.cjs.map +1 -1
  19. package/dist/docs/index.d.cts +1 -1
  20. package/dist/docs/index.d.cts.map +1 -1
  21. package/dist/docs/index.d.ts +1 -1
  22. package/dist/docs/index.d.ts.map +1 -1
  23. package/dist/docs/index.js +92 -31
  24. package/dist/docs/index.js.map +1 -1
  25. package/dist/{index-Dg9Fpz0R.d.ts → index-CPebddth.d.cts} +56 -4
  26. package/dist/index-CPebddth.d.cts.map +1 -0
  27. package/dist/{index-C1gGgUeB.d.cts → index-DR9HLxIP.d.ts} +56 -4
  28. package/dist/index-DR9HLxIP.d.ts.map +1 -0
  29. package/dist/index.cjs +5 -3
  30. package/dist/index.d.cts +36 -4
  31. package/dist/index.d.cts.map +1 -1
  32. package/dist/index.d.ts +36 -4
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -4
  35. package/dist/log-collector-Cd2_mv87.cjs.map +1 -1
  36. package/dist/log-collector-Cu6MCtAx.js.map +1 -1
  37. package/dist/prompt/clack/index.cjs.map +1 -1
  38. package/dist/prompt/clack/index.d.cts +1 -1
  39. package/dist/prompt/clack/index.d.cts.map +1 -1
  40. package/dist/prompt/clack/index.d.ts +1 -1
  41. package/dist/prompt/clack/index.d.ts.map +1 -1
  42. package/dist/prompt/clack/index.js.map +1 -1
  43. package/dist/prompt/index.d.cts +1 -1
  44. package/dist/prompt/index.d.cts.map +1 -1
  45. package/dist/prompt/index.d.ts +1 -1
  46. package/dist/prompt/index.d.ts.map +1 -1
  47. package/dist/prompt/inquirer/index.cjs.map +1 -1
  48. package/dist/prompt/inquirer/index.d.cts +1 -1
  49. package/dist/prompt/inquirer/index.d.cts.map +1 -1
  50. package/dist/prompt/inquirer/index.d.ts +1 -1
  51. package/dist/prompt/inquirer/index.d.ts.map +1 -1
  52. package/dist/prompt/inquirer/index.js.map +1 -1
  53. package/dist/prompt-BKHqGrFw.js.map +1 -1
  54. package/dist/prompt-aXfSf27y.cjs.map +1 -1
  55. package/dist/{runner-DKAQBNNh.js → runner-BHeCMEa5.js} +309 -45
  56. package/dist/runner-BHeCMEa5.js.map +1 -0
  57. package/dist/{runner-CriXJlm4.cjs → runner-BcyR6Z8r.cjs} +320 -44
  58. package/dist/runner-BcyR6Z8r.cjs.map +1 -0
  59. package/dist/{subcommand-router-CqZX3orq.cjs → subcommand-router-DQy0KZU-.cjs} +41 -3
  60. package/dist/subcommand-router-DQy0KZU-.cjs.map +1 -0
  61. package/dist/{subcommand-router-ENeCymvX.js → subcommand-router-XZBWe8HN.js} +41 -3
  62. package/dist/subcommand-router-XZBWe8HN.js.map +1 -0
  63. package/package.json +16 -16
  64. package/dist/arg-registry-CB5gGtzp.d.cts.map +0 -1
  65. package/dist/arg-registry-Dw0f11Zc.d.ts.map +0 -1
  66. package/dist/completion-B5fgnUGm.cjs.map +0 -1
  67. package/dist/completion-Ca5ESJlG.js.map +0 -1
  68. package/dist/index-C1gGgUeB.d.cts.map +0 -1
  69. package/dist/index-Dg9Fpz0R.d.ts.map +0 -1
  70. package/dist/runner-CriXJlm4.cjs.map +0 -1
  71. package/dist/runner-DKAQBNNh.js.map +0 -1
  72. package/dist/subcommand-router-CqZX3orq.cjs.map +0 -1
  73. package/dist/subcommand-router-ENeCymvX.js.map +0 -1
@@ -1,6 +1,8 @@
1
1
  const require_log_collector = require('./log-collector-Cd2_mv87.cjs');
2
- const require_subcommand_router = require('./subcommand-router-CqZX3orq.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
@@ -139,6 +141,8 @@ function fieldToOption(field) {
139
141
  name: field.name,
140
142
  cliName: field.cliName,
141
143
  alias: field.alias,
144
+ negation: field.negationDisplay,
145
+ negationDescription: field.negationDescription,
142
146
  description: field.description,
143
147
  takesValue: field.type !== "boolean",
144
148
  valueType: field.type,
@@ -290,6 +294,79 @@ function extractCompletionData(command, programName, globalArgsSchema) {
290
294
  };
291
295
  }
292
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
+
293
370
  //#endregion
294
371
  //#region src/completion/bash.ts
295
372
  /** Escape a string for use inside bash double-quotes */
@@ -417,7 +494,9 @@ function availableOptionLines$2(options, fn) {
417
494
  else {
418
495
  const patterns = [`"--${opt.cliName}"`];
419
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}"`);
420
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})`);
421
500
  }
422
501
  lines.push(` __${fn}_not_used "--help" && _avail+=(--help)`);
423
502
  return lines;
@@ -466,7 +545,12 @@ function generateBashCompletion(command, options) {
466
545
  const root = data.command;
467
546
  const visibleSubs = getVisibleSubs(root.subcommands);
468
547
  const lines = [];
469
- lines.push(`# Bash completion for ${programName}`);
548
+ lines.push(...buildHeaderLines({
549
+ programName,
550
+ shell: "bash",
551
+ binPath: options.binPath,
552
+ programVersion: options.programVersion
553
+ }));
470
554
  lines.push(`# Generated by politty`);
471
555
  lines.push(``);
472
556
  lines.push(`__${fn}_not_used() {`);
@@ -605,13 +689,21 @@ source ~/.bashrc`
605
689
  * Completion directive flags (bitwise)
606
690
  */
607
691
  const CompletionDirective = {
692
+ /** Default completion behavior */
608
693
  Default: 0,
694
+ /** Don't add space after completion */
609
695
  NoSpace: 1,
696
+ /** Don't offer file completion (even if no other completions) */
610
697
  NoFileCompletion: 2,
698
+ /** Filter completions using current word as prefix */
611
699
  FilterPrefix: 4,
700
+ /** Keep the order of completions */
612
701
  KeepOrder: 8,
702
+ /** Trigger file completion */
613
703
  FileCompletion: 16,
704
+ /** Trigger directory completion */
614
705
  DirectoryCompletion: 32,
706
+ /** Error occurred during completion */
615
707
  Error: 64
616
708
  };
617
709
  /**
@@ -737,13 +829,21 @@ function generateOptionNameCandidates(context) {
737
829
  if (opt.valueType === "array") return true;
738
830
  if (context.usedOptions.has(opt.cliName)) return false;
739
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;
740
833
  return true;
741
834
  });
742
- for (const opt of availableOptions) candidates.push({
743
- value: `--${opt.cliName}`,
744
- description: opt.description,
745
- type: "option"
746
- });
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
+ }
747
847
  if (!context.usedOptions.has("help")) candidates.push({
748
848
  value: "--help",
749
849
  description: "Show help information",
@@ -809,6 +909,8 @@ function extractOptions(command) {
809
909
  name: field.name,
810
910
  cliName: field.cliName,
811
911
  alias: field.alias,
912
+ negation: field.negationDisplay,
913
+ negationDescription: field.negationDescription,
812
914
  description: field.description,
813
915
  takesValue: field.type !== "boolean",
814
916
  valueType: field.type,
@@ -890,6 +992,10 @@ function findOption(options, nameOrAlias) {
890
992
  if (nameOrAlias.length > 1) {
891
993
  if (opt.cliName.includes("-") && require_subcommand_router.toCamelCase(opt.cliName) === nameOrAlias) return true;
892
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
+ }
893
999
  }
894
1000
  return false;
895
1001
  });
@@ -922,6 +1028,7 @@ function parseCompletionContext(argv, rootCommand) {
922
1028
  if (opt) {
923
1029
  usedOptions.add(opt.cliName);
924
1030
  if (opt.alias) for (const a of opt.alias) usedOptions.add(a);
1031
+ if (opt.negation) usedOptions.add(opt.negation);
925
1032
  if (opt.takesValue && !hasInlineValue(word)) i++;
926
1033
  }
927
1034
  i++;
@@ -1240,11 +1347,14 @@ function availableOptionLines$1(options, fn) {
1240
1347
  const lines = [];
1241
1348
  for (const opt of options) {
1242
1349
  const desc = escapeDesc$1(opt.description ?? "");
1350
+ const negDesc = opt.negationDescription ? escapeDesc$1(opt.negationDescription) : desc;
1243
1351
  if (opt.valueType === "array") lines.push(` echo "--${opt.cliName}\t${desc}"`);
1244
1352
  else {
1245
1353
  const checks = [`"--${opt.cliName}"`];
1246
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}"`);
1247
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}"`);
1248
1358
  }
1249
1359
  }
1250
1360
  lines.push(` __${fn}_not_used "--help"; and echo "--help\tShow help"`);
@@ -1314,9 +1424,32 @@ function generateFishCompletion(command, options) {
1314
1424
  const root = data.command;
1315
1425
  const visibleSubs = getVisibleSubs(root.subcommands);
1316
1426
  const lines = [];
1317
- lines.push(`# Fish completion for ${programName}`);
1427
+ lines.push(...buildHeaderLines({
1428
+ programName,
1429
+ shell: "fish",
1430
+ binPath: options.binPath,
1431
+ programVersion: options.programVersion
1432
+ }));
1318
1433
  lines.push(`# Generated by politty`);
1319
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(``);
1320
1453
  lines.push(`function __${fn}_not_used --no-scope-shadowing`);
1321
1454
  lines.push(` for _chk in $argv`);
1322
1455
  lines.push(` if contains -- "$_chk" $_used_opts`);
@@ -1442,6 +1575,241 @@ source ~/.config/fish/completions/${programName}.fish`
1442
1575
  };
1443
1576
  }
1444
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
+
1445
1813
  //#endregion
1446
1814
  //#region src/completion/zsh.ts
1447
1815
  function escapeDesc(s) {
@@ -1510,11 +1878,14 @@ function availableOptionLines(options, fn) {
1510
1878
  const lines = [];
1511
1879
  for (const opt of options) {
1512
1880
  const desc = opt.description ? `:${escapeDesc(opt.description)}` : "";
1881
+ const negDesc = opt.negationDescription ? `:${escapeDesc(opt.negationDescription)}` : desc;
1513
1882
  if (opt.valueType === "array") lines.push(` _opts+=("--${opt.cliName}${desc}")`);
1514
1883
  else {
1515
1884
  const patterns = [`"--${opt.cliName}"`];
1516
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}"`);
1517
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}")`);
1518
1889
  }
1519
1890
  }
1520
1891
  lines.push(` __${fn}_not_used "--help" && _opts+=("--help:Show help")`);
@@ -1568,7 +1939,12 @@ function generateZshCompletion(command, options) {
1568
1939
  const lines = [];
1569
1940
  lines.push(`#compdef ${programName}`);
1570
1941
  lines.push(``);
1571
- lines.push(`# Zsh completion for ${programName}`);
1942
+ lines.push(...buildHeaderLines({
1943
+ programName,
1944
+ shell: "zsh",
1945
+ binPath: options.binPath,
1946
+ programVersion: options.programVersion
1947
+ }));
1572
1948
  lines.push(`# Generated by politty`);
1573
1949
  lines.push(``);
1574
1950
  lines.push(`__${fn}_not_used() {`);
@@ -1764,8 +2140,19 @@ const completionArgsSchema = zod.z.object({
1764
2140
  instructions: require_subcommand_router.arg(zod.z.boolean().default(false), {
1765
2141
  alias: "i",
1766
2142
  description: "Show installation instructions"
1767
- })
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." })
1768
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
+ }) });
1769
2156
  /**
1770
2157
  * Create a completion subcommand for your CLI
1771
2158
  *
@@ -1781,12 +2168,30 @@ const completionArgsSchema = zod.z.object({
1781
2168
  * });
1782
2169
  * ```
1783
2170
  */
1784
- function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
2171
+ function createCompletionCommand(rootCommand, programName, globalArgsSchema, extra = {}) {
1785
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
+ };
1786
2187
  if (!rootCommand.subCommands?.__complete) rootCommand.subCommands = {
1787
2188
  ...rootCommand.subCommands,
1788
2189
  __complete: createDynamicCompleteCommand(rootCommand, resolvedProgramName)
1789
2190
  };
2191
+ if (!rootCommand.subCommands?.["__refresh-completion"]) rootCommand.subCommands = {
2192
+ ...rootCommand.subCommands,
2193
+ "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
2194
+ };
1790
2195
  return defineCommand({
1791
2196
  name: "completion",
1792
2197
  description: "Generate shell completion script",
@@ -1798,11 +2203,43 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1798
2203
  process.exitCode = 1;
1799
2204
  return;
1800
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
+ }
1801
2236
  const result = generateCompletion(rootCommand, {
1802
2237
  shell: shellType,
1803
2238
  programName: resolvedProgramName,
1804
2239
  includeDescriptions: true,
1805
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2240
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
2241
+ ...programVersion !== void 0 && { programVersion },
2242
+ ...cacheDir !== void 0 && { cacheDir }
1806
2243
  });
1807
2244
  if (args.instructions) console.log(result.installInstructions);
1808
2245
  else console.log(result.script);
@@ -1810,6 +2247,25 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1810
2247
  });
1811
2248
  }
1812
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
+ /**
1813
2269
  * Wrap a command with a completion subcommand
1814
2270
  *
1815
2271
  * This avoids circular references that occur when a command references itself
@@ -1830,15 +2286,53 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1830
2286
  * ```
1831
2287
  */
1832
2288
  function withCompletionCommand(command, options) {
1833
- 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
+ };
1834
2296
  const wrappedCommand = { ...command };
1835
2297
  wrappedCommand.subCommands = {
1836
2298
  ...command.subCommands,
1837
- completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema),
1838
- __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
+ });
1839
2308
  };
1840
2309
  return wrappedCommand;
1841
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
+ }
1842
2336
 
1843
2337
  //#endregion
1844
2338
  Object.defineProperty(exports, 'CompletionDirective', {
@@ -1865,6 +2359,12 @@ Object.defineProperty(exports, 'createDynamicCompleteCommand', {
1865
2359
  return createDynamicCompleteCommand;
1866
2360
  }
1867
2361
  });
2362
+ Object.defineProperty(exports, 'createRefreshCompletionCommand', {
2363
+ enumerable: true,
2364
+ get: function () {
2365
+ return createRefreshCompletionCommand;
2366
+ }
2367
+ });
1868
2368
  Object.defineProperty(exports, 'defineCommand', {
1869
2369
  enumerable: true,
1870
2370
  get: function () {
@@ -1937,4 +2437,4 @@ Object.defineProperty(exports, 'withCompletionCommand', {
1937
2437
  return withCompletionCommand;
1938
2438
  }
1939
2439
  });
1940
- //# sourceMappingURL=completion-B5fgnUGm.cjs.map
2440
+ //# sourceMappingURL=completion-BlZxMSeU.cjs.map