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
- import { c as resolveSubCommandMeta, h as arg, i as resolveSubCommandAlias, l as extractFields, p as toCamelCase } from "./subcommand-router-ENeCymvX.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) {
@@ -138,6 +140,8 @@ function fieldToOption(field) {
138
140
  name: field.name,
139
141
  cliName: field.cliName,
140
142
  alias: field.alias,
143
+ negation: field.negationDisplay,
144
+ negationDescription: field.negationDescription,
141
145
  description: field.description,
142
146
  takesValue: field.type !== "boolean",
143
147
  valueType: field.type,
@@ -289,6 +293,79 @@ function extractCompletionData(command, programName, globalArgsSchema) {
289
293
  };
290
294
  }
291
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
+
292
369
  //#endregion
293
370
  //#region src/completion/bash.ts
294
371
  /** Escape a string for use inside bash double-quotes */
@@ -416,7 +493,9 @@ function availableOptionLines$2(options, fn) {
416
493
  else {
417
494
  const patterns = [`"--${opt.cliName}"`];
418
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}"`);
419
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})`);
420
499
  }
421
500
  lines.push(` __${fn}_not_used "--help" && _avail+=(--help)`);
422
501
  return lines;
@@ -465,7 +544,12 @@ function generateBashCompletion(command, options) {
465
544
  const root = data.command;
466
545
  const visibleSubs = getVisibleSubs(root.subcommands);
467
546
  const lines = [];
468
- lines.push(`# Bash completion for ${programName}`);
547
+ lines.push(...buildHeaderLines({
548
+ programName,
549
+ shell: "bash",
550
+ binPath: options.binPath,
551
+ programVersion: options.programVersion
552
+ }));
469
553
  lines.push(`# Generated by politty`);
470
554
  lines.push(``);
471
555
  lines.push(`__${fn}_not_used() {`);
@@ -604,13 +688,21 @@ source ~/.bashrc`
604
688
  * Completion directive flags (bitwise)
605
689
  */
606
690
  const CompletionDirective = {
691
+ /** Default completion behavior */
607
692
  Default: 0,
693
+ /** Don't add space after completion */
608
694
  NoSpace: 1,
695
+ /** Don't offer file completion (even if no other completions) */
609
696
  NoFileCompletion: 2,
697
+ /** Filter completions using current word as prefix */
610
698
  FilterPrefix: 4,
699
+ /** Keep the order of completions */
611
700
  KeepOrder: 8,
701
+ /** Trigger file completion */
612
702
  FileCompletion: 16,
703
+ /** Trigger directory completion */
613
704
  DirectoryCompletion: 32,
705
+ /** Error occurred during completion */
614
706
  Error: 64
615
707
  };
616
708
  /**
@@ -736,13 +828,21 @@ function generateOptionNameCandidates(context) {
736
828
  if (opt.valueType === "array") return true;
737
829
  if (context.usedOptions.has(opt.cliName)) return false;
738
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;
739
832
  return true;
740
833
  });
741
- for (const opt of availableOptions) candidates.push({
742
- value: `--${opt.cliName}`,
743
- description: opt.description,
744
- type: "option"
745
- });
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
+ }
746
846
  if (!context.usedOptions.has("help")) candidates.push({
747
847
  value: "--help",
748
848
  description: "Show help information",
@@ -808,6 +908,8 @@ function extractOptions(command) {
808
908
  name: field.name,
809
909
  cliName: field.cliName,
810
910
  alias: field.alias,
911
+ negation: field.negationDisplay,
912
+ negationDescription: field.negationDescription,
811
913
  description: field.description,
812
914
  takesValue: field.type !== "boolean",
813
915
  valueType: field.type,
@@ -889,6 +991,10 @@ function findOption(options, nameOrAlias) {
889
991
  if (nameOrAlias.length > 1) {
890
992
  if (opt.cliName.includes("-") && toCamelCase(opt.cliName) === nameOrAlias) return true;
891
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
+ }
892
998
  }
893
999
  return false;
894
1000
  });
@@ -921,6 +1027,7 @@ function parseCompletionContext(argv, rootCommand) {
921
1027
  if (opt) {
922
1028
  usedOptions.add(opt.cliName);
923
1029
  if (opt.alias) for (const a of opt.alias) usedOptions.add(a);
1030
+ if (opt.negation) usedOptions.add(opt.negation);
924
1031
  if (opt.takesValue && !hasInlineValue(word)) i++;
925
1032
  }
926
1033
  i++;
@@ -1239,11 +1346,14 @@ function availableOptionLines$1(options, fn) {
1239
1346
  const lines = [];
1240
1347
  for (const opt of options) {
1241
1348
  const desc = escapeDesc$1(opt.description ?? "");
1349
+ const negDesc = opt.negationDescription ? escapeDesc$1(opt.negationDescription) : desc;
1242
1350
  if (opt.valueType === "array") lines.push(` echo "--${opt.cliName}\t${desc}"`);
1243
1351
  else {
1244
1352
  const checks = [`"--${opt.cliName}"`];
1245
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}"`);
1246
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}"`);
1247
1357
  }
1248
1358
  }
1249
1359
  lines.push(` __${fn}_not_used "--help"; and echo "--help\tShow help"`);
