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