argsbarg 1.4.3 → 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.
Files changed (44) hide show
  1. package/.private/scratch.md +1 -1
  2. package/CHANGELOG.md +16 -1
  3. package/README.md +17 -13
  4. package/docs/ai-skills.md +24 -52
  5. package/docs/install.md +84 -0
  6. package/docs/mcp.md +5 -5
  7. package/index.d.ts +9 -18
  8. package/package.json +1 -1
  9. package/src/builtins/builtins.test.ts +101 -0
  10. package/src/builtins/completion-bash.ts +240 -0
  11. package/src/builtins/completion-fish.ts +73 -0
  12. package/src/builtins/completion-group.ts +50 -0
  13. package/src/builtins/completion-zsh.ts +244 -0
  14. package/src/builtins/dispatch.ts +123 -0
  15. package/src/builtins/export.ts +46 -0
  16. package/src/builtins/index.ts +10 -0
  17. package/src/builtins/install.ts +99 -0
  18. package/src/builtins/mcp.ts +13 -0
  19. package/src/builtins/presentation.ts +39 -0
  20. package/src/builtins/scopes.ts +45 -0
  21. package/src/builtins/shell-helpers.ts +24 -0
  22. package/src/completion.ts +10 -693
  23. package/src/index.test.ts +44 -55
  24. package/src/index.ts +1 -0
  25. package/src/install/binary.ts +82 -0
  26. package/src/install/compiled.ts +15 -0
  27. package/src/install/completions.ts +52 -0
  28. package/src/install/detect-installed.ts +67 -0
  29. package/src/install/index.ts +196 -0
  30. package/src/install/install.test.ts +124 -0
  31. package/src/install/mcp-config.ts +70 -0
  32. package/src/install/paths.ts +69 -0
  33. package/src/install/plan.ts +183 -0
  34. package/src/install/shell.ts +56 -0
  35. package/src/install/status.ts +63 -0
  36. package/src/install/uninstall.ts +111 -0
  37. package/src/mcp/tools.ts +1 -1
  38. package/src/runtime.ts +21 -83
  39. package/src/schema.ts +7 -49
  40. package/src/skill/generate.ts +4 -4
  41. package/src/skill/install.ts +20 -18
  42. package/src/types.ts +9 -9
  43. package/src/validate.ts +10 -22
  44. package/src/ai.ts +0 -7
