argsbarg 1.5.0 → 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.
Files changed (49) hide show
  1. package/.cursor/plans/cliprogram_capabilities_refactor_081e1737.plan.md +224 -0
  2. package/CHANGELOG.md +24 -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 +40 -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 +21 -6
  25. package/src/help.ts +21 -9
  26. package/src/index.test.ts +71 -64
  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 +33 -24
  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 +89 -71
package/src/schema.ts CHANGED
@@ -2,12 +2,12 @@
2
2
  This module serializes the CLI schema tree to JSON for machine-readable introspection.
3
3
  */
4
4
 
5
- import { CliCommand } from "./types.ts";
5
+ import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
6
6
  import { exportPresentationBuiltins, type CliSchemaExport } from "./builtins/export.ts";
7
7
 
8
8
  const RESERVED = new Set(["completion", "install", "mcp"]);
9
9
 
10
- function exportCommand(cmd: CliCommand): CliSchemaExport {
10
+ function exportCommand(cmd: CliNode): CliSchemaExport {
11
11
  const out: CliSchemaExport = {
12
12
  key: cmd.key,
13
13
  description: cmd.description,
@@ -21,11 +21,11 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
21
21
  out.options = cmd.options;
22
22
  }
23
23
 
24
- if ("handler" in cmd && cmd.handler) {
24
+ if (isCliLeaf(cmd)) {
25
25
  if ((cmd.positionals ?? []).length > 0) {
26
26
  out.positionals = cmd.positionals;
27
27
  }
28
- out.commands = exportPresentationBuiltins(cmd);
28
+ out.commands = exportPresentationBuiltins(cmd as CliProgram);
29
29
  return out;
30
30
  }
31
31
 
@@ -36,7 +36,7 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
36
36
  out.fallbackMode = cmd.fallbackMode;
37
37
  }
38
38
 
39
- const children = (cmd.commands ?? []).filter((ch) => !RESERVED.has(ch.key));
39
+ const children = isCliRouter(cmd) ? cmd.commands.filter((ch) => !RESERVED.has(ch.key)) : [];
40
40
  if (children.length > 0) {
41
41
  out.commands = children.map(exportCommand);
42
42
  }
@@ -44,7 +44,7 @@ function exportCommand(cmd: CliCommand): CliSchemaExport {
44
44
  return out;
45
45
  }
46
46
 
47
- export function cliSchemaJson(root: CliCommand): string {
47
+ export function cliSchemaJson(root: CliProgram): string {
48
48
  return JSON.stringify(exportCommand(root), null, 2) + "\n";
49
49
  }
50
50
 
@@ -5,7 +5,7 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
5
5
  import { collectOptionDefs } from "../parse.ts";
6
6
  import { cliSchemaJson } from "../schema.ts";
7
7
  import { collectMcpTools, sanitizeToolSegment } from "../mcp/tools.ts";
8
- import { CliCommand, CliOptionKind } from "../types.ts";
8
+ import { CliProgram, CliOptionKind } from "../types.ts";
9
9
 
10
10
  export type SkillTarget = "cursor" | "claude";
11
11
 
@@ -22,7 +22,7 @@ function truncate(text: string, maxLen: number): string {
22
22
  }
23
23
 
24
24
  /** Builds third-person skill description for YAML frontmatter. */
25
- function skillDescription(root: CliCommand): string {
25
+ function skillDescription(root: CliProgram): string {
26
26
  const tools = collectMcpTools(root);
27
27
  const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
28
28
  const sample = paths.slice(0, 5).join(", ");
@@ -32,7 +32,7 @@ function skillDescription(root: CliCommand): string {
32
32
  }
33
33
 
34
34
  /** Formats one command line for the catalog section. */
35
- function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcpTools>[number]): string {
35
+ function formatCommandEntry(root: CliProgram, tool: ReturnType<typeof collectMcpTools>[number]): string {
36
36
  const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
37
37
  let line = `- **\`${cliPath}\`** — ${tool.description}`;
38
38
  const opts = collectOptionDefs(root, tool.path);
@@ -52,7 +52,7 @@ function formatCommandEntry(root: CliCommand, tool: ReturnType<typeof collectMcp
52
52
  }
53
53
 
54
54
  /** Builds SKILL.md body for the given target. */
55
- function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): string {
55
+ function buildSkillMd(root: CliProgram, target: SkillTarget, dirName: string): string {
56
56
  const name = sanitizeToolSegment(root.key);
57
57
  const description = skillDescription(root);
58
58
  const tools = collectMcpTools(root);
@@ -159,7 +159,7 @@ function buildSkillMd(root: CliCommand, target: SkillTarget, dirName: string): s
159
159
  }
160
160
 
161
161
  /** Builds reference.md with pretty-printed schema JSON. */
162
- function buildReferenceMd(root: CliCommand): string {
162
+ function buildReferenceMd(root: CliProgram): string {
163
163
  return [
164
164
  `# ${root.key} — CLI reference`,
165
165
  "",
@@ -173,7 +173,7 @@ function buildReferenceMd(root: CliCommand): string {
173
173
  }
174
174
 
175
175
  /** Generates SKILL.md and reference.md for Cursor or Claude Code. */
176
- export function generateSkillBundle(root: CliCommand, target: SkillTarget): SkillBundle {
176
+ export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
177
177
  const dirName = sanitizeToolSegment(root.key);
178
178
  return {
179
179
  dirName,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { CliCommand } from "../types.ts";
4
+ import { CliProgram } from "../types.ts";
5
5
  import { generateSkillBundle, type SkillTarget } from "./generate.ts";
6
6
 
7
7
  export interface SkillInstallOpts {
@@ -24,7 +24,7 @@ function resolveSkillDir(target: SkillTarget, dirName: string, global: boolean):
24
24
  }
25
25
 
26
26
  /** Writes SKILL.md and reference.md; returns changed file paths. */
27
- export function cliSkillInstall(root: CliCommand, target: SkillTarget, opts: SkillInstallOpts): string[] {
27
+ export function cliSkillInstall(root: CliProgram, target: SkillTarget, opts: SkillInstallOpts): string[] {
28
28
  const bundle = generateSkillBundle(root, target);
29
29
  const dir = resolveSkillDir(target, bundle.dirName, opts.global ?? false);
30
30
  const changed: string[] = [];
@@ -0,0 +1,40 @@
1
+ /*
2
+ Compile-only checks that invalid schema shapes fail type-checking.
3
+ */
4
+
5
+ import type { CliLeaf, CliNode, CliProgram, CliRouter } from "./types.ts";
6
+
7
+ const _routerOnly: CliRouter = {
8
+ key: "app",
9
+ description: "",
10
+ commands: [],
11
+ };
12
+
13
+ const _leafOnly: CliLeaf = {
14
+ key: "run",
15
+ description: "",
16
+ handler: () => {},
17
+ };
18
+
19
+ const _program: CliProgram = {
20
+ key: "app",
21
+ description: "",
22
+ mcpServer: {},
23
+ commands: [],
24
+ };
25
+
26
+ const _badMcpOnNode = {
27
+ key: "x",
28
+ description: "",
29
+ // @ts-expect-error mcpServer is program-root only
30
+ mcpServer: {},
31
+ commands: [],
32
+ } satisfies CliNode;
33
+
34
+ const _badInstallOnNode = {
35
+ key: "x",
36
+ description: "",
37
+ // @ts-expect-error install is program-root only
38
+ install: { enabled: false },
39
+ handler: () => {},
40
+ } satisfies CliNode;
package/src/types.ts CHANGED
@@ -2,8 +2,6 @@
2
2
  This module defines the CLI schema, option kinds, and fallback modes.
3
3
  It is the shared declarative model that parsing, validation, help, and completion all
4
4
  read from, so the package has one source of truth.
5
-
6
- It gives the package one shared model for both library users and internal modules.
7
5
  */
8
6
 
9
7
  import type { CliContext } from "./context.ts";
@@ -47,7 +45,7 @@ export enum CliFallbackMode {
47
45
  }
48
46
 
49
47
  /**
50
- * A named flag or value option (`--long`, `-short`), listed on `CliCommand.options`.
48
+ * A named flag or value option (`--long`, `-short`), listed on command `options`.
51
49
  */
52
50
  export interface CliOption {
53
51
  /** Option name (e.g., "name", "verbose"). */
@@ -68,7 +66,7 @@ export interface CliOption {
68
66
  }
69
67
 
70
68
  /**
71
- * An ordered positional argument slot, listed on `CliCommand.positionals`.
69
+ * An ordered positional argument slot, listed on leaf `positionals`.
72
70
  */
73
71
  export interface CliPositional {
74
72
  /** Positional name (used in help and error messages). */
@@ -90,7 +88,7 @@ export interface CliPositional {
90
88
  }
91
89
 
92
90
  /**
93
- * Root-only. Enables `myapp mcp` and MCP stdio server metadata.
91
+ * Enables `myapp mcp` and MCP stdio server metadata (program root only).
94
92
  */
95
93
  export interface CliMcpServerConfig {
96
94
  /** `initialize` serverInfo.name (default: root `key`). */
@@ -154,7 +152,7 @@ export interface CliMcpToolConfig {
154
152
  }
155
153
 
156
154
  /**
157
- * Root-only. Opt-out and defaults for the `install` built-in (compiled binaries only).
155
+ * Opt-out and defaults for the `install` built-in (compiled binaries only; program root only).
158
156
  */
159
157
  export interface CliInstallConfig {
160
158
  /** When `false`, hide/disable `install` (default: enabled). */
@@ -164,9 +162,9 @@ export interface CliInstallConfig {
164
162
  }
165
163
 
166
164
  /**
167
- * Base properties shared by all command nodes.
165
+ * Base properties shared by all nodes in the user command tree.
168
166
  */
169
- export interface CliCommandBase {
167
+ export interface CliNodeBase {
170
168
  /** Program or command key (e.g., "myapp", "stat", "owner"). */
171
169
  key: string;
172
170
  /** Short description shown in help. */
@@ -175,45 +173,57 @@ export interface CliCommandBase {
175
173
  notes?: string;
176
174
  /** Global or command-level flags/options. */
177
175
  options?: CliOption[];
178
- /** Root-only. When set, enables the `mcp` built-in subcommand. */
176
+ }
177
+
178
+ /**
179
+ * A leaf command node with a handler and optional positionals.
180
+ */
181
+ export type CliLeaf = CliNodeBase & {
182
+ /** Handler function for leaf commands. */
183
+ handler: CliHandler;
184
+ /** Positional argument definitions. */
185
+ positionals?: CliPositional[];
186
+ /** Per-tool MCP exposure and metadata. */
187
+ mcpTool?: CliMcpToolConfig;
188
+ };
189
+
190
+ /**
191
+ * A routing command node with nested subcommands.
192
+ */
193
+ export type CliRouter = CliNodeBase & {
194
+ /** Nested subcommands. */
195
+ commands: CliNode[];
196
+ /** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
197
+ fallbackCommand?: string;
198
+ /** How fallbackCommand is applied at this routing node. */
199
+ fallbackMode?: CliFallbackMode;
200
+ };
201
+
202
+ /**
203
+ * A node in the user-defined command tree (router or leaf).
204
+ */
205
+ export type CliNode = CliLeaf | CliRouter;
206
+
207
+ /**
208
+ * Program root passed to `cliRun` / `cliInvoke`.
209
+ * May be a leaf or router, plus optional program-level MCP and install config.
210
+ */
211
+ export type CliProgram = CliNode & {
212
+ /** When set, enables the `mcp` built-in subcommand. */
179
213
  mcpServer?: CliMcpServerConfig;
180
- /** Root-only. Opt-out and defaults for `install` (compiled binaries only). */
214
+ /** Opt-out and defaults for `install` (compiled binaries only). */
181
215
  install?: CliInstallConfig;
182
- /** Leaf-only. Per-tool MCP exposure and metadata. */
183
- mcpTool?: CliMcpToolConfig;
216
+ };
217
+
218
+ /** True when the node is a leaf (has a handler). */
219
+ export function isCliLeaf(node: CliNode): node is CliLeaf {
220
+ return "handler" in node && typeof node.handler === "function";
184
221
  }
185
222
 
186
- /**
187
- * A command node: either a routing group (has commands) or a leaf (has handler).
188
- *
189
- * The value passed to cliRun is the program root: name is the app/binary name.
190
- * The root may be a routing group or a leaf command.
191
- */
192
- export type CliCommand =
193
- | (CliCommandBase & {
194
- /** Handler function for leaf commands. */
195
- handler: CliHandler;
196
- /** Positional argument definitions. */
197
- positionals?: CliPositional[];
198
- /** Nested subcommands (empty for leaf commands). */
199
- commands?: never;
200
- /** Default subcommand (routing commands only). */
201
- fallbackCommand?: never;
202
- /** How fallbackCommand is applied at this routing node (routing commands only). */
203
- fallbackMode?: never;
204
- })
205
- | (CliCommandBase & {
206
- /** Nested subcommands. */
207
- commands: CliCommand[];
208
- /** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
209
- fallbackCommand?: string;
210
- /** How fallbackCommand is applied at this routing node (not root-only). */
211
- fallbackMode?: CliFallbackMode;
212
- /** Handler function (leaf commands only). */
213
- handler?: never;
214
- /** Positional argument definitions (leaf commands only). */
215
- positionals?: never;
216
- });
223
+ /** True when the node is a router (has subcommands). */
224
+ export function isCliRouter(node: CliNode): node is CliRouter {
225
+ return "commands" in node && Array.isArray(node.commands);
226
+ }
217
227
 
218
228
  /**
219
229
  * Handler closure type for leaf commands.
@@ -222,7 +232,7 @@ export type CliCommand =
222
232
  export type CliHandler = (ctx: CliContext) => void | Promise<void>;
223
233
 
224
234
  /**
225
- * Error thrown when the static CliCommand tree violates ArgsBarg rules.
235
+ * Error thrown when the static CLI tree violates ArgsBarg rules.
226
236
  */
227
237
  export class CliSchemaValidationError extends Error {
228
238
  /** Creates a schema validation error with a human-readable rule violation. */
package/src/validate.ts CHANGED
@@ -2,58 +2,68 @@
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
- }
33
+ /** @deprecated Internal alias use cliValidateProgram */
34
+ export const cliValidateRoot = cliValidateProgram;
37
35
 
38
- if (!isRoot && cmd.install !== undefined) {
39
- throw new CliSchemaValidationError(
40
- "install is only supported on the program root (not on " + cmd.key + ")",
41
- );
36
+ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
37
+ if (!isRoot) {
38
+ const rogue = node as CliProgram;
39
+ if (rogue.mcpServer !== undefined) {
40
+ throw new CliSchemaValidationError(
41
+ "mcpServer is only supported on the program root (not on " + node.key + ")",
42
+ );
43
+ }
44
+ if (rogue.install !== undefined) {
45
+ throw new CliSchemaValidationError(
46
+ "install is only supported on the program root (not on " + node.key + ")",
47
+ );
48
+ }
42
49
  }
43
50
 
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");
51
+ if (isCliLeaf(node)) {
52
+ if (isRoot && node.mcpTool !== undefined) {
53
+ throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
54
+ }
55
+ } else {
56
+ const rogue = node as unknown as CliLeaf;
57
+ if (rogue.mcpTool !== undefined) {
58
+ throw new CliSchemaValidationError(
59
+ "mcpTool is only supported on leaf commands (not on " + node.key + ")",
60
+ );
61
+ }
52
62
  }
53
63
 
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);
64
+ if (isRoot && program.mcpServer?.resources) {
65
+ const schemaUri = program.mcpServer.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
66
+ const uris = program.mcpServer.resources.map((r) => r.uri);
57
67
  if (uris.includes(schemaUri)) {
58
68
  throw new CliSchemaValidationError(
59
69
  `mcpServer.resources URI '${schemaUri}' conflicts with the built-in schema resource`,
@@ -64,53 +74,64 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
64
74
  }
65
75
  }
66
76
 
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}`);
77
+ if (isCliRouter(node)) {
78
+ const seenNames = new Set<string>();
79
+ for (const child of node.commands) {
80
+ if (seenNames.has(child.key)) {
81
+ throw new CliSchemaValidationError(`Duplicate command name: ${child.key}`);
82
+ }
83
+ seenNames.add(child.key);
71
84
  }
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
85
 
81
- if (cmd.fallbackCommand !== undefined) {
82
- const children = cmd.commands ?? [];
83
- const valid = children.find((c) => c.key === cmd.fallbackCommand);
84
- if (!valid) {
86
+ if (node.fallbackMode !== undefined && node.fallbackCommand === undefined) {
85
87
  throw new CliSchemaValidationError(
86
- `fallbackCommand '${cmd.fallbackCommand}' is not a child of '${cmd.key}'`,
88
+ `fallbackMode requires fallbackCommand on '${node.key}'`,
87
89
  );
88
90
  }
91
+
92
+ if (node.fallbackCommand !== undefined) {
93
+ const valid = node.commands.find((c) => c.key === node.fallbackCommand);
94
+ if (!valid) {
95
+ throw new CliSchemaValidationError(
96
+ `fallbackCommand '${node.fallbackCommand}' is not a child of '${node.key}'`,
97
+ );
98
+ }
99
+ }
100
+
101
+ for (const child of node.commands) {
102
+ walkNode(child, program, false);
103
+ }
89
104
  }
90
105
 
106
+ const positionals = isCliLeaf(node) ? (node.positionals ?? []) : [];
107
+ validateOptions(node.key, node.options ?? []);
108
+ validatePositionals(node.key, positionals);
109
+ }
110
+
111
+ function validateOptions(scopeKey: string, options: import("./types.ts").CliOption[]): void {
91
112
  const seenShorts = new Set<string>();
92
- for (const opt of cmd.options ?? []) {
113
+ for (const opt of options) {
93
114
  if (opt.required && opt.kind === CliOptionKind.Presence) {
94
115
  throw new CliSchemaValidationError(
95
- `Presence option cannot be required: ${cmd.key}/${opt.name}`,
116
+ `Presence option cannot be required: ${scopeKey}/${opt.name}`,
96
117
  );
97
118
  }
98
119
 
99
120
  if (opt.name === "schema") {
100
121
  throw new CliSchemaValidationError(
101
- `Option name "schema" is reserved for --schema: ${cmd.key}/${opt.name}`,
122
+ `Option name "schema" is reserved for --schema: ${scopeKey}/${opt.name}`,
102
123
  );
103
124
  }
104
125
 
105
126
  if (opt.shortName !== undefined) {
106
127
  if (opt.shortName === "h") {
107
128
  throw new CliSchemaValidationError(
108
- `Short alias -h is reserved for help: ${cmd.key}/${opt.name}`,
129
+ `Short alias -h is reserved for help: ${scopeKey}/${opt.name}`,
109
130
  );
110
131
  }
111
132
  if (seenShorts.has(opt.shortName)) {
112
133
  throw new CliSchemaValidationError(
113
- `Duplicate short alias -${opt.shortName} in scope ${cmd.key}`,
134
+ `Duplicate short alias -${opt.shortName} in scope ${scopeKey}`,
114
135
  );
115
136
  }
116
137
  seenShorts.add(opt.shortName);
@@ -119,42 +140,43 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
119
140
  if (opt.kind === CliOptionKind.Enum) {
120
141
  if (!opt.choices || opt.choices.length === 0) {
121
142
  throw new CliSchemaValidationError(
122
- `Option '${opt.name}' on '${cmd.key}': Enum kind requires non-empty choices`,
143
+ `Option '${opt.name}' on '${scopeKey}': Enum kind requires non-empty choices`,
123
144
  );
124
145
  }
125
146
  if (new Set(opt.choices).size !== opt.choices.length) {
126
147
  throw new CliSchemaValidationError(
127
- `Option '${opt.name}' on '${cmd.key}': Enum choices must be distinct`,
148
+ `Option '${opt.name}' on '${scopeKey}': Enum choices must be distinct`,
128
149
  );
129
150
  }
130
151
  for (const choice of opt.choices) {
131
152
  if (choice.length === 0) {
132
153
  throw new CliSchemaValidationError(
133
- `Option '${opt.name}' on '${cmd.key}': Enum choices must be non-empty strings`,
154
+ `Option '${opt.name}' on '${scopeKey}': Enum choices must be non-empty strings`,
134
155
  );
135
156
  }
136
157
  }
137
158
  } else if (opt.choices !== undefined) {
138
159
  throw new CliSchemaValidationError(
139
- `Option '${opt.name}' on '${cmd.key}': choices is only valid for Enum kind`,
160
+ `Option '${opt.name}' on '${scopeKey}': choices is only valid for Enum kind`,
140
161
  );
141
162
  }
142
163
  }
164
+ }
143
165
 
144
- const positionals = cmd.positionals ?? [];
166
+ function validatePositionals(scopeKey: string, positionals: import("./types.ts").CliPositional[]): void {
145
167
  for (const p of positionals) {
146
168
  if (p.argMin !== undefined && p.argMin < 0) {
147
- throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${cmd.key}/${p.name}`);
169
+ throw new CliSchemaValidationError(`argMin must be >= 0 for positional ${scopeKey}/${p.name}`);
148
170
  }
149
171
  if (p.argMax !== undefined && p.argMax < 0) {
150
172
  throw new CliSchemaValidationError(
151
- `argMax must be >= 0 (use 0 for unlimited) for positional ${cmd.key}/${p.name}`,
173
+ `argMax must be >= 0 (use 0 for unlimited) for positional ${scopeKey}/${p.name}`,
152
174
  );
153
175
  }
154
176
  const { argMin = 1, argMax = 1 } = p;
155
177
  if (argMax > 0 && argMin > argMax) {
156
178
  throw new CliSchemaValidationError(
157
- `argMin must not exceed argMax for positional ${cmd.key}/${p.name}`,
179
+ `argMin must not exceed argMax for positional ${scopeKey}/${p.name}`,
158
180
  );
159
181
  }
160
182
  }
@@ -165,7 +187,7 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
165
187
  if (argMin === 0) {
166
188
  sawOptional = true;
167
189
  } else if (sawOptional) {
168
- throw new CliSchemaValidationError(`Required positional after optional in scope ${cmd.key}`);
190
+ throw new CliSchemaValidationError(`Required positional after optional in scope ${scopeKey}`);
169
191
  }
170
192
  }
171
193
 
@@ -173,12 +195,8 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
173
195
  const { argMax = 1 } = positionals[idx]!;
174
196
  if (argMax === 0 && idx + 1 < positionals.length) {
175
197
  throw new CliSchemaValidationError(
176
- `Unlimited positional (argMax == 0) must be last in scope ${cmd.key}`,
198
+ `Unlimited positional (argMax == 0) must be last in scope ${scopeKey}`,
177
199
  );
178
200
  }
179
201
  }
180
-
181
- for (const child of cmd.commands ?? []) {
182
- walkCommand(child, false);
183
- }
184
202
  }