politty 0.5.0 → 0.6.0

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 +47 -0
  2. package/dist/{arg-registry-6E0WHOh_.d.ts → arg-registry-BoqVZRFO.d.cts} +24 -22
  3. package/dist/arg-registry-BoqVZRFO.d.cts.map +1 -0
  4. package/dist/{arg-registry--NRaNFJM.d.cts → arg-registry-BoqVZRFO.d.ts} +24 -22
  5. package/dist/arg-registry-BoqVZRFO.d.ts.map +1 -0
  6. package/dist/augment.d.cts +1 -1
  7. package/dist/augment.d.ts +1 -1
  8. package/dist/cli.cjs +54 -0
  9. package/dist/cli.cjs.map +1 -0
  10. package/dist/cli.d.cts +1 -0
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +55 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/completion/index.cjs +6 -1
  15. package/dist/completion/index.d.cts +3 -3
  16. package/dist/completion/index.d.ts +3 -3
  17. package/dist/completion/index.js +2 -2
  18. package/dist/{completion-Cqs1Ja7C.cjs → completion-CLHO3Xaz.cjs} +1915 -315
  19. package/dist/completion-CLHO3Xaz.cjs.map +1 -0
  20. package/dist/{completion-BA5JMvVG.js → completion-DHnVx9Zk.js} +1889 -318
  21. package/dist/completion-DHnVx9Zk.js.map +1 -0
  22. package/dist/docs/index.cjs +3 -3
  23. package/dist/docs/index.cjs.map +1 -1
  24. package/dist/docs/index.d.cts +1 -1
  25. package/dist/docs/index.d.cts.map +1 -1
  26. package/dist/docs/index.d.ts +1 -1
  27. package/dist/docs/index.d.ts.map +1 -1
  28. package/dist/docs/index.js +3 -3
  29. package/dist/docs/index.js.map +1 -1
  30. package/dist/{index-DBMfKZ34.d.ts → index-Csk1VFou.d.ts} +120 -4
  31. package/dist/index-Csk1VFou.d.ts.map +1 -0
  32. package/dist/{index-DJp8k5Bq.d.cts → index-Ct48_myg.d.cts} +120 -4
  33. package/dist/index-Ct48_myg.d.cts.map +1 -0
  34. package/dist/index.cjs +4 -3
  35. package/dist/index.d.cts +3 -3
  36. package/dist/index.d.cts.map +1 -1
  37. package/dist/index.d.ts +3 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +4 -4
  40. package/dist/{log-collector-Cu6MCtAx.js → log-collector-DK32-73m.js} +1 -1
  41. package/dist/{log-collector-Cu6MCtAx.js.map → log-collector-DK32-73m.js.map} +1 -1
  42. package/dist/{log-collector-Cd2_mv87.cjs → log-collector-DUqC427m.cjs} +1 -1
  43. package/dist/{log-collector-Cd2_mv87.cjs.map → log-collector-DUqC427m.cjs.map} +1 -1
  44. package/dist/prompt/clack/index.cjs +1 -2
  45. package/dist/prompt/clack/index.cjs.map +1 -1
  46. package/dist/prompt/clack/index.d.cts +1 -1
  47. package/dist/prompt/clack/index.d.ts +1 -1
  48. package/dist/prompt/clack/index.js +1 -1
  49. package/dist/prompt/index.cjs +1 -1
  50. package/dist/prompt/index.d.cts +1 -1
  51. package/dist/prompt/index.d.cts.map +1 -1
  52. package/dist/prompt/index.d.ts +1 -1
  53. package/dist/prompt/index.d.ts.map +1 -1
  54. package/dist/prompt/index.js +1 -1
  55. package/dist/prompt/inquirer/index.cjs +1 -2
  56. package/dist/prompt/inquirer/index.cjs.map +1 -1
  57. package/dist/prompt/inquirer/index.d.cts +1 -1
  58. package/dist/prompt/inquirer/index.d.ts +1 -1
  59. package/dist/prompt/inquirer/index.js +1 -1
  60. package/dist/{prompt-aXfSf27y.cjs → prompt-Bs9e-Em3.cjs} +1 -1
  61. package/dist/{prompt-aXfSf27y.cjs.map → prompt-Bs9e-Em3.cjs.map} +1 -1
  62. package/dist/{prompt-BKHqGrFw.js → prompt-Cc8Tfmdv.js} +1 -1
  63. package/dist/{prompt-BKHqGrFw.js.map → prompt-Cc8Tfmdv.js.map} +1 -1
  64. package/dist/{runner-BmSEiD9A.js → runner--Zn4KN9B.js} +3 -3
  65. package/dist/{runner-BmSEiD9A.js.map → runner--Zn4KN9B.js.map} +1 -1
  66. package/dist/{runner-CRZ_7Y9i.cjs → runner-BloFWJEB.cjs} +3 -3
  67. package/dist/{runner-CRZ_7Y9i.cjs.map → runner-BloFWJEB.cjs.map} +1 -1
  68. package/dist/{schema-extractor-SLPgBNgZ.cjs → schema-extractor-BxSRwLrx.cjs} +1 -2
  69. package/dist/schema-extractor-BxSRwLrx.cjs.map +1 -0
  70. package/dist/{schema-extractor-C50R-1re.js → schema-extractor-Dqe7_kyQ.js} +1 -1
  71. package/dist/schema-extractor-Dqe7_kyQ.js.map +1 -0
  72. package/package.json +14 -11
  73. package/dist/arg-registry--NRaNFJM.d.cts.map +0 -1
  74. package/dist/arg-registry-6E0WHOh_.d.ts.map +0 -1
  75. package/dist/completion-BA5JMvVG.js.map +0 -1
  76. package/dist/completion-Cqs1Ja7C.cjs.map +0 -1
  77. package/dist/index-DBMfKZ34.d.ts.map +0 -1
  78. package/dist/index-DJp8k5Bq.d.cts.map +0 -1
  79. package/dist/schema-extractor-C50R-1re.js.map +0 -1
  80. package/dist/schema-extractor-SLPgBNgZ.cjs.map +0 -1
@@ -1,8 +1,9 @@
1
- import { a as toCamelCase, h as arg, m as resolveSubCommandMeta, n as getAllAliases, t as extractFields, u as resolveSubCommandAlias } from "./schema-extractor-C50R-1re.js";
1
+ import { a as toCamelCase, h as arg, m as resolveSubCommandMeta, n as getAllAliases, t as extractFields, u as resolveSubCommandAlias } from "./schema-extractor-Dqe7_kyQ.js";
2
2
  import { z } from "zod";
3
- import { execSync, spawn } from "node:child_process";
4
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
5
- import { dirname, join } from "node:path";
3
+ import { execFile, execSync, spawn } from "node:child_process";
4
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { promisify } from "node:util";
6
7
 
7
8
  //#region src/core/command.ts
