argsbarg 1.5.0 → 2.0.1
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/CHANGELOG.md +31 -1
- package/README.md +12 -8
- package/docs/install.md +2 -2
- package/docs/mcp.md +3 -3
- 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 +38 -37
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +3 -3
- package/src/builtins/completion-bash.ts +3 -3
- package/src/builtins/completion-fish.ts +2 -2
- package/src/builtins/completion-group.ts +2 -2
- package/src/builtins/completion-zsh.ts +3 -3
- package/src/builtins/dispatch.ts +41 -26
- package/src/builtins/export.ts +15 -8
- package/src/builtins/install.ts +3 -3
- package/src/builtins/mcp.ts +2 -2
- package/src/builtins/presentation.ts +34 -23
- package/src/builtins/scopes.ts +9 -8
- package/src/capabilities.ts +32 -0
- package/src/context.ts +17 -7
- package/src/help.ts +21 -9
- package/src/index.test.ts +128 -121
- package/src/index.ts +1 -1
- package/src/install/binary.ts +3 -3
- package/src/install/completions.ts +2 -2
- package/src/install/detect-installed.ts +1 -1
- package/src/install/index.ts +4 -4
- package/src/install/install.test.ts +2 -2
- package/src/install/mcp-config.ts +2 -2
- package/src/install/paths.ts +3 -3
- package/src/install/plan.ts +4 -4
- package/src/install/status.ts +2 -2
- package/src/install/uninstall.ts +2 -2
- package/src/invoke.ts +14 -5
- package/src/mcp/server.ts +3 -3
- package/src/mcp/tools.ts +16 -16
- package/src/mcp.ts +2 -2
- package/src/parse.ts +55 -27
- package/src/runtime.ts +34 -25
- package/src/schema.ts +6 -6
- package/src/skill/generate.ts +6 -6
- package/src/skill/install.ts +2 -2
- package/src/types.test.ts +40 -0
- package/src/types.ts +54 -44
- package/src/validate.ts +87 -72
package/src/validate.ts
CHANGED
|
@@ -2,58 +2,65 @@
|
|
|
2
2
|
This module validates CLI schemas before execution.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
type CliLeaf,
|
|
8
|
+
type CliNode,
|
|
9
|
+
type CliProgram,
|
|
8
10
|
CliOptionKind,
|
|
9
11
|
CliSchemaValidationError,
|
|
12
|
+
isCliLeaf,
|
|
13
|
+
isCliRouter,
|
|
10
14
|
} from "./types.ts";
|
|
11
15
|
import { MCP_SCHEMA_URI_DEFAULT } from "./mcp/tools.ts";
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return names;
|
|
19
|
-
}
|
|
17
|
+
/** Validates a program schema. */
|
|
18
|
+
export function cliValidateProgram(program: CliProgram): void {
|
|
19
|
+
const caps = resolveCapabilities(program);
|
|
20
|
+
const reserved = reservedCommandNames(caps);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
if (isCliRouter(program)) {
|
|
23
|
+
for (const child of program.commands) {
|
|
24
|
+
if (reserved.includes(child.key)) {
|
|
25
|
+
throw new CliSchemaValidationError(`Reserved command name: ${child.key}`);
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
walkNode(program, program, true);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
function
|
|
32
|
-
if (!isRoot
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
34
|
+
if (!isRoot) {
|
|
35
|
+
const rogue = node as CliProgram;
|
|
36
|
+
if (rogue.mcpServer !== undefined) {
|
|
37
|
+
throw new CliSchemaValidationError(
|
|
38
|
+
"mcpServer is only supported on the program root (not on " + node.key + ")",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
if (rogue.install !== undefined) {
|
|
42
|
+
throw new CliSchemaValidationError(
|
|
43
|
+
"install is only supported on the program root (not on " + node.key + ")",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if (isCliLeaf(node)) {
|
|
49
|
+
if (isRoot && node.mcpTool !== undefined) {
|
|
50
|
+
throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
const rogue = node as unknown as CliLeaf;
|
|
54
|
+
if (rogue.mcpTool !== undefined) {
|
|
55
|
+
throw new CliSchemaValidationError(
|
|
56
|
+
"mcpTool is only supported on leaf commands (not on " + node.key + ")",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
if (isRoot &&
|
|
55
|
-
const schemaUri =
|
|
56
|
-
const uris =
|
|
61
|
+
if (isRoot && program.mcpServer?.resources) {
|
|
62
|
+
const schemaUri = program.mcpServer.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
|
|
63
|
+
const uris = program.mcpServer.resources.map((r) => r.uri);
|
|
57
64
|
if (uris.includes(schemaUri)) {
|
|
58
65
|
throw new CliSchemaValidationError(
|
|
59
66
|
`mcpServer.resources URI '${schemaUri}' conflicts with the built-in schema resource`,
|
|
@@ -64,53 +71,64 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
if (isCliRouter(node)) {
|
|
75
|
+
const seenNames = new Set<string>();
|
|
76
|
+
for (const child of node.commands) {
|
|
77
|
+
if (seenNames.has(child.key)) {
|
|
78
|
+
throw new CliSchemaValidationError(`Duplicate command name: ${child.key}`);
|
|
79
|
+
}
|
|
80
|
+
seenNames.add(child.key);
|
|
71
81
|
}
|
|
72
|
-
seenNames.add(child.key);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (cmd.fallbackMode !== undefined && cmd.fallbackCommand === undefined) {
|
|
76
|
-
throw new CliSchemaValidationError(
|
|
77
|
-
`fallbackMode requires fallbackCommand on '${cmd.key}'`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
const children = cmd.commands ?? [];
|
|
83
|
-
const valid = children.find((c) => c.key === cmd.fallbackCommand);
|
|
84
|
-
if (!valid) {
|
|
83
|
+
if (node.fallbackMode !== undefined && node.fallbackCommand === undefined) {
|
|
85
84
|
throw new CliSchemaValidationError(
|
|
86
|
-
`
|
|
85
|
+
`fallbackMode requires fallbackCommand on '${node.key}'`,
|
|
87
86
|
);
|
|
88
87
|
}
|
|
88
|
+
|
|
89
|
+
if (node.fallbackCommand !== undefined) {
|
|
90
|
+
const valid = node.commands.find((c) => c.key === node.fallbackCommand);
|
|
91
|
+
if (!valid) {
|
|
92
|
+
throw new CliSchemaValidationError(
|
|
93
|
+
`fallbackCommand '${node.fallbackCommand}' is not a child of '${node.key}'`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const child of node.commands) {
|
|
99
|
+
walkNode(child, program, false);
|
|
100
|
+
}
|
|
89
101
|
}
|
|
90
102
|
|
|
103
|
+
const positionals = isCliLeaf(node) ? (node.positionals ?? []) : [];
|
|
104
|
+
validateOptions(node.key, node.options ?? []);
|
|
105
|
+
validatePositionals(node.key, positionals);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateOptions(scopeKey: string, options: import("./types.ts").CliOption[]): void {
|
|
91
109
|
const seenShorts = new Set<string>();
|
|
92
|
-
for (const opt of
|
|
110
|
+
for (const opt of options) {
|
|
93
111
|
if (opt.required && opt.kind === CliOptionKind.Presence) {
|
|
94
112
|
throw new CliSchemaValidationError(
|
|
95
|
-
`Presence option cannot be required: ${
|
|
113
|
+
`Presence option cannot be required: ${scopeKey}/${opt.name}`,
|
|
96
114
|
);
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
if (opt.name === "schema") {
|
|
100
118
|
throw new CliSchemaValidationError(
|
|
101
|
-
`Option name "schema" is reserved for --schema: ${
|
|
119
|
+
`Option name "schema" is reserved for --schema: ${scopeKey}/${opt.name}`,
|
|
102
120
|
);
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
if (opt.shortName !== undefined) {
|
|
106
124
|
if (opt.shortName === "h") {
|
|
107
125
|
throw new CliSchemaValidationError(
|
|
108
|
-
`Short alias -h is reserved for help: ${
|
|
126
|
+
`Short alias -h is reserved for help: ${scopeKey}/${opt.name}`,
|
|
109
127
|
);
|
|
110
128
|
}
|
|
111
129
|
if (seenShorts.has(opt.shortName)) {
|
|
112
130
|
throw new CliSchemaValidationError(
|
|
113
|
-
`Duplicate short alias -${opt.shortName} in scope ${
|
|
131
|
+
`Duplicate short alias -${opt.shortName} in scope ${scopeKey}`,
|
|
114
132
|
);
|
|
115
133
|
}
|
|
116
134
|
seenShorts.add(opt.shortName);
|
|
@@ -119,42 +137,43 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
119
137
|
if (opt.kind === CliOptionKind.Enum) {
|
|
120
138
|
if (!opt.choices || opt.choices.length === 0) {
|
|
121
139
|
throw new CliSchemaValidationError(
|
|
122
|
-
`Option '${opt.name}' on '${
|
|
140
|
+
`Option '${opt.name}' on '${scopeKey}': Enum kind requires non-empty choices`,
|
|
123
141
|
);
|
|
124
142
|
}
|
|
125
143
|
if (new Set(opt.choices).size !== opt.choices.length) {
|
|
126
144
|
throw new CliSchemaValidationError(
|
|
127
|
-
`Option '${opt.name}' on '${
|
|
145
|
+
`Option '${opt.name}' on '${scopeKey}': Enum choices must be distinct`,
|
|
128
146
|
);
|
|
129
147
|
}
|
|
130
148
|
for (const choice of opt.choices) {
|
|
131
149
|
if (choice.length === 0) {
|
|
132
150
|
throw new CliSchemaValidationError(
|
|
133
|
-
`Option '${opt.name}' on '${
|
|
151
|
+
`Option '${opt.name}' on '${scopeKey}': Enum choices must be non-empty strings`,
|
|
134
152
|
);
|
|
135
153
|
}
|
|
136
154
|
}
|
|
137
155
|
} else if (opt.choices !== undefined) {
|
|
138
156
|
throw new CliSchemaValidationError(
|
|
139
|
-
`Option '${opt.name}' on '${
|
|
157
|
+
`Option '${opt.name}' on '${scopeKey}': choices is only valid for Enum kind`,
|
|
140
158
|
);
|
|
141
159
|
}
|
|
142
160
|
}
|
|
161
|
+
}
|
|
143
162
|
|
|
144
|
-
|
|
163
|
+
function validatePositionals(scopeKey: string, positionals: import("./types.ts").CliPositional[]): void {
|
|
145
164
|
for (const p of positionals) {
|
|
146
165
|
if (p.argMin !== undefined && p.argMin < 0) {
|
|
147
|
-
throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${
|
|
166
|
+
throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${scopeKey}/${p.name}`);
|
|
148
167
|
}
|
|
149
168
|
if (p.argMax !== undefined && p.argMax < 0) {
|
|
150
169
|
throw new CliSchemaValidationError(
|
|
151
|
-
`argMax must be >= 0 (use 0 for unlimited) for positional ${
|
|
170
|
+
`argMax must be >= 0 (use 0 for unlimited) for positional ${scopeKey}/${p.name}`,
|
|
152
171
|
);
|
|
153
172
|
}
|
|
154
173
|
const { argMin = 1, argMax = 1 } = p;
|
|
155
174
|
if (argMax > 0 && argMin > argMax) {
|
|
156
175
|
throw new CliSchemaValidationError(
|
|
157
|
-
`argMin must not exceed argMax for positional ${
|
|
176
|
+
`argMin must not exceed argMax for positional ${scopeKey}/${p.name}`,
|
|
158
177
|
);
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -165,7 +184,7 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
165
184
|
if (argMin === 0) {
|
|
166
185
|
sawOptional = true;
|
|
167
186
|
} else if (sawOptional) {
|
|
168
|
-
throw new CliSchemaValidationError(`Required positional after optional in scope ${
|
|
187
|
+
throw new CliSchemaValidationError(`Required positional after optional in scope ${scopeKey}`);
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
190
|
|
|
@@ -173,12 +192,8 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
173
192
|
const { argMax = 1 } = positionals[idx]!;
|
|
174
193
|
if (argMax === 0 && idx + 1 < positionals.length) {
|
|
175
194
|
throw new CliSchemaValidationError(
|
|
176
|
-
`Unlimited positional (argMax == 0) must be last in scope ${
|
|
195
|
+
`Unlimited positional (argMax == 0) must be last in scope ${scopeKey}`,
|
|
177
196
|
);
|
|
178
197
|
}
|
|
179
198
|
}
|
|
180
|
-
|
|
181
|
-
for (const child of cmd.commands ?? []) {
|
|
182
|
-
walkCommand(child, false);
|
|
183
|
-
}
|
|
184
199
|
}
|