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