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
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: full_example
3
- description: Operates the full-example CLI (echo, status). Use when the user mentions full-example, echo, status, or related tasks.
3
+ description: Operates the full-example CLI (echo, render-json, status, workspaces get, workspaces post, and 4 more). Use when the user mentions full-example, echo, render-json, status, or related tasks.
4
4
  ---
5
5
  <!-- Generated by full-example docs skill --save; do not edit. -->
6
6
 
@@ -20,14 +20,14 @@ full-example <subcommand> [options] [args]
20
20
  ## Commands
21
21
 
22
22
  - **`full-example echo`** — Echo a message (MCP-friendly leaf).
23
- - **`full-example status`** — Show resolved config and app version. (flags: --json)
24
-
25
- ## Configuration
26
-
27
- - **apiToken** (`apiToken` (env: `FULL_EXAMPLE_API_TOKEN`)) — Create at https://example.com/settings/tokens
28
- - **defaultRegion** (`defaultRegion`) — AWS region for API calls.
29
- - **maxRetries** (`maxRetries`) — HTTP retry count (0–10).
30
- - **prefs** (`prefs`) — Local cache preferences (not exported to env).
23
+ - **`full-example render-json`** — Echo a JSON message (schema-first JSON leaf demo).
24
+ - **`full-example status`** — Show app version. (flags: --json)
25
+ - **`full-example workspaces get`** — List workspaces.
26
+ - **`full-example workspaces post`** — Create a workspace.
27
+ - **`full-example workspaces :id get`** — Get one workspace.
28
+ - **`full-example workspaces :id put`** — Replace a workspace.
29
+ - **`full-example workspaces :id patch`** — Patch a workspace name.
30
+ - **`full-example workspaces :id delete`** — Delete a workspace.
31
31
 
32
32
  ## Pitfalls
33
33
 
@@ -35,7 +35,7 @@ full-example <subcommand> [options] [args]
35
35
 
36
36
  ## Reference
37
37
 
38
- For full detail, open `reference.md` in this skill directory (same as `full-example docs api`).
38
+ For full detail, open `reference.md` in this skill directory (same as `full-example docs cli`).
39
39
 
40
40
  ## Cursor install location
41
41
 
@@ -21,6 +21,16 @@ build:
21
21
  bun build ./src/index.ts --compile --outfile=dist/{{cli_key}}
22
22
  @rm -f .*.bun-build
23
23
 
