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
@@ -1,4 +1,4 @@
1
- import { CliCommand, CliOptionKind } from "../types.ts";
1
+ import { CliNode, CliRouter, CliOptionKind } from "../types.ts";
2
2
  import { collectScopes, type ScopeRec } from "./scopes.ts";
3
3
  import {
4
4
  escShellSingleQuoted,
@@ -161,7 +161,7 @@ function emitEnumReplyBash(ident: string, scopes: ScopeRec[]): string {
161
161
  return o;
162
162
  }
163
163
 
164
- function emitMainBodyBash(schema: CliCommand, ident: string): string {
164
+ function emitMainBodyBash(schema: CliRouter, ident: string): string {
165
165
  const main = mainName(schema.key);
166
166
  let o = "_${main}() {\n".replace("${main}", main);
167
167
  o += " local cur=\"${COMP_WORDS[COMP_CWORD]}\"\n";
@@ -196,7 +196,7 @@ function emitMainBodyBash(schema: CliCommand, ident: string): string {
196
196
  }
197
197
 
198
198
  /** Returns a self-contained bash `complete` script for the given program schema. */
199
- export function completionBashScript(schema: CliCommand): string {
199
+ export function completionBashScript(schema: CliRouter): string {
200
200
  const ident = identToken(schema.key);
201
201
  const scopes = collectScopes(schema);
202
202
  const pathIndex: Record<string, number> = {};
@@ -1,4 +1,4 @@
1
- import { CliCommand, CliOptionKind } from "../types.ts";
1
+ import { CliNode, CliRouter, CliOptionKind } from "../types.ts";
2
2
  import { collectScopes } from "./scopes.ts";
3
3
  import {
4
4
  escFishSingleQuoted,
@@ -27,7 +27,7 @@ function scopeCondition(ident: string, scopeIndex: number, path: string): string
27
27
  }
28
28
 
29
29
  /** Returns a self-contained fish completion script for the given program schema. */
30
- export function completionFishScript(schema: CliCommand): string {
30
+ export function completionFishScript(schema: CliRouter): string {
31
31
  const ident = identToken(schema.key);
32
32
  const app = schema.key;
33
33
  const scopes = collectScopes(schema);
@@ -1,9 +1,9 @@
1
- import { CliCommand } from "../types.ts";
1
+ import { type CliLeaf, type CliNode, type CliRouter } from "../types.ts";
2
2
 
3
3
  /**
4
4
  * Builds the static `completion` / `bash` / `zsh` / `fish` command subtree (merged into the program root at runtime).
5
5
  */
6
- export function cliBuiltinCompletionGroup(appName: string): CliCommand {
6
+ export function cliBuiltinCompletionGroup(appName: string): CliRouter {
7
7
  return {
8
8
  key: "completion",
9
9
  description: "Generate the autocompletion script for shells.",
@@ -1,4 +1,4 @@
1
- import { CliCommand, CliOptionKind } from "../types.ts";
1
+ import { CliNode, CliRouter, CliOptionKind } from "../types.ts";
2
2
  import { collectScopes, type ScopeRec } from "./scopes.ts";
3
3
  import {
4
4
  escShellSingleQuoted,
@@ -191,7 +191,7 @@ function emitEnumReplyZsh(ident: string, scopes: ScopeRec[]): string {
191
191
  return o;
192
192
  }
193
193
 
194
- function emitMainBodyZsh(schema: CliCommand, ident: string): string {
194
+ function emitMainBodyZsh(schema: CliRouter, ident: string): string {
195
195
  const main = mainName(schema.key);
196
196
  let o = "_${main}() {\n".replace("${main}", main);
197
197
  o += " local curcontext=\"$curcontext\" ret=1\n";
@@ -224,7 +224,7 @@ function emitMainBodyZsh(schema: CliCommand, ident: string): string {
224
224
  }
225
225
 
226
226
  /** Returns a self-contained zsh completion script for the given program schema. */
227
- export function completionZshScript(schema: CliCommand): string {
227
+ export function completionZshScript(schema: CliRouter): string {
228
228
  const ident = identToken(schema.key);
229
229
  const scopes = collectScopes(schema);
230
230
  const pathIndex: Record<string, number> = {};
@@ -1,10 +1,13 @@
1
- import { CliCommand } from "../types.ts";
1
+ import { resolveCapabilities } from "../capabilities.ts";
2
+ import type { CliNode, CliProgram, CliRouter } from "../types.ts";
3
+ import { isCliLeaf, isCliRouter } from "../types.ts";
2
4
  import { completionBashScript } from "./completion-bash.ts";
3
5
  import { completionFishScript } from "./completion-fish.ts";
4
6
  import { completionZshScript } from "./completion-zsh.ts";
5
7
  import { cliBuiltinInstallCommand } from "./install.ts";
6
8
  import { cliBuiltinMcpCommand } from "./mcp.ts";
7
9
  import { cliBuiltinCompletionGroup as completionGroup } from "./completion-group.ts";
10
+ import { cliPresentationRoot } from "./presentation.ts";
8
11
  import { cliMcpServeStdio } from "../mcp.ts";
9
12
  import { cliInstall } from "../install/index.ts";
10
13
  import { isCompiledExecutable } from "../install/compiled.ts";
@@ -13,22 +16,32 @@ import { ParseKind } from "../parse.ts";
13
16
 
14
17
  export interface DispatchBuiltinOpts {
15
18
  isLeafCompletionIntercept: boolean;
16
- parseRoot: CliCommand;
19
+ parseRoot: CliRouter;
20
+ }
21
+
22
+ function completionSchema(program: CliProgram, opts: DispatchBuiltinOpts): CliRouter {
23
+ if (opts.isLeafCompletionIntercept) {
24
+ return cliPresentationRoot(program);
25
+ }
26
+ return opts.parseRoot;
17
27
  }
18
28
 
19
29
  /**
20
30
  * Handles built-in commands after parse.
21
31
  */
22
32
  export async function dispatchBuiltin(
23
- root: CliCommand,
33
+ program: CliProgram,
24
34
  pr: ParseResult,
25
35
  opts: DispatchBuiltinOpts,
26
36
  ): Promise<void> {
27
37
  if (pr.kind !== ParseKind.Ok) {
28
38
  return;
29
39
  }
40
+
41
+ const caps = resolveCapabilities(program);
42
+
30
43
  if (pr.path[0] === "completion") {
31
- const schemaForCompletion = opts.isLeafCompletionIntercept ? root : opts.parseRoot;
44
+ const schemaForCompletion = completionSchema(program, opts);
32
45
  if (pr.path[1] === "bash") {
33
46
  process.stdout.write(completionBashScript(schemaForCompletion));
34
47
  process.exit(0);
@@ -45,7 +58,7 @@ export async function dispatchBuiltin(
45
58
  }
46
59
 
47
60
  if (pr.path[0] === "mcp") {
48
- if (!root.mcpServer) {
61
+ if (!caps.mcp) {
49
62
  process.stderr.write("MCP is not enabled. Set mcpServer on the program root.\n");
50
63
  process.exit(1);
51
64
  }
@@ -53,7 +66,7 @@ export async function dispatchBuiltin(
53
66
  process.stderr.write("Unknown subcommand: mcp " + pr.path.slice(1).join(" ") + "\n");
54
67
  process.exit(1);
55
68
  }
56
- await cliMcpServeStdio(root);
69
+ await cliMcpServeStdio(program);
57
70
  process.exit(0);
58
71
  }
59
72
 
@@ -64,7 +77,7 @@ export async function dispatchBuiltin(
64
77
  );
65
78
  process.exit(1);
66
79
  }
67
- if (root.install?.enabled === false) {
80
+ if (!caps.install) {
68
81
  process.stderr.write("install is disabled. Remove install.enabled: false from the program root.\n");
69
82
  process.exit(1);
70
83
  }
@@ -72,52 +85,54 @@ export async function dispatchBuiltin(
72
85
  process.stderr.write("Unknown subcommand: install " + pr.path.slice(1).join(" ") + "\n");
73
86
  process.exit(1);
74
87
  }
75
- await cliInstall(root, pr.opts);
88
+ await cliInstall(program, pr.opts);
76
89
  }
77
90
  }
78
91
 
79
92
  /** Built-in intercept roots for leaf programs. */
80
93
  export function builtinInterceptRoot(
81
- root: CliCommand,
94
+ program: CliProgram,
82
95
  argv: string[],
83
- ): { parseRoot: CliCommand; isLeafCompletionIntercept: boolean } {
84
- if (!root.handler || argv.length < 1) {
85
- return { parseRoot: root, isLeafCompletionIntercept: false };
96
+ ): { parseRoot: CliNode; isLeafCompletionIntercept: boolean } {
97
+ if (!isCliLeaf(program) || argv.length < 1) {
98
+ return { parseRoot: program, isLeafCompletionIntercept: false };
86
99
  }
87
100
 
101
+ const caps = resolveCapabilities(program);
88
102
  const first = argv[0];
103
+
89
104
  if (first === "completion") {
90
105
  return {
91
106
  parseRoot: {
92
- key: root.key,
93
- description: root.description,
94
- commands: [completionGroup(root.key)],
95
- } as CliCommand,
107
+ key: program.key,
108
+ description: program.description,
109
+ commands: [completionGroup(program.key)],
110
+ },
96
111
  isLeafCompletionIntercept: true,
97
112
  };
98
113
  }
99
114
 
100
- if (first === "install" && isCompiledExecutable() && root.install?.enabled !== false) {
115
+ if (first === "install" && caps.install) {
101
116
  return {
102
117
  parseRoot: {
103
- key: root.key,
104
- description: root.description,
105
- commands: [cliBuiltinInstallCommand(root)],
106
- } as CliCommand,
118
+ key: program.key,
119
+ description: program.description,
120
+ commands: [cliBuiltinInstallCommand(program)],
121
+ },
107
122
  isLeafCompletionIntercept: false,
108
123
  };
109
124
  }
110
125
 
111
- if (first === "mcp" && root.mcpServer !== undefined) {
126
+ if (first === "mcp" && caps.mcp) {
112
127
  return {
113
128
  parseRoot: {
114
- key: root.key,
115
- description: root.description,
129
+ key: program.key,
130
+ description: program.description,
116
131
  commands: [cliBuiltinMcpCommand()],
117
- } as CliCommand,
132
+ },
118
133
  isLeafCompletionIntercept: false,
119
134
  };
120
135
  }
121
136
 
122
- return { parseRoot: root, isLeafCompletionIntercept: false };
137
+ return { parseRoot: program, isLeafCompletionIntercept: false };
123
138
  }
@@ -1,8 +1,8 @@
1
- import { CliCommand, CliFallbackMode, CliOption, CliPositional } from "../types.ts";
1
+ import { type CliCapabilities, resolveCapabilities } from "../capabilities.ts";
2
+ import type { CliFallbackMode, CliOption, CliPositional, CliProgram } from "../types.ts";
2
3
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
3
4
  import { cliBuiltinInstallCommand } from "./install.ts";
4
5
  import { cliBuiltinMcpCommand } from "./mcp.ts";
5
- import { isCompiledExecutable } from "../install/compiled.ts";
6
6
 
7
7
  /** JSON-safe command node (no handlers). */
8
8
  export interface CliSchemaExport {
@@ -16,7 +16,13 @@ export interface CliSchemaExport {
16
16
  positionals?: CliPositional[];
17
17
  }
18
18
 
19
- function exportBuiltinNode(cmd: CliCommand): CliSchemaExport {
19
+ function exportBuiltinNode(cmd: {
20
+ key: string;
21
+ description: string;
22
+ notes?: string;
23
+ options?: CliOption[];
24
+ commands?: CliSchemaExport[];
25
+ }): CliSchemaExport {
20
26
  const out: CliSchemaExport = {
21
27
  key: cmd.key,
22
28
  description: cmd.description,
@@ -34,12 +40,13 @@ function exportBuiltinNode(cmd: CliCommand): CliSchemaExport {
34
40
  }
35
41
 
36
42
  /** Built-in subtrees matching help visibility for `--schema` export. */
37
- export function exportPresentationBuiltins(root: CliCommand): CliSchemaExport[] {
38
- const builtins: CliSchemaExport[] = [exportBuiltinNode(cliBuiltinCompletionGroup(root.key))];
39
- if (isCompiledExecutable() && root.install?.enabled !== false) {
40
- builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(root)));
43
+ export function exportPresentationBuiltins(program: CliProgram, caps?: CliCapabilities): CliSchemaExport[] {
44
+ const resolved = caps ?? resolveCapabilities(program);
45
+ const builtins: CliSchemaExport[] = [exportBuiltinNode(cliBuiltinCompletionGroup(program.key))];
46
+ if (resolved.install) {
47
+ builtins.push(exportBuiltinNode(cliBuiltinInstallCommand(program)));
41
48
  }
42
- if (root.mcpServer !== undefined) {
49
+ if (resolved.mcp) {
43
50
  builtins.push(exportBuiltinNode(cliBuiltinMcpCommand()));
44
51
  }
45
52
  return builtins;
@@ -1,7 +1,7 @@
1
- import { CliCommand, CliOption, CliOptionKind } from "../types.ts";
1
+ import { CliProgram, CliOption, CliOptionKind, type CliLeaf } from "../types.ts";
2
2
 
3
3
  /** Install command options (dynamic: `--mcp` only when MCP is enabled). */
4
- export function installBuiltinOptions(root: CliCommand): CliOption[] {
4
+ export function installBuiltinOptions(root: CliProgram): CliOption[] {
5
5
  const opts: CliOption[] = [
6
6
  {
7
7
  name: "all",
@@ -78,7 +78,7 @@ export function installBuiltinOptions(root: CliCommand): CliOption[] {
78
78
  }
79
79
 
80
80
  /** Builds the `install` built-in command (compiled binaries only). */
81
- export function cliBuiltinInstallCommand(root: CliCommand): CliCommand {
81
+ export function cliBuiltinInstallCommand(root: CliProgram): CliLeaf {
82
82
  return {
83
83
  key: "install",
84
84
  description: "Install the binary, shell completions, agent skills, and MCP config to your user environment.",
@@ -1,7 +1,7 @@
1
- import { CliCommand, CliOption, CliOptionKind } from "../types.ts";
1
+ import { type CliLeaf } from "../types.ts";
2
2
 
3
3
  /** Presence options for the top-level `mcp` built-in (leaf). */
4
- export function cliBuiltinMcpCommand(): CliCommand {
4
+ export function cliBuiltinMcpCommand(): CliLeaf {
5
5
  return {
6
6
  key: "mcp",
7
7
  description: "Run as an MCP server over stdio for AI agents.",
@@ -1,39 +1,50 @@
1
- import { CliCommand } from "../types.ts";
2
- import { isCompiledExecutable } from "../install/compiled.ts";
1
+ import type { CliCapabilities } from "../capabilities.ts";
2
+ import { resolveCapabilities } from "../capabilities.ts";
3
+ import type { CliLeaf, CliNode, CliProgram, CliRouter } from "../types.ts";
4
+ import { isCliLeaf, isCliRouter } from "../types.ts";
3
5
  import { cliBuiltinCompletionGroup } from "./completion-group.ts";
4
6
  import { cliBuiltinInstallCommand } from "./install.ts";
5
7
  import { cliBuiltinMcpCommand } from "./mcp.ts";
6
8
 
7
- /** Built-in commands shown in help and merged for routing CLIs. */
8
- export function presentationBuiltins(root: CliCommand): CliCommand[] {
9
- const builtins: CliCommand[] = [cliBuiltinCompletionGroup(root.key)];
10
- if (isCompiledExecutable() && root.install?.enabled !== false) {
11
- builtins.push(cliBuiltinInstallCommand(root));
9
+ /** Built-in command nodes injected for help, schema, and completions. */
10
+ export function presentationBuiltins(program: CliProgram, caps: CliCapabilities): CliNode[] {
11
+ const builtins: CliNode[] = [cliBuiltinCompletionGroup(program.key)];
12
+ if (caps.install) {
13
+ builtins.push(cliBuiltinInstallCommand(program));
12
14
  }
13
- if (root.mcpServer !== undefined) {
15
+ if (caps.mcp) {
14
16
  builtins.push(cliBuiltinMcpCommand());
15
17
  }
16
18
  return builtins;
17
19
  }
18
20
 
19
21
  /**
20
- * Returns a schema suitable for help display, including reserved built-in subtrees.
21
- * Routing roots get builtins merged; leaf roots are wrapped as a tiny router.
22
+ * Returns a schema suitable for help display, including capability-built-in subtrees.
23
+ * Routing programs get builtins merged; leaf programs are wrapped as a tiny router.
22
24
  */
23
- export function cliPresentationRoot(root: CliCommand): CliCommand {
24
- if ((root.commands ?? []).some((c) => c.key === "completion")) {
25
- return root;
26
- }
27
- if ("handler" in root && root.handler) {
25
+ export function cliPresentationRoot(program: CliProgram): CliRouter {
26
+ const caps = resolveCapabilities(program);
27
+ const builtins = presentationBuiltins(program, caps);
28
+
29
+ if (isCliLeaf(program)) {
28
30
  return {
29
- key: root.key,
30
- description: root.description,
31
- options: root.options,
32
- commands: presentationBuiltins(root),
33
- } as CliCommand;
31
+ key: program.key,
32
+ description: program.description,
33
+ options: program.options,
34
+ commands: builtins,
35
+ };
34
36
  }
37
+
35
38
  return {
36
- ...root,
37
- commands: [...(root.commands ?? []), ...presentationBuiltins(root)],
38
- } as CliCommand;
39
+ key: program.key,
40
+ description: program.description,
41
+ notes: program.notes,
42
+ options: program.options,
43
+ fallbackCommand: program.fallbackCommand,
44
+ fallbackMode: program.fallbackMode,
45
+ commands: [...program.commands, ...builtins],
46
+ };
39
47
  }
48
+
49
+ /** Presentation tree may include builtin leaf stubs. */
50
+ export type CliPresentationNode = CliNode | CliLeaf;
@@ -2,35 +2,36 @@
2
2
  Shared completion scope walk used by bash, zsh, and fish emitters.
3
3
  */
4
4
 
5
- import { CliCommand } from "../types.ts";
5
+ import { type CliNode, type CliRouter, isCliLeaf, isCliRouter } from "../types.ts";
6
6
 
7
7
  /** One tab-completion scope: child commands, options, and path key for the schema walk. */
8
8
  export interface ScopeRec {
9
- kids: CliCommand[];
9
+ kids: CliNode[];
10
10
  opts: import("../types.ts").CliOption[];
11
11
  path: string;
12
12
  wantsFiles: boolean;
13
13
  }
14
14
 
15
- function hasPositionalArguments(cmd: CliCommand): boolean {
16
- return (cmd.positionals ?? []).length > 0;
15
+ function hasPositionalArguments(cmd: CliNode): boolean {
16
+ return isCliLeaf(cmd) && (cmd.positionals ?? []).length > 0;
17
17
  }
18
18
 
19
- function walkScopes(cmdPath: string, cmd: CliCommand, acc: ScopeRec[]): void {
19
+ function walkScopes(cmdPath: string, cmd: CliNode, acc: ScopeRec[]): void {
20
+ const kids = isCliRouter(cmd) ? cmd.commands : [];
20
21
  acc.push({
21
- kids: cmd.commands ?? [],
22
+ kids,
22
23
  opts: cmd.options ?? [],
23
24
  path: cmdPath,
24
25
  wantsFiles: hasPositionalArguments(cmd),
25
26
  });
26
- for (const ch of cmd.commands ?? []) {
27
+ for (const ch of kids) {
27
28
  const nextPath = cmdPath === "" ? ch.key : cmdPath + "/" + ch.key;
28
29
  walkScopes(nextPath, ch, acc);
29
30
  }
30
31
  }
31
32
 
32
33
  /** Flattens the schema into a list of completion scopes (root + every command path). */
33
- export function collectScopes(schema: CliCommand): ScopeRec[] {
34
+ export function collectScopes(schema: CliRouter): ScopeRec[] {
34
35
  const acc: ScopeRec[] = [];
35
36
  acc.push({
36
37
  kids: schema.commands ?? [],
@@ -0,0 +1,32 @@
1
+ /*
2
+ Internal capability resolver — decides which platform builtins are active for a program.
3
+ Not exported from the public package barrel.
4
+ */
5
+
6
+ import type { CliProgram } from "./types.ts";
7
+ import { isCompiledExecutable } from "./install/compiled.ts";
8
+
9
+ /** Platform builtins derived from program config and runtime. */
10
+ export interface CliCapabilities {
11
+ completion: true;
12
+ mcp: boolean;
13
+ install: boolean;
14
+ }
15
+
16
+ /** Resolves which capabilities are enabled for a program. */
17
+ export function resolveCapabilities(program: CliProgram): CliCapabilities {
18
+ return {
19
+ completion: true,
20
+ mcp: program.mcpServer !== undefined,
21
+ install: isCompiledExecutable() && program.install?.enabled !== false,
22
+ };
23
+ }
24
+
25
+ /** Reserved top-level command names for the given capabilities. */
26
+ export function reservedCommandNames(caps: CliCapabilities): string[] {
27
+ const names = ["completion", "install"];
28
+ if (caps.mcp) {
29
+ names.push("mcp");
30
+ }
31
+ return names;
32
+ }
package/src/context.ts CHANGED
@@ -7,7 +7,8 @@ It keeps handlers small with a typed read API for flags, strings, numbers, and c
7
7
  parsed values.
8
8
  */
9
9
 
10
- import type { CliCommand, CliInvocation } from "./types.ts";
10
+ import type { CliInvocation, CliNode, CliProgram } from "./types.ts";
11
+ import { isCliLeaf, isCliRouter } from "./types.ts";
11
12
  import { strictParseDouble } from "./utils.ts";
12
13
 
13
14
  /**
@@ -17,24 +18,24 @@ export class CliContext {
17
18
  readonly appName: string;
18
19
  readonly commandPath: string[];
19
20
  readonly args: string[];
20
- readonly schema: CliCommand;
21
+ readonly program: CliProgram;
21
22
  readonly opts: Record<string, string>;
22
23
  readonly invocation: CliInvocation;
23
24
 
24
- /** Captures the merged program root, routed path, positional words, and option map for a leaf handler. */
25
+ /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
25
26
  constructor(
26
27
  appName: string,
27
28
  commandPath: string[],
28
29
  args: string[],
29
30
  opts: Record<string, string>,
30
- schema: CliCommand,
31
+ program: CliProgram,
31
32
  invocation: CliInvocation = "cli",
32
33
  ) {
33
34
  this.appName = appName;
34
35
  this.commandPath = commandPath;
35
36
  this.args = args;
36
37
  this.opts = opts;
37
- this.schema = schema;
38
+ this.program = program;
38
39
  this.invocation = invocation;
39
40
  }
40
41
 
@@ -79,9 +80,13 @@ export class CliContext {
79
80
  private _positionalMap(): Record<string, string | string[]> {
80
81
  if (this._posMap) return this._posMap;
81
82
 
82
- let node: CliCommand = this.schema;
83
+ let node: CliNode = this.program;
83
84
  for (const seg of this.commandPath) {
84
- const child = (node.commands ?? []).find((c) => c.key === seg);
85
+ if (!isCliRouter(node)) {
86
+ this._posMap = {};
87
+ return {};
88
+ }
89
+ const child = node.commands.find((c) => c.key === seg);
85
90
  if (!child) {
86
91
  this._posMap = {};
87
92
  return {};
@@ -89,6 +94,11 @@ export class CliContext {
89
94
  node = child;
90
95
  }
91
96
 
97
+ if (!isCliLeaf(node)) {
98
+ this._posMap = {};
99
+ return {};
100
+ }
101
+
92
102
  const map: Record<string, string | string[]> = {};
93
103
  let argIdx = 0;
94
104
  for (const p of node.positionals ?? []) {
package/src/help.ts CHANGED
@@ -7,7 +7,7 @@ It keeps help formatting shared across help and error paths so users see one con
7
7
  style no matter how help is reached.
8
8
  */
9
9
 
10
- import { CliCommand, CliOption, CliOptionKind, CliPositional } from "./types.ts";
10
+ import { CliNode, CliOption, CliOptionKind, CliPositional, CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
11
11
 
12
12
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
13
13
 
@@ -369,7 +369,7 @@ function rowsForPositionals(defs: CliPositional[], color: boolean): HelpRow[] {
369
369
  }
370
370
 
371
371
  /** Table rows for subcommands, sorted by key. */
372
- function rowsForSubcommands(cmds: CliCommand[]): HelpRow[] {
372
+ function rowsForSubcommands(cmds: CliNode[]): HelpRow[] {
373
373
  return cmds
374
374
  .sort((a, b) => a.key.localeCompare(b.key))
375
375
  .map((c) => ({ label: c.key, description: c.description }));
@@ -381,7 +381,7 @@ function rowsForSubcommands(cmds: CliCommand[]): HelpRow[] {
381
381
  * Renders full help for the app root or a nested command, following `helpPath` from the root key.
382
382
  * `useStderr` is reserved for call-site consistency; width and color use stdout TTY.
383
383
  */
384
- export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr: boolean): string {
384
+ export function cliHelpRender(schema: CliRouter, helpPath: string[], useStderr: boolean): string {
385
385
  const hw = getHelpWidth();
386
386
  const color = isStdoutTTY();
387
387
 
@@ -416,14 +416,14 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
416
416
  }
417
417
 
418
418
  let layer = schema.commands ?? [];
419
- let node: CliCommand | undefined;
419
+ let node: CliNode | undefined;
420
420
  for (const seg of helpPath) {
421
- const ch = layer.find((c) => c.key === seg);
421
+ const ch = layer.find((c: CliNode) => c.key === seg);
422
422
  if (!ch) {
423
423
  return (color ? style.red("Unknown help path.") : "Unknown help path.") + "\n";
424
424
  }
425
425
  node = ch;
426
- layer = ch.commands ?? [];
426
+ layer = isCliRouter(ch) ? ch.commands : [];
427
427
  }
428
428
  if (!node) {
429
429
  return (color ? style.red("Unknown help path.") : "Unknown help path.") + "\n";
@@ -438,7 +438,13 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
438
438
  lines.push(
439
439
  renderTextBox(
440
440
  "Usage",
441
- usageLines(schema.key, helpPath, (node.commands ?? []).length > 0, (node.positionals ?? []).length > 0, color),
441
+ usageLines(
442
+ schema.key,
443
+ helpPath,
444
+ isCliRouter(node) && node.commands.length > 0,
445
+ isCliLeaf(node) && (node.positionals ?? []).length > 0,
446
+ color,
447
+ ),
442
448
  hw,
443
449
  color,
444
450
  ).join("\n"),
@@ -450,13 +456,19 @@ export function cliHelpRender(schema: CliCommand, helpPath: string[], useStderr:
450
456
  lines.push(optBox.join("\n"));
451
457
  }
452
458
 
453
- const posBox = renderTableBox("Arguments", rowsForPositionals(node.positionals ?? [], color), hw, color);
459
+ const posBox = renderTableBox(
460
+ "Arguments",
461
+ rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
462
+ hw,
463
+ color,
464
+ );
454
465
  if (posBox.length > 0) {
455
466
  lines.push("");
456
467
  lines.push(posBox.join("\n"));
457
468
  }
458
469
 
459
- const subBox = renderTableBox("Subcommands", rowsForSubcommands(node.commands ?? []), hw, color);
470
+ const subcmds = isCliRouter(node) ? node.commands : [];
471
+ const subBox = renderTableBox("Subcommands", rowsForSubcommands(subcmds), hw, color);
460
472
  if (subBox.length > 0) {
461
473
  lines.push("");
462
474
  lines.push(subBox.join("\n"));