@@ -1313,9 +1423,32 @@ function generateFishCompletion(command, options) {
1313
1423
  const root = data.command;
1314
1424
  const visibleSubs = getVisibleSubs(root.subcommands);
1315
1425
  const lines = [];
1316
- lines.push(`# Fish completion for ${programName}`);
1426
+ lines.push(...buildHeaderLines({
1427
+ programName,
1428
+ shell: "fish",
1429
+ binPath: options.binPath,
1430
+ programVersion: options.programVersion
1431
+ }));
1317
1432
  lines.push(`# Generated by politty`);
1318
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(``);
1319
1452
  lines.push(`function __${fn}_not_used --no-scope-shadowing`);
1320
1453
  lines.push(` for _chk in $argv`);
1321
1454
  lines.push(` if contains -- "$_chk" $_used_opts`);
@@ -1441,6 +1574,241 @@ source ~/.config/fish/completions/${programName}.fish`
1441
1574
  };
1442
1575
  }
1443
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
+
1444
1812
  //#endregion
1445
1813
  //#region src/completion/zsh.ts
1446
1814
  function escapeDesc(s) {
@@ -1509,11 +1877,14 @@ function availableOptionLines(options, fn) {
1509
1877
  const lines = [];
1510
1878
  for (const opt of options) {
1511
1879
  const desc = opt.description ? `:${escapeDesc(opt.description)}` : "";
1880
+ const negDesc = opt.negationDescription ? `:${escapeDesc(opt.negationDescription)}` : desc;
1512
1881
  if (opt.valueType === "array") lines.push(` _opts+=("--${opt.cliName}${desc}")`);
1513
1882
  else {
1514
1883
  const patterns = [`"--${opt.cliName}"`];
1515
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}"`);
1516
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}")`);
1517
1888
  }
1518
1889
  }
1519
1890
  lines.push(` __${fn}_not_used "--help" && _opts+=("--help:Show help")`);
@@ -1567,7 +1938,12 @@ function generateZshCompletion(command, options) {
1567
1938
  const lines = [];
1568
1939
  lines.push(`#compdef ${programName}`);
1569
1940
  lines.push(``);
1570
- lines.push(`# Zsh completion for ${programName}`);
1941
+ lines.push(...buildHeaderLines({
1942
+ programName,
1943
+ shell: "zsh",
1944
+ binPath: options.binPath,
1945
+ programVersion: options.programVersion
1946
+ }));
1571
1947
  lines.push(`# Generated by politty`);
1572
1948
  lines.push(``);
1573
1949
  lines.push(`__${fn}_not_used() {`);
@@ -1763,8 +2139,19 @@ const completionArgsSchema = z.object({
1763
2139
  instructions: arg(z.boolean().default(false), {
1764
2140
  alias: "i",
1765
2141
  description: "Show installation instructions"
1766
- })
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." })
1767
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
+ }) });
1768
2155
  /**
1769
2156
  * Create a completion subcommand for your CLI
1770
2157
  *
@@ -1780,12 +2167,30 @@ const completionArgsSchema = z.object({
1780
2167
  * });
1781
2168
  * ```
1782
2169
  */
1783
- function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
2170
+ function createCompletionCommand(rootCommand, programName, globalArgsSchema, extra = {}) {
1784
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
+ };
1785
2186
  if (!rootCommand.subCommands?.__complete) rootCommand.subCommands = {
1786
2187
  ...rootCommand.subCommands,
1787
2188
  __complete: createDynamicCompleteCommand(rootCommand, resolvedProgramName)
1788
2189
  };
2190
+ if (!rootCommand.subCommands?.["__refresh-completion"]) rootCommand.subCommands = {
2191
+ ...rootCommand.subCommands,
2192
+ "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
2193
+ };
1789
2194
  return defineCommand({
1790
2195
  name: "completion",
1791
2196
  description: "Generate shell completion script",
@@ -1797,11 +2202,43 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1797
2202
  process.exitCode = 1;
1798
2203
  return;
1799
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
+ }
1800
2235
  const result = generateCompletion(rootCommand, {
1801
2236
  shell: shellType,
1802
2237
  programName: resolvedProgramName,
1803
2238
  includeDescriptions: true,
1804
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
2239
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
2240
+ ...programVersion !== void 0 && { programVersion },
2241
+ ...cacheDir !== void 0 && { cacheDir }
1805
2242
  });
1806
2243
  if (args.instructions) console.log(result.installInstructions);
1807
2244
  else console.log(result.script);
@@ -1809,6 +2246,25 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1809
2246
  });
1810
2247
  }
1811
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
+ /**
1812
2268
  * Wrap a command with a completion subcommand
1813
2269
  *
1814
2270
  * This avoids circular references that occur when a command references itself
@@ -1829,16 +2285,54 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema) {
1829
2285
  * ```
1830
2286
  */
1831
2287
  function withCompletionCommand(command, options) {
1832
- 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
+ };
1833
2295
  const wrappedCommand = { ...command };
1834
2296
  wrappedCommand.subCommands = {
1835
2297
  ...command.subCommands,
1836
- completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema),
1837
- __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
+ });
1838
2307
  };
1839
2308
  return wrappedCommand;
1840
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
+ }
1841
2335
 
1842
2336
  //#endregion
1843
- 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 };
1844
- //# sourceMappingURL=completion-Ca5ESJlG.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