8
9
  function defineCommand(config) {
@@ -132,6 +133,33 @@ function ansiC(s) {
132
133
  return out;
133
134
  }
134
135
  /**
136
+ * Single-quote escape for a POSIX shell literal: `'` -> `'\''`. Inside single
137
+ * quotes the shell performs no expansion at all, so `$`, backticks, and
138
+ * `$(...)` stay inert. Used for hardcoded paths that may originate from
139
+ * env/config so path metachars never execute as commands when the generated
140
+ * snippet is sourced. Safe for bash, zsh, and fish.
141
+ */
142
+ function shSingleQuote(s) {
143
+ return `'${s.replace(/'/g, "'\\''")}'`;
144
+ }
145
+ /**
146
+ * Shell sub-expression that prints a file's stat signature, trying GNU
147
+ * `stat -c` first (BSD `-f` is filesystem mode there) then falling back to
148
+ * BSD `stat -f`. `-L` follows symlinks so the shell-side mtime matches Node's
149
+ * `fs.statSync`. `posix` (bash/zsh) wraps it as `$(… || …)`; fish has no
150
+ * `$(…)` capture inside `(…)`, so it uses `(…; or …)`. With `withSize`, the
151
+ * signature is `mtime:size` so a worker cache rewritten within the same
152
+ * second still reads as stale; otherwise it is the bare mtime in whole
153
+ * seconds.
154
+ */
155
+ function statSigExpr(fileVar, opts) {
156
+ const gnuFmt = opts.withSize ? "%Y:%s" : "%Y";
157
+ const bsdFmt = opts.withSize ? "%m:%z" : "%m";
158
+ const gnu = `stat -L -c '${gnuFmt}' "${fileVar}" 2>/dev/null`;
159
+ const bsd = `stat -L -f '${bsdFmt}' "${fileVar}" 2>/dev/null`;
160
+ return opts.shell === "fish" ? `(${gnu}; or ${bsd})` : `$(${gnu} || ${bsd})`;
161
+ }
162
+ /**
135
163
  * Render an alias as its CLI token form: single-char aliases become `-x`,
136
164
  * multi-char aliases become `--long`. Mirrors the parser's accepted shapes
137
165
  * and is the bare-token form (no quoting) used inside generated case
@@ -293,13 +321,19 @@ function resolveValueCompletion(field) {
293
321
  * Parse completion context from partial command line
294
322
  */
295
323
  /**
296
- * The dynamic completion path runs `__complete` at TAB time and never sees
297
- * "expand" fields (those are handled inline by the static shell script).
298
- * Strip the transient pending sentinel here so the rest of the runtime path
299
- * can stay strict about handling only resolved `ValueCompletion` values.
324
+ * `completion.custom.expand` is baked into static scripts, but dispatcher
325
+ * scripts call `__complete` at TAB time. Convert the pending sentinel into a
326
+ * runtime form that can call `enumerate` with the dependency values already
327
+ * typed on the command line.
300
328
  */
301
- function stripPendingExpand(vc) {
302
- return vc?.type === "pending-expand" ? void 0 : vc;
329
+ function resolveRuntimeCompletion(vc) {
330
+ if (!vc) return void 0;
331
+ if (vc.type !== "pending-expand") return vc;
332
+ return {
333
+ type: "runtime-expand",
334
+ dependsOn: vc.spec.dependsOn,
335
+ enumerate: vc.spec.enumerate
336
+ };
303
337
  }
304
338
  /**
305
339
  * Extract options from a command
@@ -322,7 +356,7 @@ function extractOptionsFromSchema(schema) {
322
356
  valueType: field.type,
323
357
  required: field.required,
324
358
  defaultNegationAccepted: field.type === "boolean" && (field.negation === void 0 || field.negation === true),
325
- valueCompletion: stripPendingExpand(resolveValueCompletion(field))
359
+ valueCompletion: resolveRuntimeCompletion(resolveValueCompletion(field))
326
360
  };
327
361
  });
328
362
  }
@@ -453,7 +487,7 @@ function extractPositionalsForContext(command) {
453
487
  description: field.description,
454
488
  required: field.required,
455
489
  variadic: field.type === "array",
456
- valueCompletion: stripPendingExpand(resolveValueCompletion(field))
490
+ valueCompletion: resolveRuntimeCompletion(resolveValueCompletion(field))
457
491
  }));
458
492
  }
459
493
  /**
@@ -881,11 +915,15 @@ async function generateCandidates(context, options) {
881
915
  return generateValueCandidates(inlinePrefix ? {
882
916
  ...context,
883
917
  currentWord: context.currentWord.slice(inlinePrefix.length)
884
- } : context, options, opt?.name, opt?.valueCompletion);
918
+ } : context, options, opt?.name, opt?.valueCompletion, void 0, opt?.valueType === "array");
885
919
  }
886
920
  case "positional": {
887
921
  const positional = resolvePositionalTarget(context);
888
- return generateValueCandidates(context, options, positional?.name, positional?.valueCompletion, positional?.description);
922
+ if (!positional) return {
923
+ candidates: [],
924
+ directive: CompletionDirective.NoFileCompletion
925
+ };
926
+ return generateValueCandidates(context, options, positional.name, positional.valueCompletion, positional.description, false);
889
927
  }
890
928
  }
891
929
  }
@@ -920,6 +958,16 @@ function executeShellCommand(command) {
920
958
  return [];
921
959
  }
922
960
  }
961
+ function staticChoices(vc) {
962
+ return vc?.type === "choices" ? vc.choices : void 0;
963
+ }
964
+ function runtimeExpandDepChoices(context, dep) {
965
+ const localOption = context.options.find((opt) => opt.name === dep && opt.isGlobal !== true);
966
+ if (localOption) return staticChoices(localOption.valueCompletion);
967
+ const positional = context.positionals.find((pos) => pos.name === dep);
968
+ if (positional) return staticChoices(positional.valueCompletion);
969
+ return staticChoices(context.options.find((opt) => opt.name === dep && opt.isGlobal === true)?.valueCompletion);
970
+ }
923
971
  /**
924
972
  * Two-stage `key=value` post-processing. Returns the transformed candidate
925
973
  * list plus whether it contains a bare `key=` entry so the caller can flip
@@ -968,7 +1016,7 @@ function dropBareKeyEcho(candidates, currentWord) {
968
1016
  /**
969
1017
  * Resolve value completion, executing shell commands and file lookups in JS
970
1018
  */
971
- async function resolveValueCandidates(vc, ctx, description) {
1019
+ async function resolveValueCandidates(vc, ctx, completionContext, description, dedupeKeyValues) {
972
1020
  const candidates = [];
973
1021
  let directive = CompletionDirective.FilterPrefix;
974
1022
  let fileExtensions;
@@ -1025,9 +1073,40 @@ async function resolveValueCandidates(vc, ctx, description) {
1025
1073
  case "expand":
1026
1074
  directive |= CompletionDirective.NoFileCompletion;
1027
1075
  break;
1076
+ case "runtime-expand": {
1077
+ const deps = {};
1078
+ let missingDep = false;
1079
+ for (const dep of vc.dependsOn) {
1080
+ const raw = ctx.parsedArgs[dep];
1081
+ const value = Array.isArray(raw) ? raw[raw.length - 1] : raw;
1082
+ const allowedValues = runtimeExpandDepChoices(completionContext, dep);
1083
+ if (typeof value !== "string" || allowedValues === void 0 || !allowedValues.includes(value)) {
1084
+ missingDep = true;
1085
+ break;
1086
+ }
1087
+ deps[dep] = value;
1088
+ }
1089
+ if (!missingDep) try {
1090
+ const seen = /* @__PURE__ */ new Set();
1091
+ for (const item of vc.enumerate(deps)) {
1092
+ const normalized = typeof item === "string" ? { value: item } : item;
1093
+ if (seen.has(normalized.value)) continue;
1094
+ seen.add(normalized.value);
1095
+ candidates.push({
1096
+ ...normalized,
1097
+ type: "value"
1098
+ });
1099
+ }
1100
+ } catch {
1101
+ directive = CompletionDirective.NoFileCompletion | CompletionDirective.Error;
1102
+ break;
1103
+ }
1104
+ directive |= CompletionDirective.NoFileCompletion;
1105
+ break;
1106
+ }
1028
1107
  }
1029
- if (vc.type === "dynamic" || vc.type === "expand") {
1030
- const processed = applyKeyValuePostProcessing(candidates, ctx.currentWord);
1108
+ if (vc.type === "dynamic" || vc.type === "expand" || vc.type === "runtime-expand") {
1109
+ const processed = applyKeyValuePostProcessing(vc.type === "runtime-expand" && dedupeKeyValues === true ? dropAlreadyUsedKeyCandidates(candidates, ctx.previousValues) : candidates, ctx.currentWord);
1031
1110
  if (processed.hasEqSuffix) directive |= CompletionDirective.NoSpace;
1032
1111
  return {
1033
1112
  candidates: processed.candidates,
@@ -1043,6 +1122,19 @@ async function resolveValueCandidates(vc, ctx, description) {
1043
1122
  fileMatchers
1044
1123
  };
1045
1124
  }
1125
+ function dropAlreadyUsedKeyCandidates(candidates, previousValues) {
1126
+ const used = /* @__PURE__ */ new Set();
1127
+ for (const value of previousValues) {
1128
+ const eqIdx = value.indexOf("=");
1129
+ if (eqIdx > 0) used.add(value.slice(0, eqIdx));
1130
+ }
1131
+ if (used.size === 0) return [...candidates];
1132
+ return candidates.filter((candidate) => {
1133
+ const eqIdx = candidate.value.indexOf("=");
1134
+ if (eqIdx <= 0) return true;
1135
+ return !used.has(candidate.value.slice(0, eqIdx));
1136
+ });
1137
+ }
1046
1138
  /**
1047
1139
  * Generate subcommand candidates
1048
1140
  */
@@ -1066,7 +1158,7 @@ function generateSubcommandCandidates(context) {
1066
1158
  }
1067
1159
  return {
1068
1160
  candidates,
1069
- directive: CompletionDirective.FilterPrefix
1161
+ directive: CompletionDirective.FilterPrefix | CompletionDirective.NoFileCompletion
1070
1162
  };
1071
1163
  }
1072
1164
  /**
@@ -1100,7 +1192,7 @@ function generateOptionNameCandidates(context) {
1100
1192
  });
1101
1193
  return {
1102
1194
  candidates,
1103
- directive: CompletionDirective.FilterPrefix
1195
+ directive: CompletionDirective.FilterPrefix | CompletionDirective.NoFileCompletion
1104
1196
  };
1105
1197
  }
1106
1198
  /**
@@ -1136,12 +1228,12 @@ function parsedArgsWithoutTarget(parsedArgs, key) {
1136
1228
  * is propagated to choices candidates (positional path supplies it; option
1137
1229
  * path does not, mirroring the prior split implementations).
1138
1230
  */
1139
- async function generateValueCandidates(context, options, targetFieldName, vc, description) {
1231
+ async function generateValueCandidates(context, options, targetFieldName, vc, description, dedupeKeyValues) {
1140
1232
  if (!vc) return {
1141
1233
  candidates: [],
1142
1234
  directive: CompletionDirective.FilterPrefix
1143
1235
  };
1144
- return resolveValueCandidates(vc, resolverContext(context, options, targetFieldName), description);
1236
+ return resolveValueCandidates(vc, resolverContext(context, options, targetFieldName), context, description, dedupeKeyValues);
1145
1237
  }
1146
1238
 
1147
1239
  //#endregion
@@ -1152,10 +1244,10 @@ async function generateValueCandidates(context, options, targetFieldName, vc, de
1152
1244
  * sentinel via `resolveValueCompletion`) and passes them here once siblings
1153
1245
  * are known.
1154
1246
  */
1155
- function resolveExpandTargets(sub, targets, globalOptions = []) {
1247
+ function resolveExpandTargets(sub, targets, globalOptions = [], validateOnly = false) {
1156
1248
  if (targets.length === 0) return;
1157
1249
  const siblingIndex = buildSiblingIndex(sub, globalOptions);
1158
- for (const target of targets) target.set(resolveOne(target, siblingIndex));
1250
+ for (const target of targets) target.set(resolveOne(target, siblingIndex, validateOnly));
1159
1251
  }
1160
1252
  /**
1161
1253
  * Build a name → static-values map for siblings, using each field's already
@@ -1182,7 +1274,7 @@ function buildSiblingIndex(sub, globalOptions) {
1182
1274
  visit(globalOptions, true);
1183
1275
  return index;
1184
1276
  }
1185
- function resolveOne(target, siblings) {
1277
+ function resolveOne(target, siblings, validateOnly = false) {
1186
1278
  const { spec } = target;
1187
1279
  const deps = spec.dependsOn;
1188
1280
  if (deps.length === 0) throw new Error(`Field "${target.describe}": completion.custom.expand.dependsOn must list at least one sibling arg.`);
@@ -1193,6 +1285,11 @@ function resolveOne(target, siblings) {
1193
1285
  if (!values) throw new Error(`Field "${target.describe}": completion.custom.expand.dependsOn references "${dep}", which is not a sibling arg with a static \`choices\`/enum schema on the same command. Chaining expand specs is not supported.`);
1194
1286
  valueLists.push([...values]);
1195
1287
  }
1288
+ if (validateOnly) return {
1289
+ type: "expand",
1290
+ dependsOn: deps,
1291
+ table: []
1292
+ };
1196
1293
  const table = [];
1197
1294
  for (const combo of cartesian(valueLists)) {
1198
1295
  const depsRecord = {};
@@ -1403,11 +1500,11 @@ function fieldsToPositionals(fields, pending) {
1403
1500
  /**
1404
1501
  * Extract a completable subcommand from a command
1405
1502
  */
1406
- function extractSubcommand(name, command, globalOptions = []) {
1503
+ function extractSubcommand(name, command, globalOptions = [], validateOnly = false) {
1407
1504
  const subcommands = [];
1408
1505
  if (command.subCommands) for (const [subName, subCommand] of Object.entries(command.subCommands)) {
1409
1506
  const resolved = resolveSubCommandMeta(subCommand);
1410
- if (resolved) subcommands.push(extractSubcommand(subName, resolved, globalOptions));
1507
+ if (resolved) subcommands.push(extractSubcommand(subName, resolved, globalOptions, validateOnly));
1411
1508
  else subcommands.push({
1412
1509
  name: subName,
1413
1510
  description: "(lazy loaded)",
@@ -1426,7 +1523,7 @@ function extractSubcommand(name, command, globalOptions = []) {
1426
1523
  options: fieldsToOptions(fields, pending),
1427
1524
  positionals: fieldsToPositionals(fields, pending)
1428
1525
  };
1429
- resolveExpandTargets(node, pending, globalOptions);
1526
+ resolveExpandTargets(node, pending, globalOptions, validateOnly);
1430
1527
  return node;
1431
1528
  }
1432
1529
  /** Join parent and child with a separator, omitting separator when parent is empty. */
@@ -1825,7 +1922,7 @@ function propagateGlobalOptions(sub, globalOptions) {
1825
1922
  * @param globalArgsSchema - Optional global args schema. When provided, global options
1826
1923
  * are derived from this schema instead of the root command's options.
1827
1924
  */
1828
- function extractCompletionData(command, programName, globalArgsSchema) {
1925
+ function extractCompletionData(command, programName, globalArgsSchema, validateOnly = false) {
1829
1926
  let globalOptions = [];
1830
1927
  if (globalArgsSchema) {
1831
1928
  const globalPending = [];
@@ -1838,9 +1935,9 @@ function extractCompletionData(command, programName, globalArgsSchema) {
1838
1935
  subcommands: [],
1839
1936
  options: globalOptions,
1840
1937
  positionals: []
1841
- }, globalPending);
1938
+ }, globalPending, [], validateOnly);
1842
1939
  }
1843
- const rootSubcommand = extractSubcommand(programName, command, globalOptions);
1940
+ const rootSubcommand = extractSubcommand(programName, command, globalOptions, validateOnly);
1844
1941
  if (globalArgsSchema) propagateGlobalOptions(rootSubcommand, globalOptions);
1845
1942
  else globalOptions = rootSubcommand.options;
1846
1943
  return {
@@ -1857,8 +1954,9 @@ function extractCompletionData(command, programName, globalArgsSchema) {
1857
1954
  *
1858
1955
  * Every completion script generated by politty starts with a small
1859
1956
  * machine-readable header. The rc loader and the runMain background
1860
- * refresh path use the `# politty-bin-sig:` line to detect when the
1861
- * cached script is stale relative to the binary on disk.
1957
+ * refresh path use the `# politty-bin-sig:` and `# politty-bin-path:`
1958
+ * lines to detect when the cached script is stale relative to the
1959
+ * binary on disk.
1862
1960
  */
1863
1961
  /** Schema version of the header itself. Bump when the header layout changes. */
1864
1962
  const COMPLETION_VERSION = 1;
@@ -1874,6 +1972,9 @@ function computeBinSig(binPath) {
1874
1972
  return "0";
1875
1973
  }
1876
1974
  }
1975
+ function headerValue(value) {
1976
+ return value.replace(/\r?\n/g, " ");
1977
+ }
1877
1978
  /**
1878
1979
  * Walk `$PATH` looking for an executable named `programName`. Returns
1879
1980
  * the first match's full path, or `null` when not found. We mirror the
@@ -1904,6 +2005,8 @@ function findOnPath(programName) {
1904
2005
  */
1905
2006
  function resolveBinPath(programName, override) {
1906
2007
  if (override) return override;
2008
+ const envOverride = process.env[binEnvVarName(sanitize(programName))];
2009
+ if (envOverride) return envOverride;
1907
2010
  return findOnPath(programName) ?? process.argv[1] ?? "";
1908
2011
  }
1909
2012
  /**
@@ -1912,10 +2015,12 @@ function resolveBinPath(programName, override) {
1912
2015
  * marker.
1913
2016
  */
1914
2017
  function buildHeaderLines(opts) {
1915
- const sig = computeBinSig(resolveBinPath(opts.programName, opts.binPath));
2018
+ const binPath = resolveBinPath(opts.programName, opts.binPath);
2019
+ const sig = computeBinSig(binPath);
1916
2020
  const lines = [
1917
2021
  `# politty-completion-version: ${1}`,
1918
2022
  `# politty-bin-sig: ${sig}`,
2023
+ `# politty-bin-path: ${headerValue(binPath)}`,
1919
2024
  `# program: ${opts.programName}`
1920
2025
  ];
1921
2026
  if (opts.programVersion) lines.push(`# program-version: ${opts.programVersion}`);
@@ -1923,6 +2028,92 @@ function buildHeaderLines(opts) {
1923
2028
  return lines;
1924
2029
  }
1925
2030
 
2031
+ //#endregion
2032
+ //#region src/completion/self-refresh.ts
2033
+ /**
2034
+ * Self-refresh guards embedded in generated bash/zsh scripts.
2035
+ *
2036
+ * These guards make the default `completion <shell>` output safe to
2037
+ * save as a static completion file: when the CLI binary changes, the
2038
+ * script asks the hidden refresh subcommand to rewrite the sourced
2039
+ * file in place, then sources the fresh file and stops executing the
2040
+ * stale body.
2041
+ */
2042
+ function generateBashSelfRefresh(opts) {
2043
+ const { programName, binPath } = opts;
2044
+ const fn = sanitize(programName);
2045
+ const envName = binEnvVarName(fn);
2046
+ const resolvedBinPath = resolveBinPath(programName, binPath);
2047
+ const sig = computeBinSig(resolvedBinPath);
2048
+ const quotedBinPath = shSingleQuote(resolvedBinPath);
2049
+ const refreshFn = `__${fn}_self_refresh`;
2050
+ return [
2051
+ `${refreshFn}() {`,
2052
+ ` local _self _bin _sig`,
2053
+ ` _self=\${BASH_SOURCE[0]:-}`,
2054
+ ` [[ -n "$_self" && -f "$_self" ]] || return 1`,
2055
+ ` head -n 8 "$_self" 2>/dev/null | grep -qF "# politty-completion-version:" || return 1`,
2056
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# program: ${programName}" || return 1`,
2057
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# shell: bash" || return 1`,
2058
+ ` _bin="\${${envName}:-$(type -P ${programName} 2>/dev/null)}"`,
2059
+ ` [[ -n "$_bin" ]] || return 1`,
2060
+ ` _sig=${statSigExpr("$_bin", { shell: "posix" })} || return 1`,
2061
+ ` [[ "$_sig" != "${sig}" || "$_bin" != ${quotedBinPath} ]] || return 1`,
2062
+ ` "$_bin" __refresh-completion bash "$_self" --static 2>/dev/null || return 1`,
2063
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# politty-bin-sig: $_sig" || return 1`,
2064
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# politty-bin-path: $_bin" || return 1`,
2065
+ ` source "$_self" 2>/dev/null || return 1`,
2066
+ ` return 0`,
2067
+ `}`,
2068
+ `if ${refreshFn}; then`,
2069
+ ` unset -f ${refreshFn}`,
2070
+ ` return 0 2>/dev/null || true`,
2071
+ `else`,
2072
+ ` unset -f ${refreshFn}`,
2073
+ `fi`,
2074
+ ``
2075
+ ];
2076
+ }
2077
+ function generateZshSelfRefresh(opts) {
2078
+ const { programName, binPath } = opts;
2079
+ const fn = sanitize(programName);
2080
+ const envName = binEnvVarName(fn);
2081
+ const completionFn = `_${programName}`;
2082
+ const resolvedBinPath = resolveBinPath(programName, binPath);
2083
+ const sig = computeBinSig(resolvedBinPath);
2084
+ const quotedBinPath = shSingleQuote(resolvedBinPath);
2085
+ const refreshFn = `__${fn}_self_refresh`;
2086
+ return [
2087
+ `${refreshFn}() {`,
2088
+ ` emulate -L zsh`,
2089
+ ` setopt local_options no_aliases`,
2090
+ ` local _self _bin _sig`,
2091
+ ` _self="\${(%):-%x}"`,
2092
+ ` [[ -n "$_self" && -f "$_self" ]] || return 1`,
2093
+ ` head -n 8 "$_self" 2>/dev/null | grep -qF "# politty-completion-version:" || return 1`,
2094
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# program: ${programName}" || return 1`,
2095
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# shell: zsh" || return 1`,
2096
+ ` _bin="\${${envName}:-$(whence -p ${programName} 2>/dev/null)}"`,
2097
+ ` [[ -n "$_bin" ]] || return 1`,
2098
+ ` _sig=${statSigExpr("$_bin", { shell: "posix" })} || return 1`,
2099
+ ` [[ "$_sig" != "${sig}" || "$_bin" != ${quotedBinPath} ]] || return 1`,
2100
+ ` "$_bin" __refresh-completion zsh "$_self" --static 2>/dev/null || return 1`,
2101
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# politty-bin-sig: $_sig" || return 1`,
2102
+ ` head -n 8 "$_self" 2>/dev/null | grep -qxF "# politty-bin-path: $_bin" || return 1`,
2103
+ ` source "$_self" 2>/dev/null || return 1`,
2104
+ ` ${completionFn} "$@"`,
2105
+ ` return 0`,
2106
+ `}`,
2107
+ `if ${refreshFn} "$@"; then`,
2108
+ ` unfunction ${refreshFn} 2>/dev/null`,
2109
+ ` return 0 2>/dev/null || true`,
2110
+ `else`,
2111
+ ` unfunction ${refreshFn} 2>/dev/null`,
2112
+ `fi`,
2113
+ ``
2114
+ ];
2115
+ }
2116
+
1926
2117
  //#endregion
1927
2118
  //#region src/completion/bash.ts
1928
2119
  /** Escape a string for use inside bash double-quotes */
@@ -2024,6 +2215,7 @@ function bashValueLines(vc, inline, fn, location) {
2024
2215
  case "directory": return [`COMPREPLY=($(compgen -P "$_inline_prefix" -d -- "$_cur"))`, `compopt -o filenames`];
2025
2216
  case "command": return [`COMPREPLY=($(compgen -P "$_inline_prefix" -W "$(${vc.shellCommand})" -- "$_cur"))`];
2026
2217
  case "none": return [`compopt +o default 2>/dev/null`];
2218
+ case "runtime-expand": return [];
2027
2219
  }
2028
2220
  }
2029
2221
  /**
@@ -2086,6 +2278,28 @@ function positionalBlock$2(positionals, fn, funcSuffix, options = []) {
2086
2278
  lines.push(` esac`);
2087
2279
  return lines;
2088
2280
  }
2281
+ /**
2282
+ * Subcommand-name completion. When the same node also has positionals, emit a
2283
+ * runtime check that completes subcommand names while the cursor still prefixes
2284
+ * one and falls through to positional completion otherwise. Returns lines at
2285
+ * base indentation; callers re-indent for their handler depth.
2286
+ */
2287
+ function subOrPositionalLines$2(subNames, positionals, fn, funcSuffix, options) {
2288
+ const subReply = [`COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`, `compopt +o default 2>/dev/null`];
2289
+ if (positionals.length === 0) return subReply;
2290
+ return [
2291
+ `local -a _sub_names=(${subNames})`,
2292
+ `local _sub_name _sub_match=0`,
2293
+ `for _sub_name in "\${_sub_names[@]}"; do`,
2294
+ ` [[ "$_sub_name" == "$_cur"* ]] && _sub_match=1 && break`,
2295
+ `done`,
2296
+ `if (( _sub_match )); then`,
2297
+ ...subReply.map((l) => ` ${l}`),
2298
+ `else`,
2299
+ ...positionalBlock$2(positionals, fn, funcSuffix, options),
2300
+ `fi`
2301
+ ];
2302
+ }
2089
2303
  /** Generate prev/inline value completion blocks for options */
2090
2304
  function valueCompletionBlocks(options, positionals, fn, funcSuffix) {
2091
2305
  if (!options.some((o) => o.takesValue && o.valueCompletion)) return [];
@@ -2161,8 +2375,7 @@ function generateSubHandler$2(sub, fn, path) {
2161
2375
  lines.push(` fi`);
2162
2376
  if (visibleSubs.length > 0) {
2163
2377
  const subNames = getSubNamesWithAliases(sub.subcommands).map((s) => s.name).join(" ");
2164
- lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
2165
- lines.push(` compopt +o default 2>/dev/null`);
2378
+ lines.push(...subOrPositionalLines$2(subNames, sub.positionals, fn, funcSuffix, sub.options).map((l) => ` ${l}`));
2166
2379
  } else if (sub.positionals.length > 0) lines.push(...positionalBlock$2(sub.positionals, fn, funcSuffix, sub.options));
2167
2380
  lines.push(`}`);
2168
2381
  lines.push(``);
@@ -2171,7 +2384,9 @@ function generateSubHandler$2(sub, fn, path) {
2171
2384
  function generateBashCompletion(command, options) {
2172
2385
  const { programName } = options;
2173
2386
  const data = extractCompletionData(command, programName, options.globalArgsSchema);
2174
- const fn = sanitize(programName);
2387
+ const baseFn = sanitize(programName);
2388
+ const fn = options.staticWorker ? `${baseFn}_${sanitize(options.staticWorker.functionSuffix)}` : baseFn;
2389
+ const isWorker = options.staticWorker !== void 0;
2175
2390
  const root = data.command;
2176
2391
  const visibleSubs = getVisibleSubs(root.subcommands);
2177
2392
  const expandSpecs = collectExpandSpecs(root);
@@ -2183,8 +2398,14 @@ function generateBashCompletion(command, options) {
2183
2398
  binPath: options.binPath,
2184
2399
  programVersion: options.programVersion
2185
2400
  }));
2401
+ lines.push(`# politty-completion-mode: ${isWorker ? "worker" : "static"}`);
2402
+ if (isWorker) lines.push(`# politty-completion-worker: true`);
2186
2403
  lines.push(`# Generated by politty`);
2187
2404
  lines.push(``);
2405
+ if (!isWorker) lines.push(...generateBashSelfRefresh({
2406
+ programName,
2407
+ binPath: options.binPath
2408
+ }));
2188
2409
  const hasExpand = expandSpecs.length > 0;
2189
2410
  const arrayExpandSpecs = expandSpecs.filter((s) => s.isArrayOption);
2190
2411
  const hasArrayExpand = arrayExpandSpecs.length > 0;
@@ -2325,8 +2546,7 @@ function generateBashCompletion(command, options) {
2325
2546
  if (visibleSubs.length > 0) {
2326
2547
  lines.push(` else`);
2327
2548
  const subNames = getSubNamesWithAliases(root.subcommands).map((s) => s.name).join(" ");
2328
- lines.push(` COMPREPLY=($(compgen -W "${subNames}" -- "$_cur"))`);
2329
- lines.push(` compopt +o default 2>/dev/null`);
2549
+ lines.push(...subOrPositionalLines$2(subNames, root.positionals, fn, "root", root.options).map((l) => ` ${l}`));
2330
2550
  } else if (root.positionals.length > 0) {
2331
2551
  lines.push(` else`);
2332
2552
  lines.push(...positionalBlock$2(root.positionals, fn, "root", root.options).map((l) => ` ${l}`));
@@ -2424,18 +2644,17 @@ function generateBashCompletion(command, options) {
2424
2644
  lines.push(` esac`);
2425
2645
  lines.push(`}`);
2426
2646
  lines.push(``);
2427
- lines.push(`complete -o default -F _${fn}_completions ${programName}`);
2647
+ if (!isWorker) lines.push(`complete -o default -F _${fn}_completions ${programName}`);
2428
2648
  lines.push(``);
2429
2649
  return {
2430
2650
  script: lines.join("\n"),
2431
2651
  shell: "bash",
2432
- installInstructions: `# To enable completions, add the following to your ~/.bashrc:
2433
-
2434
- # Option 1: Source directly
2435
- eval "$(${programName} completion bash)"
2652
+ installInstructions: `# To enable auto-refreshing bash completions, add this to your ~/.bashrc:
2653
+ eval "$(${programName} completion bash --static)"
2436
2654
 
2437
- # Option 2: Save to a file
2438
- ${programName} completion bash > ~/.local/share/bash-completion/completions/${programName}
2655
+ # For faster shell startup, save the script instead:
2656
+ mkdir -p ~/.local/share/bash-completion/completions
2657
+ ${programName} completion bash --static > ~/.local/share/bash-completion/completions/${programName}
2439
2658
 
2440
2659
  # Then reload your shell or run:
2441
2660
  source ~/.bashrc`
@@ -2443,182 +2662,1317 @@ source ~/.bashrc`
2443
2662
  }
2444
2663
 
2445
2664
  //#endregion
2446
- //#region src/completion/dynamic/shell-formatter.ts
2447
- /**
2448
- * Format completion candidates for the specified shell
2449
- *
2450
- * @returns Shell-ready output string (lines separated by newline, last line is :directive)
2451
- */
2452
- function formatForShell(result, options) {
2453
- switch (options.shell) {
2454
- case "bash": return formatForBash(result, options);
2455
- case "zsh": return formatForZsh(result, options);
2456
- case "fish": return formatForFish(result, options);
2665
+ //#region src/completion/bundled-worker.ts
2666
+ const execFileAsync = promisify(execFile);
2667
+ const SHELL_EXT = {
2668
+ bash: "bash",
2669
+ zsh: "zsh",
2670
+ fish: "fish"
2671
+ };
2672
+ const REQUIRED_WORKER_HEADERS = [
2673
+ "# politty-completion-version: 1",
2674
+ "# politty-completion-mode: worker",
2675
+ "# politty-completion-worker: true"
2676
+ ];
2677
+ function bundledWorkerShellExtension(shell) {
2678
+ return SHELL_EXT[shell];
2679
+ }
2680
+ function defaultBundledWorkerOutputPath(shell) {
2681
+ return join("dist", "completion", `${shell}-worker.${bundledWorkerShellExtension(shell)}`);
2682
+ }
2683
+ function defaultBundledWorkerRelativePaths(shell) {
2684
+ const ext = SHELL_EXT[shell];
2685
+ return [
2686
+ `completion/${shell}-worker.${ext}`,
2687
+ `../completion/${shell}-worker.${ext}`,
2688
+ `dist/completion/${shell}-worker.${ext}`,
2689
+ `../dist/completion/${shell}-worker.${ext}`,
2690
+ `completion-worker.${shell}`,
2691
+ `../completion-worker.${shell}`
2692
+ ];
2693
+ }
2694
+ function bundledWorkerRelativePaths(programName, shell, options) {
2695
+ if (options?.disabled) return [];
2696
+ const configured = options?.relativePaths?.[shell];
2697
+ return (configured && configured.length > 0 ? configured : defaultBundledWorkerRelativePaths(shell)).map((p) => p.replaceAll("{shell}", shell).replaceAll("{ext}", SHELL_EXT[shell]).replaceAll("{program}", programName));
2698
+ }
2699
+ function readCmdShimTarget(path) {
2700
+ try {
2701
+ const content = readFileSync(path, "utf8");
2702
+ let target = null;
2703
+ for (const line of content.split("\n")) {
2704
+ const match = line.match(/^# cmd-shim-target=(.+)$/);
2705
+ if (match) target = match[1];
2706
+ }
2707
+ return target;
2708
+ } catch {
2709
+ return null;
2457
2710
  }
2458
2711
  }
2459
- /**
2460
- * Append extension metadata and directive to output lines
2461
- */
2462
- function appendMetadata(lines, result) {
2463
- if (result.fileExtensions && result.fileExtensions.length > 0) lines.push(`@ext:${result.fileExtensions.join(",")}`);
2464
- if (result.fileMatchers && result.fileMatchers.length > 0) lines.push(`@matcher:${result.fileMatchers.join(",")}`);
2465
- lines.push(`:${result.directive}`);
2712
+ function addBaseDirs(out, path) {
2713
+ if (!path) return;
2714
+ out.add(dirname(resolve(path)));
2715
+ try {
2716
+ out.add(dirname(realpathSync(path)));
2717
+ } catch {}
2718
+ const shimTarget = readCmdShimTarget(path);
2719
+ if (shimTarget) addBaseDirs(out, shimTarget);
2466
2720
  }
2467
- /**
2468
- * Format for bash
2469
- *
2470
- * - Pre-filters candidates by currentWord prefix (replaces compgen -W)
2471
- * - Handles --opt=value inline values by prepending prefix
2472
- * - Outputs plain values only (no descriptions - bash COMPREPLY doesn't support them)
2473
- * - Last line: :directive
2474
- */
2475
- function formatForBash(result, options) {
2476
- const lines = ((result.directive & CompletionDirective.FilterPrefix) !== 0 && options.currentWord ? result.candidates.filter((c) => c.value.startsWith(options.currentWord)) : result.candidates).map((c) => options.inlinePrefix ? `${options.inlinePrefix}${c.value}` : c.value);
2477
- appendMetadata(lines, result);
2478
- return lines.join("\n");
2721
+ function workerHead(path) {
2722
+ return readFileSync(path, "utf8").split("\n", 24).join("\n");
2479
2723
  }
2480
- /**
2481
- * Format for zsh
2482
- *
2483
- * - Outputs value:description pairs for _describe
2484
- * - Colons in values/descriptions are escaped with backslash
2485
- * - Last line: :directive
2486
- */
2487
- function formatForZsh(result, _options) {
2488
- const lines = result.candidates.map((c) => {
2489
- const escapedValue = c.value.replace(/:/g, "\\:");
2490
- if (c.description) return `${escapedValue}:${c.description.replace(/:/g, "\\:")}`;
2491
- return escapedValue;
2492
- });
2493
- appendMetadata(lines, result);
2494
- return lines.join("\n");
2724
+ function requiredBundledWorkerHeaders(programName, shell) {
2725
+ return [
2726
+ ...REQUIRED_WORKER_HEADERS,
2727
+ `# program: ${programName}`,
2728
+ `# shell: ${shell}`
2729
+ ];
2495
2730
  }
2496
- /**
2497
- * Format for fish
2498
- *
2499
- * - Outputs value\tdescription pairs
2500
- * - Last line: :directive
2501
- */
2502
- function formatForFish(result, _options) {
2503
- const lines = result.candidates.map((c) => {
2504
- if (c.description) return `${c.value}\t${c.description}`;
2505
- return c.value;
2731
+ function missingBundledWorkerHeaders(head, programName, shell) {
2732
+ const lines = head.split("\n").map((line) => line.trimEnd());
2733
+ return requiredBundledWorkerHeaders(programName, shell).filter((header) => !lines.includes(header));
2734
+ }
2735
+ function validateBundledWorkerFile(path, programName, shell) {
2736
+ if (!existsSync(path)) throw new Error(`Bundled completion worker does not exist: ${path}`);
2737
+ const missing = missingBundledWorkerHeaders(workerHead(path), programName, shell);
2738
+ if (missing.length > 0) throw new Error(`Invalid bundled completion worker ${path}: missing ${missing.map((h) => JSON.stringify(h)).join(", ")}`);
2739
+ }
2740
+ function isBundledWorkerFile(path, programName, shell) {
2741
+ try {
2742
+ validateBundledWorkerFile(path, programName, shell);
2743
+ return true;
2744
+ } catch {
2745
+ return false;
2746
+ }
2747
+ }
2748
+ function resolveBundledWorkerPath(opts) {
2749
+ const rels = bundledWorkerRelativePaths(opts.programName, opts.shell, opts.bundledWorker);
2750
+ if (rels.length === 0) return null;
2751
+ const bases = /* @__PURE__ */ new Set();
2752
+ if (opts.binPath !== void 0) addBaseDirs(bases, opts.binPath);
2753
+ if (process.argv[1]) addBaseDirs(bases, process.argv[1]);
2754
+ addBaseDirs(bases, resolveBinPath(opts.programName, opts.binPath));
2755
+ for (const rel of rels) if (isAbsolute(rel) && isBundledWorkerFile(rel, opts.programName, opts.shell)) return rel;
2756
+ for (const base of bases) for (const rel of rels) {
2757
+ if (isAbsolute(rel)) continue;
2758
+ const candidate = join(base, rel);
2759
+ if (isBundledWorkerFile(candidate, opts.programName, opts.shell)) return candidate;
2760
+ }
2761
+ return null;
2762
+ }
2763
+ function resolvePathFromCwd(path, cwd) {
2764
+ return isAbsolute(path) ? path : resolve(cwd, path);
2765
+ }
2766
+ function executableCommand(bin, args, cwd) {
2767
+ const binPath = bin.startsWith(".") || bin.includes("/") || bin.includes("\\") ? resolvePathFromCwd(bin, cwd) : bin;
2768
+ const ext = extname(binPath).toLowerCase();
2769
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") return {
2770
+ command: process.execPath,
2771
+ args: [binPath, ...args]
2772
+ };
2773
+ return {
2774
+ command: binPath,
2775
+ args: [...args]
2776
+ };
2777
+ }
2778
+ async function runTargetBin(bin, args, opts) {
2779
+ const command = executableCommand(bin, args, opts.cwd);
2780
+ try {
2781
+ const { stdout, stderr } = await execFileAsync(command.command, command.args, {
2782
+ cwd: opts.cwd,
2783
+ env: {
2784
+ ...process.env,
2785
+ ...opts.env
2786
+ },
2787
+ encoding: "utf8",
2788
+ maxBuffer: 10 * 1024 * 1024
2789
+ });
2790
+ return {
2791
+ stdout,
2792
+ stderr
2793
+ };
2794
+ } catch (error) {
2795
+ const stderr = typeof error === "object" && error !== null && "stderr" in error ? String(error.stderr ?? "").trim() : "";
2796
+ const detail = stderr ? `\n${stderr}` : "";
2797
+ throw new Error(`Command failed: ${command.command} ${command.args.join(" ")}${detail}`, error instanceof Error ? { cause: error } : void 0);
2798
+ }
2799
+ }
2800
+ function assertNonEmptyFile(path) {
2801
+ const stat = statSync(path);
2802
+ if (!stat.isFile() || stat.size === 0) throw new Error(`Generated bundled completion worker is empty: ${path}`);
2803
+ return stat.size;
2804
+ }
2805
+ function formatSize(size) {
2806
+ if (size < 1024) return `${size} B`;
2807
+ return `${(size / 1024).toFixed(1)} KiB`;
2808
+ }
2809
+ function printSuccess(path, size, cwd) {
2810
+ const rel = relative(cwd, path);
2811
+ console.log(`Generated bundled completion worker: ${rel && !rel.startsWith("..") ? rel : path} (${formatSize(size)})`);
2812
+ }
2813
+ async function generateBundledCompletionWorker(options) {
2814
+ const cwd = options.cwd ?? process.cwd();
2815
+ const outputPath = resolvePathFromCwd(options.outputPath ?? defaultBundledWorkerOutputPath(options.shell), cwd);
2816
+ mkdirSync(dirname(outputPath), { recursive: true });
2817
+ rmSync(outputPath, { force: true });
2818
+ await runTargetBin(options.bin, [
2819
+ "__refresh-completion",
2820
+ options.shell,
2821
+ outputPath,
2822
+ "--static",
2823
+ "--worker"
2824
+ ], {
2825
+ cwd,
2826
+ env: options.env
2506
2827
  });
2507
- appendMetadata(lines, result);
2508
- return lines.join("\n");
2828
+ const size = assertNonEmptyFile(outputPath);
2829
+ validateBundledWorkerFile(outputPath, options.programName, options.shell);
2830
+ let reportedPath;
2831
+ if (options.verify) {
2832
+ const lines = (await runTargetBin(options.bin, ["__completion-worker-path", options.shell], {
2833
+ cwd,
2834
+ env: options.env
2835
+ })).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
2836
+ if (lines.length !== 1) throw new Error(`Expected __completion-worker-path ${options.shell} to print exactly one path, got ${lines.length}.`);
2837
+ reportedPath = resolvePathFromCwd(lines[0], cwd);
2838
+ const generatedReal = realpathSync(outputPath);
2839
+ const reportedReal = realpathSync(reportedPath);
2840
+ if (reportedReal !== generatedReal) throw new Error(`Bundled completion worker path mismatch: generated ${generatedReal}, reported ${reportedReal}`);
2841
+ }
2842
+ if (!options.quiet) printSuccess(outputPath, size, cwd);
2843
+ return {
2844
+ outputPath,
2845
+ size,
2846
+ ...reportedPath !== void 0 && { reportedPath }
2847
+ };
2509
2848
  }
2510
2849
 
2511
2850
  //#endregion
2512
- //#region src/completion/dynamic/complete-command.ts
2513
- /**
2514
- * Dynamic completion command implementation
2515
- *
2516
- * This creates a hidden `__complete` command that outputs completion candidates
2517
- * for shell scripts to consume. Usage:
2518
- *
2519
- * mycli __complete --shell bash -- build --fo
2520
- * mycli __complete --shell zsh -- plugin add
2521
- *
2522
- * Output format depends on the target shell:
2523
- * bash: plain values (pre-filtered by prefix), last line :directive
2524
- * zsh: value:description pairs, last line :directive
2525
- * fish: value\tdescription pairs, last line :directive
2526
- */
2527
- /**
2528
- * Schema for the __complete command
2529
- */
2530
- const completeArgsSchema = z.object({
2531
- shell: arg(z.enum([
2532
- "bash",
2533
- "zsh",
2534
- "fish"
2535
- ]), { description: "Target shell for output formatting" }),
2536
- args: arg(z.array(z.string()).default([]), {
2537
- positional: true,
2538
- description: "Arguments to complete",
2539
- variadic: true
2540
- })
2541
- });
2542
- /**
2543
- * Create the dynamic completion command
2544
- *
2545
- * @param rootCommand - The root command to generate completions for
2546
- * @param programName - The program name (optional, defaults to rootCommand.name)
2547
- * @param globalArgsSchema - Global args schema. Forwarded to
2548
- * `parseCompletionContext` so resolvers attached to global options remain
2549
- * reachable at every subcommand level.
2550
- * @returns A command that outputs completion candidates
2551
- */
2552
- function createDynamicCompleteCommand(rootCommand, _programName, globalArgsSchema) {
2553
- return defineCommand({
2554
- name: "__complete",
2555
- args: completeArgsSchema,
2556
- async run(args) {
2557
- const context = parseCompletionContext(args.args, rootCommand, globalArgsSchema);
2558
- const inlinePrefix = context.completionType === "option-value" && context.targetOption ? detectInlineOptionPrefix(context.currentWord) : void 0;
2559
- const effectiveWord = inlinePrefix ? context.currentWord.slice(inlinePrefix.length) : context.currentWord;
2560
- const output = formatForShell(await generateCandidates(context, { shell: args.shell }), {
2561
- shell: args.shell,
2562
- currentWord: effectiveWord,
2563
- inlinePrefix
2564
- });
2565
- console.log(output);
2566
- }
2567
- });
2851
+ //#region src/completion/dispatcher.ts
2852
+ function compileCacheSuffix(programName) {
2853
+ return shSingleQuote(`/${programName}/node-compile-cache`);
2568
2854
  }
2569
- /**
2570
- * Check if a command tree contains the __complete command
2571
- */
2572
- function hasCompleteCommand(command) {
2573
- return Boolean(command.subCommands?.["__complete"]);
2855
+ function hardcodedCompileCacheDir(cacheDir) {
2856
+ return cacheDir ? shSingleQuote(`${cacheDir}/node-compile-cache`) : void 0;
2574
2857
  }
2575
-
2576
- //#endregion
2577
- //#region src/completion/fish.ts
2578
- /** Escape shell-special characters for fish double-quoted strings */
2579
- function escapeDesc$1(s) {
2580
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
2858
+ function hardcodedWorkerPath(cacheDir, shell) {
2859
+ return cacheDir ? shSingleQuote(`${cacheDir}/completion-worker.${shell}`) : void 0;
2860
+ }
2861
+ function workerPathSuffix(programName, shell) {
2862
+ return shSingleQuote(`/${programName}/completion-worker.${shell}`);
2581
2863
  }
2582
2864
  /**
2583
- * Escape a fish `switch` case pattern. Fish's `case` interprets its
2584
- * arguments as globs even when double-quoted, so glob metacharacters
2585
- * (`*`, `?`, `[`, `]`) must be backslash-escaped to keep the comparison
2586
- * literal — otherwise a key like `prod*` would also match a runtime
2587
- * value of `production`. Quote/dollar/backslash are escaped first so the
2588
- * resulting string remains valid inside a double-quoted literal.
2865
+ * `<printCmd> <dir>` line resolving a cache path: the hardcoded `cacheDir` when
2866
+ * configured, else `${XDG_CACHE_HOME:-$HOME/.cache}` joined with `suffix`. Used
2867
+ * by bash and zsh, which both expand the XDG default inline.
2589
2868
  */
2590
- function fishCaseEscape(s) {
2591
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/\*/g, "\\*").replace(/\?/g, "\\?").replace(/\[/g, "\\[").replace(/]/g, "\\]");
2869
+ function posixCacheDefault(printCmd, hardcoded, suffix) {
2870
+ return hardcoded ? ` ${printCmd} ${hardcoded}` : ` ${printCmd} "\${XDG_CACHE_HOME:-$HOME/.cache}"${suffix}`;
2592
2871
  }
2593
2872
  /**
2594
- * Generate fish value completion lines for a ValueCompletion spec.
2595
- * Each line outputs candidates via echo (tab-separated value\tdescription).
2596
- *
2597
- * `location` is required for the expand variant (carries fieldName +
2598
- * isArrayOption); other variants ignore it.
2873
+ * fish equivalent of {@link posixCacheDefault}. fish has no `${VAR:-default}`
2874
+ * form, so the XDG fallback is materialized over three lines.
2599
2875
  */
2600
- function fishValueLines(vc, fn, location) {
2601
- if (!vc) return [];
2602
- switch (vc.type) {
2603
- case "expand": {
2604
- if (!location) throw new Error("fishValueLines: expand variant requires a location");
2605
- const depExpr = (d) => {
2606
- const safe = sanitize(d.name);
2607
- return d.isGlobal ? `$_global_arg_values_${safe}` : `$_arg_values_${safe}`;
2608
- };
2609
- const depKey = location.resolvedDeps.map((d) => `"${depExpr(d)}"`).join(`\\x1f`);
2610
- const bucket = sanitize(location.fieldName);
2611
- const bucketList = location.isGlobal ? `$_global_used_field_keys_${bucket}` : `$_used_field_keys_${bucket}`;
2612
- const out = [`switch ${depKey}`];
2613
- for (const entry of vc.table) {
2614
- const casePattern = entry.key.map((k) => `"${fishCaseEscape(k)}"`).join(`\\x1f`);
2615
- out.push(` case ${casePattern}`);
2616
- const keyOnlyLines = [];
2617
- const fullLines = [];
2618
- const seenKeys = /* @__PURE__ */ new Set();
2619
- const printfLine = (value, description) => description ? `printf '%s\\t%s\\n' "${escapeDesc$1(value)}" "${escapeDesc$1(description)}"` : `printf '%s\\n' "${escapeDesc$1(value)}"`;
2620
- const wrapWithDedup = (echoLine, keyPart) => location.isArrayOption && keyPart.length > 0 ? [
2621
- ` if not contains -- "${escapeDesc$1(keyPart)}" ${bucketList}`,
2876
+ function fishCacheDefault(hardcoded, suffix) {
2877
+ if (hardcoded) return ` printf '%s\\n' ${hardcoded}`;
2878
+ return [
2879
+ ` set -l _cache_root "$XDG_CACHE_HOME"`,
2880
+ ` test -n "$_cache_root"; or set _cache_root "$HOME/.cache"`,
2881
+ ` printf '%s\\n' "$_cache_root"${suffix}`
2882
+ ].join("\n");
2883
+ }
2884
+ function shellWorkerRelList(options, shell) {
2885
+ return bundledWorkerRelativePaths(options.programName, shell, options.bundledWorker).map(shSingleQuote).join(" ");
2886
+ }
2887
+ function fishDQ(s) {
2888
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
2889
+ }
2890
+ function fishWorkerRelList(options) {
2891
+ return bundledWorkerRelativePaths(options.programName, "fish", options.bundledWorker).map(fishDQ).join(" ");
2892
+ }
2893
+ function bundledWorkerPathCommandEnabled(options) {
2894
+ return options.bundledWorker?.queryCommand === true && !options.bundledWorker.disabled;
2895
+ }
2896
+ function bashDispatcher(_command, options) {
2897
+ const { programName } = options;
2898
+ const fn = sanitize(programName);
2899
+ const workerFn = `${fn}_worker`;
2900
+ const workerBinEnvName = binEnvVarName(workerFn);
2901
+ const envName = binEnvVarName(fn);
2902
+ const programLookup = shSingleQuote(programName);
2903
+ const compileCacheDefault = posixCacheDefault("printf '%s\\n'", hardcodedCompileCacheDir(options.cacheDir), compileCacheSuffix(programName));
2904
+ const workerPathDefault = posixCacheDefault("printf '%s\\n'", hardcodedWorkerPath(options.cacheDir, "bash"), workerPathSuffix(programName, "bash"));
2905
+ const workerRelList = shellWorkerRelList(options, "bash");
2906
+ const canQueryBundledWorkerPath = bundledWorkerPathCommandEnabled(options);
2907
+ const lines = [];
2908
+ lines.push(...buildHeaderLines({
2909
+ programName,
2910
+ shell: "bash",
2911
+ binPath: options.binPath,
2912
+ programVersion: options.programVersion
2913
+ }));
2914
+ lines.push(`# Generated by politty`);
2915
+ lines.push(`# politty-completion-mode: dispatcher`);
2916
+ lines.push(``);
2917
+ lines.push(`__${fn}_resolve_bin() {`);
2918
+ lines.push(` if [[ -n "\${${envName}:-}" ]]; then`);
2919
+ lines.push(` printf '%s\\n' "\${${envName}}"`);
2920
+ lines.push(` return 0`);
2921
+ lines.push(` fi`);
2922
+ lines.push(` type -P ${programLookup} 2>/dev/null`);
2923
+ lines.push(`}`);
2924
+ lines.push(``);
2925
+ lines.push(`__${fn}_node_compile_cache_dir() {`);
2926
+ lines.push(` if [[ -n "\${NODE_COMPILE_CACHE:-}" ]]; then`);
2927
+ lines.push(` printf '%s\\n' "$NODE_COMPILE_CACHE"`);
2928
+ lines.push(` return 0`);
2929
+ lines.push(` fi`);
2930
+ lines.push(compileCacheDefault);
2931
+ lines.push(`}`);
2932
+ lines.push(``);
2933
+ lines.push(`__${fn}_static_worker_path() {`);
2934
+ lines.push(workerPathDefault);
2935
+ lines.push(`}`);
2936
+ lines.push(``);
2937
+ lines.push(`__${fn}_worker_file_sig() {`);
2938
+ lines.push(` local _worker="$1" _sig`);
2939
+ lines.push(` _sig=${statSigExpr("$_worker", {
2940
+ shell: "posix",
2941
+ withSize: true
2942
+ })} || return 1`);
2943
+ lines.push(` printf '%s\\n' "$_sig"`);
2944
+ lines.push(`}`);
2945
+ lines.push(``);
2946
+ lines.push(`__${fn}_is_worker_file() {`);
2947
+ lines.push(` local _worker="$1" _head`);
2948
+ lines.push(` [[ -f "$_worker" ]] || return 1`);
2949
+ lines.push(` _head="$(head -n 24 "$_worker" 2>/dev/null)" || return 1`);
2950
+ lines.push(` _head=$'\\n'"$_head"$'\\n'`);
2951
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-version: 1"$'\\n'* ]] || return 1`);
2952
+ lines.push(` [[ "$_head" == *$'\\n'"# program: ${programName}"$'\\n'* ]] || return 1`);
2953
+ lines.push(` [[ "$_head" == *$'\\n'"# shell: bash"$'\\n'* ]] || return 1`);
2954
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-mode: worker"$'\\n'* ]] || return 1`);
2955
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-worker: true"$'\\n'* ]] || return 1`);
2956
+ lines.push(`}`);
2957
+ lines.push(``);
2958
+ lines.push(`__${fn}_load_worker() {`);
2959
+ lines.push(` local _worker="$1" _sig _key`);
2960
+ lines.push(` __${fn}_is_worker_file "$_worker" || return 1`);
2961
+ lines.push(` _sig="$(__${fn}_worker_file_sig "$_worker")" || return 1`);
2962
+ lines.push(` _key="$_worker:$_sig"`);
2963
+ lines.push(` if [[ "\${__${fn}_loaded_worker_key:-}" != "$_key" ]]; then`);
2964
+ lines.push(` source "$_worker" 2>/dev/null || return 1`);
2965
+ lines.push(` __${fn}_loaded_worker_key="$_key"`);
2966
+ lines.push(` fi`);
2967
+ lines.push(` declare -F _${workerFn}_completions >/dev/null 2>&1`);
2968
+ lines.push(`}`);
2969
+ lines.push(``);
2970
+ lines.push(`__${fn}_realpath() {`);
2971
+ lines.push(` local _path="$1" _dir _target _limit=0`);
2972
+ lines.push(` while [[ -L "$_path" && $_limit -lt 40 ]]; do`);
2973
+ lines.push(` _dir="$(cd -P "$(dirname "$_path")" 2>/dev/null && pwd)" || return 1`);
2974
+ lines.push(` _target="$(readlink "$_path")" || return 1`);
2975
+ lines.push(` if [[ "$_target" == /* ]]; then _path="$_target"; else _path="$_dir/$_target"; fi`);
2976
+ lines.push(` (( _limit++ ))`);
2977
+ lines.push(` done`);
2978
+ lines.push(` _dir="$(cd -P "$(dirname "$_path")" 2>/dev/null && pwd)" || return 1`);
2979
+ lines.push(` printf '%s\\n' "$_dir/$(basename "$_path")"`);
2980
+ lines.push(`}`);
2981
+ lines.push(``);
2982
+ lines.push(`__${fn}_worker_from_dir() {`);
2983
+ lines.push(` local _dir="$1" _rel _candidate`);
2984
+ if (workerRelList.length > 0) {
2985
+ lines.push(` for _rel in ${workerRelList}; do`);
2986
+ lines.push(` [[ "$_rel" == /* ]] && _candidate="$_rel" || _candidate="$_dir/$_rel"`);
2987
+ lines.push(` if __${fn}_is_worker_file "$_candidate"; then`);
2988
+ lines.push(` printf '%s\\n' "$_candidate"`);
2989
+ lines.push(` return 0`);
2990
+ lines.push(` fi`);
2991
+ lines.push(` done`);
2992
+ }
2993
+ lines.push(` return 1`);
2994
+ lines.push(`}`);
2995
+ lines.push(``);
2996
+ lines.push(`__${fn}_cmd_shim_target() {`);
2997
+ lines.push(` sed -n 's/^# cmd-shim-target=//p' "$1" 2>/dev/null | tail -n 1`);
2998
+ lines.push(`}`);
2999
+ lines.push(``);
3000
+ lines.push(`__${fn}_bundled_worker_path() {`);
3001
+ lines.push(` local _bin="$1" _node_compile_cache="\${2:-}" _real _dir _target _reported`);
3002
+ lines.push(` _real="$(__${fn}_realpath "$_bin" 2>/dev/null)" || _real=""`);
3003
+ lines.push(` if [[ -n "$_real" ]]; then`);
3004
+ lines.push(` _dir="$(dirname "$_real")"`);
3005
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3006
+ lines.push(` fi`);
3007
+ lines.push(` _dir="$(dirname "$_bin")"`);
3008
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3009
+ lines.push(` _target="$(__${fn}_cmd_shim_target "$_bin")"`);
3010
+ lines.push(` if [[ -n "$_target" ]]; then`);
3011
+ lines.push(` _real="$(__${fn}_realpath "$_target" 2>/dev/null)" || _real="$_target"`);
3012
+ lines.push(` _dir="$(dirname "$_real")"`);
3013
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3014
+ lines.push(` fi`);
3015
+ if (canQueryBundledWorkerPath) {
3016
+ lines.push(` if [[ "\${__${fn}_queried_bin:-}" == "$_bin" && -n "\${__${fn}_queried_worker:-}" ]] && __${fn}_is_worker_file "\${__${fn}_queried_worker}"; then`);
3017
+ lines.push(` printf '%s\\n' "\${__${fn}_queried_worker}"`);
3018
+ lines.push(` return 0`);
3019
+ lines.push(` fi`);
3020
+ lines.push(` _reported="$(NODE_COMPILE_CACHE="$_node_compile_cache" "$_bin" __completion-worker-path bash 2>/dev/null)" || _reported=""`);
3021
+ lines.push(` if [[ -n "$_reported" ]] && __${fn}_is_worker_file "$_reported"; then`);
3022
+ lines.push(` __${fn}_queried_bin="$_bin"`);
3023
+ lines.push(` __${fn}_queried_worker="$_reported"`);
3024
+ lines.push(` printf '%s\\n' "$_reported"`);
3025
+ lines.push(` return 0`);
3026
+ lines.push(` fi`);
3027
+ }
3028
+ lines.push(` return 1`);
3029
+ lines.push(`}`);
3030
+ lines.push(``);
3031
+ lines.push(`__${fn}_bin_sig() {`);
3032
+ lines.push(` local _bin="$1" _sig`);
3033
+ lines.push(` _sig=${statSigExpr("$_bin", { shell: "posix" })} || return 1`);
3034
+ lines.push(` printf '%s\\n' "$_sig"`);
3035
+ lines.push(`}`);
3036
+ lines.push(``);
3037
+ lines.push(`__${fn}_worker_matches_bin() {`);
3038
+ lines.push(` local _worker="$1" _sig="$2" _bin="$3" _head`);
3039
+ lines.push(` [[ -f "$_worker" ]] || return 1`);
3040
+ lines.push(` _head="$(head -n 12 "$_worker" 2>/dev/null)" || return 1`);
3041
+ lines.push(` grep -qxF "# politty-bin-sig: $_sig" <<< "$_head" || return 1`);
3042
+ lines.push(` grep -qxF "# politty-bin-path: $_bin" <<< "$_head" || return 1`);
3043
+ lines.push(`}`);
3044
+ lines.push(``);
3045
+ lines.push(`__${fn}_apply_dynamic_output() {`);
3046
+ lines.push(` local _raw="$1" _cur="$2" _inline_prefix="$3"`);
3047
+ lines.push(` COMPREPLY=()`);
3048
+ lines.push(` local _directive=0`);
3049
+ lines.push(` local -a _lines=() _exts=() _matchers=()`);
3050
+ lines.push(` local _line`);
3051
+ lines.push(` while IFS= read -r _line; do _lines+=("$_line"); done <<< "$_raw"`);
3052
+ lines.push(` local _last=$((\${#_lines[@]} - 1))`);
3053
+ lines.push(` if (( _last >= 0 )) && [[ "\${_lines[$_last]}" == :[0-9]* ]]; then`);
3054
+ lines.push(` local -a _meta=()`);
3055
+ lines.push(` IFS=$'\\t' read -r -a _meta <<< "\${_lines[$_last]}"`);
3056
+ lines.push(` unset '_lines[_last]'`);
3057
+ lines.push(` _directive="\${_meta[0]#:}"`);
3058
+ lines.push(` local _m`);
3059
+ lines.push(` for _m in "\${_meta[@]:1}"; do`);
3060
+ lines.push(` if [[ "$_m" == @ext:* ]]; then`);
3061
+ lines.push(` IFS=',' read -r -a _exts <<< "\${_m#@ext:}"`);
3062
+ lines.push(` elif [[ "$_m" == @matcher:* ]]; then`);
3063
+ lines.push(` IFS=',' read -r -a _matchers <<< "\${_m#@matcher:}"`);
3064
+ lines.push(` fi`);
3065
+ lines.push(` done`);
3066
+ lines.push(` fi`);
3067
+ lines.push(` for _line in "\${_lines[@]}"; do`);
3068
+ lines.push(` [[ -z "$_line" ]] && continue`);
3069
+ lines.push(` COMPREPLY+=("$_line")`);
3070
+ lines.push(` done`);
3071
+ lines.push(` local _ip="\${_inline_prefix:-}"`);
3072
+ lines.push(` if (( \${#_exts[@]} > 0 || \${#_matchers[@]} > 0 )); then`);
3073
+ lines.push(` local _need_empty=0`);
3074
+ lines.push(` compopt +o default 2>/dev/null || _need_empty=1`);
3075
+ lines.push(` compopt -o filenames 2>/dev/null`);
3076
+ lines.push(` local _f _ext _pat _base _ok`);
3077
+ lines.push(` while IFS= read -r _f; do`);
3078
+ lines.push(` if [[ -d "$_f" ]]; then`);
3079
+ lines.push(` COMPREPLY+=("\${_ip}\${_f}")`);
3080
+ lines.push(` continue`);
3081
+ lines.push(` fi`);
3082
+ lines.push(` _ok=0`);
3083
+ lines.push(` if (( \${#_exts[@]} > 0 )); then`);
3084
+ lines.push(` for _ext in "\${_exts[@]}"; do`);
3085
+ lines.push(` [[ -n "$_ext" && "$_f" == *."$_ext" ]] && _ok=1`);
3086
+ lines.push(` done`);
3087
+ lines.push(` fi`);
3088
+ lines.push(` if (( \${#_matchers[@]} > 0 )); then`);
3089
+ lines.push(` _base="\${_f##*/}"`);
3090
+ lines.push(` for _pat in "\${_matchers[@]}"; do`);
3091
+ lines.push(` [[ -n "$_pat" && "$_base" == $_pat ]] && _ok=1`);
3092
+ lines.push(` done`);
3093
+ lines.push(` fi`);
3094
+ lines.push(` (( _ok )) && COMPREPLY+=("\${_ip}\${_f}")`);
3095
+ lines.push(` done < <(compgen -f -- "$_cur")`);
3096
+ lines.push(` if (( _need_empty && \${#COMPREPLY[@]} == 0 )); then COMPREPLY=( "" ); fi`);
3097
+ lines.push(` elif (( _directive & ${CompletionDirective.DirectoryCompletion} )); then`);
3098
+ lines.push(` local _need_empty=0`);
3099
+ lines.push(` compopt +o default 2>/dev/null || _need_empty=1`);
3100
+ lines.push(` compopt -o filenames 2>/dev/null`);
3101
+ lines.push(` local _d`);
3102
+ lines.push(` while IFS= read -r _d; do COMPREPLY+=("\${_ip}\${_d}"); done < <(compgen -d -- "$_cur")`);
3103
+ lines.push(` if (( _need_empty && \${#COMPREPLY[@]} == 0 )); then COMPREPLY=( "" ); fi`);
3104
+ lines.push(` elif (( _directive & ${CompletionDirective.FileCompletion} )); then`);
3105
+ lines.push(` compopt -o filenames 2>/dev/null`);
3106
+ lines.push(` local _p`);
3107
+ lines.push(` while IFS= read -r _p; do COMPREPLY+=("\${_ip}\${_p}"); done < <(compgen -f -- "$_cur")`);
3108
+ lines.push(` elif (( _directive & ${CompletionDirective.NoFileCompletion} )); then`);
3109
+ lines.push(` local _need_empty=0`);
3110
+ lines.push(` compopt +o default 2>/dev/null || _need_empty=1`);
3111
+ lines.push(` if (( _need_empty && \${#COMPREPLY[@]} == 0 )); then COMPREPLY=( "" ); fi`);
3112
+ lines.push(` fi`);
3113
+ lines.push(` if (( _directive & ${CompletionDirective.NoSpace} )); then`);
3114
+ lines.push(` compopt -o nospace 2>/dev/null`);
3115
+ lines.push(` fi`);
3116
+ lines.push(`}`);
3117
+ lines.push(``);
3118
+ lines.push(`_${fn}_completions() {`);
3119
+ lines.push(` COMPREPLY=()`);
3120
+ lines.push(` local -a _words=()`);
3121
+ lines.push(` local _i=1`);
3122
+ lines.push(` while (( _i <= COMP_CWORD )); do`);
3123
+ lines.push(` if [[ "\${COMP_WORDS[_i]}" == "=" && \${#_words[@]} -gt 0 ]]; then`);
3124
+ lines.push(` _words[\${#_words[@]}-1]+="=\${COMP_WORDS[_i+1]:-}"`);
3125
+ lines.push(` (( _i += 2 ))`);
3126
+ lines.push(` else`);
3127
+ lines.push(` _words+=("\${COMP_WORDS[_i]}")`);
3128
+ lines.push(` (( _i++ ))`);
3129
+ lines.push(` fi`);
3130
+ lines.push(` done`);
3131
+ lines.push(` local _cur="" _inline_prefix=""`);
3132
+ lines.push(` (( \${#_words[@]} > 0 )) && _cur="\${_words[\${#_words[@]}-1]}"`);
3133
+ lines.push(` local _after_dd=0 _di=0`);
3134
+ lines.push(` while (( _di < \${#_words[@]} - 1 )); do`);
3135
+ lines.push(` [[ "\${_words[_di]}" == "--" ]] && { _after_dd=1; break; }`);
3136
+ lines.push(` (( _di++ ))`);
3137
+ lines.push(` done`);
3138
+ lines.push(` if (( ! _after_dd )) && [[ "$_cur" == -*=* ]]; then`);
3139
+ lines.push(` _inline_prefix="\${_cur%%=*}="`);
3140
+ lines.push(` _cur="\${_cur#*=}"`);
3141
+ lines.push(` fi`);
3142
+ lines.push(` local _bin _out _node_compile_cache _worker _bundled_worker _sig`);
3143
+ lines.push(` _bin="$(__${fn}_resolve_bin)" || _bin=""`);
3144
+ lines.push(` [[ -n "$_bin" ]] || return 0`);
3145
+ lines.push(` _node_compile_cache="$(__${fn}_node_compile_cache_dir)" || _node_compile_cache=""`);
3146
+ lines.push(` _bundled_worker="$(__${fn}_bundled_worker_path "$_bin" "$_node_compile_cache")" || _bundled_worker=""`);
3147
+ lines.push(` if [[ -n "$_bundled_worker" ]] && __${fn}_load_worker "$_bundled_worker"; then`);
3148
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${workerBinEnvName}="$_bin" _${workerFn}_completions`);
3149
+ lines.push(` return 0`);
3150
+ lines.push(` fi`);
3151
+ lines.push(` _worker="$(__${fn}_static_worker_path)" || _worker=""`);
3152
+ lines.push(` _sig="$(__${fn}_bin_sig "$_bin")" || _sig=""`);
3153
+ lines.push(` if [[ -n "$_worker" && -n "$_sig" ]]; then`);
3154
+ lines.push(` if ! __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin"; then`);
3155
+ lines.push(` mkdir -p "\${_worker%/*}" 2>/dev/null`);
3156
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${envName}="$_bin" "$_bin" __refresh-completion bash "$_worker" --static --worker 2>/dev/null`);
3157
+ lines.push(` fi`);
3158
+ lines.push(` if __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin" && __${fn}_load_worker "$_worker"; then`);
3159
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${workerBinEnvName}="$_bin" _${workerFn}_completions`);
3160
+ lines.push(` return 0`);
3161
+ lines.push(` fi`);
3162
+ lines.push(` fi`);
3163
+ lines.push(` _out=$(NODE_COMPILE_CACHE="$_node_compile_cache" "$_bin" __complete --shell bash -- "\${_words[@]}" 2>/dev/null) || return 0`);
3164
+ lines.push(` __${fn}_apply_dynamic_output "$_out" "$_cur" "$_inline_prefix"`);
3165
+ lines.push(`}`);
3166
+ lines.push(``);
3167
+ lines.push(`complete -o default -F _${fn}_completions ${programName}`);
3168
+ lines.push(``);
3169
+ return {
3170
+ script: lines.join("\n"),
3171
+ shell: "bash",
3172
+ installInstructions: `# To enable dispatcher bash completions, add this to your ~/.bashrc:
3173
+ eval "$(${programName} completion bash)"
3174
+
3175
+ # To save the dispatcher script instead:
3176
+ mkdir -p ~/.local/share/bash-completion/completions
3177
+ ${programName} completion bash > ~/.local/share/bash-completion/completions/${programName}
3178
+
3179
+ # To generate the older static script:
3180
+ ${programName} completion bash --static`
3181
+ };
3182
+ }
3183
+ function zshDispatcher(_command, options) {
3184
+ const { programName } = options;
3185
+ const fn = sanitize(programName);
3186
+ const workerFn = `${fn}_worker`;
3187
+ const workerBinEnvName = binEnvVarName(workerFn);
3188
+ const envName = binEnvVarName(fn);
3189
+ const programLookup = shSingleQuote(programName);
3190
+ const completionFn = `_${programName}`;
3191
+ const autoloadCheck = `"\${funcstack[1]:-}" == "${completionFn}"`;
3192
+ const compileCacheDefault = posixCacheDefault("print -r --", hardcodedCompileCacheDir(options.cacheDir), compileCacheSuffix(programName));
3193
+ const workerPathDefault = posixCacheDefault("print -r --", hardcodedWorkerPath(options.cacheDir, "zsh"), workerPathSuffix(programName, "zsh"));
3194
+ const workerRelList = shellWorkerRelList(options, "zsh");
3195
+ const canQueryBundledWorkerPath = bundledWorkerPathCommandEnabled(options);
3196
+ const lines = [];
3197
+ lines.push(`#compdef ${programName}`);
3198
+ lines.push(...buildHeaderLines({
3199
+ programName,
3200
+ shell: "zsh",
3201
+ binPath: options.binPath,
3202
+ programVersion: options.programVersion
3203
+ }));
3204
+ lines.push(`# Generated by politty`);
3205
+ lines.push(`# politty-completion-mode: dispatcher`);
3206
+ lines.push(``);
3207
+ lines.push(`__${fn}_resolve_bin() {`);
3208
+ lines.push(` emulate -L zsh`);
3209
+ lines.push(` setopt local_options no_aliases`);
3210
+ lines.push(` if [[ -n "\${${envName}:-}" ]]; then`);
3211
+ lines.push(` print -r -- "\${${envName}}"`);
3212
+ lines.push(` return 0`);
3213
+ lines.push(` fi`);
3214
+ lines.push(` whence -p ${programLookup} 2>/dev/null`);
3215
+ lines.push(`}`);
3216
+ lines.push(``);
3217
+ lines.push(`__${fn}_node_compile_cache_dir() {`);
3218
+ lines.push(` emulate -L zsh`);
3219
+ lines.push(` setopt local_options no_aliases`);
3220
+ lines.push(` if [[ -n "\${NODE_COMPILE_CACHE:-}" ]]; then`);
3221
+ lines.push(` print -r -- "$NODE_COMPILE_CACHE"`);
3222
+ lines.push(` return 0`);
3223
+ lines.push(` fi`);
3224
+ lines.push(compileCacheDefault);
3225
+ lines.push(`}`);
3226
+ lines.push(``);
3227
+ lines.push(`__${fn}_static_worker_path() {`);
3228
+ lines.push(` emulate -L zsh`);
3229
+ lines.push(` setopt local_options no_aliases`);
3230
+ lines.push(workerPathDefault);
3231
+ lines.push(`}`);
3232
+ lines.push(``);
3233
+ lines.push(`__${fn}_worker_file_sig() {`);
3234
+ lines.push(` emulate -L zsh`);
3235
+ lines.push(` setopt local_options no_aliases`);
3236
+ lines.push(` local _worker="$1" _sig`);
3237
+ lines.push(` _sig=${statSigExpr("$_worker", {
3238
+ shell: "posix",
3239
+ withSize: true
3240
+ })} || return 1`);
3241
+ lines.push(` print -r -- "$_sig"`);
3242
+ lines.push(`}`);
3243
+ lines.push(``);
3244
+ lines.push(`__${fn}_is_worker_file() {`);
3245
+ lines.push(` emulate -L zsh`);
3246
+ lines.push(` setopt local_options no_aliases`);
3247
+ lines.push(` local _worker="$1" _head`);
3248
+ lines.push(` [[ -f "$_worker" ]] || return 1`);
3249
+ lines.push(` _head="$(<$_worker)" || return 1`);
3250
+ lines.push(` _head=$'\\n'"$_head"$'\\n'`);
3251
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-version: 1"$'\\n'* ]] || return 1`);
3252
+ lines.push(` [[ "$_head" == *$'\\n'"# program: ${programName}"$'\\n'* ]] || return 1`);
3253
+ lines.push(` [[ "$_head" == *$'\\n'"# shell: zsh"$'\\n'* ]] || return 1`);
3254
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-mode: worker"$'\\n'* ]] || return 1`);
3255
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-completion-worker: true"$'\\n'* ]] || return 1`);
3256
+ lines.push(`}`);
3257
+ lines.push(``);
3258
+ lines.push(`__${fn}_load_worker() {`);
3259
+ lines.push(` emulate -L zsh`);
3260
+ lines.push(` setopt local_options no_aliases`);
3261
+ lines.push(` local _worker="$1" _sig _key`);
3262
+ lines.push(` __${fn}_is_worker_file "$_worker" || return 1`);
3263
+ lines.push(` _sig="$(__${fn}_worker_file_sig "$_worker")" || return 1`);
3264
+ lines.push(` _key="$_worker:$_sig"`);
3265
+ lines.push(` if [[ "\${__${fn}_loaded_worker_key:-}" != "$_key" ]]; then`);
3266
+ lines.push(` source "$_worker" 2>/dev/null || return 1`);
3267
+ lines.push(` typeset -g __${fn}_loaded_worker_key="$_key"`);
3268
+ lines.push(` fi`);
3269
+ lines.push(` (( $+functions[_${workerFn}_completions] ))`);
3270
+ lines.push(`}`);
3271
+ lines.push(``);
3272
+ lines.push(`__${fn}_realpath() {`);
3273
+ lines.push(` emulate -L zsh`);
3274
+ lines.push(` setopt local_options no_aliases`);
3275
+ lines.push(` local _path="$1"`);
3276
+ lines.push(` [[ -n "$_path" ]] || return 1`);
3277
+ lines.push(` print -r -- "\${_path:A}"`);
3278
+ lines.push(`}`);
3279
+ lines.push(``);
3280
+ lines.push(`__${fn}_worker_from_dir() {`);
3281
+ lines.push(` emulate -L zsh`);
3282
+ lines.push(` setopt local_options no_aliases`);
3283
+ lines.push(` local _dir="$1" _rel _candidate`);
3284
+ if (workerRelList.length > 0) {
3285
+ lines.push(` for _rel in ${workerRelList}; do`);
3286
+ lines.push(` [[ "$_rel" == /* ]] && _candidate="$_rel" || _candidate="$_dir/$_rel"`);
3287
+ lines.push(` if __${fn}_is_worker_file "$_candidate"; then`);
3288
+ lines.push(` print -r -- "$_candidate"`);
3289
+ lines.push(` return 0`);
3290
+ lines.push(` fi`);
3291
+ lines.push(` done`);
3292
+ }
3293
+ lines.push(` return 1`);
3294
+ lines.push(`}`);
3295
+ lines.push(``);
3296
+ lines.push(`__${fn}_cmd_shim_target() {`);
3297
+ lines.push(` emulate -L zsh`);
3298
+ lines.push(` setopt local_options no_aliases`);
3299
+ lines.push(` [[ -f "$1" ]] || return 0`);
3300
+ lines.push(` local _l _t=""`);
3301
+ lines.push(` for _l in "\${(@f)$(<$1)}"; do`);
3302
+ lines.push(` [[ "$_l" == "# cmd-shim-target="* ]] && _t="\${_l#\\# cmd-shim-target=}"`);
3303
+ lines.push(` done`);
3304
+ lines.push(` [[ -n "$_t" ]] && print -r -- "$_t"`);
3305
+ lines.push(`}`);
3306
+ lines.push(``);
3307
+ lines.push(`__${fn}_bundled_worker_path() {`);
3308
+ lines.push(` emulate -L zsh`);
3309
+ lines.push(` setopt local_options no_aliases`);
3310
+ lines.push(` local _bin="$1" _node_compile_cache="\${2:-}" _real _dir _target _reported`);
3311
+ lines.push(` _real="$(__${fn}_realpath "$_bin" 2>/dev/null)" || _real=""`);
3312
+ lines.push(` if [[ -n "$_real" ]]; then`);
3313
+ lines.push(` _dir="\${_real:h}"`);
3314
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3315
+ lines.push(` fi`);
3316
+ lines.push(` _dir="\${_bin:h}"`);
3317
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3318
+ lines.push(` _target="$(__${fn}_cmd_shim_target "$_bin")"`);
3319
+ lines.push(` if [[ -n "$_target" ]]; then`);
3320
+ lines.push(` _real="$(__${fn}_realpath "$_target" 2>/dev/null)" || _real="$_target"`);
3321
+ lines.push(` _dir="\${_real:h}"`);
3322
+ lines.push(` __${fn}_worker_from_dir "$_dir" && return 0`);
3323
+ lines.push(` fi`);
3324
+ if (canQueryBundledWorkerPath) {
3325
+ lines.push(` if [[ "\${__${fn}_queried_bin:-}" == "$_bin" && -n "\${__${fn}_queried_worker:-}" ]] && __${fn}_is_worker_file "\${__${fn}_queried_worker}"; then`);
3326
+ lines.push(` print -r -- "\${__${fn}_queried_worker}"`);
3327
+ lines.push(` return 0`);
3328
+ lines.push(` fi`);
3329
+ lines.push(` _reported="$(NODE_COMPILE_CACHE="$_node_compile_cache" "$_bin" __completion-worker-path zsh 2>/dev/null)" || _reported=""`);
3330
+ lines.push(` if [[ -n "$_reported" ]] && __${fn}_is_worker_file "$_reported"; then`);
3331
+ lines.push(` typeset -g __${fn}_queried_bin="$_bin"`);
3332
+ lines.push(` typeset -g __${fn}_queried_worker="$_reported"`);
3333
+ lines.push(` print -r -- "$_reported"`);
3334
+ lines.push(` return 0`);
3335
+ lines.push(` fi`);
3336
+ }
3337
+ lines.push(` return 1`);
3338
+ lines.push(`}`);
3339
+ lines.push(``);
3340
+ lines.push(`__${fn}_bin_sig() {`);
3341
+ lines.push(` emulate -L zsh`);
3342
+ lines.push(` setopt local_options no_aliases`);
3343
+ lines.push(` local _bin="$1" _sig`);
3344
+ lines.push(` _sig=${statSigExpr("$_bin", { shell: "posix" })} || return 1`);
3345
+ lines.push(` print -r -- "$_sig"`);
3346
+ lines.push(`}`);
3347
+ lines.push(``);
3348
+ lines.push(`__${fn}_worker_matches_bin() {`);
3349
+ lines.push(` emulate -L zsh`);
3350
+ lines.push(` setopt local_options no_aliases`);
3351
+ lines.push(` local _worker="$1" _sig="$2" _bin="$3" _head`);
3352
+ lines.push(` [[ -f "$_worker" ]] || return 1`);
3353
+ lines.push(` _head=$'\\n'"$(<$_worker)"$'\\n'`);
3354
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-bin-sig: $_sig"$'\\n'* ]] || return 1`);
3355
+ lines.push(` [[ "$_head" == *$'\\n'"# politty-bin-path: $_bin"$'\\n'* ]] || return 1`);
3356
+ lines.push(`}`);
3357
+ lines.push(``);
3358
+ lines.push(`__${fn}_cdescribe() {`);
3359
+ lines.push(` _describe "$@" 2>/dev/null && return 0`);
3360
+ lines.push(` shift`);
3361
+ lines.push(` local _cd_arr="$1"`);
3362
+ lines.push(` shift`);
3363
+ lines.push(` local -a _cd_vals=("\${(@)\${(P)_cd_arr}%%:*}")`);
3364
+ lines.push(` compadd "$@" -a _cd_vals 2>/dev/null`);
3365
+ lines.push(` return 0`);
3366
+ lines.push(`}`);
3367
+ lines.push(``);
3368
+ lines.push(`__${fn}_add_path_candidate() {`);
3369
+ lines.push(` compadd -f -- "$1" 2>/dev/null && return 0`);
3370
+ lines.push(` print -r -- "$1"`);
3371
+ lines.push(`}`);
3372
+ lines.push(``);
3373
+ lines.push(`__${fn}_apply_dynamic_output() {`);
3374
+ lines.push(` local _raw="$1"`);
3375
+ lines.push(` local _directive=0`);
3376
+ lines.push(` local -a _vals _lines _exts _matchers`);
3377
+ lines.push(` local _meta`);
3378
+ lines.push(` _lines=("\${(@f)_raw}")`);
3379
+ lines.push(` local _last=$#_lines`);
3380
+ lines.push(` if (( _last >= 1 )) && [[ "\${_lines[$_last]}" == :<->* ]]; then`);
3381
+ lines.push(` local _dline="\${_lines[$_last]}"`);
3382
+ lines.push(` _lines[$_last]=()`);
3383
+ lines.push(` local -a _dfields`);
3384
+ lines.push(` _dfields=("\${(@ps:\\t:)_dline}")`);
3385
+ lines.push(` _directive="\${_dfields[1]#:}"`);
3386
+ lines.push(` local _m`);
3387
+ lines.push(` for _m in "\${(@)_dfields[2,-1]}"; do`);
3388
+ lines.push(` if [[ "$_m" == @ext:* ]]; then`);
3389
+ lines.push(` _meta="\${_m#@ext:}"`);
3390
+ lines.push(` _exts=("\${(@s:,:)_meta}")`);
3391
+ lines.push(` elif [[ "$_m" == @matcher:* ]]; then`);
3392
+ lines.push(` _meta="\${_m#@matcher:}"`);
3393
+ lines.push(` _matchers=("\${(@s:,:)_meta}")`);
3394
+ lines.push(` fi`);
3395
+ lines.push(` done`);
3396
+ lines.push(` fi`);
3397
+ lines.push(` local _l`);
3398
+ lines.push(` for _l in "\${_lines[@]}"; do`);
3399
+ lines.push(` [[ -z "$_l" ]] && continue`);
3400
+ lines.push(` _vals+=("$_l")`);
3401
+ lines.push(` done`);
3402
+ lines.push(` if (( \${#_vals[@]} > 0 )); then`);
3403
+ lines.push(` if (( _directive & ${CompletionDirective.NoSpace} )); then`);
3404
+ lines.push(` __${fn}_cdescribe 'completions' _vals -S ''`);
3405
+ lines.push(` else`);
3406
+ lines.push(` __${fn}_cdescribe 'completions' _vals`);
3407
+ lines.push(` fi`);
3408
+ lines.push(` fi`);
3409
+ lines.push(` if (( \${#_exts[@]} > 0 || \${#_matchers[@]} > 0 )); then`);
3410
+ lines.push(` setopt local_options null_glob`);
3411
+ lines.push(` local _cur="\${words[CURRENT]:-}" _dir _prefix _f _out _ext _pat`);
3412
+ lines.push(` if [[ -z "$_cur" || "$_cur" != */* ]]; then`);
3413
+ lines.push(` _dir="."`);
3414
+ lines.push(` _prefix="$_cur"`);
3415
+ lines.push(` elif [[ "$_cur" == */ ]]; then`);
3416
+ lines.push(` _dir="\${_cur%/}"`);
3417
+ lines.push(` _prefix=""`);
3418
+ lines.push(` else`);
3419
+ lines.push(` _dir="\${_cur%/*}"`);
3420
+ lines.push(` _prefix="\${_cur##*/}"`);
3421
+ lines.push(` fi`);
3422
+ lines.push(` for _f in "$_dir"/"$_prefix"*(N/); do`);
3423
+ lines.push(` _out="\${_f#./}"`);
3424
+ lines.push(` __${fn}_add_path_candidate "$_out"`);
3425
+ lines.push(` done`);
3426
+ lines.push(` for _f in "$_dir"/"$_prefix"*(N.); do`);
3427
+ lines.push(` _out="\${_f#./}"`);
3428
+ lines.push(` for _ext in "\${_exts[@]}"; do`);
3429
+ lines.push(` [[ -n "$_ext" && "$_out" == *."$_ext" ]] && { __${fn}_add_path_candidate "$_out"; break; }`);
3430
+ lines.push(` done`);
3431
+ lines.push(` done`);
3432
+ lines.push(` for _pat in "\${_matchers[@]}"; do`);
3433
+ lines.push(` [[ -n "$_pat" ]] || continue`);
3434
+ lines.push(` for _f in "$_dir"/\${~_pat}(N.); do`);
3435
+ lines.push(` _out="\${_f#./}"`);
3436
+ lines.push(` [[ "$_out" == "$_cur"* ]] && __${fn}_add_path_candidate "$_out"`);
3437
+ lines.push(` done`);
3438
+ lines.push(` done`);
3439
+ lines.push(` elif (( _directive & ${CompletionDirective.DirectoryCompletion} )); then`);
3440
+ lines.push(` _files -/`);
3441
+ lines.push(` elif (( _directive & ${CompletionDirective.FileCompletion} )); then`);
3442
+ lines.push(` _files`);
3443
+ lines.push(` elif (( \${#_vals[@]} == 0 )) && ! (( _directive & ${CompletionDirective.NoFileCompletion} )); then`);
3444
+ lines.push(` _files`);
3445
+ lines.push(` fi`);
3446
+ lines.push(`}`);
3447
+ lines.push(``);
3448
+ lines.push(`${completionFn}() {`);
3449
+ lines.push(` emulate -L zsh`);
3450
+ lines.push(` setopt local_options no_aliases`);
3451
+ lines.push(` local _bin _out _node_compile_cache _worker _bundled_worker _sig`);
3452
+ lines.push(` _bin="$(__${fn}_resolve_bin)"`);
3453
+ lines.push(` [[ -n "$_bin" ]] || return 1`);
3454
+ lines.push(` _node_compile_cache="$(__${fn}_node_compile_cache_dir)"`);
3455
+ lines.push(` _bundled_worker="$(__${fn}_bundled_worker_path "$_bin" "$_node_compile_cache")"`);
3456
+ lines.push(` if [[ -n "$_bundled_worker" ]] && __${fn}_load_worker "$_bundled_worker"; then`);
3457
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${workerBinEnvName}="$_bin" _${workerFn}_completions "$@"`);
3458
+ lines.push(` return 0`);
3459
+ lines.push(` fi`);
3460
+ lines.push(` _worker="$(__${fn}_static_worker_path)"`);
3461
+ lines.push(` _sig="$(__${fn}_bin_sig "$_bin")"`);
3462
+ lines.push(` if [[ -n "$_worker" && -n "$_sig" ]]; then`);
3463
+ lines.push(` if ! __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin"; then`);
3464
+ lines.push(` mkdir -p "\${_worker%/*}" 2>/dev/null`);
3465
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${envName}="$_bin" "$_bin" __refresh-completion zsh "$_worker" --static --worker 2>/dev/null`);
3466
+ lines.push(` fi`);
3467
+ lines.push(` if __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin" && __${fn}_load_worker "$_worker"; then`);
3468
+ lines.push(` NODE_COMPILE_CACHE="$_node_compile_cache" ${workerBinEnvName}="$_bin" _${workerFn}_completions "$@"`);
3469
+ lines.push(` return 0`);
3470
+ lines.push(` fi`);
3471
+ lines.push(` fi`);
3472
+ lines.push(` _out=$(NODE_COMPILE_CACHE="$_node_compile_cache" "$_bin" __complete --shell zsh -- "\${(@)words[2,CURRENT]}" 2>/dev/null) || return 1`);
3473
+ lines.push(` __${fn}_apply_dynamic_output "$_out"`);
3474
+ lines.push(`}`);
3475
+ lines.push(``);
3476
+ lines.push(`zstyle ':completion:*:*:${programName}:*' file-patterns '%p:globbed-files *(-/):directories'`);
3477
+ lines.push(``);
3478
+ lines.push(`if [[ ${autoloadCheck} ]]; then`);
3479
+ lines.push(` ${completionFn} "$@"`);
3480
+ lines.push(`else`);
3481
+ lines.push(` compdef ${completionFn} ${programName}`);
3482
+ lines.push(`fi`);
3483
+ lines.push(``);
3484
+ return {
3485
+ script: lines.join("\n"),
3486
+ shell: "zsh",
3487
+ installInstructions: `# To enable dispatcher zsh completions, add this to your ~/.zshrc after compinit:
3488
+ eval "$(${programName} completion zsh)"
3489
+
3490
+ # To save the dispatcher script in your fpath:
3491
+ mkdir -p ~/.zsh/completions
3492
+ ${programName} completion zsh > ~/.zsh/completions/_${programName}
3493
+
3494
+ # Make sure your ~/.zshrc includes the fpath line before compinit:
3495
+ fpath=(~/.zsh/completions $fpath)
3496
+ autoload -Uz compinit && compinit
3497
+
3498
+ # To generate the older static script:
3499
+ ${programName} completion zsh --static`
3500
+ };
3501
+ }
3502
+ function fishDispatcher(_command, options) {
3503
+ const { programName } = options;
3504
+ const fn = sanitize(programName);
3505
+ const workerFn = `${fn}_worker`;
3506
+ const workerBinEnvName = binEnvVarName(workerFn);
3507
+ const envName = binEnvVarName(fn);
3508
+ const programLookup = shSingleQuote(programName);
3509
+ const compileCacheDefault = fishCacheDefault(hardcodedCompileCacheDir(options.cacheDir), compileCacheSuffix(programName));
3510
+ const workerPathDefault = fishCacheDefault(hardcodedWorkerPath(options.cacheDir, "fish"), workerPathSuffix(programName, "fish"));
3511
+ const workerRelList = fishWorkerRelList(options);
3512
+ const canQueryBundledWorkerPath = bundledWorkerPathCommandEnabled(options);
3513
+ const lines = [];
3514
+ lines.push(...buildHeaderLines({
3515
+ programName,
3516
+ shell: "fish",
3517
+ binPath: options.binPath,
3518
+ programVersion: options.programVersion
3519
+ }));
3520
+ lines.push(`# Generated by politty`);
3521
+ lines.push(`# politty-completion-mode: dispatcher`);
3522
+ lines.push(``);
3523
+ lines.push(`function __${fn}_resolve_bin`);
3524
+ lines.push(` if set -q ${envName}; and test -n "$${envName}"`);
3525
+ lines.push(` printf '%s\\n' "$${envName}"`);
3526
+ lines.push(` return 0`);
3527
+ lines.push(` end`);
3528
+ lines.push(` set -l _bin (command -v ${programLookup})`);
3529
+ lines.push(` test -n "$_bin"; and test -x "$_bin"; and not test -d "$_bin"; and printf '%s\\n' "$_bin"`);
3530
+ lines.push(`end`);
3531
+ lines.push(``);
3532
+ lines.push(`function __${fn}_node_compile_cache_dir`);
3533
+ lines.push(` if set -q NODE_COMPILE_CACHE; and test -n "$NODE_COMPILE_CACHE"`);
3534
+ lines.push(` printf '%s\\n' "$NODE_COMPILE_CACHE"`);
3535
+ lines.push(` return 0`);
3536
+ lines.push(` end`);
3537
+ lines.push(compileCacheDefault);
3538
+ lines.push(`end`);
3539
+ lines.push(``);
3540
+ lines.push(`function __${fn}_static_worker_path`);
3541
+ lines.push(workerPathDefault);
3542
+ lines.push(`end`);
3543
+ lines.push(``);
3544
+ lines.push(`function __${fn}_worker_file_sig`);
3545
+ lines.push(` set -l _worker $argv[1]`);
3546
+ lines.push(` set -l _sig ${statSigExpr("$_worker", {
3547
+ shell: "fish",
3548
+ withSize: true
3549
+ })}`);
3550
+ lines.push(` test -n "$_sig"; and printf '%s\\n' "$_sig"`);
3551
+ lines.push(`end`);
3552
+ lines.push(``);
3553
+ lines.push(`function __${fn}_is_worker_file`);
3554
+ lines.push(` set -l _worker $argv[1]`);
3555
+ lines.push(` test -f "$_worker"; or return 1`);
3556
+ lines.push(` set -l _head (head -n 24 "$_worker" 2>/dev/null)`);
3557
+ lines.push(` string match -q -- "# politty-completion-version: 1" $_head; or return 1`);
3558
+ lines.push(` string match -q -- "# program: ${programName}" $_head; or return 1`);
3559
+ lines.push(` string match -q -- "# shell: fish" $_head; or return 1`);
3560
+ lines.push(` string match -q -- "# politty-completion-mode: worker" $_head; or return 1`);
3561
+ lines.push(` string match -q -- "# politty-completion-worker: true" $_head; or return 1`);
3562
+ lines.push(`end`);
3563
+ lines.push(``);
3564
+ lines.push(`function __${fn}_load_worker`);
3565
+ lines.push(` set -l _worker $argv[1]`);
3566
+ lines.push(` __${fn}_is_worker_file "$_worker"; or return 1`);
3567
+ lines.push(` set -l _sig (__${fn}_worker_file_sig "$_worker")`);
3568
+ lines.push(` test -n "$_sig"; or return 1`);
3569
+ lines.push(` set -l _key "$_worker:$_sig"`);
3570
+ lines.push(` if not set -q __${fn}_loaded_worker_key; or test "$__${fn}_loaded_worker_key" != "$_key"`);
3571
+ lines.push(` source "$_worker" 2>/dev/null; or return 1`);
3572
+ lines.push(` set -g __${fn}_loaded_worker_key "$_key"`);
3573
+ lines.push(` end`);
3574
+ lines.push(` functions -q __fish_${workerFn}_complete`);
3575
+ lines.push(`end`);
3576
+ lines.push(``);
3577
+ lines.push(`function __${fn}_realpath`);
3578
+ lines.push(` set -l _path $argv[1]`);
3579
+ lines.push(` set -l _old_pwd (pwd)`);
3580
+ lines.push(` set -l _limit 0`);
3581
+ lines.push(` while test -L "$_path"; and test $_limit -lt 40`);
3582
+ lines.push(` set -l _dir (dirname "$_path")`);
3583
+ lines.push(` if not cd "$_dir" 2>/dev/null`);
3584
+ lines.push(` cd "$_old_pwd" 2>/dev/null`);
3585
+ lines.push(` return 1`);
3586
+ lines.push(` end`);
3587
+ lines.push(` set -l _absdir (pwd -P)`);
3588
+ lines.push(` cd "$_old_pwd" 2>/dev/null`);
3589
+ lines.push(` test -n "$_absdir"; or return 1`);
3590
+ lines.push(` set -l _target (readlink "$_path")`);
3591
+ lines.push(` test -n "$_target"; or return 1`);
3592
+ lines.push(` if string match -q '/*' -- "$_target"`);
3593
+ lines.push(` set _path "$_target"`);
3594
+ lines.push(` else`);
3595
+ lines.push(` set _path "$_absdir/$_target"`);
3596
+ lines.push(` end`);
3597
+ lines.push(` set _limit (math $_limit + 1)`);
3598
+ lines.push(` end`);
3599
+ lines.push(` set -l _dir (dirname "$_path")`);
3600
+ lines.push(` if not cd "$_dir" 2>/dev/null`);
3601
+ lines.push(` cd "$_old_pwd" 2>/dev/null`);
3602
+ lines.push(` return 1`);
3603
+ lines.push(` end`);
3604
+ lines.push(` set -l _absdir (pwd -P)`);
3605
+ lines.push(` cd "$_old_pwd" 2>/dev/null`);
3606
+ lines.push(` test -n "$_absdir"; or return 1`);
3607
+ lines.push(` set -l _base (basename "$_path")`);
3608
+ lines.push(` printf '%s\\n' "$_absdir/$_base"`);
3609
+ lines.push(`end`);
3610
+ lines.push(``);
3611
+ lines.push(`function __${fn}_worker_from_dir`);
3612
+ lines.push(` set -l _dir $argv[1]`);
3613
+ if (workerRelList.length > 0) {
3614
+ lines.push(` for _rel in ${workerRelList}`);
3615
+ lines.push(` if string match -q '/*' -- "$_rel"`);
3616
+ lines.push(` set _candidate "$_rel"`);
3617
+ lines.push(` else`);
3618
+ lines.push(` set _candidate "$_dir/$_rel"`);
3619
+ lines.push(` end`);
3620
+ lines.push(` if __${fn}_is_worker_file "$_candidate"`);
3621
+ lines.push(` printf '%s\\n' "$_candidate"`);
3622
+ lines.push(` return 0`);
3623
+ lines.push(` end`);
3624
+ lines.push(` end`);
3625
+ }
3626
+ lines.push(` return 1`);
3627
+ lines.push(`end`);
3628
+ lines.push(``);
3629
+ lines.push(`function __${fn}_cmd_shim_target`);
3630
+ lines.push(` sed -n 's/^# cmd-shim-target=//p' "$argv[1]" 2>/dev/null | tail -n 1`);
3631
+ lines.push(`end`);
3632
+ lines.push(``);
3633
+ lines.push(`function __${fn}_bundled_worker_path`);
3634
+ lines.push(` set -l _bin $argv[1]`);
3635
+ lines.push(` set -l _node_compile_cache $argv[2]`);
3636
+ lines.push(` set -l _real (__${fn}_realpath "$_bin" 2>/dev/null)`);
3637
+ lines.push(` if test -n "$_real"`);
3638
+ lines.push(` set -l _dir (dirname "$_real")`);
3639
+ lines.push(` __${fn}_worker_from_dir "$_dir"; and return 0`);
3640
+ lines.push(` end`);
3641
+ lines.push(` set -l _dir (dirname "$_bin")`);
3642
+ lines.push(` __${fn}_worker_from_dir "$_dir"; and return 0`);
3643
+ lines.push(` set -l _target (__${fn}_cmd_shim_target "$_bin")`);
3644
+ lines.push(` if test -n "$_target"`);
3645
+ lines.push(` set _real (__${fn}_realpath "$_target" 2>/dev/null)`);
3646
+ lines.push(` test -n "$_real"; or set _real "$_target"`);
3647
+ lines.push(` set _dir (dirname "$_real")`);
3648
+ lines.push(` __${fn}_worker_from_dir "$_dir"; and return 0`);
3649
+ lines.push(` end`);
3650
+ if (canQueryBundledWorkerPath) {
3651
+ lines.push(` if set -q __${fn}_queried_bin; and test "$__${fn}_queried_bin" = "$_bin"; and test -n "$__${fn}_queried_worker"; and __${fn}_is_worker_file "$__${fn}_queried_worker"`);
3652
+ lines.push(` printf '%s\\n' "$__${fn}_queried_worker"`);
3653
+ lines.push(` return 0`);
3654
+ lines.push(` end`);
3655
+ lines.push(` set -l _reported (env NODE_COMPILE_CACHE="$_node_compile_cache" "$_bin" __completion-worker-path fish 2>/dev/null)`);
3656
+ lines.push(` if test -n "$_reported"; and __${fn}_is_worker_file "$_reported"`);
3657
+ lines.push(` set -g __${fn}_queried_bin "$_bin"`);
3658
+ lines.push(` set -g __${fn}_queried_worker "$_reported"`);
3659
+ lines.push(` printf '%s\\n' "$_reported"`);
3660
+ lines.push(` return 0`);
3661
+ lines.push(` end`);
3662
+ }
3663
+ lines.push(` return 1`);
3664
+ lines.push(`end`);
3665
+ lines.push(``);
3666
+ lines.push(`function __${fn}_bin_sig`);
3667
+ lines.push(` set -l _bin $argv[1]`);
3668
+ lines.push(` set -l _sig ${statSigExpr("$_bin", { shell: "fish" })}`);
3669
+ lines.push(` test -n "$_sig"; and printf '%s\\n' "$_sig"`);
3670
+ lines.push(`end`);
3671
+ lines.push(``);
3672
+ lines.push(`function __${fn}_worker_matches_bin`);
3673
+ lines.push(` set -l _worker $argv[1]`);
3674
+ lines.push(` set -l _sig $argv[2]`);
3675
+ lines.push(` set -l _bin $argv[3]`);
3676
+ lines.push(` test -f "$_worker"; or return 1`);
3677
+ lines.push(` set -l _head (head -n 12 "$_worker" 2>/dev/null)`);
3678
+ lines.push(` contains -- "# politty-bin-sig: $_sig" $_head; or return 1`);
3679
+ lines.push(` contains -- "# politty-bin-path: $_bin" $_head; or return 1`);
3680
+ lines.push(`end`);
3681
+ lines.push(``);
3682
+ lines.push(`function __${fn}_apply_dynamic_output`);
3683
+ lines.push(` set -l _cur $argv[1]`);
3684
+ lines.push(` set -l _directive 0`);
3685
+ lines.push(` set -l _emitted 0`);
3686
+ lines.push(` set -l _exts`);
3687
+ lines.push(` set -l _matchers`);
3688
+ lines.push(` set -l _lines`);
3689
+ lines.push(` while read -l _l`);
3690
+ lines.push(` set -a _lines $_l`);
3691
+ lines.push(` end`);
3692
+ lines.push(` set -l _n (count $_lines)`);
3693
+ lines.push(` if test $_n -ge 1; and string match -qr '^:[0-9]+' -- $_lines[$_n]`);
3694
+ lines.push(` set -l _fields (string split \\t -- $_lines[$_n])`);
3695
+ lines.push(` set -e _lines[$_n]`);
3696
+ lines.push(` set _directive (string sub -s 2 -- $_fields[1])`);
3697
+ lines.push(` for _f in $_fields[2..-1]`);
3698
+ lines.push(` if string match -q '@ext:*' -- $_f`);
3699
+ lines.push(` set _exts (string split , -- (string sub -s 6 -- $_f))`);
3700
+ lines.push(` else if string match -q '@matcher:*' -- $_f`);
3701
+ lines.push(` set _matchers (string split , -- (string sub -s 10 -- $_f))`);
3702
+ lines.push(` end`);
3703
+ lines.push(` end`);
3704
+ lines.push(` end`);
3705
+ lines.push(` for _l in $_lines`);
3706
+ lines.push(` if test -n "$_l"`);
3707
+ lines.push(` printf '%s\\n' $_l`);
3708
+ lines.push(` set _emitted 1`);
3709
+ lines.push(` end`);
3710
+ lines.push(` end`);
3711
+ lines.push(` if test (count $_exts) -gt 0; or test (count $_matchers) -gt 0`);
3712
+ lines.push(` __fish_complete_directories "$_cur"`);
3713
+ lines.push(` set -l _dir ""`);
3714
+ lines.push(` if string match -q '*/*' "$_cur"`);
3715
+ lines.push(` set _dir (string replace -r '[^/]*$' '' "$_cur")`);
3716
+ lines.push(` end`);
3717
+ lines.push(` for _f in "$_cur"*`);
3718
+ lines.push(` test -f "$_f"; or continue`);
3719
+ lines.push(` for _ext in $_exts`);
3720
+ lines.push(` if string match -q -- "*.$_ext" "$_f"`);
3721
+ lines.push(` printf '%s\\n' "$_f"`);
3722
+ lines.push(` break`);
3723
+ lines.push(` end`);
3724
+ lines.push(` end`);
3725
+ lines.push(` end`);
3726
+ lines.push(` for _pat in $_matchers`);
3727
+ lines.push(` for _f in "$_dir"$_pat`);
3728
+ lines.push(` test -f "$_f"; and string match -q "$_cur*" "$_f"; and printf '%s\\n' "$_f"`);
3729
+ lines.push(` end`);
3730
+ lines.push(` end`);
3731
+ lines.push(` else if test (math "bitand($_directive, ${CompletionDirective.DirectoryCompletion})") -ne 0`);
3732
+ lines.push(` __fish_complete_directories "$_cur"`);
3733
+ lines.push(` else if test (math "bitand($_directive, ${CompletionDirective.FileCompletion})") -ne 0`);
3734
+ lines.push(` __fish_complete_path "$_cur"`);
3735
+ lines.push(` else if test $_emitted -eq 0; and test (math "bitand($_directive, ${CompletionDirective.NoFileCompletion})") -eq 0`);
3736
+ lines.push(` __fish_complete_path "$_cur"`);
3737
+ lines.push(` end`);
3738
+ lines.push(`end`);
3739
+ lines.push(``);
3740
+ lines.push(`function __fish_${fn}_complete`);
3741
+ lines.push(` set -l _bin (__${fn}_resolve_bin)`);
3742
+ lines.push(` test -n "$_bin"; or return 0`);
3743
+ lines.push(` set -l _node_compile_cache (__${fn}_node_compile_cache_dir)`);
3744
+ lines.push(` set -l _bundled_worker (__${fn}_bundled_worker_path "$_bin" "$_node_compile_cache")`);
3745
+ lines.push(` if test -n "$_bundled_worker"; and __${fn}_load_worker "$_bundled_worker"`);
3746
+ lines.push(` set -lx ${workerBinEnvName} "$_bin"`);
3747
+ lines.push(` set -lx NODE_COMPILE_CACHE "$_node_compile_cache"`);
3748
+ lines.push(` __fish_${workerFn}_complete`);
3749
+ lines.push(` return 0`);
3750
+ lines.push(` end`);
3751
+ lines.push(` set -l _worker (__${fn}_static_worker_path)`);
3752
+ lines.push(` set -l _sig (__${fn}_bin_sig "$_bin")`);
3753
+ lines.push(` if test -n "$_worker"; and test -n "$_sig"`);
3754
+ lines.push(` if not __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin"`);
3755
+ lines.push(` mkdir -p (dirname "$_worker") 2>/dev/null`);
3756
+ lines.push(` env NODE_COMPILE_CACHE="$_node_compile_cache" ${envName}="$_bin" $_bin __refresh-completion fish "$_worker" --static --worker 2>/dev/null`);
3757
+ lines.push(` end`);
3758
+ lines.push(` if __${fn}_worker_matches_bin "$_worker" "$_sig" "$_bin"`);
3759
+ lines.push(` if __${fn}_load_worker "$_worker"`);
3760
+ lines.push(` set -lx ${workerBinEnvName} "$_bin"`);
3761
+ lines.push(` set -lx NODE_COMPILE_CACHE "$_node_compile_cache"`);
3762
+ lines.push(` __fish_${workerFn}_complete`);
3763
+ lines.push(` return 0`);
3764
+ lines.push(` end`);
3765
+ lines.push(` end`);
3766
+ lines.push(` end`);
3767
+ lines.push(` set -l _args (commandline -opc)`);
3768
+ lines.push(` set -l _cur (commandline -ct)`);
3769
+ lines.push(` if test (count $_args) -gt 0`);
3770
+ lines.push(` set _args $_args[2..]`);
3771
+ lines.push(` end`);
3772
+ lines.push(` env NODE_COMPILE_CACHE="$_node_compile_cache" $_bin __complete --shell fish -- $_args "$_cur" 2>/dev/null | __${fn}_apply_dynamic_output "$_cur"`);
3773
+ lines.push(`end`);
3774
+ lines.push(``);
3775
+ lines.push(`complete -e -c ${programName}`);
3776
+ lines.push(`complete -c ${programName} -f -a '(__fish_${fn}_complete)'`);
3777
+ lines.push(``);
3778
+ return {
3779
+ script: lines.join("\n"),
3780
+ shell: "fish",
3781
+ installInstructions: `# To enable dispatcher fish completions, run:
3782
+ ${programName} completion fish --install
3783
+
3784
+ # To generate the older static script:
3785
+ ${programName} completion fish --static`
3786
+ };
3787
+ }
3788
+ function generateDispatcherCompletion(command, options) {
3789
+ extractCompletionData(command, options.programName, options.globalArgsSchema, true);
3790
+ switch (options.shell) {
3791
+ case "bash": return bashDispatcher(command, options);
3792
+ case "zsh": return zshDispatcher(command, options);
3793
+ case "fish": return fishDispatcher(command, options);
3794
+ default: throw new Error(`Unsupported shell: ${options.shell}`);
3795
+ }
3796
+ }
3797
+
3798
+ //#endregion
3799
+ //#region src/completion/dynamic/shell-formatter.ts
3800
+ /**
3801
+ * Format completion candidates for the specified shell
3802
+ *
3803
+ * @returns Shell-ready output string (lines separated by newline, last line is :directive)
3804
+ */
3805
+ function formatForShell(result, options) {
3806
+ switch (options.shell) {
3807
+ case "bash": return formatForBash(result, options);
3808
+ case "zsh": return formatForZsh(result, options);
3809
+ case "fish": return formatForFish(result, options);
3810
+ }
3811
+ }
3812
+ /**
3813
+ * Append extension metadata and directive to output lines
3814
+ */
3815
+ function appendMetadata(lines, result) {
3816
+ let directiveLine = `:${result.directive}`;
3817
+ if (result.fileExtensions && result.fileExtensions.length > 0) directiveLine += `\t@ext:${result.fileExtensions.join(",")}`;
3818
+ if (result.fileMatchers && result.fileMatchers.length > 0) directiveLine += `\t@matcher:${result.fileMatchers.join(",")}`;
3819
+ lines.push(directiveLine);
3820
+ }
3821
+ /**
3822
+ * Format for bash
3823
+ *
3824
+ * - Pre-filters candidates by currentWord prefix (replaces compgen -W)
3825
+ * - Handles --opt=value inline values by prepending prefix
3826
+ * - Outputs plain values only (no descriptions - bash COMPREPLY doesn't support them)
3827
+ * - Last line: :directive
3828
+ */
3829
+ function formatForBash(result, options) {
3830
+ const lines = ((result.directive & CompletionDirective.FilterPrefix) !== 0 && options.currentWord ? result.candidates.filter((c) => c.value.startsWith(options.currentWord)) : result.candidates).map((c) => options.inlinePrefix ? `${options.inlinePrefix}${c.value}` : c.value);
3831
+ appendMetadata(lines, result);
3832
+ return lines.join("\n");
3833
+ }
3834
+ /**
3835
+ * Format for zsh
3836
+ *
3837
+ * - Outputs value:description pairs for _describe
3838
+ * - Colons in values/descriptions are escaped with backslash
3839
+ * - Last line: :directive
3840
+ */
3841
+ function formatForZsh(result, _options) {
3842
+ const lines = result.candidates.map((c) => {
3843
+ const escapedValue = c.value.replace(/:/g, "\\:");
3844
+ if (c.description) return `${escapedValue}:${c.description.replace(/:/g, "\\:")}`;
3845
+ return escapedValue;
3846
+ });
3847
+ appendMetadata(lines, result);
3848
+ return lines.join("\n");
3849
+ }
3850
+ /**
3851
+ * Format for fish
3852
+ *
3853
+ * - Outputs value\tdescription pairs
3854
+ * - Last line: :directive
3855
+ */
3856
+ function formatForFish(result, _options) {
3857
+ const lines = result.candidates.map((c) => {
3858
+ if (c.description) return `${c.value}\t${c.description}`;
3859
+ return c.value;
3860
+ });
3861
+ appendMetadata(lines, result);
3862
+ return lines.join("\n");
3863
+ }
3864
+
3865
+ //#endregion
3866
+ //#region src/completion/dynamic/complete-command.ts
3867
+ /**
3868
+ * Dynamic completion command implementation
3869
+ *
3870
+ * This creates a hidden `__complete` command that outputs completion candidates
3871
+ * for shell scripts to consume. Usage:
3872
+ *
3873
+ * mycli __complete --shell bash -- build --fo
3874
+ * mycli __complete --shell zsh -- plugin add
3875
+ *
3876
+ * Output format depends on the target shell:
3877
+ * bash: plain values (pre-filtered by prefix), last line :directive
3878
+ * zsh: value:description pairs, last line :directive
3879
+ * fish: value\tdescription pairs, last line :directive
3880
+ */
3881
+ /**
3882
+ * Schema for the __complete command
3883
+ */
3884
+ const completeArgsSchema = z.object({
3885
+ shell: arg(z.enum([
3886
+ "bash",
3887
+ "zsh",
3888
+ "fish"
3889
+ ]), { description: "Target shell for output formatting" }),
3890
+ args: arg(z.array(z.string()).default([]), {
3891
+ positional: true,
3892
+ description: "Arguments to complete",
3893
+ variadic: true
3894
+ })
3895
+ });
3896
+ /**
3897
+ * Create the dynamic completion command
3898
+ *
3899
+ * @param rootCommand - The root command to generate completions for
3900
+ * @param programName - The program name (optional, defaults to rootCommand.name)
3901
+ * @param globalArgsSchema - Global args schema. Forwarded to
3902
+ * `parseCompletionContext` so resolvers attached to global options remain
3903
+ * reachable at every subcommand level.
3904
+ * @returns A command that outputs completion candidates
3905
+ */
3906
+ function createDynamicCompleteCommand(rootCommand, _programName, globalArgsSchema) {
3907
+ return defineCommand({
3908
+ name: "__complete",
3909
+ args: completeArgsSchema,
3910
+ async run(args) {
3911
+ const context = parseCompletionContext(args.args, rootCommand, globalArgsSchema);
3912
+ const inlinePrefix = context.completionType === "option-value" && context.targetOption ? detectInlineOptionPrefix(context.currentWord) : void 0;
3913
+ const effectiveWord = inlinePrefix ? context.currentWord.slice(inlinePrefix.length) : context.currentWord;
3914
+ const output = formatForShell(await generateCandidates(context, { shell: args.shell }), {
3915
+ shell: args.shell,
3916
+ currentWord: effectiveWord,
3917
+ inlinePrefix
3918
+ });
3919
+ console.log(output);
3920
+ }
3921
+ });
3922
+ }
3923
+ /**
3924
+ * Check if a command tree contains the __complete command
3925
+ */
3926
+ function hasCompleteCommand(command) {
3927
+ return Boolean(command.subCommands?.["__complete"]);
3928
+ }
3929
+
3930
+ //#endregion
3931
+ //#region src/completion/fish.ts
3932
+ /** Escape shell-special characters for fish double-quoted strings */
3933
+ function escapeDesc$1(s) {
3934
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$");
3935
+ }
3936
+ /**
3937
+ * Escape a fish `switch` case pattern. Fish's `case` interprets its
3938
+ * arguments as globs even when double-quoted, so glob metacharacters
3939
+ * (`*`, `?`, `[`, `]`) must be backslash-escaped to keep the comparison
3940
+ * literal — otherwise a key like `prod*` would also match a runtime
3941
+ * value of `production`. Quote/dollar/backslash are escaped first so the
3942
+ * resulting string remains valid inside a double-quoted literal.
3943
+ */
3944
+ function fishCaseEscape(s) {
3945
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/\*/g, "\\*").replace(/\?/g, "\\?").replace(/\[/g, "\\[").replace(/]/g, "\\]");
3946
+ }
3947
+ /**
3948
+ * Generate fish value completion lines for a ValueCompletion spec.
3949
+ * Each line outputs candidates via echo (tab-separated value\tdescription).
3950
+ *
3951
+ * `location` is required for the expand variant (carries fieldName +
3952
+ * isArrayOption); other variants ignore it.
3953
+ */
3954
+ function fishValueLines(vc, fn, location) {
3955
+ if (!vc) return [];
3956
+ switch (vc.type) {
3957
+ case "expand": {
3958
+ if (!location) throw new Error("fishValueLines: expand variant requires a location");
3959
+ const depExpr = (d) => {
3960
+ const safe = sanitize(d.name);
3961
+ return d.isGlobal ? `$_global_arg_values_${safe}` : `$_arg_values_${safe}`;
3962
+ };
3963
+ const depKey = location.resolvedDeps.map((d) => `"${depExpr(d)}"`).join(`\\x1f`);
3964
+ const bucket = sanitize(location.fieldName);
3965
+ const bucketList = location.isGlobal ? `$_global_used_field_keys_${bucket}` : `$_used_field_keys_${bucket}`;
3966
+ const out = [`switch ${depKey}`];
3967
+ for (const entry of vc.table) {
3968
+ const casePattern = entry.key.map((k) => `"${fishCaseEscape(k)}"`).join(`\\x1f`);
3969
+ out.push(` case ${casePattern}`);
3970
+ const keyOnlyLines = [];
3971
+ const fullLines = [];
3972
+ const seenKeys = /* @__PURE__ */ new Set();
3973
+ const printfLine = (value, description) => description ? `printf '%s\\t%s\\n' "${escapeDesc$1(value)}" "${escapeDesc$1(description)}"` : `printf '%s\\n' "${escapeDesc$1(value)}"`;
3974
+ const wrapWithDedup = (echoLine, keyPart) => location.isArrayOption && keyPart.length > 0 ? [
3975
+ ` if not contains -- "${escapeDesc$1(keyPart)}" ${bucketList}`,
2622
3976
  ` ${echoLine}`,
2623
3977
  ` end`
2624
3978
  ] : [` ${echoLine}`];
@@ -2657,6 +4011,7 @@ function fishValueLines(vc, fn, location) {
2657
4011
  `end`
2658
4012
  ];
2659
4013
  case "none": return [];
4014
+ case "runtime-expand": return [];
2660
4015
  }
2661
4016
  }
2662
4017
  /** Generate fish matcher-filtered file completion */
@@ -2678,9 +4033,13 @@ function fishMatcherLines(patterns) {
2678
4033
  function fishExtensionLines(extensions) {
2679
4034
  const lines = [];
2680
4035
  lines.push(`__fish_complete_directories "$_cur"`);
2681
- for (const ext of extensions) {
2682
- lines.push(`for _f in "$_cur"*.${ext}`);
2683
- lines.push(` test -f "$_f"; and echo "$_f"`);
4036
+ if (extensions.length > 0) {
4037
+ const extMatch = extensions.map((ext) => `string match -q -- "*.${ext}" "$_f"`).join("; or ");
4038
+ lines.push(`for _f in "$_cur"*`);
4039
+ lines.push(` test -f "$_f"; or continue`);
4040
+ lines.push(` if ${extMatch}`);
4041
+ lines.push(` echo "$_f"`);
4042
+ lines.push(` end`);
2684
4043
  lines.push(`end`);
2685
4044
  }
2686
4045
  return lines;
@@ -2717,6 +4076,27 @@ function positionalBlock$1(positionals, fn, options = []) {
2717
4076
  }
2718
4077
  return lines;
2719
4078
  }
4079
+ /**
4080
+ * Subcommand-name echoes. When the same node also has positionals, complete
4081
+ * subcommand names only while the cursor still prefixes one and fall through to
4082
+ * positional completion otherwise. Returns lines at base indentation; callers
4083
+ * re-indent for their handler depth.
4084
+ */
4085
+ function subOrPositionalLines$1(subItems, positionals, fn, options) {
4086
+ const echoSubs = subItems.map((s) => `echo "${s.name}\t${escapeDesc$1(s.description ?? "")}"`);
4087
+ if (positionals.length === 0) return echoSubs;
4088
+ return [
4089
+ `set -l _sub_match 0`,
4090
+ `for _sub_name in ${subItems.map((s) => `"${escapeDesc$1(s.name)}"`).join(" ")}`,
4091
+ ` test (string sub -l (string length -- "$_cur") -- "$_sub_name") = "$_cur"; and set _sub_match 1; and break`,
4092
+ `end`,
4093
+ `if test $_sub_match -eq 1`,
4094
+ ...echoSubs.map((l) => ` ${l}`),
4095
+ `else`,
4096
+ ...positionalBlock$1(positionals, fn, options),
4097
+ `end`
4098
+ ];
4099
+ }
2720
4100
  /** Generate available-option echo lines for fish */
2721
4101
  function availableOptionLines$1(options, fn) {
2722
4102
  const lines = [];
@@ -2772,11 +4152,10 @@ function generateSubHandler$1(sub, fn, path) {
2772
4152
  lines.push(...availableOptionLines$1(sub.options, fn));
2773
4153
  lines.push(` return`);
2774
4154
  lines.push(` end`);
2775
- if (visibleSubs.length > 0) for (const s of getSubNamesWithAliases(sub.subcommands)) {
2776
- const desc = escapeDesc$1(s.description ?? "");
2777
- lines.push(` echo "${s.name}\t${desc}"`);
2778
- }
2779
- else if (sub.positionals.length > 0) lines.push(...positionalBlock$1(sub.positionals, fn, sub.options));
4155
+ if (visibleSubs.length > 0) {
4156
+ const subItems = getSubNamesWithAliases(sub.subcommands);
4157
+ lines.push(...subOrPositionalLines$1(subItems, sub.positionals, fn, sub.options).map((l) => ` ${l}`));
4158
+ } else if (sub.positionals.length > 0) lines.push(...positionalBlock$1(sub.positionals, fn, sub.options));
2780
4159
  lines.push(`end`);
2781
4160
  lines.push(``);
2782
4161
  return lines;
@@ -2794,7 +4173,9 @@ function optTakesValueCases(sub, parentPath) {
2794
4173
  function generateFishCompletion(command, options) {
2795
4174
  const { programName } = options;
2796
4175
  const data = extractCompletionData(command, programName, options.globalArgsSchema);
2797
- const fn = sanitize(programName);
4176
+ const baseFn = sanitize(programName);
4177
+ const fn = options.staticWorker ? `${baseFn}_${sanitize(options.staticWorker.functionSuffix)}` : baseFn;
4178
+ const isWorker = options.staticWorker !== void 0;
2798
4179
  const root = data.command;
2799
4180
  const visibleSubs = getVisibleSubs(root.subcommands);
2800
4181
  const expandSpecs = collectExpandSpecs(root);
@@ -2809,26 +4190,34 @@ function generateFishCompletion(command, options) {
2809
4190
  binPath: options.binPath,
2810
4191
  programVersion: options.programVersion
2811
4192
  }));
4193
+ lines.push(`# politty-completion-mode: ${isWorker ? "worker" : "static"}`);
4194
+ if (isWorker) lines.push(`# politty-completion-worker: true`);
2812
4195
  lines.push(`# Generated by politty`);
2813
4196
  lines.push(``);
2814
- const sig = computeBinSig(resolveBinPath(programName, options.binPath));
2815
- const refreshFn = `__${fn}_refresh_completion`;
2816
- lines.push(`function ${refreshFn} --no-scope-shadowing`);
2817
- lines.push(` set -l _bin (command -v ${programName})`);
2818
- lines.push(` test -z "$_bin"; and return 1`);
2819
- lines.push(` set -l _sig (stat -L -c '%Y' "$_bin" 2>/dev/null; or stat -L -f '%m' "$_bin" 2>/dev/null)`);
2820
- lines.push(` test "$_sig" = "${sig}"; and return 1`);
2821
- lines.push(` set -l _target "$__fish_config_dir/completions/${programName}.fish"`);
2822
- lines.push(` "$_bin" __refresh-completion fish 2>/dev/null`);
2823
- lines.push(` and source "$_target" 2>/dev/null`);
2824
- lines.push(` and return 0`);
2825
- lines.push(` return 1`);
2826
- lines.push(`end`);
2827
- lines.push(`${refreshFn}`);
2828
- lines.push(`set -l _politty_refreshed $status`);
2829
- lines.push(`functions -e ${refreshFn}`);
2830
- lines.push(`test $_politty_refreshed -eq 0; and return`);
2831
- lines.push(``);
4197
+ if (!isWorker) {
4198
+ const resolvedBinPath = resolveBinPath(programName, options.binPath);
4199
+ const sig = computeBinSig(resolvedBinPath);
4200
+ const refreshEnvName = binEnvVarName(baseFn);
4201
+ const refreshFn = `__${fn}_refresh_completion`;
4202
+ lines.push(`function ${refreshFn} --no-scope-shadowing`);
4203
+ lines.push(` set -l _bin $${refreshEnvName}`);
4204
+ lines.push(` test -z "$_bin"; and set _bin (command -v ${programName})`);
4205
+ lines.push(` test -z "$_bin"; and return 1`);
4206
+ lines.push(` set -l _sig ${statSigExpr("$_bin", { shell: "fish" })}`);
4207
+ lines.push(` test "$_sig" = "${sig}"; and test "$_bin" = "${escapeDesc$1(resolvedBinPath)}"; and return 1`);
4208
+ lines.push(` set -l _target (status current-filename)`);
4209
+ lines.push(` test -n "$_target"; and test -f "$_target"; or return 1`);
4210
+ lines.push(` "$_bin" __refresh-completion fish "$_target" --static 2>/dev/null`);
4211
+ lines.push(` and source "$_target" 2>/dev/null`);
4212
+ lines.push(` and return 0`);
4213
+ lines.push(` return 1`);
4214
+ lines.push(`end`);
4215
+ lines.push(`${refreshFn}`);
4216
+ lines.push(`set -l _politty_refreshed $status`);
4217
+ lines.push(`functions -e ${refreshFn}`);
4218
+ lines.push(`test $_politty_refreshed -eq 0; and return`);
4219
+ lines.push(``);
4220
+ }
2832
4221
  if (hasDynamicCompletion(root)) {
2833
4222
  lines.push(`function __${fn}_invoke_complete`);
2834
4223
  lines.push(` set -l _shell $argv[1]`);
@@ -2975,10 +4364,8 @@ function generateFishCompletion(command, options) {
2975
4364
  lines.push(...availableOptionLines$1(root.options, fn));
2976
4365
  if (visibleSubs.length > 0) {
2977
4366
  lines.push(` else`);
2978
- for (const s of getSubNamesWithAliases(root.subcommands)) {
2979
- const desc = escapeDesc$1(s.description ?? "");
2980
- lines.push(` echo "${s.name}\t${desc}"`);
2981
- }
4367
+ const subItems = getSubNamesWithAliases(root.subcommands);
4368
+ lines.push(...subOrPositionalLines$1(subItems, root.positionals, fn, root.options).map((l) => ` ${l}`));
2982
4369
  } else if (root.positionals.length > 0) {
2983
4370
  lines.push(` else`);
2984
4371
  lines.push(...positionalBlock$1(root.positionals, fn, root.options));
@@ -3087,26 +4474,19 @@ function generateFishCompletion(command, options) {
3087
4474
  lines.push(` end`);
3088
4475
  lines.push(`end`);
3089
4476
  lines.push(``);
3090
- lines.push(`# Clear existing completions`);
3091
- lines.push(`complete -e -c ${programName}`);
3092
- lines.push(``);
3093
- lines.push(`# Register completion`);
3094
- lines.push(`complete -c ${programName} -f -a '(__fish_${fn}_complete)'`);
4477
+ if (!isWorker) {
4478
+ lines.push(`# Clear existing completions`);
4479
+ lines.push(`complete -e -c ${programName}`);
4480
+ lines.push(``);
4481
+ lines.push(`# Register completion`);
4482
+ lines.push(`complete -c ${programName} -f -a '(__fish_${fn}_complete)'`);
4483
+ }
3095
4484
  lines.push(``);
3096
4485
  return {
3097
4486
  script: lines.join("\n"),
3098
4487
  shell: "fish",
3099
- installInstructions: `# To enable completions, run one of the following:
3100
-
3101
- # Option 1: Source directly
3102
- ${programName} completion fish | source
3103
-
3104
- # Option 2: Save to the fish completions directory
3105
- ${programName} completion fish > ~/.config/fish/completions/${programName}.fish
3106
-
3107
- # The completion will be available immediately in new shell sessions.
3108
- # To use in the current session, run:
3109
- source ~/.config/fish/completions/${programName}.fish`
4488
+ installInstructions: `# To enable auto-refreshing fish completions, run:
4489
+ ${programName} completion fish --install --static`
3110
4490
  };
3111
4491
  }
3112
4492
 
@@ -3121,38 +4501,30 @@ source ~/.config/fish/completions/${programName}.fish`
3121
4501
  * 1. Looks up the binary on $PATH.
3122
4502
  * 2. Reads its mtime.
3123
4503
  * 3. If the on-disk completion cache is missing or its
3124
- * `# politty-bin-sig:` header differs, regenerates the cache by
3125
- * spawning the binary once.
4504
+ * `# politty-bin-sig:` / `# politty-bin-path:` headers differ,
4505
+ * regenerates the cache by spawning the binary once.
3126
4506
  * 4. Sources the cache.
3127
4507
  *
3128
4508
  * All failure modes are silent no-ops so a broken / missing CLI never
3129
4509
  * blocks shell startup.
3130
4510
  */
3131
- /**
3132
- * Single-quote escape: `'` -> `'\''`. Inside single quotes the shell
3133
- * performs no expansion at all, so `$`, backticks, and `$(...)` are
3134
- * inert. Used for hardcoded paths because callers may sources them
3135
- * from env / config — we must not let metachars in the path execute as
3136
- * commands when the rc snippet is sourced.
3137
- */
3138
- function shSingleQuote(s) {
3139
- return `'${s.replace(/'/g, "'\\''")}'`;
3140
- }
3141
4511
  function bashCachePathExpr(programName, cacheDir, shell) {
3142
4512
  if (cacheDir) return shSingleQuote(`${cacheDir}/completion.${shell}`);
3143
4513
  return `"\${XDG_CACHE_HOME:-$HOME/.cache}/${programName}/completion.${shell}"`;
3144
4514
  }
3145
4515
  function generateBashLoader(opts) {
3146
4516
  const fn = sanitize(opts.programName);
4517
+ const envName = binEnvVarName(fn);
3147
4518
  const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "bash");
3148
4519
  return `__${fn}_load_completion() {
3149
- local _bin _cache _sig _hdr
3150
- _bin=$(type -P ${opts.programName} 2>/dev/null)
4520
+ local _bin _cache _sig _sig_hdr _path_hdr
4521
+ _bin="\${${envName}:-$(type -P ${opts.programName} 2>/dev/null)}"
3151
4522
  [[ -n "$_bin" ]] || return 0
3152
4523
  _cache=${cache}
3153
4524
  _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
3154
- _hdr="# politty-bin-sig: $_sig"
3155
- if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
4525
+ _sig_hdr="# politty-bin-sig: $_sig"
4526
+ _path_hdr="# politty-bin-path: $_bin"
4527
+ if [[ ! -f "$_cache" ]] || ! head -8 "$_cache" 2>/dev/null | grep -qxF "$_sig_hdr" || ! head -8 "$_cache" 2>/dev/null | grep -qxF "$_path_hdr"; then
3156
4528
  # Use the hidden __refresh-completion subcommand instead of
3157
4529
  # \`$_bin completion bash\`: the foreground completion command
3158
4530
  # is subject to user setup/cleanup/prompt and required
@@ -3174,17 +4546,19 @@ unset -f __${fn}_load_completion
3174
4546
  }
3175
4547
  function generateZshLoader(opts) {
3176
4548
  const fn = sanitize(opts.programName);
4549
+ const envName = binEnvVarName(fn);
3177
4550
  const cache = bashCachePathExpr(opts.programName, opts.cacheDir, "zsh");
3178
4551
  return `__${fn}_load_completion() {
3179
4552
  emulate -L zsh
3180
4553
  setopt local_options no_aliases
3181
- local _bin _cache _sig _hdr
3182
- _bin=$(whence -p ${opts.programName} 2>/dev/null)
4554
+ local _bin _cache _sig _sig_hdr _path_hdr
4555
+ _bin="\${${envName}:-$(whence -p ${opts.programName} 2>/dev/null)}"
3183
4556
  [[ -n "$_bin" ]] || return 0
3184
4557
  _cache=${cache}
3185
4558
  _sig=$(stat -L -c '%Y' "$_bin" 2>/dev/null || stat -L -f '%m' "$_bin" 2>/dev/null) || return 0
3186
- _hdr="# politty-bin-sig: $_sig"
3187
- if [[ ! -f "$_cache" ]] || ! head -5 "$_cache" 2>/dev/null | grep -qF "$_hdr"; then
4559
+ _sig_hdr="# politty-bin-sig: $_sig"
4560
+ _path_hdr="# politty-bin-path: $_bin"
4561
+ if [[ ! -f "$_cache" ]] || ! head -8 "$_cache" 2>/dev/null | grep -qxF "$_sig_hdr" || ! head -8 "$_cache" 2>/dev/null | grep -qxF "$_path_hdr"; then
3188
4562
  # See bash loader for why we use __refresh-completion instead
3189
4563
  # of \`$_bin completion zsh\`.
3190
4564
  "$_bin" __refresh-completion zsh 2>/dev/null
@@ -3256,11 +4630,14 @@ function generateScript(ctx, shell) {
3256
4630
  return generateCompletion(ctx.rootCommand, {
3257
4631
  shell,
3258
4632
  programName: ctx.programName,
4633
+ mode: ctx.completionMode ?? "dispatcher",
3259
4634
  includeDescriptions: true,
3260
4635
  ...ctx.programVersion !== void 0 && { programVersion: ctx.programVersion },
3261
4636
  ...ctx.binPath !== void 0 && { binPath: ctx.binPath },
3262
4637
  ...ctx.cacheDir !== void 0 && { cacheDir: ctx.cacheDir },
3263
- ...ctx.globalArgsSchema !== void 0 && { globalArgsSchema: ctx.globalArgsSchema }
4638
+ ...ctx.globalArgsSchema !== void 0 && { globalArgsSchema: ctx.globalArgsSchema },
4639
+ ...ctx.bundledWorker !== void 0 && { bundledWorker: ctx.bundledWorker },
4640
+ ...ctx.staticWorker !== void 0 && { staticWorker: ctx.staticWorker }
3264
4641
  }).script;
3265
4642
  }
3266
4643
  /** Write the script for `shell` to its install path. Returns the path. */
@@ -3283,6 +4660,34 @@ function readCachedSig(path) {
3283
4660
  return null;
3284
4661
  }
3285
4662
  }
4663
+ function readCachedMode(path) {
4664
+ try {
4665
+ if (!existsSync(path)) return void 0;
4666
+ const m = readFileSync(path, "utf8").split("\n", 10).join("\n").match(/^# politty-completion-mode: (dispatcher|static)$/m);
4667
+ if (m) return m[1];
4668
+ return;
4669
+ } catch {
4670
+ return;
4671
+ }
4672
+ }
4673
+ function readCachedBinPath(path) {
4674
+ try {
4675
+ if (!existsSync(path)) return null;
4676
+ const m = readFileSync(path, "utf8").split("\n", 10).join("\n").match(/^# politty-bin-path: (.*)$/m);
4677
+ return m ? m[1] : null;
4678
+ } catch {
4679
+ return null;
4680
+ }
4681
+ }
4682
+ function isManagedTarget(path, programName, shell) {
4683
+ try {
4684
+ if (!existsSync(path)) return false;
4685
+ const lines = readFileSync(path, "utf8").split("\n", 8).map((line) => line.trimEnd());
4686
+ return lines.some((line) => /^# politty-completion-version: \S+$/.test(line)) && lines.includes(`# program: ${programName}`) && lines.includes(`# shell: ${shell}`);
4687
+ } catch {
4688
+ return false;
4689
+ }
4690
+ }
3286
4691
  /**
3287
4692
  * Rewrite the cache only when stale. Used by:
3288
4693
  * - `<program> __refresh-completion <shell>` (the hidden subcommand
@@ -3298,7 +4703,9 @@ function readCachedSig(path) {
3298
4703
  */
3299
4704
  function refreshIfStale(ctx, shell) {
3300
4705
  try {
3301
- const target = installPath(ctx.programName, shell, ctx.cacheDir);
4706
+ const target = ctx.targetPath ? existsSync(ctx.targetPath) ? realpathSync(ctx.targetPath) : ctx.targetPath : installPath(ctx.programName, shell, ctx.cacheDir);
4707
+ if (ctx.targetPath && existsSync(target) && !isManagedTarget(target, ctx.programName, shell)) return;
4708
+ if (ctx.targetPath && !existsSync(target) && !ctx.allowTargetCreate) return;
3302
4709
  const binPath = resolveBinPath(ctx.programName, ctx.binPath);
3303
4710
  if (!binPath) return;
3304
4711
  let currentSig;
@@ -3307,8 +4714,12 @@ function refreshIfStale(ctx, shell) {
3307
4714
  } catch {
3308
4715
  return;
3309
4716
  }
3310
- if (readCachedSig(target) === currentSig) return;
3311
- writeAtomic(target, generateScript(ctx, shell));
4717
+ if (readCachedSig(target) === currentSig && readCachedBinPath(target) === binPath) return;
4718
+ const completionMode = ctx.completionMode ?? readCachedMode(target) ?? (existsSync(target) ? "static" : "dispatcher");
4719
+ writeAtomic(target, generateScript({
4720
+ ...ctx,
4721
+ completionMode
4722
+ }, shell));
3312
4723
  } catch {}
3313
4724
  }
3314
4725
  /**
@@ -3360,6 +4771,53 @@ function escapeDesc(s) {
3360
4771
  function escapeDescribeValue(s) {
3361
4772
  return s.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
3362
4773
  }
4774
+ /** Escape a string for use inside zsh double-quotes. */
4775
+ function escapeZshDQ(s) {
4776
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$").replace(/`/g, "\\`");
4777
+ }
4778
+ function zshArrayLiteral(values) {
4779
+ return values.map((v) => `"${escapeZshDQ(v)}"`).join(" ");
4780
+ }
4781
+ function zshFilteredFileLines(fn, kind, values) {
4782
+ const lines = [
4783
+ `setopt local_options null_glob`,
4784
+ `local _cur="\${words[CURRENT]:-}" _dir _prefix _f _out`,
4785
+ `local -a ${kind === "extensions" ? "_exts" : "_matchers"}=(${zshArrayLiteral(values)})`,
4786
+ `if [[ -z "$_cur" || "$_cur" != */* ]]; then`,
4787
+ ` _dir="."`,
4788
+ ` _prefix="$_cur"`,
4789
+ `elif [[ "$_cur" == */ ]]; then`,
4790
+ ` _dir="\${_cur%/}"`,
4791
+ ` _prefix=""`,
4792
+ `else`,
4793
+ ` _dir="\${_cur%/*}"`,
4794
+ ` _prefix="\${_cur##*/}"`,
4795
+ `fi`,
4796
+ `for _f in "$_dir"/"$_prefix"*(N/); do`,
4797
+ ` _out="\${_f#./}"`,
4798
+ ` __${fn}_add_path_candidate "$_out"`,
4799
+ `done`
4800
+ ];
4801
+ if (kind === "extensions") {
4802
+ lines.push(`local _ext`);
4803
+ lines.push(`for _f in "$_dir"/"$_prefix"*(N.); do`);
4804
+ lines.push(` _out="\${_f#./}"`);
4805
+ lines.push(` for _ext in "\${_exts[@]}"; do`);
4806
+ lines.push(` [[ -n "$_ext" && "$_out" == *."$_ext" ]] && { __${fn}_add_path_candidate "$_out"; break; }`);
4807
+ lines.push(` done`);
4808
+ lines.push(`done`);
4809
+ } else {
4810
+ lines.push(`local _pat`);
4811
+ lines.push(`for _pat in "\${_matchers[@]}"; do`);
4812
+ lines.push(` [[ -n "$_pat" ]] || continue`);
4813
+ lines.push(` for _f in "$_dir"/\${~_pat}(N.); do`);
4814
+ lines.push(` _out="\${_f#./}"`);
4815
+ lines.push(` [[ "$_out" == "$_cur"* ]] && __${fn}_add_path_candidate "$_out"`);
4816
+ lines.push(` done`);
4817
+ lines.push(`done`);
4818
+ }
4819
+ return lines;
4820
+ }
3363
4821
  /**
3364
4822
  * Generate zsh value completion lines for a ValueCompletion spec.
3365
4823
  * Uses `_vals` array (must be declared in the calling function scope).
@@ -3416,12 +4874,13 @@ function zshValueLines(vc, fn, location) {
3416
4874
  case "dynamic": return [`__${fn}_apply_dynamic_output "$(__${fn}_invoke_complete zsh "\${(@)words[2,CURRENT]}")"`];
3417
4875
  case "choices": return [`_vals=(${vc.choices.map((c) => `"${escapeDesc(c)}"`).join(" ")})`, `__${fn}_cdescribe 'completions' _vals`];
3418
4876
  case "file":
3419
- if (vc.matcher?.length) return vc.matcher.map((p) => `_files -g "${p}"`);
3420
- if (vc.extensions?.length) return vc.extensions.map((ext) => `_files -g "*.${ext}"`);
4877
+ if (vc.matcher?.length) return zshFilteredFileLines(fn, "matchers", vc.matcher);
4878
+ if (vc.extensions?.length) return zshFilteredFileLines(fn, "extensions", vc.extensions);
3421
4879
  return [`_files`];
3422
4880
  case "directory": return [`_files -/`];
3423
4881
  case "command": return [`_vals=("\${(@f)$(${vc.shellCommand})}")`, `__${fn}_cdescribe 'completions' _vals`];
3424
4882
  case "none": return [];
4883
+ case "runtime-expand": return [];
3425
4884
  }
3426
4885
  }
3427
4886
  /** Generate option-value case branches */
@@ -3460,6 +4919,31 @@ function positionalBlock(positionals, fn, funcSuffix, options = []) {
3460
4919
  lines.push(` esac`);
3461
4920
  return lines;
3462
4921
  }
4922
+ /**
4923
+ * Subcommand completion via `_describe`. When the same node also has
4924
+ * positionals, complete subcommand names only while the cursor still prefixes
4925
+ * one and fall through to positional completion otherwise. Returns lines at
4926
+ * base indentation; callers re-indent for their handler depth.
4927
+ */
4928
+ function subOrPositionalLines(subItems, positionals, fn, funcSuffix, options) {
4929
+ const describe = [`local -a _subs=(${subItems.map((s) => {
4930
+ const desc = s.description ? `:${escapeDesc(s.description)}` : "";
4931
+ return `"${s.name}${desc}"`;
4932
+ }).join(" ")})`, `__${fn}_cdescribe 'subcommands' _subs`];
4933
+ if (positionals.length === 0) return describe;
4934
+ return [
4935
+ `local -a _sub_names=(${subItems.map((s) => `"${escapeZshDQ(s.name)}"`).join(" ")})`,
4936
+ `local _cur_word="\${words[CURRENT]:-}" _sub_name _sub_match=0`,
4937
+ `for _sub_name in "\${_sub_names[@]}"; do`,
4938
+ ` [[ "$_sub_name" == "$_cur_word"* ]] && _sub_match=1 && break`,
4939
+ `done`,
4940
+ `if (( _sub_match )); then`,
4941
+ ...describe.map((l) => ` ${l}`),
4942
+ `else`,
4943
+ ...positionalBlock(positionals, fn, funcSuffix, options),
4944
+ `fi`
4945
+ ];
4946
+ }
3463
4947
  /** Generate prev-word value completion case block */
3464
4948
  function valueCompletionBlock(options, positionals, fn, funcSuffix) {
3465
4949
  if (!options.some((o) => o.takesValue && o.valueCompletion)) return [];
@@ -3531,12 +5015,8 @@ function generateSubHandler(sub, fn, path) {
3531
5015
  lines.push(` return 0`);
3532
5016
  lines.push(` fi`);
3533
5017
  if (visibleSubs.length > 0) {
3534
- const subItems = getSubNamesWithAliases(sub.subcommands).map((s) => {
3535
- const desc = s.description ? `:${escapeDesc(s.description)}` : "";
3536
- return `"${s.name}${desc}"`;
3537
- }).join(" ");
3538
- lines.push(` local -a _subs=(${subItems})`);
3539
- lines.push(` __${fn}_cdescribe 'subcommands' _subs`);
5018
+ const subItems = getSubNamesWithAliases(sub.subcommands);
5019
+ lines.push(...subOrPositionalLines(subItems, sub.positionals, fn, funcSuffix, sub.options).map((l) => ` ${l}`));
3540
5020
  } else if (sub.positionals.length > 0) lines.push(...positionalBlock(sub.positionals, fn, funcSuffix, sub.options));
3541
5021
  lines.push(`}`);
3542
5022
  lines.push(``);
@@ -3545,7 +5025,11 @@ function generateSubHandler(sub, fn, path) {
3545
5025
  function generateZshCompletion(command, options) {
3546
5026
  const { programName } = options;
3547
5027
  const data = extractCompletionData(command, programName, options.globalArgsSchema);
3548
- const fn = sanitize(programName);
5028
+ const baseFn = sanitize(programName);
5029
+ const fn = options.staticWorker ? `${baseFn}_${sanitize(options.staticWorker.functionSuffix)}` : baseFn;
5030
+ const isWorker = options.staticWorker !== void 0;
5031
+ const completionFn = isWorker ? `_${fn}_completions` : `_${programName}`;
5032
+ const autoloadCheck = `"\${funcstack[1]:-}" == "${escapeZshDQ(completionFn)}"`;
3549
5033
  const root = data.command;
3550
5034
  const visibleSubs = getVisibleSubs(root.subcommands);
3551
5035
  const expandSpecs = collectExpandSpecs(root);
@@ -3554,16 +5038,24 @@ function generateZshCompletion(command, options) {
3554
5038
  const arrayExpandSpecs = expandSpecs.filter((s) => s.isArrayOption);
3555
5039
  const hasArrayExpand = arrayExpandSpecs.length > 0;
3556
5040
  const lines = [];
3557
- lines.push(`#compdef ${programName}`);
3558
- lines.push(``);
5041
+ if (!isWorker) {
5042
+ lines.push(`#compdef ${programName}`);
5043
+ lines.push(``);
5044
+ }
3559
5045
  lines.push(...buildHeaderLines({
3560
5046
  programName,
3561
5047
  shell: "zsh",
3562
5048
  binPath: options.binPath,
3563
5049
  programVersion: options.programVersion
3564
5050
  }));
5051
+ lines.push(`# politty-completion-mode: ${isWorker ? "worker" : "static"}`);
5052
+ if (isWorker) lines.push(`# politty-completion-worker: true`);
3565
5053
  lines.push(`# Generated by politty`);
3566
5054
  lines.push(``);
5055
+ if (!isWorker) lines.push(...generateZshSelfRefresh({
5056
+ programName,
5057
+ binPath: options.binPath
5058
+ }));
3567
5059
  for (const spec of expandSpecs) {
3568
5060
  const varName = expandTableVarName(fn, spec.funcSuffix, spec.fieldName);
3569
5061
  if (spec.vc.table.length === 0) lines.push(`typeset -gA ${varName}=()`);
@@ -3636,6 +5128,11 @@ function generateZshCompletion(command, options) {
3636
5128
  lines.push(` return 0`);
3637
5129
  lines.push(`}`);
3638
5130
  lines.push(``);
5131
+ lines.push(`__${fn}_add_path_candidate() {`);
5132
+ lines.push(` compadd -f -- "$1" 2>/dev/null && return 0`);
5133
+ lines.push(` print -r -- "$1"`);
5134
+ lines.push(`}`);
5135
+ lines.push(``);
3639
5136
  lines.push(`__${fn}_opt_takes_value() {`);
3640
5137
  lines.push(` case "$1:$2" in`);
3641
5138
  lines.push(...optTakesValueEntries(root, ""));
@@ -3692,12 +5189,8 @@ function generateZshCompletion(command, options) {
3692
5189
  lines.push(` __${fn}_cdescribe 'options' _opts`);
3693
5190
  if (visibleSubs.length > 0) {
3694
5191
  lines.push(` else`);
3695
- const subItems = getSubNamesWithAliases(root.subcommands).map((s) => {
3696
- const desc = s.description ? `:${escapeDesc(s.description)}` : "";
3697
- return `"${s.name}${desc}"`;
3698
- }).join(" ");
3699
- lines.push(` local -a _subs=(${subItems})`);
3700
- lines.push(` __${fn}_cdescribe 'subcommands' _subs`);
5192
+ const subItems = getSubNamesWithAliases(root.subcommands);
5193
+ lines.push(...subOrPositionalLines(subItems, root.positionals, fn, "root", root.options).map((l) => ` ${l}`));
3701
5194
  } else if (root.positionals.length > 0) {
3702
5195
  lines.push(` else`);
3703
5196
  lines.push(...positionalBlock(root.positionals, fn, "root", root.options).map((l) => ` ${l}`));
@@ -3706,7 +5199,7 @@ function generateZshCompletion(command, options) {
3706
5199
  lines.push(`}`);
3707
5200
  lines.push(``);
3708
5201
  const subRouting = subDispatchCaseLines(routeEntries, fn).join("\n");
3709
- lines.push(`_${fn}() {`);
5202
+ lines.push(`${completionFn}() {`);
3710
5203
  lines.push(` (( CURRENT )) || CURRENT=\${#words}`);
3711
5204
  lines.push(``);
3712
5205
  lines.push(` local _subcmd="" _after_dd=0 _pos_count=0 _skip_next=0`);
@@ -3772,22 +5265,30 @@ function generateZshCompletion(command, options) {
3772
5265
  lines.push(``);
3773
5266
  lines.push(`zstyle ':completion:*:*:${programName}:*' file-patterns '%p:globbed-files *(-/):directories'`);
3774
5267
  lines.push(``);
3775
- lines.push(`compdef _${fn} ${programName}`);
3776
- lines.push(``);
5268
+ if (!isWorker) {
5269
+ lines.push(`if [[ ${autoloadCheck} ]]; then`);
5270
+ lines.push(` ${completionFn} "$@"`);
5271
+ lines.push(`else`);
5272
+ lines.push(` compdef ${completionFn} ${programName}`);
5273
+ lines.push(`fi`);
5274
+ lines.push(``);
5275
+ }
3777
5276
  return {
3778
5277
  script: lines.join("\n"),
3779
5278
  shell: "zsh",
3780
- installInstructions: `# To enable completions, add the following to your ~/.zshrc:
5279
+ installInstructions: `# To enable auto-refreshing zsh completions, add this to your ~/.zshrc after compinit:
5280
+ eval "$(${programName} completion zsh --static)"
3781
5281
 
3782
- # Option 1: Source directly (add before compinit)
3783
- eval "$(${programName} completion zsh)"
5282
+ # For faster shell startup, save the script in your fpath:
5283
+ mkdir -p ~/.zsh/completions
5284
+ ${programName} completion zsh --static > ~/.zsh/completions/_${programName}
3784
5285
 
3785
- # Option 2: Save to a file in your fpath
3786
- ${programName} completion zsh > ~/.zsh/completions/_${programName}
5286
+ # Make sure your ~/.zshrc includes the fpath line before compinit:
5287
+ fpath=(~/.zsh/completions $fpath)
5288
+ autoload -Uz compinit && compinit
3787
5289
 
3788
- # Make sure your fpath includes the completions directory:
3789
- # fpath=(~/.zsh/completions $fpath)
3790
- # autoload -Uz compinit && compinit
5290
+ # If ~/.zshrc already calls compinit, add only the fpath line before
5291
+ # the existing compinit call.
3791
5292
 
3792
5293
  # Then reload your shell or run:
3793
5294
  source ~/.zshrc`
@@ -3825,6 +5326,7 @@ source ~/.zshrc`
3825
5326
  * Generate completion script for the specified shell
3826
5327
  */
3827
5328
  function generateCompletion(command, options) {
5329
+ if (options.mode === "dispatcher") return generateDispatcherCompletion(command, options);
3828
5330
  switch (options.shell) {
3829
5331
  case "bash": return generateBashCompletion(command, options);
3830
5332
  case "zsh": return generateZshCompletion(command, options);
@@ -3842,6 +5344,18 @@ function getSupportedShells() {
3842
5344
  "fish"
3843
5345
  ];
3844
5346
  }
5347
+ function printZshFpathSetup(programName, target) {
5348
+ console.error("");
5349
+ console.error("Configure zsh fpath with:");
5350
+ console.error("");
5351
+ console.error(" mkdir -p ~/.zsh/completions");
5352
+ console.error(` ln -sf ${shSingleQuote(target)} ~/.zsh/completions/_${programName}`);
5353
+ console.error("");
5354
+ console.error("Add only this block to your ~/.zshrc before compinit:");
5355
+ console.error("");
5356
+ console.error(" fpath=(~/.zsh/completions $fpath)");
5357
+ console.error(" autoload -Uz compinit && compinit");
5358
+ }
3845
5359
  /**
3846
5360
  * Detect the current shell from environment
3847
5361
  */
@@ -3870,15 +5384,36 @@ const completionArgsSchema = z.object({
3870
5384
  description: "Show installation instructions"
3871
5385
  }),
3872
5386
  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." }),
3873
- 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." })
5387
+ 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." }),
5388
+ static: arg(z.boolean().default(false), { description: "Generate the legacy static completion script with command metadata baked in." }),
5389
+ dispatcher: arg(z.boolean().default(false), { description: "Generate the runtime dispatcher completion script. This is the default." }),
5390
+ worker: arg(z.boolean().default(false), { description: "Generate an internal static worker artifact for dispatcher mode." })
5391
+ });
5392
+ const refreshArgsSchema = z.object({
5393
+ shell: arg(z.enum([
5394
+ "bash",
5395
+ "zsh",
5396
+ "fish"
5397
+ ]), {
5398
+ positional: true,
5399
+ description: "Shell to refresh",
5400
+ placeholder: "SHELL"
5401
+ }),
5402
+ target: arg(z.string().optional(), {
5403
+ positional: true,
5404
+ description: "Existing politty-generated completion file to refresh",
5405
+ placeholder: "TARGET"
5406
+ }),
5407
+ static: arg(z.boolean().default(false), { description: "Refresh using the legacy static completion script mode." }),
5408
+ worker: arg(z.boolean().default(false), { description: "Refresh an internal static worker completion script." })
3874
5409
  });
3875
- const refreshArgsSchema = z.object({ shell: arg(z.enum([
5410
+ const workerPathArgsSchema = z.object({ shell: arg(z.enum([
3876
5411
  "bash",
3877
5412
  "zsh",
3878
5413
  "fish"
3879
5414
  ]), {
3880
5415
  positional: true,
3881
- description: "Shell to refresh",
5416
+ description: "Shell worker to locate",
3882
5417
  placeholder: "SHELL"
3883
5418
  }) });
3884
5419
  /**
@@ -3902,7 +5437,8 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema, ext
3902
5437
  const refreshExtra = {
3903
5438
  ...cacheDir !== void 0 && { cacheDir },
3904
5439
  ...programVersion !== void 0 && { programVersion },
3905
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
5440
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
5441
+ ...extra.bundledWorker !== void 0 && { bundledWorker: extra.bundledWorker }
3906
5442
  };
3907
5443
  const installCtxBase = {
3908
5444
  programName: resolvedProgramName,
@@ -3920,6 +5456,10 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema, ext
3920
5456
  ...rootCommand.subCommands,
3921
5457
  "__refresh-completion": createRefreshCompletionCommand(rootCommand, resolvedProgramName, refreshExtra)
3922
5458
  };
5459
+ if (!rootCommand.subCommands?.["__completion-worker-path"]) rootCommand.subCommands = {
5460
+ ...rootCommand.subCommands,
5461
+ "__completion-worker-path": createCompletionWorkerPathCommand(resolvedProgramName, refreshExtra)
5462
+ };
3923
5463
  return defineCommand({
3924
5464
  name: "completion",
3925
5465
  description: "Generate shell completion script",
@@ -3931,18 +5471,23 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema, ext
3931
5471
  process.exitCode = 1;
3932
5472
  return;
3933
5473
  }
5474
+ if (args.static && args.dispatcher) throw new Error("Choose only one completion mode: --dispatcher or --static.");
5475
+ if (args.worker && !args.static) throw new Error("`--worker` requires `--static`.");
5476
+ if (args.worker && (args.install || args.loader || args.instructions)) throw new Error("`--worker` can only print a worker artifact.");
5477
+ const completionMode = args.static ? "static" : "dispatcher";
3934
5478
  if (args.install) {
3935
5479
  let target;
3936
5480
  try {
3937
5481
  target = install({
3938
5482
  rootCommand,
3939
- ...installCtxBase
5483
+ ...installCtxBase,
5484
+ completionMode
3940
5485
  }, shellType);
3941
5486
  } catch (e) {
3942
5487
  throw new Error(`install failed: ${e instanceof Error ? e.message : String(e)}`);
3943
5488
  }
3944
5489
  console.error(`installed: ${target}`);
3945
- if (shellType !== "fish") {
5490
+ if (shellType === "bash") {
3946
5491
  console.error("");
3947
5492
  console.error(`Add to your ~/.${shellType}rc:`);
3948
5493
  console.error("");
@@ -3950,7 +5495,7 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema, ext
3950
5495
  ...loaderOptsBase,
3951
5496
  shell: shellType
3952
5497
  }).trim().replace(/^/gm, " "));
3953
- }
5498
+ } else if (shellType === "zsh") printZshFpathSetup(resolvedProgramName, target);
3954
5499
  return;
3955
5500
  }
3956
5501
  if (args.loader) {
@@ -3964,10 +5509,13 @@ function createCompletionCommand(rootCommand, programName, globalArgsSchema, ext
3964
5509
  const result = generateCompletion(rootCommand, {
3965
5510
  shell: shellType,
3966
5511
  programName: resolvedProgramName,
5512
+ mode: completionMode,
3967
5513
  includeDescriptions: true,
3968
5514
  ...globalArgsSchema !== void 0 && { globalArgsSchema },
3969
5515
  ...programVersion !== void 0 && { programVersion },
3970
- ...cacheDir !== void 0 && { cacheDir }
5516
+ ...cacheDir !== void 0 && { cacheDir },
5517
+ ...extra.bundledWorker !== void 0 && { bundledWorker: extra.bundledWorker },
5518
+ ...args.worker && { staticWorker: { functionSuffix: "worker" } }
3971
5519
  });
3972
5520
  if (args.instructions) console.log(result.installInstructions);
3973
5521
  else console.log(result.script);
@@ -3988,11 +5536,32 @@ function createRefreshCompletionCommand(rootCommand, programName, extra = {}) {
3988
5536
  refreshIfStale({
3989
5537
  rootCommand,
3990
5538
  programName,
3991
- ...extra
5539
+ ...extra,
5540
+ completionMode: args.static || args.worker ? "static" : void 0,
5541
+ ...args.worker && { staticWorker: { functionSuffix: "worker" } },
5542
+ ...args.worker && { allowTargetCreate: true },
5543
+ ...args.target !== void 0 && { targetPath: args.target }
3992
5544
  }, args.shell);
3993
5545
  }
3994
5546
  });
3995
5547
  }
5548
+ function createCompletionWorkerPathCommand(programName, extra = {}) {
5549
+ return defineCommand({
5550
+ name: "__completion-worker-path",
5551
+ description: "(internal) Print the bundled completion worker path when available.",
5552
+ args: workerPathArgsSchema,
5553
+ run(args) {
5554
+ const path = resolveBundledWorkerPath({
5555
+ programName,
5556
+ shell: args.shell,
5557
+ ...extra.binPath !== void 0 && { binPath: extra.binPath },
5558
+ ...extra.bundledWorker !== void 0 && { bundledWorker: extra.bundledWorker }
5559
+ });
5560
+ if (!path) throw new Error(`No bundled completion worker found for ${programName} (${args.shell}).`);
5561
+ console.log(path);
5562
+ }
5563
+ });
5564
+ }
3996
5565
  /**
3997
5566
  * Wrap a command with a completion subcommand
3998
5567
  *
@@ -4014,19 +5583,21 @@ function createRefreshCompletionCommand(rootCommand, programName, extra = {}) {
4014
5583
  * ```
4015
5584
  */
4016
5585
  function withCompletionCommand(command, options) {
4017
- const { programName, globalArgsSchema, cacheDir, programVersion } = typeof options === "string" ? { programName: options } : options ?? {};
5586
+ const { programName, globalArgsSchema, cacheDir, programVersion, bundledWorker } = typeof options === "string" ? { programName: options } : options ?? {};
4018
5587
  const resolvedProgramName = programName ?? command.name;
4019
5588
  const extra = {
4020
5589
  ...cacheDir !== void 0 && { cacheDir },
4021
5590
  ...programVersion !== void 0 && { programVersion },
4022
- ...globalArgsSchema !== void 0 && { globalArgsSchema }
5591
+ ...globalArgsSchema !== void 0 && { globalArgsSchema },
5592
+ ...bundledWorker !== void 0 && { bundledWorker }
4023
5593
  };
4024
5594
  const wrappedCommand = { ...command };
4025
5595
  wrappedCommand.subCommands = {
4026
5596
  ...command.subCommands,
4027
5597
  completion: createCompletionCommand(wrappedCommand, programName, globalArgsSchema, extra),
4028
5598
  __complete: createDynamicCompleteCommand(wrappedCommand, programName, globalArgsSchema),
4029
- "__refresh-completion": createRefreshCompletionCommand(wrappedCommand, resolvedProgramName, extra)
5599
+ "__refresh-completion": createRefreshCompletionCommand(wrappedCommand, resolvedProgramName, extra),
5600
+ "__completion-worker-path": createCompletionWorkerPathCommand(resolvedProgramName, extra)
4030
5601
  };
4031
5602
  wrappedCommand.runMainHook = (argv) => {
4032
5603
  maybeSpawnRefresh(argv, {
@@ -4053,7 +5624,7 @@ function withCompletionCommand(command, options) {
4053
5624
  function maybeSpawnRefresh(argv, ctx) {
4054
5625
  if (process.env.POLITTY_NO_COMPLETION_REFRESH) return;
4055
5626
  const firstPositional = argv.find((a) => !a.startsWith("-"));
4056
- if (firstPositional === "__complete" || firstPositional === "__refresh-completion" || firstPositional === "completion") return;
5627
+ if (firstPositional === "__complete" || firstPositional === "__refresh-completion" || firstPositional === "__completion-worker-path" || firstPositional === "completion") return;
4057
5628
  const shell = detectShell();
4058
5629
  if (!shell) return;
4059
5630
  const argv0 = process.argv[1];
@@ -4063,5 +5634,5 @@ function maybeSpawnRefresh(argv, ctx) {
4063
5634
  }
4064
5635
 
4065
5636
  //#endregion
4066
- export { defineCommand as _, getSupportedShells as a, hasCompleteCommand as c, extractPositionals as d, CompletionDirective as f, createDefineCommand as g, resolveValueCompletion as h, generateCompletion as i, formatForShell as l, parseCompletionContext as m, createRefreshCompletionCommand as n, withCompletionCommand as o, generateCandidates as p, detectShell as r, createDynamicCompleteCommand as s, createCompletionCommand as t, extractCompletionData as u };
4067
- //# sourceMappingURL=completion-BA5JMvVG.js.map
5637
+ export { defineCommand as S, CompletionDirective as _, generateCompletion as a, resolveValueCompletion as b, createDynamicCompleteCommand as c, bundledWorkerShellExtension as d, defaultBundledWorkerOutputPath as f, extractPositionals as g, extractCompletionData as h, detectShell as i, hasCompleteCommand as l, validateBundledWorkerFile as m, createCompletionWorkerPathCommand as n, getSupportedShells as o, generateBundledCompletionWorker as p, createRefreshCompletionCommand as r, withCompletionCommand as s, createCompletionCommand as t, formatForShell as u, generateCandidates as v, createDefineCommand as x, parseCompletionContext as y };
5638
+ //# sourceMappingURL=completion-DHnVx9Zk.js.map