24
+ # Apply SQL migrations in src/db/migrations/ to a SQLite database file
25
+ migrate DB="./workspaces.db":
26
+ #!/usr/bin/env bash
27
+ set -euo pipefail
28
+ mkdir -p "$(dirname '{{DB}}')"
29
+ for f in $(ls src/db/migrations/*.sql | sort); do
30
+ echo "==> $f"
31
+ sqlite3 '{{DB}}' < "$f"
32
+ done
33
+
24
34
  # Run schemagen, typecheck, and format
25
35
  check: schemagen format typecheck
26
36
 
@@ -31,7 +41,7 @@ dev *ARGS:
31
41
  # Regenerate consumer docs under ./docs/
32
42
  docgen: schemagen
33
43
  @just run docs cli-schema --save
34
- @just run docs api --save
44
+ @just run docs cli --save
35
45
  @just run docs skill --save
36
46
  @just run docs mcp --save
37
47
  @just run docs http --save
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "message": {
6
+ "type": "string",
7
+ "description": "Message to echo back."
8
+ }
9
+ },
10
+ "required": [
11
+ "message"
12
+ ],
13
+ "additionalProperties": false,
14
+ "definitions": {}
15
+ }
@@ -0,0 +1,5 @@
1
+ // Auto-generated by argsbarg schemagen — do not edit by hand.
2
+
3
+ import RenderJsonInputSchemaJson from "./RenderJsonInputSchema.json";
4
+
5
+ export const RenderJsonInputSchema = RenderJsonInputSchemaJson as Record<string, unknown>;
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Cli, type CliProgram } from "argsbarg";
3
+ import { renderJsonCommand, renderJsonTestProgram } from "./command.ts";
4
+
5
+ const baseProgram = {
6
+ key: "full-example",
7
+ version: "1.0.0",
8
+ description: "Demo.",
9
+ httpServer: { enabled: true },
10
+ commands: [],
11
+ } satisfies CliProgram;
12
+
13
+ describe("render-json command", () => {
14
+ const program = renderJsonTestProgram(baseProgram);
15
+ const cli = new Cli(program);
16
+
17
+ test("HTTP invoke returns echoed message", async () => {
18
+ const result = await cli.invoke(["render-json"], {
19
+ invocation: "http",
20
+ toolArgs: { message: "hello" },
21
+ });
22
+ expect(result.kind).toBe("ok");
23
+ expect(result.response?.body).toEqual({ message: "hello" });
24
+ });
25
+
26
+ test("rejects invalid input before handler via inputSchema", async () => {
27
+ let handlerCalled = false;
28
+ const badProgram = renderJsonTestProgram({
29
+ ...baseProgram,
30
+ commands: [
31
+ {
32
+ ...renderJsonCommand,
33
+ handler: () => {
34
+ handlerCalled = true;
35
+ },
36
+ },
37
+ ],
38
+ });
39
+ const result = await new Cli(badProgram).invoke(["render-json"], {
40
+ invocation: "http",
41
+ toolArgs: { message: 123 },
42
+ });
43
+ expect(result.kind).toBe("error");
44
+ expect(handlerCalled).toBe(false);
45
+ });
46
+ });
@@ -0,0 +1,30 @@
1
+ /*
2
+ Render-json leaf — JSON body demo with schemagen inputSchema and ctx.inputsAs.
3
+ */
4
+
5
+ import type { CliLeaf, CliProgram } from "argsbarg";
6
+ import { RenderJsonInputSchema } from "./__generated__";
7
+ import type { RenderJsonInput } from "./types.ts";
8
+
9
+ export const renderJsonCommand = {
10
+ key: "render-json",
11
+ description: "Echo a JSON message (schema-first JSON leaf demo).",
12
+ kind: "json",
13
+ inputSchema: RenderJsonInputSchema,
14
+ handler: (ctx) => {
15
+ const { message } = ctx.inputsAs<RenderJsonInput>();
16
+ if (ctx.invocation === "cli") {
17
+ console.log(message);
18
+ return;
19
+ }
20
+ return { message };
21
+ },
22
+ } satisfies CliLeaf;
23
+
24
+ /** Program stub for colocated tests. */
25
+ export function renderJsonTestProgram(base: CliProgram): CliProgram {
26
+ return {
27
+ ...base,
28
+ commands: [renderJsonCommand],
29
+ };
30
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ Input types for the render-json demo leaf.
3
+ */
4
+
5
+ /** @sg */
6
+ export interface RenderJsonInput {
7
+ /** Message to echo back. */
8
+ message: string;
9
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "version": {
6
+ "type": "string",
7
+ "description": "App version from program root."
8
+ }
9
+ },
10
+ "required": [
11
+ "version"
12
+ ],
13
+ "additionalProperties": false,
14
+ "definitions": {}
15
+ }
@@ -1,5 +1,5 @@
1
1
  // Auto-generated by argsbarg schemagen — do not edit by hand.
2
2
 
3
- import outputSchemaJson from "./outputSchema.json";
3
+ import StatusJsonOutputSchemaJson from "./StatusJsonOutputSchema.json";
4
4
 
5
- export const outputSchema = outputSchemaJson as Record<string, unknown>;
5
+ export const StatusJsonOutputSchema = StatusJsonOutputSchemaJson as Record<string, unknown>;
@@ -1,14 +1,14 @@
1
1
  /*
2
- Status leaf — demonstrates outputSchema and ctx.appConfig.
2
+ Status leaf — demonstrates outputSchema.
3
3
  */
4
4
 
5
5
  import { type CliLeaf, CliOptionKind } from "argsbarg";
6
- import { outputSchema } from "./__generated__/index.ts";
6
+ import { StatusJsonOutputSchema } from "./__generated__";
7
7
  import type { StatusJsonOutput } from "./types.ts";
8
8
 
9
9
  export const statusCommand = {
10
10
  key: "status",
11
- description: "Show resolved config and app version.",
11
+ description: "Show app version.",
12
12
  options: [
13
13
  {
14
14
  name: "json",
@@ -16,21 +16,13 @@ export const statusCommand = {
16
16
  kind: CliOptionKind.Presence,
17
17
  },
18
18
  ],
19
- outputSchema,
19
+ outputSchema: StatusJsonOutputSchema,
20
20
  handler: (ctx) => {
21
- const out: StatusJsonOutput = {
22
- defaultRegion: ctx.appConfig.get("defaultRegion") as string | undefined,
23
- maxRetries: ctx.appConfig.get("maxRetries") as number | undefined,
24
- apiTokenSet: ctx.appConfig.get("apiToken") !== undefined,
25
- version: ctx.program.version,
26
- };
21
+ const out: StatusJsonOutput = { version: ctx.program.version };
27
22
  if (ctx.hasFlag("json")) {
28
23
  console.log(JSON.stringify(out, null, 2));
29
24
  } else {
30
25
  console.log(`version=${out.version}`);
31
- console.log(`region=${out.defaultRegion ?? "(not set)"}`);
32
- console.log(`maxRetries=${out.maxRetries ?? "(not set)"}`);
33
- console.log(`apiToken=${out.apiTokenSet ? "set" : "missing"}`);
34
26
  }
35
27
  },
36
28
  } satisfies CliLeaf;
@@ -1,19 +1,6 @@
1
1
  /** JSON stdout for `full-example status --json`. */
2
+ /** @sg */
2
3
  export interface StatusJsonOutput {
3
- /** Resolved AWS region. */
4
- defaultRegion?: string;
5
- /** Resolved retry count. */
6
- maxRetries?: number;
7
- /** Whether apiToken is set (value never included). */
8
- apiTokenSet: boolean;
9
4
  /** App version from program root. */
10
5
  version: string;
11
6
  }
12
-
13
- /** Returns status JSON (identity helper for schema generation). */
14
- export function buildStatusJson(output: StatusJsonOutput): StatusJsonOutput {
15
- return output;
16
- }
17
-
18
- /** Schemagen root for leaf outputSchema. */
19
- export type outputType = StatusJsonOutput;
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "type": "object",
4
+ "properties": {
5
+ "name": {
6
+ "type": "string",
7
+ "description": "Workspace display name."
8
+ }
9
+ },
10
+ "required": [
11
+ "name"
12
+ ],
13
+ "additionalProperties": false,
14
+ "definitions": {}
15
+ }
@@ -0,0 +1,5 @@
1
+ // Auto-generated by argsbarg schemagen — do not edit by hand.
2
+
3
+ import WorkspaceNameInputSchemaJson from "./WorkspaceNameInputSchema.json";
4
+
5
+ export const WorkspaceNameInputSchema = WorkspaceNameInputSchemaJson as Record<string, unknown>;
@@ -0,0 +1,58 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { Cli, type CliProgram } from "argsbarg";
3
+ import { AppDb } from "~/db";
4
+ import { workspacesTestProgram } from "./command.ts";
5
+
6
+ const baseProgram = {
7
+ key: "full-example",
8
+ version: "1.0.0",
9
+ description: "Demo.",
10
+ httpServer: { enabled: true },
11
+ commands: [],
12
+ hooks: {
13
+ beforeInvoke: AppDb.attach,
14
+ },
15
+ } satisfies CliProgram;
16
+
17
+ describe("workspaces command", () => {
18
+ const program = workspacesTestProgram(baseProgram);
19
+ const cli = new Cli(program);
20
+
21
+ beforeEach(() => {
22
+ AppDb.resetForTests();
23
+ });
24
+
25
+ test("GET workspaces lists empty collection", async () => {
26
+ const result = await cli.invoke(["workspaces", "get"], { invocation: "http" });
27
+ expect(result.kind).toBe("ok");
28
+ expect(result.response?.body).toEqual({ workspaces: [] });
29
+ });
30
+
31
+ test("POST workspaces creates resource", async () => {
32
+ const created = await cli.invoke(["workspaces", "post"], {
33
+ invocation: "http",
34
+ toolArgs: { name: "qa2" },
35
+ });
36
+ expect(created.kind).toBe("ok");
37
+ const body = created.response?.body as { id: string; name: string };
38
+ expect(body.name).toBe("qa2");
39
+ expect(body.id.length).toBeGreaterThan(0);
40
+
41
+ const got = await cli.invoke(["workspaces", body.id, "get"], { invocation: "http" });
42
+ expect(got.kind).toBe("ok");
43
+ expect(got.response?.body).toEqual(body);
44
+ });
45
+
46
+ test("CLI workspaces :id get resolves path param", async () => {
47
+ const created = await cli.invoke(["workspaces", "post"], {
48
+ invocation: "http",
49
+ toolArgs: { name: "cli-ws" },
50
+ });
51
+ expect(created.kind).toBe("ok");
52
+ const id = (created.response?.body as { id: string }).id;
53
+
54
+ const got = await cli.invoke(["workspaces", id, "get"], { invocation: "http" });
55
+ expect(got.kind).toBe("ok");
56
+ expect(got.response?.body).toEqual({ id, name: "cli-ws" });
57
+ });
58
+ });
@@ -0,0 +1,94 @@
1
+ /*
2
+ Workspaces CRUD — demonstrates REST verbs, :id param routers, and ctx.inputs path params.
3
+ */
4
+
5
+ import { type CliProgram, type CliRouter, cliErrWithHelp } from "argsbarg";
6
+ import { WorkspaceNameInputSchema } from "./__generated__";
7
+
8
+ function notFound(ctx: Parameters<typeof cliErrWithHelp>[0], id: string): never {
9
+ cliErrWithHelp(ctx, `Workspace not found: ${id}`);
10
+ }
11
+
12
+ export const workspacesCommand = {
13
+ key: "workspaces",
14
+ description: "Workspace collection and CRUD.",
15
+ commands: [
16
+ {
17
+ key: "get",
18
+ description: "List workspaces.",
19
+ handler: (ctx) => ({ workspaces: ctx.locals.db.workspaces.list() }),
20
+ },
21
+ {
22
+ key: "post",
23
+ description: "Create a workspace.",
24
+ inputSchema: WorkspaceNameInputSchema,
25
+ handler: (ctx) => {
26
+ const { name } = ctx.inputsAs<{ name: string }>();
27
+ return ctx.locals.db.workspaces.create(name);
28
+ },
29
+ },
30
+ {
31
+ key: ":id",
32
+ description: "One workspace by id.",
33
+ commands: [
34
+ {
35
+ key: "get",
36
+ description: "Get one workspace.",
37
+ handler: (ctx) => {
38
+ const id = ctx.inputsAs<{ id: string }>().id;
39
+ const ws = ctx.locals.db.workspaces.get(id);
40
+ if (!ws) {
41
+ notFound(ctx, id);
42
+ }
43
+ return ws;
44
+ },
45
+ },
46
+ {
47
+ key: "put",
48
+ description: "Replace a workspace.",
49
+ inputSchema: WorkspaceNameInputSchema,
50
+ handler: (ctx) => {
51
+ const { id, name } = ctx.inputsAs<{ id: string; name: string }>();
52
+ const ws = ctx.locals.db.workspaces.replace(id, name);
53
+ if (!ws) {
54
+ notFound(ctx, id);
55
+ }
56
+ return ws;
57
+ },
58
+ },
59
+ {
60
+ key: "patch",
61
+ description: "Patch a workspace name.",
62
+ inputSchema: WorkspaceNameInputSchema,
63
+ handler: (ctx) => {
64
+ const { id, name } = ctx.inputsAs<{ id: string; name: string }>();
65
+ const ws = ctx.locals.db.workspaces.patch(id, name);
66
+ if (!ws) {
67
+ notFound(ctx, id);
68
+ }
69
+ return ws;
70
+ },
71
+ },
72
+ {
73
+ key: "delete",
74
+ description: "Delete a workspace.",
75
+ handler: (ctx) => {
76
+ const id = ctx.inputsAs<{ id: string }>().id;
77
+ if (!ctx.locals.db.workspaces.delete(id)) {
78
+ notFound(ctx, id);
79
+ }
80
+ ctx.respond({ status: 204, body: "" });
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ ],
86
+ } satisfies CliRouter;
87
+
88
+ /** Test program with workspaces registered. */
89
+ export function workspacesTestProgram(base: CliProgram): CliProgram {
90
+ return {
91
+ ...base,
92
+ commands: [workspacesCommand],
93
+ };
94
+ }
@@ -0,0 +1,6 @@
1
+ /** Body for workspace create/replace/patch. */
2
+ /** @sg */
3
+ export interface WorkspaceNameInput {
4
+ /** Workspace display name. */
5
+ name: string;
6
+ }
@@ -0,0 +1,86 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import type { ReadinessContext } from "argsbarg";
3
+ import { AppDb } from ".";
4
+
5
+ describe("AppDb", () => {
6
+ let appDb: AppDb;
7
+
8
+ beforeEach(() => {
9
+ appDb = AppDb.open();
10
+ });
11
+
12
+ afterEach(() => {
13
+ appDb.close();
14
+ });
15
+
16
+ test("workspace CRUD round trip", () => {
17
+ const workspaces = appDb.workspaces;
18
+ expect(workspaces.list()).toEqual([]);
19
+ const created = workspaces.create("alpha");
20
+ expect(workspaces.get(created.id)).toEqual(created);
21
+ expect(workspaces.list()).toEqual([created]);
22
+ expect(workspaces.patch(created.id, "beta")).toEqual({ ...created, name: "beta" });
23
+ expect(workspaces.replace(created.id, "gamma")).toEqual({ id: created.id, name: "gamma" });
24
+ expect(workspaces.delete(created.id)).toBe(true);
25
+ expect(workspaces.get(created.id)).toBeUndefined();
26
+ });
27
+
28
+ test("ping succeeds on open database", () => {
29
+ expect(() => appDb.ping()).not.toThrow();
30
+ });
31
+ });
32
+
33
+ describe("AppDb.openWithRetry", () => {
34
+ test("opens an in-memory database", () => {
35
+ const appDb = AppDb.openWithRetry(1);
36
+ try {
37
+ appDb.workspaces.create("retry-ok");
38
+ expect(appDb.workspaces.list()).toHaveLength(1);
39
+ } finally {
40
+ appDb.close();
41
+ }
42
+ });
43
+ });
44
+
45
+ describe("AppDb.attach", () => {
46
+ test("sets ctx.locals.db", () => {
47
+ const locals = {} as import("argsbarg").CliLocals;
48
+ AppDb.attach({ locals, invocation: "cli" });
49
+ locals.db.workspaces.create("attached");
50
+ expect(locals.db.workspaces.list()).toHaveLength(1);
51
+ });
52
+ });
53
+
54
+ describe("AppDb.checkReadiness", () => {
55
+ test("returns false before server database is initialized", () => {
56
+ const runtime = {
57
+ state: {},
58
+ program: { key: "t", description: "d" },
59
+ surface: "http" as const,
60
+ };
61
+ const ctx = {
62
+ program: runtime.program,
63
+ surface: "http" as const,
64
+ appConfig: { read: () => ({}) },
65
+ runtime,
66
+ } as unknown as ReadinessContext;
67
+ expect(AppDb.checkReadiness(ctx)).toBe(false);
68
+ });
69
+
70
+ test("returns true when sqlite responds", () => {
71
+ AppDb.resetForTests();
72
+ const runtime = {
73
+ state: { db: AppDb.open() },
74
+ program: { key: "t", description: "d" },
75
+ surface: "http" as const,
76
+ };
77
+ const ctx = {
78
+ program: runtime.program,
79
+ surface: "http" as const,
80
+ appConfig: { read: () => ({}) },
81
+ runtime,
82
+ } as unknown as ReadinessContext;
83
+ expect(AppDb.checkReadiness(ctx)).toBe(true);
84
+ (runtime.state.db as AppDb).close();
85
+ });
86
+ });
@@ -0,0 +1,101 @@
1
+ /*
2
+ App-wide in-memory SQLite database.
3
+ */
4
+
5
+ import { Database } from "bun:sqlite";
6
+ import type { InvokeHookContext, ReadinessContext } from "argsbarg";
7
+ import { migrate } from "./migrate.ts";
8
+ import { WorkspacesTable } from "./tables/workspaces.ts";
9
+
10
+ const DEFAULT_RETRY_DELAY_MS = 200;
11
+ const MAX_RETRY_DELAY_MS = 5000;
12
+
13
+ /**
14
+ * Single SQLite database for this app.
15
+ * Migrations run on construction; table accessors hang off {@link workspaces}.
16
+ */
17
+ export class AppDb {
18
+ /** CLI singleton; server invocations use `runtime.state.db` instead. */
19
+ private static db: AppDb | undefined;
20
+
21
+ /** Workspace rows and queries for this connection. */
22
+ readonly workspaces: WorkspacesTable;
23
+
24
+ constructor(readonly sqlite: Database) {
25
+ migrate(sqlite);
26
+ this.workspaces = new WorkspacesTable(sqlite);
27
+ }
28
+
29
+ /** Open a fresh in-memory database with foreign keys enabled. */
30
+ static open(): AppDb {
31
+ const sqlite = new Database(":memory:", { create: true });
32
+ sqlite.run("PRAGMA foreign_keys = ON");
33
+ return new AppDb(sqlite);
34
+ }
35
+
36
+ /** Open with bounded backoff, retrying until SQLite accepts connections (HTTP/MCP startup). */
37
+ static openWithRetry(delayMs = DEFAULT_RETRY_DELAY_MS): AppDb {
38
+ let attempt = 0;
39
+ while (true) {
40
+ try {
41
+ const appDb = AppDb.open();
42
+ appDb.ping();
43
+ return appDb;
44
+ } catch {
45
+ attempt++;
46
+ Bun.sleepSync(Math.min(delayMs * attempt, MAX_RETRY_DELAY_MS));
47
+ }
48
+ }
49
+ }
50
+
51
+ /** Shared database for non-server invocations (lazy CLI singleton). */
52
+ static openDb(): AppDb {
53
+ AppDb.db ??= AppDb.open();
54
+ return AppDb.db;
55
+ }
56
+
57
+ /** Replace the CLI singleton with a fresh in-memory database (tests). */
58
+ static resetForTests(): void {
59
+ AppDb.db?.close();
60
+ AppDb.db = AppDb.open();
61
+ }
62
+
63
+ /**
64
+ * Wire `ctx.locals.db` before handlers run.
65
+ * Server runtimes open with retry into `runtime.state.db`; CLI uses {@link openDb}.
66
+ */
67
+ static attach(
68
+ ctx: Pick<InvokeHookContext, "locals" | "invocation"> & { runtime?: InvokeHookContext["runtime"] },
69
+ ): void {
70
+ if (ctx.runtime && (ctx.invocation === "http" || ctx.invocation === "mcp")) {
71
+ ctx.runtime.state.db ??= AppDb.openWithRetry();
72
+ ctx.locals.db = ctx.runtime.state.db;
73
+ return;
74
+ }
75
+ ctx.locals.db = AppDb.openDb();
76
+ }
77
+
78
+ /** Readiness probe: ping `runtime.state.db` when the server database is open. */
79
+ static checkReadiness(ctx: ReadinessContext): boolean {
80
+ const appDb = ctx.runtime.state.db;
81
+ if (!appDb) {
82
+ return false;
83
+ }
84
+ try {
85
+ appDb.ping();
86
+ return true;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /** Verify SQLite responds to a trivial query. */
93
+ ping(): void {
94
+ this.sqlite.query("SELECT 1 AS ok").get();
95
+ }
96
+
97
+ /** Close the underlying database handle. */
98
+ close(): void {
99
+ this.sqlite.close();
100
+ }
101
+ }
@@ -0,0 +1,35 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { appliedMigrationVersion, listMigrationFiles, migrate } from "./migrate.ts";
4
+
5
+ function openTestDb(): Database {
6
+ const db = new Database(":memory:", { create: true });
7
+ db.run("PRAGMA foreign_keys = ON");
8
+ return db;
9
+ }
10
+
11
+ describe("migrate", () => {
12
+ let db: Database;
13
+
14
+ beforeEach(() => {
15
+ db = openTestDb();
16
+ });
17
+
18
+ afterEach(() => {
19
+ db.close();
20
+ });
21
+
22
+ test("lists migration files in version order", () => {
23
+ const files = listMigrationFiles();
24
+ expect(files.length).toBeGreaterThan(0);
25
+ expect(files[0]?.name).toBe("001_workspaces.sql");
26
+ });
27
+
28
+ test("applies pending migrations once", () => {
29
+ expect(appliedMigrationVersion(db)).toBe(0);
30
+ expect(migrate(db)).toBe(1);
31
+ expect(appliedMigrationVersion(db)).toBe(1);
32
+ expect(migrate(db)).toBe(0);
33
+ expect(db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'workspaces'").get()).toBeTruthy();
34
+ });
35
+ });