argsbarg 6.1.2 → 6.1.3

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 (229) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +17 -19
  3. package/bin/argsbarg +10 -0
  4. package/docs/README.md +4 -3
  5. package/docs/ai-skills.md +4 -2
  6. package/docs/bundled-docs.md +50 -25
  7. package/docs/cli-program.md +52 -10
  8. package/docs/config-schema.md +10 -11
  9. package/docs/configure.md +2 -0
  10. package/docs/decisions.md +40 -0
  11. package/docs/developing.md +43 -5
  12. package/docs/http-server.md +171 -0
  13. package/docs/json-schema-subset.md +51 -0
  14. package/docs/mcp.md +4 -2
  15. package/docs/output-schema.md +55 -62
  16. package/examples/formats.ts +6 -6
  17. package/examples/full-example/Formula/full-example.rb +35 -0
  18. package/examples/full-example/README.md +20 -21
  19. package/examples/full-example/docs/README.md +1 -1
  20. package/examples/full-example/docs/cli-schema.json +1790 -98
  21. package/examples/full-example/docs/cli.md +1990 -0
  22. package/examples/full-example/docs/http.md +28 -29
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +783 -50
  25. package/examples/full-example/docs/skill.md +10 -10
  26. package/examples/full-example/justfile +11 -1
  27. package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  28. package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
  29. package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
  30. package/examples/full-example/src/commands/render-json/command.ts +30 -0
  31. package/examples/full-example/src/commands/render-json/types.ts +9 -0
  32. package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  33. package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
  34. package/examples/full-example/src/commands/status/command.ts +5 -13
  35. package/examples/full-example/src/commands/status/types.ts +1 -14
  36. package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  37. package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
  38. package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
  39. package/examples/full-example/src/commands/workspaces/command.ts +94 -0
  40. package/examples/full-example/src/commands/workspaces/types.ts +6 -0
  41. package/examples/full-example/src/db/index.test.ts +86 -0
  42. package/examples/full-example/src/db/index.ts +101 -0
  43. package/examples/full-example/src/db/migrate.test.ts +35 -0
  44. package/examples/full-example/src/db/migrate.ts +69 -0
  45. package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
  46. package/examples/full-example/src/db/tables/workspaces.ts +66 -0
  47. package/examples/full-example/src/program.ts +11 -36
  48. package/examples/full-example/src/types/argsbarg.d.ts +11 -0
  49. package/examples/full-example/src/types/md.d.ts +4 -0
  50. package/examples/full-example/tsconfig.json +5 -2
  51. package/examples/mcp-test.ts +1 -2
  52. package/examples/minimal.ts +1 -7
  53. package/examples/nested.ts +1 -2
  54. package/examples/option-required.ts +1 -1
  55. package/examples/servers.ts +4 -5
  56. package/index.d.ts +431 -136
  57. package/package.json +19 -2
  58. package/src/builtins/builtins.test.ts +7 -7
  59. package/src/builtins/completion-bash.ts +1 -1
  60. package/src/builtins/completion-fish.ts +1 -1
  61. package/src/builtins/completion-group.ts +4 -4
  62. package/src/builtins/completion-simulate-shared.ts +9 -0
  63. package/src/builtins/completion-zsh.ts +1 -1
  64. package/src/builtins/config.test.ts +3 -3
  65. package/src/builtins/config.ts +9 -9
  66. package/src/builtins/configure-copy.ts +2 -2
  67. package/src/builtins/configure.ts +4 -4
  68. package/src/builtins/dispatch.ts +19 -18
  69. package/src/builtins/export.ts +7 -5
  70. package/src/builtins/http.ts +68 -0
  71. package/src/builtins/mcp.ts +28 -4
  72. package/src/builtins/presentation.ts +6 -6
  73. package/src/builtins/registry.ts +6 -6
  74. package/src/builtins/scopes.ts +2 -2
  75. package/src/builtins/version.ts +1 -1
  76. package/src/cli-tool/full-example-capabilities.test.ts +10 -15
  77. package/src/cli-tool/main.ts +1 -1
  78. package/src/cli-tool/program.ts +3 -2
  79. package/src/cli-tool/prompt.ts +1 -1
  80. package/src/cli-tool/run-schemagen.ts +1 -3
  81. package/src/cli-tool/schemagen/cleanup.ts +6 -7
  82. package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
  83. package/src/cli-tool/schemagen/index.ts +2 -2
  84. package/src/cli-tool/schemagen/names.ts +8 -13
  85. package/src/cli-tool/schemagen/run.ts +21 -28
  86. package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
  87. package/src/config/bindings.test.ts +1 -1
  88. package/src/config/bindings.ts +1 -1
  89. package/src/config/bootstrap.test.ts +1 -1
  90. package/src/config/bootstrap.ts +36 -4
  91. package/src/config/context.test.ts +1 -1
  92. package/src/config/context.ts +1 -1
  93. package/src/config/entry.ts +1 -1
  94. package/src/config/file.test.ts +1 -1
  95. package/src/config/file.ts +3 -3
  96. package/src/config/manifest.ts +1 -1
  97. package/src/config/resolve.test.ts +1 -1
  98. package/src/config/resolve.ts +1 -1
  99. package/src/config/schema.ts +1 -1
  100. package/src/config/validate.ts +1 -1
  101. package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
  102. package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
  103. package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
  104. package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
  105. package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
  106. package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
  107. package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
  108. package/src/{install → configure/artifacts}/paths.ts +5 -5
  109. package/src/configure/artifacts/plan.ts +24 -0
  110. package/src/{install → configure/artifacts}/status.test.ts +1 -1
  111. package/src/{install → configure/artifacts}/status.ts +2 -2
  112. package/src/{install → configure/artifacts}/target-base.ts +1 -1
  113. package/src/{install → configure/artifacts}/target-detect.ts +1 -1
  114. package/src/{install → configure/artifacts}/target-effective.ts +3 -9
  115. package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
  116. package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
  117. package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
  118. package/src/{install → configure/artifacts}/target-registry.ts +2 -2
  119. package/src/{install → configure/artifacts}/target-scope.ts +3 -3
  120. package/src/{install → configure/artifacts}/target-skill.ts +1 -1
  121. package/src/{install → configure/artifacts}/target-types.ts +2 -2
  122. package/src/{install → configure/artifacts}/targets/app.ts +5 -5
  123. package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
  124. package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
  125. package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
  126. package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
  127. package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
  128. package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
  129. package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
  130. package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
  131. package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
  132. package/src/{install → configure/artifacts}/targets/index.ts +1 -1
  133. package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
  134. package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
  135. package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
  136. package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
  137. package/src/{install → configure/artifacts}/targets.test.ts +1 -1
  138. package/src/{install → configure/artifacts}/uninstall.ts +1 -1
  139. package/src/configure/configure.test.ts +11 -11
  140. package/src/configure/index.ts +14 -14
  141. package/src/configure/prompt.ts +2 -2
  142. package/src/{context.ts → core/context.ts} +26 -20
  143. package/src/{json-leaf.test.ts → core/json-leaf.test.ts} +4 -4
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +16 -12
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +129 -31
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +225 -35
  150. package/src/{validate.ts → core/validate.ts} +39 -29
  151. package/src/docs/builtin.ts +8 -19
  152. package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
  153. package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
  154. package/src/docs/docs.test.ts +76 -41
  155. package/src/docs/http-guide.ts +37 -34
  156. package/src/docs/mcp-guide.ts +12 -14
  157. package/src/docs/mcp-resources.test.ts +2 -3
  158. package/src/docs/mcp-resources.ts +6 -11
  159. package/src/docs/resolve.ts +22 -30
  160. package/src/docs/save.ts +3 -3
  161. package/src/exports/cli.ts +47 -0
  162. package/src/exports/headless.ts +13 -0
  163. package/src/exports/http.ts +6 -0
  164. package/src/exports/mcp.ts +6 -0
  165. package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
  166. package/src/{headless.ts → headless/routing.ts} +3 -3
  167. package/src/headless/tool-call.ts +114 -46
  168. package/src/help.test.ts +152 -0
  169. package/src/help.ts +3 -3
  170. package/src/hooks/builtin.ts +20 -0
  171. package/src/hooks/run.ts +142 -0
  172. package/src/http/openapi.ts +182 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +16 -5
  175. package/src/http/routes.ts +329 -0
  176. package/src/http/server.ts +225 -0
  177. package/src/index.ts +36 -25
  178. package/src/log/ecs.test.ts +43 -0
  179. package/src/log/ecs.ts +59 -0
  180. package/src/log/emitter.ts +166 -0
  181. package/src/mcp/bundle.ts +2 -2
  182. package/src/mcp/claude.test.ts +1 -1
  183. package/src/mcp/claude.ts +4 -4
  184. package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
  185. package/src/mcp/result.ts +2 -2
  186. package/src/mcp/server.ts +54 -6
  187. package/src/mcp/tools.ts +9 -20
  188. package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
  189. package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
  190. package/src/{cli.ts → runtime/cli.ts} +159 -49
  191. package/src/runtime/exposure.ts +102 -0
  192. package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
  193. package/src/server/context.ts +25 -0
  194. package/src/server/overrides.ts +112 -0
  195. package/src/skill/generate.ts +8 -8
  196. package/src/skill/hint.ts +1 -1
  197. package/src/skill/install.ts +2 -2
  198. package/src/skill/naming.ts +1 -1
  199. package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
  200. package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
  201. package/src/{api.integration.test.ts → test/integration/http.test.ts} +170 -67
  202. package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
  203. package/docs/api-server.md +0 -141
  204. package/examples/full-example/docs/api.md +0 -511
  205. package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
  206. package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
  207. package/examples/full-example/src/config/__generated__/index.ts +0 -5
  208. package/examples/full-example/src/config/types.ts +0 -24
  209. package/src/api/openapi.ts +0 -117
  210. package/src/api/server.ts +0 -120
  211. package/src/builtins/api.ts +0 -38
  212. package/src/hidden.ts +0 -30
  213. package/src/install/plan.ts +0 -53
  214. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  215. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  216. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  220. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  222. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  223. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  224. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  225. /package/src/{formats.ts → core/formats.ts} +0 -0
  226. /package/src/{respond.ts → core/respond.ts} +0 -0
  227. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  228. /package/src/{api → http}/schema-deref.test.ts +0 -0
  229. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -7,23 +7,23 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { $ } from "bun";
