argsbarg 1.4.3 → 2.0.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/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
- package/.private/scratch.md +1 -1
- package/CHANGELOG.md +39 -1
- package/README.md +29 -21
- package/docs/ai-skills.md +24 -52
- package/docs/install.md +84 -0
- package/docs/mcp.md +8 -8
- package/examples/mcp-test.ts +3 -3
- package/examples/minimal.ts +3 -3
- package/examples/nested.ts +3 -3
- package/examples/option-required.ts +3 -3
- package/index.d.ts +44 -50
- 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 +138 -0
- package/src/builtins/export.ts +53 -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 +50 -0
- package/src/builtins/scopes.ts +46 -0
- package/src/builtins/shell-helpers.ts +24 -0
- package/src/capabilities.ts +32 -0
- package/src/completion.ts +10 -693
- package/src/context.ts +21 -6
- package/src/help.ts +21 -9
- package/src/index.test.ts +114 -118
- package/src/index.ts +2 -1
- 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/invoke.ts +14 -5
- package/src/mcp/server.ts +3 -3
- package/src/mcp/tools.ts +17 -17
- package/src/mcp.ts +2 -2
- package/src/parse.ts +55 -27
- package/src/runtime.ts +47 -100
- package/src/schema.ts +10 -52
- package/src/skill/generate.ts +10 -10
- package/src/skill/install.ts +21 -19
- package/src/types.test.ts +40 -0
- package/src/types.ts +59 -49
- package/src/validate.ts +89 -83
- package/src/ai.ts +0 -7
package/docs/mcp.md
CHANGED
|
@@ -9,12 +9,12 @@ MCP is **opt-in**. Apps that do not set `mcpServer` on the program root behave e
|
|
|
9
9
|
1. Add `mcpServer` to your program root:
|
|
10
10
|
|
|
11
11
|
```typescript
|
|
12
|
-
const cli
|
|
12
|
+
const cli = {
|
|
13
13
|
key: "myapp",
|
|
14
14
|
description: "My app.",
|
|
15
15
|
mcpServer: { name: "myapp", version: "1.0.0" },
|
|
16
16
|
commands: [/* ... */],
|
|
17
|
-
};
|
|
17
|
+
} satisfies CliProgram;
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
`mcpServer: {}` is enough to enable the server. Optional fields override defaults (see [Configuration](#configuration)).
|
|
@@ -22,7 +22,7 @@ const cli: CliCommand = {
|
|
|
22
22
|
2. Run the MCP server:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
myapp
|
|
25
|
+
myapp mcp
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
The process reads NDJSON requests from stdin and writes NDJSON responses to stdout. It stays alive until stdin closes.
|
|
@@ -34,7 +34,7 @@ Optionally install an agent skill for discovery without MCP: see [docs/ai-skills
|
|
|
34
34
|
The `examples/nested.ts` demo enables MCP — try:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
bun run examples/nested.ts
|
|
37
|
+
bun run examples/nested.ts mcp
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
## Client setup
|
|
@@ -58,11 +58,11 @@ Use your real binary or script path. For a compiled CLI, `command` can be the in
|
|
|
58
58
|
|
|
59
59
|
### Other MCP hosts
|
|
60
60
|
|
|
61
|
-
Any host that spawns a subprocess and wires stdin/stdout works the same way: the **command** is your app, and **`
|
|
61
|
+
Any host that spawns a subprocess and wires stdin/stdout works the same way: the **command** is your app, and **`mcp`** starts the server.
|
|
62
62
|
|
|
63
63
|
## Configuration
|
|
64
64
|
|
|
65
|
-
Set `mcpServer` on the **program root only** (the `
|
|
65
|
+
Set `mcpServer` on the **program root only** (the `CliProgram` passed to `cliRun`). Validation rejects `mcpServer` on nested nodes.
|
|
66
66
|
|
|
67
67
|
| Field | Default | Purpose |
|
|
68
68
|
| --- | --- | --- |
|
|
@@ -277,7 +277,7 @@ Requests without an `id` are treated as notifications and do not receive a respo
|
|
|
277
277
|
### Manual smoke test
|
|
278
278
|
|
|
279
279
|
```bash
|
|
280
|
-
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun run examples/nested.ts
|
|
280
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun run examples/nested.ts mcp
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
You should get one JSON line on stdout with `result.capabilities` and `result.serverInfo`.
|
|
@@ -290,7 +290,7 @@ When MCP is enabled:
|
|
|
290
290
|
- Do not declare a top-level command named **`completion`** — reserved for shell completions.
|
|
291
291
|
- Do not declare an option named **`schema`** — reserved for `--schema`.
|
|
292
292
|
|
|
293
|
-
Running `myapp
|
|
293
|
+
Running `myapp mcp` without `mcpServer` on the root fails with an error (exit 1).
|
|
294
294
|
|
|
295
295
|
## Design notes
|
|
296
296
|
|
package/examples/mcp-test.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
MCP test fixture for subprocess integration tests only.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { cliRun,
|
|
6
|
+
import { cliRun, CliProgram, CliOptionKind } from "../src/index.ts";
|
|
7
7
|
|
|
8
8
|
const envFilePath = process.env.ARGS_TEST_ENV_FILE;
|
|
9
9
|
|
|
10
|
-
const cli
|
|
10
|
+
const cli = {
|
|
11
11
|
key: "mcp-test",
|
|
12
12
|
description: "MCP integration test fixture.",
|
|
13
13
|
mcpServer: {
|
|
@@ -61,6 +61,6 @@ const cli: CliCommand = {
|
|
|
61
61
|
},
|
|
62
62
|
},
|
|
63
63
|
],
|
|
64
|
-
};
|
|
64
|
+
} satisfies CliProgram;
|
|
65
65
|
|
|
66
66
|
await cliRun(cli);
|
package/examples/minimal.ts
CHANGED
|
@@ -7,9 +7,9 @@ readers can copy the pattern into their own scripts quickly.
|
|
|
7
7
|
It demonstrates the minimal Bun integration path.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliRun,
|
|
10
|
+
import { cliRun, CliProgram, CliOptionKind } from "../src/index.ts";
|
|
11
11
|
|
|
12
|
-
const cli
|
|
12
|
+
const cli = {
|
|
13
13
|
key: "minimal.ts",
|
|
14
14
|
description: "Tiny demo.",
|
|
15
15
|
positionals: [
|
|
@@ -36,6 +36,6 @@ const cli: CliCommand = {
|
|
|
36
36
|
}
|
|
37
37
|
console.log(`hello ${name}`);
|
|
38
38
|
},
|
|
39
|
-
};
|
|
39
|
+
} satisfies CliProgram;
|
|
40
40
|
|
|
41
41
|
await cliRun(cli);
|
package/examples/nested.ts
CHANGED
|
@@ -7,9 +7,9 @@ and fallback commands fit together in one schema.
|
|
|
7
7
|
It demonstrates how the schema scales beyond one command.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliRun,
|
|
10
|
+
import { cliRun, CliProgram, CliOptionKind, CliFallbackMode } from "../src/index.ts";
|
|
11
11
|
|
|
12
|
-
const cli
|
|
12
|
+
const cli = {
|
|
13
13
|
key: "nested.ts",
|
|
14
14
|
description: "Nested groups demo.",
|
|
15
15
|
mcpServer: { name: "nested-demo", version: "1.0.0" },
|
|
@@ -97,6 +97,6 @@ const cli: CliCommand = {
|
|
|
97
97
|
],
|
|
98
98
|
fallbackCommand: "read",
|
|
99
99
|
fallbackMode: CliFallbackMode.MissingOrUnknown,
|
|
100
|
-
};
|
|
100
|
+
} satisfies CliProgram;
|
|
101
101
|
|
|
102
102
|
await cliRun(cli);
|
|
@@ -7,9 +7,9 @@ readers can copy the pattern into their own scripts quickly.
|
|
|
7
7
|
It demonstrates the minimal Bun integration path.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliRun,
|
|
10
|
+
import { cliRun, CliProgram, CliOptionKind, CliFallbackMode, isInteractiveTty } from "../src/index.ts";
|
|
11
11
|
|
|
12
|
-
const cli
|
|
12
|
+
const cli = {
|
|
13
13
|
key: "option-required.ts",
|
|
14
14
|
description: "Demo of a required option.",
|
|
15
15
|
options: [
|
|
@@ -42,6 +42,6 @@ const cli: CliCommand = {
|
|
|
42
42
|
console.log(`requiredNonTty: ${requiredNonTty}`);
|
|
43
43
|
console.log(`optional: ${optional}`);
|
|
44
44
|
},
|
|
45
|
-
};
|
|
45
|
+
} satisfies CliProgram;
|
|
46
46
|
|
|
47
47
|
await cliRun(cli);
|
package/index.d.ts
CHANGED
|
@@ -7,11 +7,13 @@ export declare class CliContext {
|
|
|
7
7
|
readonly appName: string;
|
|
8
8
|
readonly commandPath: string[];
|
|
9
9
|
readonly args: string[];
|
|
10
|
-
readonly schema:
|
|
10
|
+
readonly schema: CliProgram;
|
|
11
11
|
readonly opts: Record<string, string>;
|
|
12
12
|
readonly invocation: CliInvocation;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/** Program root schema (same as {@link schema}). */
|
|
14
|
+
get program(): CliProgram;
|
|
15
|
+
/** Captures the program root, routed path, positional words, and option map for a leaf handler. */
|
|
16
|
+
constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, schema: CliProgram, invocation?: CliInvocation);
|
|
15
17
|
/** Returns whether a presence flag was set (including implicit "1" for boolean options). */
|
|
16
18
|
hasFlag(name: string): boolean;
|
|
17
19
|
/** Returns the string value for a string-valued option, if present. */
|
|
@@ -64,7 +66,7 @@ export declare enum CliFallbackMode {
|
|
|
64
66
|
UnknownOnly = "unknownOnly"
|
|
65
67
|
}
|
|
66
68
|
/**
|
|
67
|
-
* A named flag or value option (`--long`, `-short`), listed on `
|
|
69
|
+
* A named flag or value option (`--long`, `-short`), listed on command `options`.
|
|
68
70
|
*/
|
|
69
71
|
export interface CliOption {
|
|
70
72
|
/** Option name (e.g., "name", "verbose"). */
|
|
@@ -84,7 +86,7 @@ export interface CliOption {
|
|
|
84
86
|
choices?: string[];
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
87
|
-
* An ordered positional argument slot, listed on `
|
|
89
|
+
* An ordered positional argument slot, listed on leaf `positionals`.
|
|
88
90
|
*/
|
|
89
91
|
export interface CliPositional {
|
|
90
92
|
/** Positional name (used in help and error messages). */
|
|
@@ -105,7 +107,7 @@ export interface CliPositional {
|
|
|
105
107
|
argMax?: number;
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
|
-
*
|
|
110
|
+
* Enables `myapp mcp` and MCP stdio server metadata (program root only).
|
|
109
111
|
*/
|
|
110
112
|
export interface CliMcpServerConfig {
|
|
111
113
|
/** `initialize` serverInfo.name (default: root `key`). */
|
|
@@ -166,18 +168,18 @@ export interface CliMcpToolConfig {
|
|
|
166
168
|
requiresEnv?: string[];
|
|
167
169
|
}
|
|
168
170
|
/**
|
|
169
|
-
*
|
|
171
|
+
* Opt-out and defaults for the `install` built-in (compiled binaries only; program root only).
|
|
170
172
|
*/
|
|
171
|
-
export interface
|
|
172
|
-
/** When `false`, disable `
|
|
173
|
+
export interface CliInstallConfig {
|
|
174
|
+
/** When `false`, hide/disable `install` (default: enabled). */
|
|
173
175
|
enabled?: boolean;
|
|
174
|
-
/**
|
|
175
|
-
|
|
176
|
+
/** Default bin directory (default: `~/.local/bin`). Overridden by `INSTALL_PREFIX` env and `--prefix`. */
|
|
177
|
+
prefix?: string;
|
|
176
178
|
}
|
|
177
179
|
/**
|
|
178
|
-
* Base properties shared by all command
|
|
180
|
+
* Base properties shared by all nodes in the user command tree.
|
|
179
181
|
*/
|
|
180
|
-
export interface
|
|
182
|
+
export interface CliNodeBase {
|
|
181
183
|
/** Program or command key (e.g., "myapp", "stat", "owner"). */
|
|
182
184
|
key: string;
|
|
183
185
|
/** Short description shown in help. */
|
|
@@ -186,49 +188,50 @@ export interface CliCommandBase {
|
|
|
186
188
|
notes?: string;
|
|
187
189
|
/** Global or command-level flags/options. */
|
|
188
190
|
options?: CliOption[];
|
|
189
|
-
/** Root-only. When set, enables the `ai mcp` built-in subcommand. */
|
|
190
|
-
mcpServer?: CliMcpServerConfig;
|
|
191
|
-
/** Root-only. Opt out of `ai skill` install with `{ enabled: false }`. */
|
|
192
|
-
aiSkill?: CliAiSkillConfig;
|
|
193
|
-
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
194
|
-
mcpTool?: CliMcpToolConfig;
|
|
195
191
|
}
|
|
196
192
|
/**
|
|
197
|
-
* A command node
|
|
198
|
-
*
|
|
199
|
-
* The value passed to cliRun is the program root: name is the app/binary name.
|
|
200
|
-
* The root may be a routing group or a leaf command.
|
|
193
|
+
* A leaf command node with a handler and optional positionals.
|
|
201
194
|
*/
|
|
202
|
-
export type
|
|
195
|
+
export type CliLeaf = CliNodeBase & {
|
|
203
196
|
/** Handler function for leaf commands. */
|
|
204
197
|
handler: CliHandler;
|
|
205
198
|
/** Positional argument definitions. */
|
|
206
199
|
positionals?: CliPositional[];
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
/** Per-tool MCP exposure and metadata. */
|
|
201
|
+
mcpTool?: CliMcpToolConfig;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* A routing command node with nested subcommands.
|
|
205
|
+
*/
|
|
206
|
+
export type CliRouter = CliNodeBase & {
|
|
214
207
|
/** Nested subcommands. */
|
|
215
|
-
commands:
|
|
208
|
+
commands: CliNode[];
|
|
216
209
|
/** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
|
|
217
210
|
fallbackCommand?: string;
|
|
218
|
-
/** How fallbackCommand is applied at this routing node
|
|
211
|
+
/** How fallbackCommand is applied at this routing node. */
|
|
219
212
|
fallbackMode?: CliFallbackMode;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* A node in the user-defined command tree (router or leaf).
|
|
216
|
+
*/
|
|
217
|
+
export type CliNode = CliLeaf | CliRouter;
|
|
218
|
+
/**
|
|
219
|
+
* Program root passed to `cliRun` / `cliInvoke`.
|
|
220
|
+
* May be a leaf or router, plus optional program-level MCP and install config.
|
|
221
|
+
*/
|
|
222
|
+
export type CliProgram = CliNode & {
|
|
223
|
+
/** When set, enables the `mcp` built-in subcommand. */
|
|
224
|
+
mcpServer?: CliMcpServerConfig;
|
|
225
|
+
/** Opt-out and defaults for `install` (compiled binaries only). */
|
|
226
|
+
install?: CliInstallConfig;
|
|
227
|
+
};
|
|
225
228
|
/**
|
|
226
229
|
* Handler closure type for leaf commands.
|
|
227
230
|
* Supports both sync and async handlers.
|
|
228
231
|
*/
|
|
229
232
|
export type CliHandler = (ctx: CliContext) => void | Promise<void>;
|
|
230
233
|
/**
|
|
231
|
-
* Error thrown when the static
|
|
234
|
+
* Error thrown when the static CLI tree violates ArgsBarg rules.
|
|
232
235
|
*/
|
|
233
236
|
export declare class CliSchemaValidationError extends Error {
|
|
234
237
|
/** Creates a schema validation error with a human-readable rule violation. */
|
|
@@ -253,17 +256,8 @@ export interface CliInvokeResult {
|
|
|
253
256
|
* Parses argv against the user root, runs the leaf handler, and returns captured output.
|
|
254
257
|
* Never calls process.exit.
|
|
255
258
|
*/
|
|
256
|
-
export declare function cliInvoke(root:
|
|
257
|
-
|
|
258
|
-
* Validates the schema, parses argv, prints help or errors, runs completion or the leaf handler, then exits.
|
|
259
|
-
*
|
|
260
|
-
* @param root The root CliCommand.
|
|
261
|
-
* @param argv Override the default argv (process.argv.slice(2)).
|
|
262
|
-
*/
|
|
263
|
-
export declare function cliRun(root: CliCommand, argv?: string[]): Promise<never>;
|
|
264
|
-
/**
|
|
265
|
-
* Prints a red error line and contextual help on stderr, then exits with status 1.
|
|
266
|
-
*/
|
|
259
|
+
export declare function cliInvoke(root: CliProgram, argv: string[]): Promise<CliInvokeResult>;
|
|
260
|
+
export declare function cliRun(program: CliProgram, argv?: string[]): Promise<never>;
|
|
267
261
|
export declare function cliErrWithHelp(ctx: CliContext, msg: string): never;
|
|
268
262
|
/** True when stdin is a TTY. */
|
|
269
263
|
export declare const isInteractiveTty: boolean;
|
package/package.json
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach } from "bun:test";
|
|
2
|
+
import { cliBuiltinInstallCommand, installBuiltinOptions } from "./install.ts";
|
|
3
|
+
import { cliBuiltinMcpCommand } from "./mcp.ts";
|
|
4
|
+
import { cliPresentationRoot } from "./presentation.ts";
|
|
5
|
+
import { completionBashScript, completionFishScript, completionZshScript } from "./index.ts";
|
|
6
|
+
import { exportPresentationBuiltins } from "./export.ts";
|
|
7
|
+
import { CliProgram } from "../types.ts";
|
|
8
|
+
import { setCompiledExecutableOverride } from "../install/compiled.ts";
|
|
9
|
+
|
|
10
|
+
const fixture: CliProgram = {
|
|
11
|
+
key: "myapp",
|
|
12
|
+
description: "Demo app.",
|
|
13
|
+
mcpServer: { name: "myapp" },
|
|
14
|
+
commands: [
|
|
15
|
+
{
|
|
16
|
+
key: "hello",
|
|
17
|
+
description: "Say hello.",
|
|
18
|
+
handler: () => {},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
setCompiledExecutableOverride(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("builtins help copy", () => {
|
|
28
|
+
test("install command includes description and option text when compiled", () => {
|
|
29
|
+
setCompiledExecutableOverride(true);
|
|
30
|
+
const install = cliBuiltinInstallCommand(fixture);
|
|
31
|
+
expect(install.description).toContain("Install the binary");
|
|
32
|
+
expect(install.notes).toContain("bun build --compile");
|
|
33
|
+
const names = installBuiltinOptions(fixture).map((o) => o.name);
|
|
34
|
+
expect(names).toContain("all");
|
|
35
|
+
expect(names).toContain("mcp");
|
|
36
|
+
expect(names).toContain("prefix");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("install omits --mcp option when mcpServer unset", () => {
|
|
40
|
+
setCompiledExecutableOverride(true);
|
|
41
|
+
const noMcp: CliProgram = { key: "x", description: "x", handler: () => {} };
|
|
42
|
+
const names = installBuiltinOptions(noMcp).map((o) => o.name);
|
|
43
|
+
expect(names).not.toContain("mcp");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("mcp builtin description is user-facing", () => {
|
|
47
|
+
const mcp = cliBuiltinMcpCommand();
|
|
48
|
+
expect(mcp.description).toContain("MCP server");
|
|
49
|
+
expect(mcp.notes).toContain('["mcp"]');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("presentation root", () => {
|
|
54
|
+
test("includes mcp when mcpServer set", () => {
|
|
55
|
+
setCompiledExecutableOverride(false);
|
|
56
|
+
const root = cliPresentationRoot(fixture);
|
|
57
|
+
expect(root.commands?.map((c) => c.key)).toContain("mcp");
|
|
58
|
+
expect(root.commands?.map((c) => c.key)).not.toContain("install");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("includes install when compiled", () => {
|
|
62
|
+
setCompiledExecutableOverride(true);
|
|
63
|
+
const root = cliPresentationRoot(fixture);
|
|
64
|
+
expect(root.commands?.map((c) => c.key)).toContain("install");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("completion emitters", () => {
|
|
69
|
+
test("fish script references app key and subcommands", () => {
|
|
70
|
+
setCompiledExecutableOverride(true);
|
|
71
|
+
const schema = cliPresentationRoot(fixture);
|
|
72
|
+
const fish = completionFishScript(schema);
|
|
73
|
+
expect(fish).toContain("complete -c myapp");
|
|
74
|
+
expect(fish).toContain("hello");
|
|
75
|
+
expect(fish).toContain("install");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("bash script includes install flags when compiled", () => {
|
|
79
|
+
setCompiledExecutableOverride(true);
|
|
80
|
+
const schema = cliPresentationRoot(fixture);
|
|
81
|
+
const bash = completionBashScript(schema);
|
|
82
|
+
expect(bash).toContain("--all");
|
|
83
|
+
expect(bash).toContain("install");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("zsh script registers compdef", () => {
|
|
87
|
+
const schema = cliPresentationRoot({ key: "zapp", description: "z", handler: () => {} });
|
|
88
|
+
const zsh = completionZshScript(schema);
|
|
89
|
+
expect(zsh).toContain("#compdef zapp");
|
|
90
|
+
expect(zsh).toContain("compdef _zapp zapp");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("schema export builtins", () => {
|
|
95
|
+
test("exportPresentationBuiltins includes install options when compiled", () => {
|
|
96
|
+
setCompiledExecutableOverride(true);
|
|
97
|
+
const builtins = exportPresentationBuiltins(fixture);
|
|
98
|
+
const install = builtins.find((b) => b.key === "install");
|
|
99
|
+
expect(install?.options?.find((o) => o.name === "all")?.description).toContain("binary");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { CliNode, CliRouter, 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: CliRouter, 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: CliRouter): 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
|
+
}
|