@@ -0,0 +1,240 @@
1
+ import { CliCommand, CliOptionKind } from "../types.ts";
2
+ import { collectScopes, type ScopeRec } from "./scopes.ts";
3
+ import {
4
+ escShellSingleQuoted,
5
+ identToken,
6
+ kHelpLong,
7
+ kHelpShort,
8
+ kSchemaLong,
9
+ mainName,
10
+ } from "./shell-helpers.ts";
11
+
12
+ function emitConsumeLong(ident: string, scopes: ScopeRec[]): string {
13
+ let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
14
+ o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
15
+ o += " case $sid in\n";
16
+ for (const [i, sc] of scopes.entries()) {
17
+ o += " " + i + ")\n";
18
+ o += " case $w in\n";
19
+ o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
20
+ if (sc.path === "") {
21
+ o += " " + kSchemaLong + ") echo 1 ;;\n";
22
+ }
23
+ for (const op of sc.opts) {
24
+ const base = "--" + op.name;
25
+ if (op.kind === "presence") {
26
+ o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
27
+ } else {
28
+ o += " " + base + "=*) echo 1 ;;\n";
29
+ o += " " + base + ") echo 2 ;;\n";
30
+ }
31
+ }
32
+ o += " *) echo 0 ;;\n";
33
+ o += " esac\n";
34
+ o += " ;;\n";
35
+ }
36
+ o += " *) echo 0 ;;\n";
37
+ o += " esac\n";
38
+ o += "}\n";
39
+ return o;
40
+ }
41
+
42
+ function emitConsumeShort(ident: string, scopes: ScopeRec[]): string {
43
+ let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
44
+ o += " local sid=\"$1\" w=\"$2\"\n";
45
+ o += " case $sid in\n";
46
+ for (const [i, sc] of scopes.entries()) {
47
+ o += " " + i + ")\n";
48
+ o += " local rest=${w#-}\n";
49
+ o += " local ch\n";
50
+ o += " local saw=0\n";
51
+ o += " while [[ -n $rest ]]; do\n";
52
+ o += " ch=${rest:0:1}\n";
53
+ o += " rest=${rest:1}\n";
54
+ o += " case $ch in\n";
55
+ let boolChars = "";
56
+ for (const op of sc.opts) {
57
+ if (!op.shortName) continue;
58
+ if (op.kind === "presence") {
59
+ boolChars += op.shortName + "|";
60
+ } else {
61
+ o += " " + op.shortName + ")\n";
62
+ o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
63
+ o += " echo 2; return ;;\n";
64
+ }
65
+ }
66
+ if (boolChars.length > 0) {
67
+ boolChars = boolChars.slice(0, -1);
68
+ o += " " + boolChars + ") ;;\n";
69
+ }
70
+ o += " *) echo 0; return ;;\n";
71
+ o += " esac\n";
72
+ o += " saw=1\n";
73
+ o += " done\n";
74
+ o += " echo 1\n";
75
+ o += " ;;\n";
76
+ }
77
+ o += " *) echo 0 ;;\n";
78
+ o += " esac\n";
79
+ o += "}\n";
80
+ return o;
81
+ }
82
+
83
+ function emitMatchChild(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
84
+ let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
85
+ o += " local sid=\"$1\" w=\"$2\"\n";
86
+ o += " case $sid in\n";
87
+ for (const [sid, sc] of scopes.entries()) {
88
+ if (sc.kids.length === 0) continue;
89
+ o += " " + sid + ")\n";
90
+ o += " case $w in\n";
91
+ for (const ch of sc.kids) {
92
+ const childPath = sc.path === "" ? ch.key : sc.path + "/" + ch.key;
93
+ const cid = pathIndex[childPath] ?? 0;
94
+ o += " " + ch.key + ") echo " + cid + "; return 0 ;;\n";
95
+ }
96
+ o += " esac\n";
97
+ o += " ;;\n";
98
+ }
99
+ o += " esac\n";
100
+ o += " return 1\n";
101
+ o += "}\n";
102
+ return o;
103
+ }
104
+
105
+ function emitSimulate(ident: string): string {
106
+ let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
107
+ o += " local i=1 sid=0 w steps next\n";
108
+ o += " while (( i < COMP_CWORD )); do\n";
109
+ o += " w=\"${COMP_WORDS[i]}\"\n";
110
+ o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
111
+ o += " ((i++)); continue\n";
112
+ o += " fi\n";
113
+ o += " if [[ $w == --* ]]; then\n";
114
+ o += " steps=$(_${ident}_nac_consume_long \"$sid\" \"$w\" \"${COMP_WORDS[i+1]}\")\n".replace("${ident}", ident);
115
+ o += " case $steps in\n";
116
+ o += " 0) break ;;\n";
117
+ o += " 1) ((i++)) ;;\n";
118
+ o += " 2) ((i+=2)) ;;\n";
119
+ o += " *) break ;;\n";
120
+ o += " esac\n";
121
+ o += " continue\n";
122
+ o += " fi\n";
123
+ o += " if [[ $w == -* ]]; then\n";
124
+ o += " steps=$(_${ident}_nac_consume_short \"$sid\" \"$w\")\n".replace("${ident}", ident);
125
+ o += " case $steps in\n";
126
+ o += " 0) break ;;\n";
127
+ o += " 1) ((i++)) ;;\n";
128
+ o += " 2) ((i++)); break ;;\n";
129
+ o += " *) break ;;\n";
130
+ o += " esac\n";
131
+ o += " continue\n";
132
+ o += " fi\n";
133
+ o += " next=$(_${ident}_nac_match_child \"$sid\" \"$w\") || break\n".replace("${ident}", ident);
134
+ o += " sid=$next\n";
135
+ o += " ((i++))\n";
136
+ o += " done\n";
137
+ o += " REPLY_SID=$sid\n";
138
+ o += "}\n";
139
+ return o;
140
+ }
141
+
142
+ function emitEnumReplyBash(ident: string, scopes: ScopeRec[]): string {
143
+ let o = "_${ident}_nac_enum_reply() {\n".replace("${ident}", ident);
144
+ o += " local sid=\"$1\" prev=\"$2\" cur=\"$3\"\n";
145
+ o += " case $sid in\n";
146
+ for (const [i, sc] of scopes.entries()) {
147
+ const enumOpts = sc.opts.filter((op) => op.kind === CliOptionKind.Enum && (op.choices?.length ?? 0) > 0);
148
+ if (enumOpts.length === 0) continue;
149
+ o += " " + i + ")\n";
150
+ o += " case $prev in\n";
151
+ for (const op of enumOpts) {
152
+ const words = (op.choices ?? []).map((c) => escShellSingleQuoted(c)).join(" ");
153
+ o += " --" + op.name + ") COMPREPLY=( $(compgen -W '" + words + "' -- \"$cur\") ); return 0 ;;\n";
154
+ }
155
+ o += " esac\n";
156
+ o += " ;;\n";
157
+ }
158
+ o += " esac\n";
159
+ o += " return 1\n";
160
+ o += "}\n";
161
+ return o;
162
+ }
163
+
164
+ function emitMainBodyBash(schema: CliCommand, ident: string): string {
165
+ const main = mainName(schema.key);
166
+ let o = "_${main}() {\n".replace("${main}", main);
167
+ o += " local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
168
+ o += " local prev=\"${COMP_WORDS[COMP_CWORD-1]:-}\"\n";
169
+ o += " _${ident}_nac_simulate\n".replace("${ident}", ident);
170
+ o += " local sid=$REPLY_SID\n";
171
+ o += " if _${ident}_nac_enum_reply \"$sid\" \"$prev\" \"$cur\"; then return; fi\n".replace("${ident}", ident);
172
+ o += " if [[ $cur == -* ]]; then\n";
173
+ o += " local oname=\"A_${ident}_${sid}_opts\"\n".replace("${ident}", ident);
174
+ o += " local -a optsarr\n";
175
+ o += " local -n optsref=\"$oname\"\n";
176
+ o += " COMPREPLY=( $(compgen -W \"${optsref[*]}\" -- \"$cur\") )\n";
177
+ o += " else\n";
178
+ o += " local lname=\"A_${ident}_${sid}_leaf\"\n".replace("${ident}", ident);
179
+ o += " local -n leafref=\"$lname\"\n";
180
+ o += " if [[ $leafref -eq 0 ]]; then\n";
181
+ o += " local cname=\"A_${ident}_${sid}_cmds\"\n".replace("${ident}", ident);
182
+ o += " local -a cmdsarr\n";
183
+ o += " local -n cmdsref=\"$cname\"\n";
184
+ o += " COMPREPLY=( $(compgen -W \"${cmdsref[*]}\" -- \"$cur\") )\n";
185
+ o += " else\n";
186
+ o += " local pname=\"A_${ident}_${sid}_pos\"\n".replace("${ident}", ident);
187
+ o += " local -n posref=\"$pname\"\n";
188
+ o += " if [[ $posref -eq 1 ]]; then\n";
189
+ o += " compopt -o filenames\n";
190
+ o += " fi\n";
191
+ o += " fi\n";
192
+ o += " fi\n";
193
+ o += "}\n\n";
194
+ o += "complete -F _${main} ${schema.key}\n".replace("${main}", main).replace("${schema.key}", schema.key);
195
+ return o;
196
+ }
197
+
198
+ /** Returns a self-contained bash `complete` script for the given program schema. */
199
+ export function completionBashScript(schema: CliCommand): string {
200
+ const ident = identToken(schema.key);
201
+ const scopes = collectScopes(schema);
202
+ const pathIndex: Record<string, number> = {};
203
+ for (const [i, s] of scopes.entries()) {
204
+ pathIndex[s.path] = i;
205
+ }
206
+
207
+ let out = "# Generated bash completion for " + schema.key + ".\n\n";
208
+
209
+ for (const [i, sc] of scopes.entries()) {
210
+ out += "A_" + ident + "_" + i + "_opts=()\n";
211
+ out += "A_" + ident + "_" + i + "_opts+=('" + kHelpLong + "' '" + kHelpShort + "')\n";
212
+ if (sc.path === "") {
213
+ out += "A_" + ident + "_" + i + "_opts+=('" + kSchemaLong + "')\n";
214
+ }
215
+ for (const o of sc.opts) {
216
+ out += "A_" + ident + "_" + i + "_opts+=('--" + o.name + "')\n";
217
+ if (o.shortName) {
218
+ out += "A_" + ident + "_" + i + "_opts+=('-" + o.shortName + "')\n";
219
+ }
220
+ }
221
+ out += "A_" + ident + "_" + i + "_leaf=" + (sc.kids.length === 0 ? "1" : "0") + "\n";
222
+ out += "A_" + ident + "_" + i + "_pos=" + (sc.wantsFiles ? "1" : "0") + "\n";
223
+ if (sc.kids.length > 0) {
224
+ out += "A_" + ident + "_" + i + "_cmds=(";
225
+ for (const ch of sc.kids) {
226
+ out += " '" + ch.key + "'";
227
+ }
228
+ out += ")\n";
229
+ }
230
+ }
231
+
232
+ out += emitConsumeLong(ident, scopes);
233
+ out += emitConsumeShort(ident, scopes);
234
+ out += emitMatchChild(ident, scopes, pathIndex);
235
+ out += emitSimulate(ident);
236
+ out += emitEnumReplyBash(ident, scopes);
237
+ out += emitMainBodyBash(schema, ident);
238
+
239
+ return out;
240
+ }
@@ -0,0 +1,73 @@
1
+ import { CliCommand, CliOptionKind } from "../types.ts";
2
+ import { collectScopes } from "./scopes.ts";
3
+ import {
4
+ escFishSingleQuoted,
5
+ identToken,
6
+ kHelpLong,
7
+ kHelpShort,
8
+ kSchemaDesc,
9
+ kSchemaLong,
10
+ } from "./shell-helpers.ts";
11
+
12
+ function scopeCondition(ident: string, scopeIndex: number, path: string): string {
13
+ const fn = `__${ident}_scope_${scopeIndex}`;
14
+ let body = `function ${fn}\n`;
15
+ body += ` set -l tokens (commandline -opc)\n`;
16
+ if (path === "") {
17
+ body += ` test (count $tokens) -eq 0\n`;
18
+ } else {
19
+ const parts = path.split("/");
20
+ body += ` test (count $tokens) -eq ${parts.length}\n`;
21
+ for (let i = 0; i < parts.length; i++) {
22
+ body += ` and test $tokens[${i + 1}] = ${parts[i]}\n`;
23
+ }
24
+ }
25
+ body += `end\n\n`;
26
+ return body;
27
+ }
28
+
29
+ /** Returns a self-contained fish completion script for the given program schema. */
30
+ export function completionFishScript(schema: CliCommand): string {
31
+ const ident = identToken(schema.key);
32
+ const app = schema.key;
33
+ const scopes = collectScopes(schema);
34
+
35
+ let out = "# Fish completion for " + app + "\n\n";
36
+
37
+ for (const [i, sc] of scopes.entries()) {
38
+ out += scopeCondition(ident, i, sc.path);
39
+ const cond = `__${ident}_scope_${i}`;
40
+
41
+ for (const ch of sc.kids) {
42
+ out += `complete -c ${app} -n '${cond}' -a '${escFishSingleQuoted(ch.key)}' -d '${escFishSingleQuoted(ch.description)}'\n`;
43
+ }
44
+
45
+ out += `complete -c ${app} -n '${cond}' -s h -l help -d '${escFishSingleQuoted("Show help for this command.")}'\n`;
46
+ if (sc.path === "") {
47
+ out += `complete -c ${app} -n '${cond}' -l schema -d '${escFishSingleQuoted(kSchemaDesc)}'\n`;
48
+ }
49
+
50
+ for (const op of sc.opts) {
51
+ if (op.kind === CliOptionKind.Presence) {
52
+ const shortPart = op.shortName ? `-s ${op.shortName} ` : "";
53
+ out += `complete -c ${app} -n '${cond}' ${shortPart}-l ${op.name} -d '${escFishSingleQuoted(op.description)}'\n`;
54
+ } else if (op.kind === CliOptionKind.Enum && (op.choices?.length ?? 0) > 0) {
55
+ const shortPart = op.shortName ? `-s ${op.shortName} ` : "";
56
+ out += `complete -c ${app} -n '${cond}' ${shortPart}-l ${op.name} -d '${escFishSingleQuoted(op.description)}'\n`;
57
+ const enumCond = `${cond}; and __fish_seen_argument -l ${op.name}`;
58
+ for (const choice of op.choices ?? []) {
59
+ out += `complete -c ${app} -n '${enumCond}' -a '${escFishSingleQuoted(choice)}'\n`;
60
+ }
61
+ } else {
62
+ const shortPart = op.shortName ? `-s ${op.shortName} ` : "";
63
+ out += `complete -c ${app} -n '${cond}' ${shortPart}-l ${op.name} -d '${escFishSingleQuoted(op.description)}' -r\n`;
64
+ }
65
+ }
66
+
67
+ if (sc.wantsFiles && sc.kids.length === 0) {
68
+ out += `complete -c ${app} -n '${cond}' -F\n`;
69
+ }
70
+ }
71
+
72
+ return out;
73
+ }
@@ -0,0 +1,50 @@
1
+ import { CliCommand } from "../types.ts";
2
+
3
+ /**
4
+ * Builds the static `completion` / `bash` / `zsh` / `fish` command subtree (merged into the program root at runtime).
5
+ */
6
+ export function cliBuiltinCompletionGroup(appName: string): CliCommand {
7
+ return {
8
+ key: "completion",
9
+ description: "Generate the autocompletion script for shells.",
10
+ commands: [
11
+ {
12
+ key: "bash",
13
+ description: "Print a bash tab-completion script.",
14
+ notes:
15
+ "Output is the whole script.\n" +
16
+ "Pipe it to a file, or feed it straight into your shell.\n\n" +
17
+ "To keep it across restarts, save it and source that file from ~/.bashrc.\n\n" +
18
+ "For example:\n\n" +
19
+ `echo 'eval \"$(${appName} completion bash)\"' >> ~/.bashrc\n` +
20
+ `\nor\n` +
21
+ ` ${appName} completion bash > ~/.bash_completion.d/${appName}\n` +
22
+ ` echo 'source ~/.bash_completion.d/${appName}' >> ~/.bashrc\n\n` +
23
+ "To try it only in this session (nothing written to disk):\n" +
24
+ ` source <(${appName} completion bash)`,
25
+ handler: () => {},
26
+ },
27
+ {
28
+ key: "zsh",
29
+ description: "Print a zsh tab-completion script.",
30
+ notes:
31
+ "Output is the whole script.\n\n" +
32
+ `fpath setup: ${appName} completion zsh > ~/.zsh/completions/_${appName}\n\n` +
33
+ `source setup: echo 'eval \"$(${appName} completion zsh)\"' >> ~/.zshrc\n\n` +
34
+ "To try it only in this session (nothing written to disk):\n" +
35
+ ` eval \"$(${appName} completion zsh)\"`,
36
+ handler: () => {},
37
+ },
38
+ {
39
+ key: "fish",
40
+ description: "Print a fish tab-completion script.",
41
+ notes:
42
+ "Output is the whole script.\n\n" +
43
+ "Install:\n" +
44
+ ` ${appName} completion fish > ~/.config/fish/completions/${appName}.fish\n\n` +
45
+ "Fish loads completions from that directory automatically.",
46
+ handler: () => {},
47
+ },
48
+ ],
49
+ };
50
+ }
@@ -0,0 +1,244 @@
1
+ import { CliCommand, CliOptionKind } from "../types.ts";
2
+ import { collectScopes, type ScopeRec } from "./scopes.ts";
3
+ import {
4
+ escShellSingleQuoted,
5
+ identToken,
6
+ kHelpLong,
7
+ kHelpShort,
8
+ kSchemaDesc,
9
+ kSchemaLong,
10
+ mainName,
11
+ } from "./shell-helpers.ts";
12
+
13
+ function emitScopeArraysZsh(ident: string, scopes: ScopeRec[]): string {
14
+ let out = "";
15
+ for (const [i, sc] of scopes.entries()) {
16
+ out += "typeset -g A_" + ident + "_" + i + "_opts\n";
17
+ out += "A_" + ident + "_" + i + "_opts=(";
18
+ out += "'" + escShellSingleQuoted(kHelpLong) + ":" + escShellSingleQuoted("Show help for this command.") + "' '" + escShellSingleQuoted(kHelpShort) + ":" + escShellSingleQuoted("Show help for this command.") + "'";
19
+ if (sc.path === "") {
20
+ out += " '" + escShellSingleQuoted(kSchemaLong) + ":" + escShellSingleQuoted(kSchemaDesc) + "'";
21
+ }
22
+ for (const o of sc.opts) {
23
+ out += " '" + escShellSingleQuoted("--" + o.name) + ":" + escShellSingleQuoted(o.description) + "'";
24
+ if (o.shortName) {
25
+ out += " '" + escShellSingleQuoted("-" + o.shortName) + ":" + escShellSingleQuoted(o.description) + "'";
26
+ }
27
+ }
28
+ out += ")\n";
29
+ out += "typeset -g A_" + ident + "_" + i + "_leaf=" + (sc.kids.length === 0 ? "1" : "0") + "\n";
30
+ out += "typeset -g A_" + ident + "_" + i + "_pos=" + (sc.wantsFiles ? "1" : "0") + "\n";
31
+ if (sc.kids.length > 0) {
32
+ out += "typeset -g A_" + ident + "_" + i + "_cmds=(";
33
+ for (const ch of sc.kids) {
34
+ out += " '" + escShellSingleQuoted(ch.key) + ":" + escShellSingleQuoted(ch.description) + "'";
35
+ }
36
+ out += ")\n";
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function emitConsumeLongZsh(ident: string, scopes: ScopeRec[]): string {
43
+ let o = "_${ident}_nac_consume_long() {\n".replace("${ident}", ident);
44
+ o += " local sid=\"$1\" w=\"$2\" nw=\"$3\"\n";
45
+ o += " case $sid in\n";
46
+ for (const [i, sc] of scopes.entries()) {
47
+ o += " " + i + ")\n";
48
+ o += " case $w in\n";
49
+ o += " " + kHelpLong + "|${kHelpLong}=*|${kHelpShort}) echo 1 ;;\n".replace(/\$\{kHelpLong\}/g, kHelpLong).replace(/\$\{kHelpShort\}/g, kHelpShort);
50
+ if (sc.path === "") {
51
+ o += " " + kSchemaLong + ") echo 1 ;;\n";
52
+ }
53
+ for (const op of sc.opts) {
54
+ const base = "--" + op.name;
55
+ if (op.kind === "presence") {
56
+ o += " " + base + "|${base}=*) echo 1 ;;\n".replace(/\$\{base\}/g, base);
57
+ } else {
58
+ o += " " + base + "=*) echo 1 ;;\n";
59
+ o += " " + base + ") echo 2 ;;\n";
60
+ }
61
+ }
62
+ o += " *) echo 0 ;;\n";
63
+ o += " esac\n";
64
+ o += " ;;\n";
65
+ }
66
+ o += " *) echo 0 ;;\n";
67
+ o += " esac\n";
68
+ o += "}\n";
69
+ return o;
70
+ }
71
+
72
+ function emitConsumeShortZsh(ident: string, scopes: ScopeRec[]): string {
73
+ let o = "_${ident}_nac_consume_short() {\n".replace("${ident}", ident);
74
+ o += " local sid=\"$1\" w=\"$2\"\n";
75
+ o += " case $sid in\n";
76
+ for (const [i, sc] of scopes.entries()) {
77
+ o += " " + i + ")\n";
78
+ o += " local rest=${w#-}\n";
79
+ o += " local ch\n";
80
+ o += " local saw=0\n";
81
+ o += " while [[ -n $rest ]]; do\n";
82
+ o += " ch=${rest[1,1]}\n";
83
+ o += " rest=${rest[2,-1]}\n";
84
+ o += " case $ch in\n";
85
+ let boolChars = "";
86
+ for (const op of sc.opts) {
87
+ if (!op.shortName) continue;
88
+ if (op.kind === "presence") {
89
+ boolChars += op.shortName + "|";
90
+ } else {
91
+ o += " " + op.shortName + ")\n";
92
+ o += " if [[ $saw -ne 0 || -n $rest ]]; then echo 0; return; fi\n";
93
+ o += " echo 2; return ;;\n";
94
+ }
95
+ }
96
+ if (boolChars.length > 0) {
97
+ boolChars = boolChars.slice(0, -1);
98
+ o += " " + boolChars + ") ;;\n";
99
+ }
100
+ o += " *) echo 0; return ;;\n";
101
+ o += " esac\n";
102
+ o += " saw=1\n";
103
+ o += " done\n";
104
+ o += " echo 1\n";
105
+ o += " ;;\n";
106
+ }
107
+ o += " *) echo 0 ;;\n";
108
+ o += " esac\n";
109
+ o += "}\n";
110
+ return o;
111
+ }
112
+
113
+ function emitMatchChildZsh(ident: string, scopes: ScopeRec[], pathIndex: Record<string, number>): string {
114
+ let o = "_${ident}_nac_match_child() {\n".replace("${ident}", ident);
115
+ o += " local sid=\"$1\" w=\"$2\"\n";
116
+ o += " case $sid in\n";
117
+ for (const [sid, sc] of scopes.entries()) {
118
+ if (sc.kids.length === 0) continue;
119
+ o += " " + sid + ")\n";
120
+ o += " case $w in\n";
121
+ for (const ch of sc.kids) {
122
+ const childPath = sc.path === "" ? ch.key : sc.path + "/" + ch.key;
123
+ const cid = pathIndex[childPath] ?? 0;
124
+ o += " " + ch.key + ") echo " + cid + "; return 0 ;;\n";
125
+ }
126
+ o += " esac\n";
127
+ o += " ;;\n";
128
+ }
129
+ o += " esac\n";
130
+ o += " return 1\n";
131
+ o += "}\n";
132
+ return o;
133
+ }
134
+
135
+ function emitSimulateZsh(ident: string): string {
136
+ let o = "_${ident}_nac_simulate() {\n".replace("${ident}", ident);
137
+ o += " local i=2 sid=0 w steps next\n";
138
+ o += " while (( i < CURRENT )); do\n";
139
+ o += " w=$words[i]\n";
140
+ o += " if [[ $w == " + kHelpShort + " || $w == " + kHelpLong + " || $w == " + kSchemaLong + " ]]; then\n";
141
+ o += " ((i++)); continue\n";
142
+ o += " fi\n";
143
+ o += " if [[ $w == --* ]]; then\n";
144
+ o += " steps=$(_${ident}_nac_consume_long \"$sid\" \"$w\" \"${words[i+1]}\")\n".replace("${ident}", ident);
145
+ o += " case $steps in\n";
146
+ o += " 0) break ;;\n";
147
+ o += " 1) ((i++)) ;;\n";
148
+ o += " 2) ((i+=2)) ;;\n";
149
+ o += " *) break ;;\n";
150
+ o += " esac\n";
151
+ o += " continue\n";
152
+ o += " fi\n";
153
+ o += " if [[ $w == -* ]]; then\n";
154
+ o += " steps=$(_${ident}_nac_consume_short \"$sid\" \"$w\")\n".replace("${ident}", ident);
155
+ o += " case $steps in\n";
156
+ o += " 0) break ;;\n";
157
+ o += " 1) ((i++)) ;;\n";
158
+ o += " 2) ((i++)); break ;;\n";
159
+ o += " *) break ;;\n";
160
+ o += " esac\n";
161
+ o += " continue\n";
162
+ o += " fi\n";
163
+ o += " next=$(_${ident}_nac_match_child \"$sid\" \"$w\") || break\n".replace("${ident}", ident);
164
+ o += " sid=$next\n";
165
+ o += " ((i++))\n";
166
+ o += " done\n";
167
+ o += " REPLY_SID=$sid\n";
168
+ o += "}\n";
169
+ return o;
170
+ }
171
+
172
+ function emitEnumReplyZsh(ident: string, scopes: ScopeRec[]): string {
173
+ let o = "_${ident}_nac_enum_reply() {\n".replace("${ident}", ident);
174
+ o += " local sid=$1 prev=$2\n";
175
+ o += " case $sid in\n";
176
+ for (const [i, sc] of scopes.entries()) {
177
+ const enumOpts = sc.opts.filter((op) => op.kind === CliOptionKind.Enum && (op.choices?.length ?? 0) > 0);
178
+ if (enumOpts.length === 0) continue;
179
+ o += " " + i + ")\n";
180
+ o += " case $prev in\n";
181
+ for (const op of enumOpts) {
182
+ const vals = (op.choices ?? []).map((c) => escShellSingleQuoted(c)).join(" ");
183
+ o += " --" + op.name + ") _values " + vals + "; return 0 ;;\n";
184
+ }
185
+ o += " esac\n";
186
+ o += " ;;\n";
187
+ }
188
+ o += " esac\n";
189
+ o += " return 1\n";
190
+ o += "}\n";
191
+ return o;
192
+ }
193
+
194
+ function emitMainBodyZsh(schema: CliCommand, ident: string): string {
195
+ const main = mainName(schema.key);
196
+ let o = "_${main}() {\n".replace("${main}", main);
197
+ o += " local curcontext=\"$curcontext\" ret=1\n";
198
+ o += " _${ident}_nac_simulate\n".replace("${ident}", ident);
199
+ o += " local sid=$REPLY_SID\n";
200
+ o += " if _${ident}_nac_enum_reply \"$sid\" \"$words[CURRENT-1]\"; then return 0; fi\n".replace("${ident}", ident);
201
+ o += " if [[ $PREFIX == -* ]]; then\n";
202
+ o += " local -a optsarr\n";
203
+ o += " local oname=\"A_${ident}_${sid}_opts\"\n".replace("${ident}", ident);
204
+ o += " optsarr=(${(@P)oname})\n";
205
+ o += " _describe -t options 'option' optsarr && ret=0\n";
206
+ o += " else\n";
207
+ o += " local lname=\"A_${ident}_${sid}_leaf\"\n".replace("${ident}", ident);
208
+ o += " if [[ ${(P)lname} -eq 0 ]]; then\n";
209
+ o += " local -a cmdsarr\n";
210
+ o += " local cname=\"A_${ident}_${sid}_cmds\"\n".replace("${ident}", ident);
211
+ o += " cmdsarr=(${(@P)cname})\n";
212
+ o += " _describe -t commands 'command' cmdsarr && ret=0\n";
213
+ o += " else\n";
214
+ o += " local pname=\"A_${ident}_${sid}_pos\"\n".replace("${ident}", ident);
215
+ o += " if [[ ${(P)pname} -eq 1 ]]; then\n";
216
+ o += " _files && ret=0\n";
217
+ o += " fi\n";
218
+ o += " fi\n";
219
+ o += " fi\n";
220
+ o += " return ret\n";
221
+ o += "}\n\n";
222
+ o += "compdef _${main} ${schema.key}\n".replace("${main}", main).replace("${schema.key}", schema.key);
223
+ return o;
224
+ }
225
+
226
+ /** Returns a self-contained zsh completion script for the given program schema. */
227
+ export function completionZshScript(schema: CliCommand): string {
228
+ const ident = identToken(schema.key);
229
+ const scopes = collectScopes(schema);
230
+ const pathIndex: Record<string, number> = {};
231
+ for (const [i, s] of scopes.entries()) {
232
+ pathIndex[s.path] = i;
233
+ }
234
+
235
+ let out = "#compdef " + schema.key + "\n\n";
236
+ out += emitScopeArraysZsh(ident, scopes);
237
+ out += emitConsumeLongZsh(ident, scopes);
238
+ out += emitConsumeShortZsh(ident, scopes);
239
+ out += emitMatchChildZsh(ident, scopes, pathIndex);
240
+ out += emitSimulateZsh(ident);
241
+ out += emitEnumReplyZsh(ident, scopes);
242
+ out += emitMainBodyZsh(schema, ident);
243
+ return out;
244
+ }