10
- import { completionBashScript, completionZshScript } from "./builtins/index.ts";
11
- import { cliPresentationRoot } from "./builtins/presentation.ts";
12
- import { cliHelpRender } from "./help.ts";
13
- import { Cli, CliFallbackMode, CliOptionKind, type CliProgram } from "./index.ts";
14
- import { applyShellEnv } from "./mcp/env.ts";
15
- import { allMcpResources, collectMcpTools, mcpToolCallToArgv, resolveMcpSchemaUri } from "./mcp/tools.ts";
16
- import { ParseKind, parse, postParseValidate } from "./parse.ts";
17
- import { cliSchemaJson } from "./schema.ts";
18
- import { generatePluginSkillBundle, generateSkillBundle } from "./skill/generate.ts";
19
- import { cliSkillInstall } from "./skill/install.ts";
10
+ import { completionBashScript, completionZshScript } from "~/builtins";
11
+ import { cliPresentationRoot } from "~/builtins/presentation.ts";
12
+ import { cliHelpRender } from "~/help.ts";
13
+ import { Cli, CliFallbackMode, CliOptionKind, type CliProgram } from "~/index";
14
+ import { applyShellEnv } from "~/mcp/env.ts";
15
+ import { allMcpResources, collectMcpTools, mcpToolCallToArgv, resolveMcpSchemaUri } from "~/mcp/tools.ts";
16
+ import { generatePluginSkillBundle, generateSkillBundle } from "~/skill/generate.ts";
17
+ import { cliSkillInstall } from "~/skill/install.ts";
20
18
  import {
21
19
  enumMcpFixture,
22
20
  nestedDocsFallbackFixture,
23
21
  nestedMcpFixture,
24
22
  testProgram,
25
23
  varargsReadFixture,
26
- } from "./test-fixtures.ts";
24
+ } from "~/test/fixtures.ts";
25
+ import { ParseKind, parse, postParseValidate } from "./parse.ts";
26
+ import { cliSchemaJson } from "./schema.ts";
27
27
  import { cliValidateProgram } from "./validate.ts";
