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.
Files changed (49) hide show
  1. package/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
  2. package/CHANGELOG.md +31 -1
  3. package/README.md +12 -8
  4. package/docs/install.md +2 -2
  5. package/docs/mcp.md +3 -3
  6. package/examples/mcp-test.ts +3 -3
  7. package/examples/minimal.ts +3 -3
  8. package/examples/nested.ts +3 -3
  9. package/examples/option-required.ts +3 -3
  10. package/index.d.ts +38 -37
  11. package/package.json +1 -1
  12. package/src/builtins/builtins.test.ts +3 -3
  13. package/src/builtins/completion-bash.ts +3 -3
  14. package/src/builtins/completion-fish.ts +2 -2
  15. package/src/builtins/completion-group.ts +2 -2
  16. package/src/builtins/completion-zsh.ts +3 -3
  17. package/src/builtins/dispatch.ts +41 -26
  18. package/src/builtins/export.ts +15 -8
  19. package/src/builtins/install.ts +3 -3
  20. package/src/builtins/mcp.ts +2 -2
  21. package/src/builtins/presentation.ts +34 -23
  22. package/src/builtins/scopes.ts +9 -8
  23. package/src/capabilities.ts +32 -0
  24. package/src/context.ts +17 -7
  25. package/src/help.ts +21 -9
  26. package/src/index.test.ts +128 -121
  27. package/src/index.ts +1 -1
  28. package/src/install/binary.ts +3 -3
  29. package/src/install/completions.ts +2 -2
  30. package/src/install/detect-installed.ts +1 -1
  31. package/src/install/index.ts +4 -4
  32. package/src/install/install.test.ts +2 -2
  33. package/src/install/mcp-config.ts +2 -2
  34. package/src/install/paths.ts +3 -3
  35. package/src/install/plan.ts +4 -4
  36. package/src/install/status.ts +2 -2
  37. package/src/install/uninstall.ts +2 -2
  38. package/src/invoke.ts +14 -5
  39. package/src/mcp/server.ts +3 -3
  40. package/src/mcp/tools.ts +16 -16
  41. package/src/mcp.ts +2 -2
  42. package/src/parse.ts +55 -27
  43. package/src/runtime.ts +34 -25
  44. package/src/schema.ts +6 -6
  45. package/src/skill/generate.ts +6 -6
  46. package/src/skill/install.ts +2 -2
  47. package/src/types.test.ts +40 -0
  48. package/src/types.ts +54 -44
  49. 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
- CliCommand,
7
- CliFallbackMode,
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
- function reservedCommandNames(root: CliCommand): string[] {
14
- const names = ["completion", "install"];
15
- if (root.mcpServer !== undefined) {
16
- names.push("mcp");
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
- export function cliValidateRoot(root: CliCommand): void {
22
- for (const child of root.commands ?? []) {
23
- if (reservedCommandNames(root).includes(child.key)) {
24
- throw new CliSchemaValidationError(`Reserved command name: ${child.key}`);
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
- walkCommand(root, true);
30
+ walkNode(program, program, true);
29
31
  }
30
32
 
31
- function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
32
- if (!isRoot && cmd.mcpServer !== undefined) {
33
- throw new CliSchemaValidationError(
34
- "mcpServer is only supported on the program root (not on " + cmd.key + ")",
35
- );
36
- }
37
-
38
- if (!isRoot && cmd.install !== undefined) {
39
- throw new CliSchemaValidationError(
40
- "install is only supported on the program root (not on " + cmd.key + ")",
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
- const isLeaf = "handler" in cmd && !!cmd.handler;
45
- if (!isLeaf && cmd.mcpTool !== undefined) {
46
- throw new CliSchemaValidationError(
47
- "mcpTool is only supported on leaf commands (not on " + cmd.key + ")",
48
- );
49
- }
50
- if (isRoot && cmd.mcpTool !== undefined) {
51
- throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
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 && cmd.mcpServer?.resources) {
55
- const schemaUri = cmd.mcpServer.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
56
- const uris = cmd.mcpServer.resources.map((r) => r.uri);
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
- const seenNames = new Set<string>();
68
- for (const child of cmd.commands ?? []) {
69
- if (seenNames.has(child.key)) {
70
- throw new CliSchemaValidationError(`Duplicate command name: ${child.key}`);
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
- if (cmd.fallbackCommand !== undefined) {
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
- `fallbackCommand '${cmd.fallbackCommand}' is not a child of '${cmd.key}'`,
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 cmd.options ?? []) {
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: ${cmd.key}/${opt.name}`,
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: ${cmd.key}/${opt.name}`,
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: ${cmd.key}/${opt.name}`,
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 ${cmd.key}`,
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 '${cmd.key}': Enum kind requires non-empty choices`,
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 '${cmd.key}': Enum choices must be distinct`,
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 '${cmd.key}': Enum choices must be non-empty strings`,
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 '${cmd.key}': choices is only valid for Enum kind`,
157
+ `Option '${opt.name}' on '${scopeKey}': choices is only valid for Enum kind`,
140
158
  );
141
159
  }
142
160
  }
161
+ }
143
162
 
144
- const positionals = cmd.positionals ?? [];
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 ${cmd.key}/${p.name}`);
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 ${cmd.key}/${p.name}`,
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 ${cmd.key}/${p.name}`,
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 ${cmd.key}`);
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 ${cmd.key}`,
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
  }