argsbarg 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.private/scratch.md +2 -1
- package/CHANGELOG.md +29 -1
- package/README.md +22 -7
- package/docs/ai-skills.md +47 -0
- package/docs/install.md +84 -0
- package/docs/mcp.md +7 -5
- package/index.d.ts +11 -9
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +101 -0
- package/src/builtins/completion-bash.ts +240 -0
- package/src/builtins/completion-fish.ts +73 -0
- package/src/builtins/completion-group.ts +50 -0
- package/src/builtins/completion-zsh.ts +244 -0
- package/src/builtins/dispatch.ts +123 -0
- package/src/builtins/export.ts +46 -0
- package/src/builtins/index.ts +10 -0
- package/src/builtins/install.ts +99 -0
- package/src/builtins/mcp.ts +13 -0
- package/src/builtins/presentation.ts +39 -0
- package/src/builtins/scopes.ts +45 -0
- package/src/builtins/shell-helpers.ts +24 -0
- package/src/completion.ts +10 -652
- package/src/index.test.ts +135 -4
- package/src/index.ts +1 -0
- package/src/install/binary.ts +82 -0
- package/src/install/compiled.ts +15 -0
- package/src/install/completions.ts +52 -0
- package/src/install/detect-installed.ts +67 -0
- package/src/install/index.ts +196 -0
- package/src/install/install.test.ts +124 -0
- package/src/install/mcp-config.ts +70 -0
- package/src/install/paths.ts +69 -0
- package/src/install/plan.ts +183 -0
- package/src/install/shell.ts +56 -0
- package/src/install/status.ts +63 -0
- package/src/install/uninstall.ts +111 -0
- package/src/mcp/tools.ts +1 -1
- package/src/runtime.ts +23 -66
- package/src/schema.ts +7 -49
- package/src/skill/generate.ts +183 -0
- package/src/skill/install.ts +47 -0
- package/src/types.ts +12 -0
- package/src/validate.ts +14 -20
package/src/completion.ts
CHANGED
|
@@ -1,655 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
This file walks the CLI tree into scopes and emits bash and zsh completion scripts.
|
|
4
|
-
It builds the shell-side candidate lists, simulates argv state, and chooses the right
|
|
5
|
-
items for each completion context.
|
|
6
|
-
|
|
7
|
-
It keeps completion aligned with the runtime schema so the generated commands,
|
|
8
|
-
options, and descriptions stay in sync with the CLI definition.
|
|
2
|
+
Re-export shim — completion emitters and built-in trees live in ./builtins/.
|
|
9
3
|
*/
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
opts: CliOption[];
|
|
21
|
-
/** `/`-separated path of command keys from the root, or `""` for the root. */
|
|
22
|
-
path: string;
|
|
23
|
-
/** True when a positional tail exists (bash/zsh may offer filenames). */
|
|
24
|
-
wantsFiles: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** True if the command declares at least one positional (used to enable file completion). */
|
|
28
|
-
function hasPositionalArguments(cmd: CliCommand): boolean {
|
|
29
|
-
return (cmd.positionals ?? []).length > 0;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Recursively appends a scope record for `cmd` and its subtree into `acc`. */
|
|
33
|
-
function walkScopes(cmdPath: string, cmd: CliCommand, acc: ScopeRec[]): void {
|
|
34
|
-
acc.push({
|
|
35
|
-
kids: cmd.commands ?? [],
|
|
36
|
-
opts: cmd.options ?? [],
|
|
37
|
-
path: cmdPath,
|
|
38
|
-
wantsFiles: hasPositionalArguments(cmd),
|
|
39
|
-
});
|
|
40
|
-
for (const ch of cmd.commands ?? []) {
|
|
41
|
-
const nextPath = cmdPath === "" ? ch.key : cmdPath + "/" + ch.key;
|
|
42
|
-
walkScopes(nextPath, ch, acc);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Flattens the schema into a list of completion scopes (root + every command path). */
|
|
47
|
-
function collectScopes(schema: CliCommand): ScopeRec[] {
|
|
48
|
-
const acc: ScopeRec[] = [];
|
|
49
|
-
acc.push({
|
|
50
|
-
kids: schema.commands ?? [],
|
|
51
|
-
opts: schema.options ?? [],
|
|
52
|
-
path: "",
|
|
53
|
-
wantsFiles: false,
|
|
54
|
-
});
|
|
55
|
-
for (const c of schema.commands ?? []) {
|
|
56
|
-
walkScopes(c.key, c, acc);
|
|
57
|
-
}
|
|
58
|
-
return acc;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
/** Produces a shell-safe identifier from the app or command name (alnum → `_`). */
|
|
64
|
-
function identToken(s: string): string {
|
|
65
|
-
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Escapes a string for use inside single quotes in generated shell scripts. */
|
|
69
|
-
function escShellSingleQuoted(s: string): string {
|
|
70
|
-
return s.replace(/'/g, "'\\''");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Sanitizes the app key for generated shell function names (non-alnum removed). */
|
|
74
|
-
function mainName(schemaName: string): string {
|
|
75
|
-
return schemaName.replace(/[^a-zA-Z0-9]/g, "_");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const kHelpLong = "--help";
|
|
79
|
-
const kHelpShort = "-h";
|
|
80
|
-
const kSchemaLong = "--schema";
|
|
81
|
-
const kSchemaDesc = "Print the full command tree as JSON.";
|
|
82
|
-
|
|
83
|
-
// ── Bash Completion ────────────────────────────────────────────────────────────
|
|
84
|
-
|
|
85
|
-
/** Emits the bash helper that classifies a `--long` or `--long=val` token for each scope. */
|
|
86
|
-
function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
|
|
87
|
-
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
88
|
-
o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
|
|
89
|
-
o += " case $sid in\n";
|
|
90
|
-
for (const [i, sc] of scopes.entries()) {
|
|
91
|
-
o += " " + i + ")\n";
|
|
92
|
-
o += " case $w in\n";
|
|
93
|
-
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
94
|
-
if (sc.path === "") {
|
|
95
|
-
o += " " + kSchemaLong + ") echo 1 ;;\n";
|
|
96
|
-
}
|
|
97
|
-
for (const op of sc.opts) {
|
|
98
|
-
const base = "--" + op.name;
|
|
99
|
-
if (op.kind === "presence") {
|
|
100
|
-
o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
|
|
101
|
-
} else {
|
|
102
|
-
o += " " + base + "=*) echo 1 ;;\n";
|
|
103
|
-
o += " " + base + ") echo 2 ;;\n";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
o += " *) echo 0 ;;\n";
|
|
107
|
-
o += " esac\n";
|
|
108
|
-
o += " ;;\n";
|
|
109
|
-
}
|
|
110
|
-
o += " *) echo 0 ;;\n";
|
|
111
|
-
o += " esac\n";
|
|
112
|
-
o += "}\n";
|
|
113
|
-
return o;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Emits the bash helper that classifies a bundled/short `-x` / `-abc` token for each scope. */
|
|
117
|
-
function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
|
|
118
|
-
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
119
|
-
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
120
|
-
o += " case $sid in\n";
|
|
121
|
-
for (const [i, sc] of scopes.entries()) {
|
|
122
|
-
o += " " + i + ")\n";
|
|
123
|
-
o += " local rest=${w#-}\n";
|
|
124
|
-
o += " local ch\n";
|
|
125
|
-
o += " local saw=0\n";
|
|
126
|
-
o += " while [[ -n $rest ]]; do\n";
|
|
127
|
-
o += " ch=${rest:0:1}\n";
|
|
128
|
-
o += " rest=${rest:1}\n";
|
|
129
|
-
o += " case $ch in\n";
|
|
130
|
-
let boolChars = "";
|
|
131
|
-
for (const op of sc.opts) {
|
|
132
|
-
if (!op.shortName) continue;
|
|
133
|
-
if (op.kind === "presence") {
|
|
134
|
-
boolChars += op.shortName + "|";
|
|
135
|
-
} else {
|
|
136
|
-
o += " " + op.shortName + ")\n";
|
|
137
|
-
o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
|
|
138
|
-
o += " echo 2; return ;;\n";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
if (boolChars.length > 0) {
|
|
142
|
-
boolChars = boolChars.slice(0, -1);
|
|
143
|
-
o += " " + boolChars + ") ;;\n";
|
|
144
|
-
}
|
|
145
|
-
o += " *) echo 0; return ;;\n";
|
|
146
|
-
o += " esac\n";
|
|
147
|
-
o += " saw=1\n";
|
|
148
|
-
o += " done\n";
|
|
149
|
-
o += " echo 1\n";
|
|
150
|
-
o += " ;;\n";
|
|
151
|
-
}
|
|
152
|
-
o += " *) echo 0 ;;\n";
|
|
153
|
-
o += " esac\n";
|
|
154
|
-
o += "}\n";
|
|
155
|
-
return o;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Emits the bash helper that maps a non-option token to the child scope id. */
|
|
159
|
-
function emitMatchChild(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
|
|
160
|
-
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
161
|
-
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
162
|
-
o += " case $sid in\n";
|
|
163
|
-
for (const [sid, sc] of scopes.entries()) {
|
|
164
|
-
if (sc.kids.length === 0) continue;
|
|
165
|
-
o += " " + sid + ")\n";
|
|
166
|
-
o += " case $w in\n";
|
|
167
|
-
for (const ch of sc.kids) {
|
|
168
|
-
const childPath = sc.path === "" ? ch.key : sc.path + "/" + ch.key;
|
|
169
|
-
const cid = pathIndex[childPath] ?? 0;
|
|
170
|
-
o += " " + ch.key + ") echo " + cid + "; return 0 ;;\n";
|
|
171
|
-
}
|
|
172
|
-
o += " esac\n";
|
|
173
|
-
o += " ;;\n";
|
|
174
|
-
}
|
|
175
|
-
o += " esac\n";
|
|
176
|
-
o += " return 1\n";
|
|
177
|
-
o += "}\n";
|
|
178
|
-
return o;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Emits bash that replays argv up to the current word to find the active completion scope. */
|
|
182
|
-
function emitSimulate(ident: string): string {
|
|
183
|
-
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
184
|
-
o += " local i=1 sid=0 w steps next\n";
|
|
185
|
-
o += " while (( i < COMP_CWORD )); do\n";
|
|
186
|
-
o += " w=\"${COMP_WORDS[i]}\"\n";
|
|
187
|
-
o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
|
|
188
|
-
o += " ((i++)); continue\n";
|
|
189
|
-
o += " fi\n";
|
|
190
|
-
o += " if [[ $w == --* ]]; then\n";
|
|
191
|
-
o += " steps=$(_${ident}_nac_consume_long \"$sid\" \"$w\" \"${COMP_WORDS[i+1]}\")\n".replace("${ident}", ident);
|
|
192
|
-
o += " case $steps in\n";
|
|
193
|
-
o += " 0) break ;;\n";
|
|
194
|
-
o += " 1) ((i++)) ;;\n";
|
|
195
|
-
o += " 2) ((i+=2)) ;;\n";
|
|
196
|
-
o += " *) break ;;\n";
|
|
197
|
-
o += " esac\n";
|
|
198
|
-
o += " continue\n";
|
|
199
|
-
o += " fi\n";
|
|
200
|
-
o += " if [[ $w == -* ]]; then\n";
|
|
201
|
-
o += " steps=$(_${ident}_nac_consume_short \"$sid\" \"$w\")\n".replace("${ident}", ident);
|
|
202
|
-
o += " case $steps in\n";
|
|
203
|
-
o += " 0) break ;;\n";
|
|
204
|
-
o += " 1) ((i++)) ;;\n";
|
|
205
|
-
o += " 2) ((i++)); break ;;\n";
|
|
206
|
-
o += " *) break ;;\n";
|
|
207
|
-
o += " esac\n";
|
|
208
|
-
o += " continue\n";
|
|
209
|
-
o += " fi\n";
|
|
210
|
-
o += " next=$(_${ident}_nac_match_child \"$sid\" \"$w\") || break\n".replace("${ident}", ident);
|
|
211
|
-
o += " sid=$next\n";
|
|
212
|
-
o += " ((i++))\n";
|
|
213
|
-
o += " done\n";
|
|
214
|
-
o += " REPLY_SID=$sid\n";
|
|
215
|
-
o += "}\n";
|
|
216
|
-
return o;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Emits bash helper to complete Enum option values when previous token is --name. */
|
|
220
|
-
function emitEnumReplyBash(ident: string, scopes: ScopeRec[]): string {
|
|
221
|
-
let o = "_${ident}_nac_enum_reply() {\n".replace("${ident}", ident);
|
|
222
|
-
o += " local sid=\"$1\" prev=\"$2\" cur=\"$3\"\n";
|
|
223
|
-
o += " case $sid in\n";
|
|
224
|
-
for (const [i, sc] of scopes.entries()) {
|
|
225
|
-
const enumOpts = sc.opts.filter((op) => op.kind === CliOptionKind.Enum && (op.choices?.length ?? 0) > 0);
|
|
226
|
-
if (enumOpts.length === 0) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
o += " " + i + ")\n";
|
|
230
|
-
o += " case $prev in\n";
|
|
231
|
-
for (const op of enumOpts) {
|
|
232
|
-
const words = (op.choices ?? []).map((c) => escShellSingleQuoted(c)).join(" ");
|
|
233
|
-
o += " --" + op.name + ") COMPREPLY=( $(compgen -W '" + words + "' -- \"$cur\") ); return 0 ;;\n";
|
|
234
|
-
}
|
|
235
|
-
o += " esac\n";
|
|
236
|
-
o += " ;;\n";
|
|
237
|
-
}
|
|
238
|
-
o += " esac\n";
|
|
239
|
-
o += " return 1\n";
|
|
240
|
-
o += "}\n";
|
|
241
|
-
return o;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** Emits the main `COMPREPLY` driver and `complete -F` registration for bash. */
|
|
245
|
-
function emitMainBodyBash(schema: CliCommand, ident: string): string {
|
|
246
|
-
const main = mainName(schema.key);
|
|
247
|
-
let o = "_${main}() {\n".replace("${main}", main);
|
|
248
|
-
o += " local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
|
|
249
|
-
o += " local prev=\"${COMP_WORDS[COMP_CWORD-1]:-}\"\n";
|
|
250
|
-
o += " _${ident}_nac_simulate\n".replace("${ident}", ident);
|
|
251
|
-
o += " local sid=$REPLY_SID\n";
|
|
252
|
-
o += " if _${ident}_nac_enum_reply \"$sid\" \"$prev\" \"$cur\"; then return; fi\n".replace("${ident}", ident);
|
|
253
|
-
o += " if [[ $cur == -* ]]; then\n";
|
|
254
|
-
o += " local oname=\"A_${ident}_${sid}_opts\"\n".replace("${ident}", ident);
|
|
255
|
-
o += " local -a optsarr\n";
|
|
256
|
-
o += " local -n optsref=\"$oname\"\n";
|
|
257
|
-
o += " COMPREPLY=( $(compgen -W \"${optsref[*]}\" -- \"$cur\") )\n";
|
|
258
|
-
o += " else\n";
|
|
259
|
-
o += " local lname=\"A_${ident}_${sid}_leaf\"\n".replace("${ident}", ident);
|
|
260
|
-
o += " local -n leafref=\"$lname\"\n";
|
|
261
|
-
o += " if [[ $leafref -eq 0 ]]; then\n";
|
|
262
|
-
o += " local cname=\"A_${ident}_${sid}_cmds\"\n".replace("${ident}", ident);
|
|
263
|
-
o += " local -a cmdsarr\n";
|
|
264
|
-
o += " local -n cmdsref=\"$cname\"\n";
|
|
265
|
-
o += " COMPREPLY=( $(compgen -W \"${cmdsref[*]}\" -- \"$cur\") )\n";
|
|
266
|
-
o += " else\n";
|
|
267
|
-
o += " local pname=\"A_${ident}_${sid}_pos\"\n".replace("${ident}", ident);
|
|
268
|
-
o += " local -n posref=\"$pname\"\n";
|
|
269
|
-
o += " if [[ $posref -eq 1 ]]; then\n";
|
|
270
|
-
o += " compopt -o filenames\n";
|
|
271
|
-
o += " fi\n";
|
|
272
|
-
o += " fi\n";
|
|
273
|
-
o += " fi\n";
|
|
274
|
-
o += "}\n\n";
|
|
275
|
-
o += "complete -F _${main} ${schema.key}\n".replace("${main}", main).replace("${schema.key}", schema.key);
|
|
276
|
-
return o;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/** Returns a self-contained bash `complete` script for the given program schema. */
|
|
280
|
-
export function completionBashScript(schema: CliCommand): string {
|
|
281
|
-
const ident = identToken(schema.key);
|
|
282
|
-
const scopes = collectScopes(schema);
|
|
283
|
-
let pathIndex: Record<string, number> = {};
|
|
284
|
-
for (const [i, s] of scopes.entries()) {
|
|
285
|
-
pathIndex[s.path] = i;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let out = "# Generated bash completion for " + schema.key + ".\n\n";
|
|
289
|
-
|
|
290
|
-
// Emit scope arrays
|
|
291
|
-
for (const [i, sc] of scopes.entries()) {
|
|
292
|
-
out += "A_" + ident + "_" + i + "_opts=()\n";
|
|
293
|
-
out += "A_" + ident + "_" + i + "_opts+=('" + kHelpLong + "' '" + kHelpShort + "')\n";
|
|
294
|
-
if (sc.path === "") {
|
|
295
|
-
out += "A_" + ident + "_" + i + "_opts+=('" + kSchemaLong + "')\n";
|
|
296
|
-
}
|
|
297
|
-
for (const o of sc.opts) {
|
|
298
|
-
out += "A_" + ident + "_" + i + "_opts+=('--" + o.name + "')\n";
|
|
299
|
-
if (o.shortName) {
|
|
300
|
-
out += "A_" + ident + "_" + i + "_opts+=('-" + o.shortName + "')\n";
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
out += "A_" + ident + "_" + i + "_leaf=" + (sc.kids.length === 0 ? "1" : "0") + "\n";
|
|
304
|
-
out += "A_" + ident + "_" + i + "_pos=" + (sc.wantsFiles ? "1" : "0") + "\n";
|
|
305
|
-
if (sc.kids.length > 0) {
|
|
306
|
-
out += "A_" + ident + "_" + i + "_cmds=(";
|
|
307
|
-
for (const ch of sc.kids) {
|
|
308
|
-
out += " '" + ch.key + "'";
|
|
309
|
-
}
|
|
310
|
-
out += ")\n";
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
out += emitConsumeLong(ident, scopes);
|
|
315
|
-
out += emitConsumeShort(ident, scopes);
|
|
316
|
-
out += emitMatchChild(ident, scopes, pathIndex);
|
|
317
|
-
out += emitSimulate(ident);
|
|
318
|
-
out += emitEnumReplyBash(ident, scopes);
|
|
319
|
-
out += emitMainBodyBash(schema, ident);
|
|
320
|
-
|
|
321
|
-
return out;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// ── Zsh Completion ─────────────────────────────────────────────────────────────
|
|
325
|
-
|
|
326
|
-
/** Emits zsh `typeset` arrays of options and subcommands for each scope. */
|
|
327
|
-
function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
|
|
328
|
-
let out = "";
|
|
329
|
-
for (const [i, sc] of scopes.entries()) {
|
|
330
|
-
out += "typeset -g A_" + ident + "_" + i + "_opts\n";
|
|
331
|
-
out += "A_" + ident + "_" + i + "_opts=(";
|
|
332
|
-
out += "'" + escShellSingleQuoted(kHelpLong) + ":" + escShellSingleQuoted("Show help for this command.") + "' '" + escShellSingleQuoted(kHelpShort) + ":" + escShellSingleQuoted("Show help for this command.") + "'";
|
|
333
|
-
if (sc.path === "") {
|
|
334
|
-
out +=
|
|
335
|
-
" '" +
|
|
336
|
-
escShellSingleQuoted(kSchemaLong) +
|
|
337
|
-
":" +
|
|
338
|
-
escShellSingleQuoted(kSchemaDesc) +
|
|
339
|
-
"'";
|
|
340
|
-
}
|
|
341
|
-
for (const o of sc.opts) {
|
|
342
|
-
out += " '" + escShellSingleQuoted("--" + o.name) + ":" + escShellSingleQuoted(o.description) + "'";
|
|
343
|
-
if (o.shortName) {
|
|
344
|
-
out += " '" + escShellSingleQuoted("-" + o.shortName) + ":" + escShellSingleQuoted(o.description) + "'";
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
out += ")\n";
|
|
348
|
-
out += "typeset -g A_" + ident + "_" + i + "_leaf=" + (sc.kids.length === 0 ? "1" : "0") + "\n";
|
|
349
|
-
out += "typeset -g A_" + ident + "_" + i + "_pos=" + (sc.wantsFiles ? "1" : "0") + "\n";
|
|
350
|
-
if (sc.kids.length > 0) {
|
|
351
|
-
out += "typeset -g A_" + ident + "_" + i + "_cmds=(";
|
|
352
|
-
for (const ch of sc.kids) {
|
|
353
|
-
out +=
|
|
354
|
-
" '" +
|
|
355
|
-
escShellSingleQuoted(ch.key) +
|
|
356
|
-
":" +
|
|
357
|
-
escShellSingleQuoted(ch.description) +
|
|
358
|
-
"'";
|
|
359
|
-
}
|
|
360
|
-
out += ")\n";
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return out;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/** Zsh: long-option classifier function source (mirrors bash consume_long). */
|
|
367
|
-
function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
|
|
368
|
-
let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
|
|
369
|
-
o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
|
|
370
|
-
o += " case $sid in\n";
|
|
371
|
-
for (const [i, sc] of scopes.entries()) {
|
|
372
|
-
o += " " + i + ")\n";
|
|
373
|
-
o += " case $w in\n";
|
|
374
|
-
o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
|
|
375
|
-
if (sc.path === "") {
|
|
376
|
-
o += " " + kSchemaLong + ") echo 1 ;;\n";
|
|
377
|
-
}
|
|
378
|
-
for (const op of sc.opts) {
|
|
379
|
-
const base = "--" + op.name;
|
|
380
|
-
if (op.kind === "presence") {
|
|
381
|
-
o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
|
|
382
|
-
} else {
|
|
383
|
-
o += " " + base + "=*) echo 1 ;;\n";
|
|
384
|
-
o += " " + base + ") echo 2 ;;\n";
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
o += " *) echo 0 ;;\n";
|
|
388
|
-
o += " esac\n";
|
|
389
|
-
o += " ;;\n";
|
|
390
|
-
}
|
|
391
|
-
o += " *) echo 0 ;;\n";
|
|
392
|
-
o += " esac\n";
|
|
393
|
-
o += "}\n";
|
|
394
|
-
return o;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/** Zsh: short-option classifier function source. */
|
|
398
|
-
function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
|
|
399
|
-
let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
|
|
400
|
-
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
401
|
-
o += " case $sid in\n";
|
|
402
|
-
for (const [i, sc] of scopes.entries()) {
|
|
403
|
-
o += " " + i + ")\n";
|
|
404
|
-
o += " local rest=${w#-}\n";
|
|
405
|
-
o += " local ch\n";
|
|
406
|
-
o += " local saw=0\n";
|
|
407
|
-
o += " while [[ -n $rest ]]; do\n";
|
|
408
|
-
o += " ch=${rest[1,1]}\n";
|
|
409
|
-
o += " rest=${rest[2,-1]}\n";
|
|
410
|
-
o += " case $ch in\n";
|
|
411
|
-
let boolChars = "";
|
|
412
|
-
for (const op of sc.opts) {
|
|
413
|
-
if (!op.shortName) continue;
|
|
414
|
-
if (op.kind === "presence") {
|
|
415
|
-
boolChars += op.shortName + "|";
|
|
416
|
-
} else {
|
|
417
|
-
o += " " + op.shortName + ")\n";
|
|
418
|
-
o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
|
|
419
|
-
o += " echo 2; return ;;\n";
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
if (boolChars.length > 0) {
|
|
423
|
-
boolChars = boolChars.slice(0, -1);
|
|
424
|
-
o += " " + boolChars + ") ;;\n";
|
|
425
|
-
}
|
|
426
|
-
o += " *) echo 0; return ;;\n";
|
|
427
|
-
o += " esac\n";
|
|
428
|
-
o += " saw=1\n";
|
|
429
|
-
o += " done\n";
|
|
430
|
-
o += " echo 1\n";
|
|
431
|
-
o += " ;;\n";
|
|
432
|
-
}
|
|
433
|
-
o += " *) echo 0 ;;\n";
|
|
434
|
-
o += " esac\n";
|
|
435
|
-
o += "}\n";
|
|
436
|
-
return o;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/** Zsh: subcommand name → scope id matching helper. */
|
|
440
|
-
function emitMatchChildZsh(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
|
|
441
|
-
let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
|
|
442
|
-
o += " local sid=\"$1\" w=\"$2\"\n";
|
|
443
|
-
o += " case $sid in\n";
|
|
444
|
-
for (const [sid, sc] of scopes.entries()) {
|
|
445
|
-
if (sc.kids.length === 0) continue;
|
|
446
|
-
o += " " + sid + ")\n";
|
|
447
|
-
o += " case $w in\n";
|
|
448
|
-
for (const ch of sc.kids) {
|
|
449
|
-
const childPath = sc.path === "" ? ch.key : sc.path + "/" + ch.key;
|
|
450
|
-
const cid = pathIndex[childPath] ?? 0;
|
|
451
|
-
o += " " + ch.key + ") echo " + cid + "; return 0 ;;\n";
|
|
452
|
-
}
|
|
453
|
-
o += " esac\n";
|
|
454
|
-
o += " ;;\n";
|
|
455
|
-
}
|
|
456
|
-
o += " esac\n";
|
|
457
|
-
o += " return 1\n";
|
|
458
|
-
o += "}\n";
|
|
459
|
-
return o;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/** Zsh: simulates word traversal to the current completion context (sets `REPLY_SID`). */
|
|
463
|
-
function emitSimulateZsh(ident: string): string {
|
|
464
|
-
let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
|
|
465
|
-
o += " local i=2 sid=0 w steps next\n";
|
|
466
|
-
o += " while (( i < CURRENT )); do\n";
|
|
467
|
-
o += " w=$words[i]\n";
|
|
468
|
-
o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
|
|
469
|
-
o += " ((i++)); continue\n";
|
|
470
|
-
o += " fi\n";
|
|
471
|
-
o += " if [[ $w == --* ]]; then\n";
|
|
472
|
-
o += " steps=$(_${ident}_nac_consume_long \"$sid\" \"$w\" \"${words[i+1]}\")\n".replace("${ident}", ident);
|
|
473
|
-
o += " case $steps in\n";
|
|
474
|
-
o += " 0) break ;;\n";
|
|
475
|
-
o += " 1) ((i++)) ;;\n";
|
|
476
|
-
o += " 2) ((i+=2)) ;;\n";
|
|
477
|
-
o += " *) break ;;\n";
|
|
478
|
-
o += " esac\n";
|
|
479
|
-
o += " continue\n";
|
|
480
|
-
o += " fi\n";
|
|
481
|
-
o += " if [[ $w == -* ]]; then\n";
|
|
482
|
-
o += " steps=$(_${ident}_nac_consume_short \"$sid\" \"$w\")\n".replace("${ident}", ident);
|
|
483
|
-
o += " case $steps in\n";
|
|
484
|
-
o += " 0) break ;;\n";
|
|
485
|
-
o += " 1) ((i++)) ;;\n";
|
|
486
|
-
o += " 2) ((i++)); break ;;\n";
|
|
487
|
-
o += " *) break ;;\n";
|
|
488
|
-
o += " esac\n";
|
|
489
|
-
o += " continue\n";
|
|
490
|
-
o += " fi\n";
|
|
491
|
-
o += " next=$(_${ident}_nac_match_child \"$sid\" \"$w\") || break\n".replace("${ident}", ident);
|
|
492
|
-
o += " sid=$next\n";
|
|
493
|
-
o += " ((i++))\n";
|
|
494
|
-
o += " done\n";
|
|
495
|
-
o += " REPLY_SID=$sid\n";
|
|
496
|
-
o += "}\n";
|
|
497
|
-
return o;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** Emits zsh helper to complete Enum option values when previous token is --name. */
|
|
501
|
-
function emitEnumReplyZsh(ident: string, scopes: ScopeRec[]): string {
|
|
502
|
-
let o = "_${ident}_nac_enum_reply() {\n".replace("${ident}", ident);
|
|
503
|
-
o += " local sid=$1 prev=$2\n";
|
|
504
|
-
o += " case $sid in\n";
|
|
505
|
-
for (const [i, sc] of scopes.entries()) {
|
|
506
|
-
const enumOpts = sc.opts.filter((op) => op.kind === CliOptionKind.Enum && (op.choices?.length ?? 0) > 0);
|
|
507
|
-
if (enumOpts.length === 0) {
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
o += " " + i + ")\n";
|
|
511
|
-
o += " case $prev in\n";
|
|
512
|
-
for (const op of enumOpts) {
|
|
513
|
-
const vals = (op.choices ?? []).map((c) => escShellSingleQuoted(c)).join(" ");
|
|
514
|
-
o += " --" + op.name + ") _values " + vals + "; return 0 ;;\n";
|
|
515
|
-
}
|
|
516
|
-
o += " esac\n";
|
|
517
|
-
o += " ;;\n";
|
|
518
|
-
}
|
|
519
|
-
o += " esac\n";
|
|
520
|
-
o += " return 1\n";
|
|
521
|
-
o += "}\n";
|
|
522
|
-
return o;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/** Zsh: `_main` completer and `compdef` registration. */
|
|
526
|
-
function emitMainBodyZsh(schema: CliCommand, ident: string): string {
|
|
527
|
-
const main = mainName(schema.key);
|
|
528
|
-
let o = "_${main}() {\n".replace("${main}", main);
|
|
529
|
-
o += " local curcontext=\"$curcontext\" ret=1\n";
|
|
530
|
-
o += " _${ident}_nac_simulate\n".replace("${ident}", ident);
|
|
531
|
-
o += " local sid=$REPLY_SID\n";
|
|
532
|
-
o += " if _${ident}_nac_enum_reply \"$sid\" \"$words[CURRENT-1]\"; then return 0; fi\n".replace("${ident}", ident);
|
|
533
|
-
o += " if [[ $PREFIX == -* ]]; then\n";
|
|
534
|
-
o += " local -a optsarr\n";
|
|
535
|
-
o += " local oname=\"A_${ident}_${sid}_opts\"\n".replace("${ident}", ident);
|
|
536
|
-
o += " optsarr=(${(@P)oname})\n";
|
|
537
|
-
o += " _describe -t options 'option' optsarr && ret=0\n";
|
|
538
|
-
o += " else\n";
|
|
539
|
-
o += " local lname=\"A_${ident}_${sid}_leaf\"\n".replace("${ident}", ident);
|
|
540
|
-
o += " if [[ ${(P)lname} -eq 0 ]]; then\n";
|
|
541
|
-
o += " local -a cmdsarr\n";
|
|
542
|
-
o += " local cname=\"A_${ident}_${sid}_cmds\"\n".replace("${ident}", ident);
|
|
543
|
-
o += " cmdsarr=(${(@P)cname})\n";
|
|
544
|
-
o += " _describe -t commands 'command' cmdsarr && ret=0\n";
|
|
545
|
-
o += " else\n";
|
|
546
|
-
o += " local pname=\"A_${ident}_${sid}_pos\"\n".replace("${ident}", ident);
|
|
547
|
-
o += " if [[ ${(P)pname} -eq 1 ]]; then\n";
|
|
548
|
-
o += " _files && ret=0\n";
|
|
549
|
-
o += " fi\n";
|
|
550
|
-
o += " fi\n";
|
|
551
|
-
o += " fi\n";
|
|
552
|
-
o += " return ret\n";
|
|
553
|
-
o += "}\n\n";
|
|
554
|
-
o += "compdef _${main} ${schema.key}\n".replace("${main}", main).replace("${schema.key}", schema.key);
|
|
555
|
-
return o;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/** Returns a self-contained zsh completion script for the given program schema. */
|
|
559
|
-
export function completionZshScript(schema: CliCommand): string {
|
|
560
|
-
const ident = identToken(schema.key);
|
|
561
|
-
const scopes = collectScopes(schema);
|
|
562
|
-
let pathIndex: Record<string, number> = {};
|
|
563
|
-
for (const [i, s] of scopes.entries()) {
|
|
564
|
-
pathIndex[s.path] = i;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
let out = "#compdef " + schema.key + "\n\n";
|
|
568
|
-
out += emitScopeArraysZsh(ident, scopes);
|
|
569
|
-
out += emitConsumeLongZsh(ident, scopes);
|
|
570
|
-
out += emitConsumeShortZsh(ident, scopes);
|
|
571
|
-
out += emitMatchChildZsh(ident, scopes, pathIndex);
|
|
572
|
-
out += emitSimulateZsh(ident);
|
|
573
|
-
out += emitEnumReplyZsh(ident, scopes);
|
|
574
|
-
out += emitMainBodyZsh(schema, ident);
|
|
575
|
-
return out;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Returns a schema suitable for help display, including reserved built-in subtrees.
|
|
580
|
-
* Routing roots get `completion` merged; leaf roots are wrapped as a tiny router.
|
|
581
|
-
*/
|
|
582
|
-
export function cliPresentationRoot(root: CliCommand): CliCommand {
|
|
583
|
-
if ((root.commands ?? []).some((c) => c.key === "completion")) {
|
|
584
|
-
return root;
|
|
585
|
-
}
|
|
586
|
-
if ("handler" in root && root.handler) {
|
|
587
|
-
return {
|
|
588
|
-
key: root.key,
|
|
589
|
-
description: root.description,
|
|
590
|
-
options: root.options,
|
|
591
|
-
commands: presentationBuiltins(root),
|
|
592
|
-
} as CliCommand;
|
|
593
|
-
}
|
|
594
|
-
return {
|
|
595
|
-
...root,
|
|
596
|
-
commands: [...(root.commands ?? []), ...presentationBuiltins(root)],
|
|
597
|
-
} as CliCommand;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/** Built-in commands shown in help and merged for routing CLIs. */
|
|
601
|
-
function presentationBuiltins(root: CliCommand): CliCommand[] {
|
|
602
|
-
const cmds: CliCommand[] = [cliBuiltinCompletionGroup(root.key)];
|
|
603
|
-
if (root.mcpServer !== undefined) {
|
|
604
|
-
cmds.push(cliBuiltinMcpCommand());
|
|
605
|
-
}
|
|
606
|
-
return cmds;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/** Builds the static `mcp` leaf command (merged when `root.mcpServer` is set). */
|
|
610
|
-
export function cliBuiltinMcpCommand(): CliCommand {
|
|
611
|
-
return {
|
|
612
|
-
key: "mcp",
|
|
613
|
-
description: "Run as an MCP server over stdio (for AI agents).",
|
|
614
|
-
handler: () => {},
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/**
|
|
619
|
-
* Builds the static `completion` / `bash` / `zsh` command subtree (merged into the program root at runtime).
|
|
620
|
-
*/
|
|
621
|
-
export function cliBuiltinCompletionGroup(appName: string): CliCommand {
|
|
622
|
-
return {
|
|
623
|
-
key: "completion",
|
|
624
|
-
description: "Generate the autocompletion script for shells.",
|
|
625
|
-
commands: [
|
|
626
|
-
{
|
|
627
|
-
key: "bash",
|
|
628
|
-
description: "Print a bash tab-completion script.",
|
|
629
|
-
notes:
|
|
630
|
-
"Output is the whole script.\n" +
|
|
631
|
-
"Pipe it to a file, or feed it straight into your shell.\n\n" +
|
|
632
|
-
"To keep it across restarts, save it and source that file from ~/.bashrc.\n\n" +
|
|
633
|
-
"For example:\n\n" +
|
|
634
|
-
`echo 'eval \"$(${appName} completion bash)\"' >> ~/.bashrc\n` +
|
|
635
|
-
`\nor\n` +
|
|
636
|
-
` ${appName} completion bash > ~/.bash_completion.d/${appName}\n` +
|
|
637
|
-
` echo 'source ~/.bash_completion.d/${appName}' >> ~/.bashrc\n\n` +
|
|
638
|
-
"To try it only in this session (nothing written to disk):\n" +
|
|
639
|
-
` source <(${appName} completion bash)`,
|
|
640
|
-
handler: () => {},
|
|
641
|
-
},
|
|
642
|
-
{
|
|
643
|
-
key: "zsh",
|
|
644
|
-
description: "Print a zsh tab-completion script.",
|
|
645
|
-
notes:
|
|
646
|
-
"Output is the whole script.\n\n" +
|
|
647
|
-
`fpath setup: ${appName} completion zsh > ~/.zsh/completions/_${appName}\n\n` +
|
|
648
|
-
`source setup: echo 'eval \"$(${appName} completion zsh)\"' >> ~/.zshrc\n\n` +
|
|
649
|
-
"To try it only in this session (nothing written to disk):\n" +
|
|
650
|
-
` eval \"$(${appName} completion zsh)\"`,
|
|
651
|
-
handler: () => {},
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
};
|
|
655
|
-
}
|
|
5
|
+
export {
|
|
6
|
+
completionBashScript,
|
|
7
|
+
completionZshScript,
|
|
8
|
+
completionFishScript,
|
|
9
|
+
cliPresentationRoot,
|
|
10
|
+
cliBuiltinCompletionGroup,
|
|
11
|
+
cliBuiltinInstallCommand,
|
|
12
|
+
cliBuiltinMcpCommand,
|
|
13
|
+
} from "./builtins/index.ts";
|