28
28
 
29
29
  /** Tests that bundled short presence flags. */
@@ -115,6 +115,85 @@ test("fallback missing or unknown root flags", () => {
115
115
  expect(pr.opts.name).toBe("bob");
116
116
  });
117
117
 
118
+ test("param router descent captures pathParams", () => {
119
+ const root = testProgram({
120
+ key: "app",
121
+ description: "",
122
+ commands: [
123
+ {
124
+ key: "workspaces",
125
+ description: "Workspaces.",
126
+ commands: [
127
+ {
128
+ key: ":id",
129
+ description: "One workspace.",
130
+ commands: [
131
+ {
132
+ key: "get",
133
+ description: "Get workspace.",
134
+ handler: () => {},
135
+ },
136
+ ],
137
+ },
138
+ ],
139
+ },
140
+ ],
141
+ });
142
+ cliValidateProgram(root);
143
+ const pr = postParseValidate(root, parse(root, ["workspaces", "qa2", "get"]));
144
+ expect(pr.kind).toBe(ParseKind.Ok);
145
+ expect(pr.path).toEqual(["workspaces", ":id", "get"]);
146
+ expect(pr.pathParams).toEqual({ id: "qa2" });
147
+ });
148
+
149
+ test("cli.enabled cascade blocks disabled router", () => {
150
+ const root = testProgram({
151
+ key: "app",
152
+ description: "",
153
+ commands: [
154
+ {
155
+ key: "workspaces",
156
+ description: "Disabled.",
157
+ cli: { enabled: false },
158
+ commands: [
159
+ {
160
+ key: "list",
161
+ description: "List.",
162
+ handler: () => {},
163
+ },
164
+ ],
165
+ },
166
+ ],
167
+ });
168
+ cliValidateProgram(root);
169
+ const pr = parse(root, ["workspaces", "list"]);
170
+ expect(pr.kind).toBe(ParseKind.Error);
171
+ expect(pr.errorMsg).toContain("Unknown command");
172
+ });
173
+
174
+ test("completion match child emits param router fallback", () => {
175
+ const root = testProgram({
176
+ key: "app",
177
+ description: "Test",
178
+ commands: [
179
+ {
180
+ key: "workspaces",
181
+ description: "Workspaces.",
182
+ commands: [
183
+ {
184
+ key: ":id",
185
+ description: "One workspace.",
186
+ commands: [{ key: "get", description: "Get.", handler: () => {} }],
187
+ },
188
+ ],
189
+ },
190
+ ],
191
+ });
192
+ cliValidateProgram(root);
193
+ const bash = completionBashScript(cliPresentationRoot(root));
194
+ expect(bash).toContain("*) echo");
195
+ });
196
+
118
197
  test("unknown command", () => {
119
198
  const root = testProgram({
120
199
  key: "app",
@@ -551,7 +630,6 @@ test("root --schema is no longer a flag", () => {
551
630
  version: "1.0.0",
552
631
  description: "demo",
553
632
  docs: {
554
- enabled: true,
555
633
  topics: { readme: { text: "# readme\n" } },
556
634
  },
557
635
  commands: [
@@ -639,98 +717,6 @@ test("cliSchemaExport resolves {argsbarg:program} in consumer notes", () => {
639
717
  expect(schema.commands[0].notes).toBe("Run `myapp run` to start.");
640
718
  });
641
719
 
642
- /** Docs help lists schema, api, and skill subcommands. */
643
- test("docs help lists schema, api, and skill subcommands", () => {
644
- const root = testProgram({
645
- key: "app",
646
- version: "1.0.0",
647
- description: "demo",
648
- docs: {
649
- enabled: true,
650
- topics: { readme: { text: "# readme\n" } },
651
- },
652
- commands: [
653
- {
654
- key: "x",
655
- description: "cmd",
656
- handler: () => {},
657
- },
658
- ],
659
- });
660
- const help = cliHelpRender(cliPresentationRoot(root), ["docs"], false);
661
- expect(help).toContain("cli-schema");
662
- expect(help).toContain("Print the full CLI command tree as JSON.");
663
- expect(help).toContain("api");
664
- expect(help).toContain("markdown");
665
- expect(help).toContain("skill");
666
- expect(help).toContain("reference agent SKILL");
667
- });
668
-
669
- /** Root help omits legacy --schema flag. */
670
- test("root help omits legacy --schema flag", () => {
671
- const root = testProgram({
672
- key: "app",
673
- version: "1.0.0",
674
- description: "demo",
675
- commands: [
676
- {
677
- key: "x",
678
- description: "cmd",
679
- handler: () => {},
680
- },
681
- ],
682
- });
683
- const help = cliHelpRender(cliPresentationRoot(root), [], false);
684
- expect(help).not.toContain("--schema");
685
- });
686
-
687
- /** Root help shows agent docs hint when docs enabled. */
688
- test("root help shows agent docs hint when docs enabled", () => {
689
- const root = testProgram({
690
- key: "myapp",
691
- version: "1.0.0",
692
- description: "demo",
693
- docs: {
694
- enabled: true,
695
- topics: { readme: { text: "# readme\n" } },
696
- },
697
- commands: [{ key: "run", description: "Run.", handler: () => {} }],
698
- });
699
- const help = cliHelpRender(cliPresentationRoot(root), [], false);
700
- expect(help).toContain("For AI agents: `myapp docs skill`.");
701
- expect(help).not.toContain("install --skill");
702
- });
703
-
704
- test("root help omits agent hint when docs disabled", () => {
705
- const root = testProgram({
706
- key: "myapp",
707
- version: "1.0.0",
708
- description: "demo",
709
- commands: [{ key: "run", description: "Run.", handler: () => {} }],
710
- });
711
- const help = cliHelpRender(cliPresentationRoot(root), [], false);
712
- expect(help).not.toContain("Agents:");
713
- expect(help).not.toContain("docs skill");
714
- });
715
-
716
- /** Root help includes program notes and agent hint. */
717
- test("root help includes program notes and agent hint", () => {
718
- const root = testProgram({
719
- key: "myapp",
720
- version: "1.0.0",
721
- description: "demo",
722
- notes: "See `{argsbarg:program} docs readme` for the user guide.",
723
- docs: {
724
- enabled: true,
725
- topics: { readme: { text: "# readme\n" } },
726
- },
727
- commands: [{ key: "run", description: "Run.", handler: () => {} }],
728
- });
729
- const help = cliHelpRender(cliPresentationRoot(root), [], false);
730
- expect(help).toContain("See `myapp docs readme` for the user guide.");
731
- expect(help).toContain("myapp docs skill");
732
- });
733
-
734
720
  test("Enum option inputSchema includes enum array", () => {
735
721
  const tools = collectMcpTools(enumMcpFixture);
736
722
  const run = tools.find((t) => t.name === "run")!;
@@ -814,14 +800,14 @@ test("cliValidateProgram rejects empty mcpServer", () => {
814
800
  expect(() => cliValidateProgram(root)).toThrow(/mcpServer requires enabled: true/);
815
801
  });
816
802
 
817
- test("cliValidateProgram rejects empty apiServer", () => {
803
+ test("cliValidateProgram rejects empty httpServer", () => {
818
804
  const root = testProgram({
819
805
  key: "app",
820
806
  description: "",
821
- apiServer: {} as { enabled: boolean },
807
+ httpServer: {} as { enabled: boolean },
822
808
  handler: () => {},
823
809
  });
824
- expect(() => cliValidateProgram(root)).toThrow(/apiServer requires enabled: true/);
810
+ expect(() => cliValidateProgram(root)).toThrow(/httpServer requires enabled: true/);
825
811
  });
826
812
 
827
813
  test("resolveMcpSchemaUri uses sanitized root key", () => {
@@ -877,7 +863,7 @@ test("cliValidateProgram rejects resource URI matching auto docs topic", () => {
877
863
  const root = testProgram({
878
864
  key: "app",
879
865
  description: "",
880
- docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
866
+ docs: { topics: { readme: { text: "# r\n" } } },
881
867
  mcpServer: {
882
868
  enabled: true,
883
869
  resources: [{ uri: "app://docs/readme", name: "dup", load: () => "" }],
@@ -908,7 +894,7 @@ test("allMcpResources includes docs topic resources", () => {
908
894
  const root = testProgram({
909
895
  key: "app",
910
896
  description: "",
911
- docs: { enabled: true, topics: { readme: { text: "# hi\n" } } },
897
+ docs: { topics: { readme: { text: "# hi\n" } } },
912
898
  mcpServer: { enabled: true },
913
899
  commands: [{ key: "leaf", description: "", handler: () => {} }],
914
900
  });
@@ -971,6 +957,7 @@ test("nested fallback MissingOrUnknown routes unknown token to default", () => {
971
957
  const root = testProgram({
972
958
  key: "app",
973
959
  description: "",
960
+ docs: { enabled: false },
974
961
  commands: [
975
962
  {
976
963
  key: "docs",
@@ -1021,6 +1008,7 @@ test("cliValidateProgram rejects invalid nested fallbackCommand", () => {
1021
1008
  const root = testProgram({
1022
1009
  key: "app",
1023
1010
  description: "",
1011
+ docs: { enabled: false },
1024
1012
  commands: [
1025
1013
  {
1026
1014
  key: "docs",
@@ -7,6 +7,8 @@ It keeps handler dispatch and help on one parser so the CLI behavior stays consi
7
7
  across every entry path.
8
8
  */
9
9
 
10
+ import { isCliCallable } from "~/runtime/exposure.ts";
11
+ import { fullStringIsDouble } from "~/utils.ts";
10
12
  import { formatValidationError, validateFormatValue } from "./formats.ts";
11
13
  import {
12
14
  CliFallbackMode,
@@ -14,11 +16,11 @@ import {
14
16
  type CliNode,
15
17
  type CliOption,
16
18
  CliOptionKind,
19
+ type CliRouter,
17
20
  isCliLeaf,
18
21
  isCliRouter,
19
22
  isJsonLeaf,
20
23
  } from "./types.ts";
21
- import { fullStringIsDouble } from "./utils.ts";
22
24
 
23
25
  // ── Parse Result ──────────────────────────────────────────────────────────────
24
26
 
@@ -48,6 +50,8 @@ export interface ParseResult {
48
50
  helpExplicit: boolean;
49
51
  /** Path segments for scoped help (empty for root help). */
50
52
  helpPath: string[];
53
+ /** Path parameter values from `:param` router descent (e.g. `{ id: "qa2" }`). */
54
+ pathParams: Record<string, string>;
51
55
  /** User-facing error message when `kind === Error`. */
52
56
  errorMsg: string;
53
57
  /** Help path to render next to an error (for contextual help). */
@@ -69,6 +73,24 @@ function findChild(cmds: CliNode[], name: string): CliNode | undefined {
69
73
  return cmds.find((c) => c.key === name);
70
74
  }
71
75
 
76
+ function isParamRouterKey(key: string): boolean {
77
+ return key.startsWith(":");
78
+ }
79
+
80
+ /** Static (non-`:param`) child by key. */
81
+ function findStaticChild(cmds: CliNode[], name: string): CliNode | undefined {
82
+ const ch = cmds.find((c) => c.key === name);
83
+ if (!ch || isParamRouterKey(ch.key)) {
84
+ return undefined;
85
+ }
86
+ return ch;
87
+ }
88
+
89
+ /** The single `:param` router child at this level, if any. */
90
+ function findParamChild(cmds: CliNode[]): CliNode | undefined {
91
+ return cmds.find((c) => isParamRouterKey(c.key));
92
+ }
93
+
72
94
  /** Resolves a long-option definition by name (without leading `--`). */
73
95
  function findOptionByName(defs: CliOption[], name: string): CliOption | undefined {
74
96
  return defs.find((o) => o.name === name);
@@ -245,6 +267,7 @@ function finishJsonLeaf(
245
267
  argv: string[],
246
268
  path: string[],
247
269
  opts: Record<string, string>,
270
+ pathParams: Record<string, string>,
248
271
  ): ParseResult {
249
272
  let idx = startIdx;
250
273
  const args: string[] = [];
@@ -252,20 +275,20 @@ function finishJsonLeaf(
252
275
  if (idx < argv.length) {
253
276
  const tok = argv[idx];
254
277
  if (isHelpTok(tok)) {
255
- return helpResult(path, true);
278
+ return helpResult(path, true, pathParams);
256
279
  }
257
280
  if (tok === "--") {
258
- return errorResult("Unexpected extra arguments", path, []);
281
+ return errorResult("Unexpected extra arguments", path, [], pathParams);
259
282
  }
260
283
  if (tok.startsWith("-")) {
261
- return errorResult(`JSON commands do not accept options: ${tok}`, path, []);
284
+ return errorResult(`JSON commands do not accept options: ${tok}`, path, [], pathParams);
262
285
  }
263
286
  args.push(tok);
264
287
  idx += 1;
265
288
  }
266
289
 
267
290
  if (idx < argv.length) {
268
- return errorResult("Unexpected extra arguments", path, []);
291
+ return errorResult("Unexpected extra arguments", path, [], pathParams);
269
292
  }
270
293
 
271
294
  return {
@@ -273,6 +296,7 @@ function finishJsonLeaf(
273
296
  path,
274
297
  opts,
275
298
  args,
299
+ pathParams,
276
300
  helpExplicit: false,
277
301
  helpPath: [],
278
302
  errorMsg: "",
@@ -289,6 +313,7 @@ function finishLeaf(
289
313
  opts: Record<string, string>,
290
314
  optionDefs: CliOption[],
291
315
  forcePositionalsIn: boolean,
316
+ pathParams: Record<string, string>,
292
317
  ): ParseResult {
293
318
  let idx = startIdx;
294
319
  const args: string[] = [];
@@ -299,7 +324,7 @@ function finishLeaf(
299
324
  if (argMax === 1) {
300
325
  if (argMin >= 1) {
301
326
  if (idx >= argv.length) {
302
- return errorResult(`Missing positional argument: ${p.name}`, path, []);
327
+ return errorResult(`Missing positional argument: ${p.name}`, path, [], pathParams);
303
328
  }
304
329
  args.push(argv[idx]);
305
330
  idx += 1;
@@ -327,14 +352,14 @@ function finishLeaf(
327
352
  }
328
353
 
329
354
  if (!forcePositionals && isHelpTok(tok)) {
330
- return helpResult(path, true);
355
+ return helpResult(path, true, pathParams);
331
356
  }
332
357
 
333
358
  if (!forcePositionals && tok.startsWith("-")) {
334
359
  // MUST be false — lenient mode swallows unknown flags as positionals silently
335
360
  const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
336
361
  if (tailRep.report.err) {
337
- return errorResult(tailRep.report.err, path, []);
362
+ return errorResult(tailRep.report.err, path, [], pathParams);
338
363
  }
339
364
  if (tailRep.report.sawDoubleDash) {
340
365
  forcePositionals = true;
@@ -343,7 +368,7 @@ function finishLeaf(
343
368
  idx = tailRep.nextIndex;
344
369
  continue;
345
370
  }
346
- return errorResult(`Unexpected option token: ${tok}`, path, []);
371
+ return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
347
372
  }
348
373
 
349
374
  args.push(tok);
@@ -358,27 +383,27 @@ function finishLeaf(
358
383
  }
359
384
  }
360
385
  if (count < argMin) {
361
- return errorResult(`Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`, path, []);
386
+ return errorResult(`Expected at least ${argMin} argument(s) for ${p.name}, got ${count}`, path, [], pathParams);
362
387
  }
363
388
  }
364
389
 
365
390
  if (idx < argv.length) {
366
391
  if (forcePositionals) {
367
- return errorResult("Unexpected extra arguments", path, []);
392
+ return errorResult("Unexpected extra arguments", path, [], pathParams);
368
393
  }
369
394
 
370
395
  if (isHelpTok(argv[idx])) {
371
- return helpResult(path, true);
396
+ return helpResult(path, true, pathParams);
372
397
  }
373
398
 
374
399
  const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
375
400
  if (tailRep.report.err) {
376
- return errorResult(tailRep.report.err, path, []);
401
+ return errorResult(tailRep.report.err, path, [], pathParams);
377
402
  }
378
403
  idx = tailRep.nextIndex;
379
404
 
380
405
  if (idx < argv.length) {
381
- return errorResult("Unexpected extra arguments", path, []);
406
+ return errorResult("Unexpected extra arguments", path, [], pathParams);
382
407
  }
383
408
  }
384
409
 
@@ -387,6 +412,7 @@ function finishLeaf(
387
412
  path,
388
413
  opts,
389
414
  args,
415
+ pathParams,
390
416
  helpExplicit: false,
391
417
  helpPath: [],
392
418
  errorMsg: "",
@@ -397,12 +423,18 @@ function finishLeaf(
397
423
  // ── Main Parser ───────────────────────────────────────────────────────────────
398
424
 
399
425
  /** Builds a user-error parse result; `path` defaults to `errorHelpPath`. */
400
- function errorResult(errorMsg: string, errorHelpPath: string[] = [], path: string[] = errorHelpPath): ParseResult {
426
+ function errorResult(
427
+ errorMsg: string,
428
+ errorHelpPath: string[] = [],
429
+ path: string[] = errorHelpPath,
430
+ pathParams: Record<string, string> = {},
431
+ ): ParseResult {
401
432
  return {
402
433
  kind: ParseKind.Error,
403
434
  path,
404
435
  opts: {},
405
436
  args: [],
437
+ pathParams,
406
438
  helpExplicit: false,
407
439
  helpPath: [],
408
440
  errorMsg,
@@ -411,12 +443,13 @@ function errorResult(errorMsg: string, errorHelpPath: string[] = [], path: strin
411
443
  }
412
444
 
413
445
  /** Builds a help-request result for the current routing path. */
414
- function helpResult(p: string[], explicit: boolean): ParseResult {
446
+ function helpResult(p: string[], explicit: boolean, pathParams: Record<string, string> = {}): ParseResult {
415
447
  return {
416
448
  kind: ParseKind.Help,
417
449
  path: [],
418
450
  opts: {},
419
451
  args: [],
452
+ pathParams,
420
453
  helpExplicit: explicit,
421
454
  helpPath: p,
422
455
  errorMsg: "",
@@ -424,13 +457,45 @@ function helpResult(p: string[], explicit: boolean): ParseResult {
424
457
  };
425
458
  }
426
459
 
460
+ type DescendResult = { ok: true; node: CliNode; cliEnabled: boolean } | { ok: false; error: ParseResult };
461
+
462
+ /** Descends into a static or `:param` child, updating path and pathParams. */
463
+ function descendChild(
464
+ parent: CliRouter,
465
+ tok: string,
466
+ path: string[],
467
+ pathParams: Record<string, string>,
468
+ cliEnabled: boolean,
469
+ ): DescendResult {
470
+ const staticChild = findStaticChild(parent.commands, tok);
471
+ if (staticChild) {
472
+ if (!isCliCallable(staticChild, cliEnabled)) {
473
+ return { ok: false, error: errorResult(`Unknown subcommand: ${tok}`, path, [], pathParams) };
474
+ }
475
+ path.push(tok);
476
+ return { ok: true, node: staticChild, cliEnabled: isCliCallable(staticChild, cliEnabled) };
477
+ }
478
+
479
+ const paramChild = findParamChild(parent.commands);
480
+ if (paramChild && isCliCallable(paramChild, cliEnabled)) {
481
+ const paramName = paramChild.key.slice(1);
482
+ path.push(paramChild.key);
483
+ pathParams[paramName] = tok;
484
+ return { ok: true, node: paramChild, cliEnabled: isCliCallable(paramChild, cliEnabled) };
485
+ }
486
+
487
+ return { ok: false, error: errorResult(`Unknown subcommand: ${tok}`, path, [], pathParams) };
488
+ }
489
+
427
490
  /**
428
491
  * Parses `argv` against the program root, routing into subcommands and filling `opts` / `args`.
429
492
  */
430
493
  export function parse(root: CliNode, argv: string[]): ParseResult {
431
494
  let i = 0;
432
495
  const path: string[] = [];
496
+ const pathParams: Record<string, string> = {};
433
497
  const opts: Record<string, string> = {};
498
+ let cliEnabled = true;
434
499
 
435
500
  const rootLenient =
436
501
  isCliRouter(root) &&
@@ -456,9 +521,9 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
456
521
 
457
522
  if (isCliLeaf(root)) {
458
523
  if (isJsonLeaf(root)) {
459
- return finishJsonLeaf(root, i, argv, path, opts);
524
+ return finishJsonLeaf(root, i, argv, path, opts, pathParams);
460
525
  }
461
- return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals);
526
+ return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals, pathParams);
462
527
  }
463
528
 
464
529
  if (i >= argv.length) {
@@ -477,12 +542,41 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
477
542
  }
478
543
  } else {
479
544
  const peek = argv[i];
480
- const childPick = !forcePositionals ? findChild(root.commands, peek) : undefined;
545
+ const childPick = !forcePositionals ? findStaticChild(root.commands, peek) : undefined;
481
546
 
482
547
  if (childPick !== undefined) {
548
+ if (!isCliCallable(childPick, cliEnabled)) {
549
+ return errorResult(`Unknown command: ${peek}`, path, [], pathParams);
550
+ }
483
551
  cmdName = peek;
484
552
  i += 1;
485
553
  node = childPick;
554
+ cliEnabled = isCliCallable(childPick, cliEnabled);
555
+ } else if (!forcePositionals && isCliRouter(root)) {
556
+ const paramChild = findParamChild(root.commands);
557
+ if (paramChild && isCliCallable(paramChild, cliEnabled)) {
558
+ cmdName = paramChild.key;
559
+ pathParams[paramChild.key.slice(1)] = peek;
560
+ i += 1;
561
+ node = paramChild;
562
+ cliEnabled = isCliCallable(paramChild, cliEnabled);
563
+ } else {
564
+ const fallbackCommand = root.fallbackCommand;
565
+ const canRouteUnknown =
566
+ fallbackCommand !== undefined &&
567
+ ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown ||
568
+ (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.UnknownOnly);
569
+
570
+ if (canRouteUnknown) {
571
+ cmdName = fallbackCommand;
572
+ node = findChild(root.commands, cmdName);
573
+ if (!node) {
574
+ return errorResult(`Unknown command: ${cmdName}`, path, [], pathParams);
575
+ }
576
+ } else {
577
+ return errorResult(`Unknown command: ${peek}`, path, [], pathParams);
578
+ }
579
+ }
486
580
  } else {
487
581
  const fallbackCommand = root.fallbackCommand;
488
582
  const canRouteUnknown =
@@ -520,7 +614,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
520
614
  // Walk the command tree
521
615
  while (true) {
522
616
  if (isCliLeaf(current) && isJsonLeaf(current)) {
523
- return finishJsonLeaf(current, i, argv, path, opts);
617
+ return finishJsonLeaf(current, i, argv, path, opts, pathParams);
524
618
  }
525
619
 
526
620
  if (!forcePositionals) {
@@ -535,7 +629,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
535
629
  }
536
630
 
537
631
  if (i < argv.length && !forcePositionals && isHelpTok(argv[i])) {
538
- return helpResult(path, true);
632
+ return helpResult(path, true, pathParams);
539
633
  }
540
634
 
541
635
  if (i >= argv.length) {
@@ -547,28 +641,29 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
547
641
  if (fbNode) {
548
642
  path.push(fb);
549
643
  current = fbNode;
644
+ cliEnabled = isCliCallable(fbNode, cliEnabled);
550
645
  continue;
551
646
  }
552
647
  }
553
- return helpResult(path, false);
648
+ return helpResult(path, false, pathParams);
554
649
  }
555
650
  if (!isCliLeaf(current)) {
556
- return helpResult(path, false);
651
+ return helpResult(path, false, pathParams);
557
652
  }
558
- return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
653
+ return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals, pathParams);
559
654
  }
560
655
 
561
656
  const tok = argv[i];
562
657
  if (!forcePositionals && tok.startsWith("-")) {
563
- return errorResult(`Unexpected option token: ${tok}`, path);
658
+ return errorResult(`Unexpected option token: ${tok}`, path, [], pathParams);
564
659
  }
565
660
 
566
661
  if (!forcePositionals && isCliRouter(current)) {
567
- const childOpt = findChild(current.commands, tok);
568
- if (childOpt !== undefined) {
662
+ const descended = descendChild(current, tok, path, pathParams, cliEnabled);
663
+ if (descended.ok) {
569
664
  i += 1;
570
- path.push(tok);
571
- current = childOpt;
665
+ current = descended.node;
666
+ cliEnabled = descended.cliEnabled;
572
667
  continue;
573
668
  }
574
669
  }
@@ -584,6 +679,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
584
679
  if (fbNode) {
585
680
  path.push(fb);
586
681
  current = fbNode;
682
+ cliEnabled = isCliCallable(fbNode, cliEnabled);
587
683
  continue;
588
684
  }
589
685
  }
@@ -591,13 +687,15 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
591
687
  return errorResult(
592
688
  forcePositionals ? `Expected subcommand but got positional: ${tok}` : `Unknown subcommand: ${tok}`,
593
689
  path,
690
+ [],
691
+ pathParams,
594
692
  );
595
693
  }
596
694
 
597
695
  if (!isCliLeaf(current)) {
598
- return helpResult(path, false);
696
+ return helpResult(path, false, pathParams);
599
697
  }
600
- return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
698
+ return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals, pathParams);
601
699
  }
602
700
  }
603
701