argsbarg 6.0.0 → 6.0.2

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 (43) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +4 -4
  3. package/docs/api-server.md +1 -1
  4. package/docs/config-schema.md +27 -15
  5. package/docs/output-schema.md +107 -75
  6. package/examples/full-example/README.md +8 -7
  7. package/examples/full-example/bun.lock +0 -29
  8. package/examples/full-example/justfile +6 -3
  9. package/examples/full-example/package.json +0 -2
  10. package/examples/full-example/src/commands/status/__generated__/index.ts +5 -0
  11. package/examples/full-example/{schemas/generated/status.json → src/commands/status/__generated__/outputSchema.json} +1 -1
  12. package/examples/full-example/src/commands/status/command.ts +2 -2
  13. package/examples/full-example/src/commands/status/schema.ts +14 -0
  14. package/examples/full-example/src/commands/status/types.ts +1 -11
  15. package/examples/full-example/{schemas/generated/app-config.json → src/config/__generated__/configSchema.json} +1 -1
  16. package/examples/full-example/src/config/__generated__/index.ts +5 -0
  17. package/examples/full-example/src/{types.ts → config/schema.ts} +3 -2
  18. package/examples/full-example/src/program.ts +4 -4
  19. package/package.json +24 -17
  20. package/src/api/openapi.ts +4 -2
  21. package/src/api/result.ts +23 -1
  22. package/src/api/schema-deref.test.ts +99 -0
  23. package/src/api/schema-deref.ts +76 -0
  24. package/src/api.integration.test.ts +85 -2
  25. package/src/cli-errors.ts +3 -0
  26. package/src/cli-tool/full-example-capabilities.test.ts +2 -1
  27. package/src/cli-tool/post-create.ts +3 -3
  28. package/src/cli-tool/program.ts +35 -0
  29. package/src/cli-tool/run-schemagen.ts +23 -0
  30. package/src/cli-tool/schemagen/cleanup.ts +60 -0
  31. package/src/cli-tool/schemagen/discover-schema-roots.ts +103 -0
  32. package/src/cli-tool/schemagen/index.ts +3 -0
  33. package/src/cli-tool/schemagen/names.ts +22 -0
  34. package/src/cli-tool/schemagen/run.ts +108 -0
  35. package/src/cli-tool/schemagen/schemagen.test.ts +125 -0
  36. package/src/docs/mcp-resources.ts +5 -2
  37. package/src/headless/tool-call.ts +16 -6
  38. package/examples/full-example/schemas/configSchemas.ts +0 -6
  39. package/examples/full-example/schemas/outputSchemas.ts +0 -6
  40. package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +0 -25
  41. package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +0 -93
  42. package/examples/full-example/scripts/schemagen/naming.ts +0 -82
  43. package/examples/full-example/scripts/schemagen.ts +0 -80
@@ -0,0 +1,14 @@
1
+ /** JSON stdout for `full-example status --json`. */
2
+ 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
+ /** App version from program root. */
10
+ version: string;
11
+ }
12
+
13
+ /** Schemagen root for leaf outputSchema. */
14
+ export type outputType = StatusJsonOutput;
@@ -1,11 +1 @@
1
- /** JSON payload for `full-example status --json`. */
2
- 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
- /** App version from program root. */
10
- version: string;
11
- }
1
+ export type { StatusJsonOutput } from "./schema.ts";
@@ -35,6 +35,6 @@
35
35
  "maxRetries"
36
36
  ],
37
37
  "additionalProperties": false,
38
- "description": "Config schema\n\nApplication settings for `full-example` (`program.appConfig`).",
38
+ "description": "Application settings for `full-example` (`program.appConfig`).",
39
39
  "definitions": {}
40
40
  }
@@ -0,0 +1,5 @@
1
+ // Auto-generated by argsbarg schemagen — do not edit by hand.
2
+
3
+ import configSchemaJson from "./configSchema.json";
4
+
5
+ export const configSchema = configSchemaJson as Record<string, unknown>;
@@ -1,6 +1,4 @@
1
1
  /**
2
- * Config schema
3
- *
4
2
  * Application settings for `full-example` (`program.appConfig`).
5
3
  */
6
4
  export interface AppConfig {
@@ -21,3 +19,6 @@ export interface AppConfig {
21
19
  ttl: number;
22
20
  };
23
21
  }
22
+
23
+ /** Schemagen root for program.appConfig.jsonSchema. */
24
+ export type configType = AppConfig;
@@ -4,12 +4,12 @@ Kitchen-sink CliProgram — every argsbarg builtin enabled; command registration
4
4
 
5
5
  import type { CliAppConfig, CliAppConfigEntry, CliProgram } from "argsbarg";
6
6
  import readmeText from "../README.md" with { type: "text" };
7
- import { APP_CONFIG_JSON_SCHEMA } from "../schemas/configSchemas.ts";
7
+ import { configSchema } from "./config/__generated__/index.ts";
8
8
  import { createIdentity } from "../scripts/create-identity.ts";
9
9
  import { echoCommand } from "./commands/echo/command.ts";
10
10
  import { statusCommand } from "./commands/status/command.ts";
11
11
 
12
- const configSchema = {
12
+ const configEntries = {
13
13
  apiToken: {
14
14
  description: "Create at https://example.com/settings/tokens",
15
15
  env: `${createIdentity.envPrefix}_API_TOKEN`,
@@ -34,8 +34,8 @@ export const program = {
34
34
  version: "1.0.0",
35
35
  description: createIdentity.desc,
36
36
  appConfig: {
37
- jsonSchema: APP_CONFIG_JSON_SCHEMA,
38
- entries: configSchema,
37
+ jsonSchema: configSchema,
38
+ entries: configEntries,
39
39
  } satisfies CliAppConfig,
40
40
  docs: {
41
41
  enabled: true,
package/package.json CHANGED
@@ -1,25 +1,33 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "6.0.0",
4
- "type": "module",
5
- "engines": {
6
- "bun": ">=1.3"
7
- },
8
- "scripts": {
9
- "//just": "echo this app uses justfile for development tasks"
10
- },
3
+ "version": "6.0.2",
11
4
  "main": "./src/index.ts",
12
5
  "module": "./src/index.ts",
13
- "types": "./index.d.ts",
14
- "bin": {
15
- "argsbarg": "src/cli-tool/main.ts"
6
+ "dependencies": {
7
+ "ts-json-schema-generator": "^2.3.0"
8
+ },
9
+ "devDependencies": {
10
+ "@biomejs/biome": "^2.5.0",
11
+ "@types/bun": "^1.3.12",
12
+ "dts-bundle-generator": "^9.5.1",
13
+ "typescript": "^5.9.3"
16
14
  },
17
15
  "exports": {
18
16
  ".": {
19
17
  "types": "./index.d.ts",
20
18
  "default": "./src/index.ts"
19
+ },
20
+ "./schemagen": {
21
+ "types": "./src/cli-tool/schemagen/index.ts",
22
+ "default": "./src/cli-tool/schemagen/index.ts"
21
23
  }
22
24
  },
25
+ "bin": {
26
+ "argsbarg": "src/cli-tool/main.ts"
27
+ },
28
+ "engines": {
29
+ "bun": ">=1.3"
30
+ },
23
31
  "files": [
24
32
  "src",
25
33
  "index.d.ts",
@@ -29,10 +37,9 @@
29
37
  "LICENSE",
30
38
  "CHANGELOG.md"
31
39
  ],
32
- "devDependencies": {
33
- "@biomejs/biome": "^2.5.0",
34
- "@types/bun": "^1.3.12",
35
- "dts-bundle-generator": "^9.5.1",
36
- "typescript": "^5.9.3"
37
- }
40
+ "scripts": {
41
+ "//just": "echo this app uses justfile for development tasks"
42
+ },
43
+ "type": "module",
44
+ "types": "./index.d.ts"
38
45
  }
@@ -4,6 +4,7 @@ Hand-built OpenAPI 3.1 document from exposed MCP tools.
4
4
 
5
5
  import { collectMcpTools } from "../mcp/tools.ts";
6
6
  import type { CliProgram } from "../types.ts";
7
+ import { dereferenceJsonSchema } from "./schema-deref.ts";
7
8
 
8
9
  const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
9
10
 
@@ -18,8 +19,9 @@ function buildSuccessResponse(tool: ReturnType<typeof collectMcpTools>[number]):
18
19
  const media: Record<string, unknown> = {};
19
20
 
20
21
  if (contentType.includes("application/json")) {
22
+ const outputSchema = tool.outputSchema ?? { type: "object" };
21
23
  media[contentType] = {
22
- schema: tool.outputSchema ?? { type: "object" },
24
+ schema: dereferenceJsonSchema(outputSchema),
23
25
  };
24
26
  } else if (contentType.includes("text/html")) {
25
27
  media[contentType] = { schema: { type: "string" } };
@@ -48,7 +50,7 @@ export function generateOpenApi(program: CliProgram): Record<string, unknown> {
48
50
  required: false,
49
51
  content: {
50
52
  [JSON_CONTENT_TYPE]: {
51
- schema: tool.inputSchema,
53
+ schema: dereferenceJsonSchema(tool.inputSchema),
52
54
  },
53
55
  },
54
56
  },
package/src/api/result.ts CHANGED
@@ -12,6 +12,21 @@ export interface ApiToolCallErrorBody {
12
12
  stderr?: string;
13
13
  }
14
14
 
15
+ /** Strips ANSI escape sequences from CLI-formatted text. */
16
+ export function stripAnsi(text: string): string {
17
+ const ansiEscape = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
18
+ return text.replace(ansiEscape, "");
19
+ }
20
+
21
+ /** Returns the first non-empty line of text with ANSI escapes removed. */
22
+ export function firstErrorLine(text: string): string {
23
+ const line = stripAnsi(text)
24
+ .split("\n")
25
+ .map((part) => part.trim())
26
+ .find((part) => part.length > 0);
27
+ return line ?? stripAnsi(text).trim();
28
+ }
29
+
15
30
  /** Wide-open CORS headers applied to all API responses. */
16
31
  export const API_CORS_HEADERS: Readonly<Record<string, string>> = {
17
32
  "access-control-allow-origin": "*",
@@ -82,8 +97,15 @@ export function apiDocsHtml(): string {
82
97
  <title>API Reference</title>
83
98
  </head>
84
99
  <body>
85
- <script id="api-reference" data-url="/openapi.json"></script>
100
+ <div id="api-reference"></div>
86
101
  <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
102
+ <script>
103
+ Scalar.createApiReference("#api-reference", {
104
+ url: "/openapi.json",
105
+ orderSchemaPropertiesBy: "preserve",
106
+ orderRequiredPropertiesFirst: false,
107
+ });
108
+ </script>
87
109
  </body>
88
110
  </html>`;
89
111
  }
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "bun:test";
2
+ import { dereferenceJsonSchema } from "./schema-deref.ts";
3
+
4
+ test("dereferenceJsonSchema inlines nested definitions", () => {
5
+ const schema = {
6
+ type: "object",
7
+ properties: {
8
+ invoice: { $ref: "#/definitions/InvoiceData" },
9
+ },
10
+ definitions: {
11
+ InvoiceData: {
12
+ type: "object",
13
+ properties: { id: { type: "string" } },
14
+ required: ["id"],
15
+ },
16
+ },
17
+ };
18
+ const out = dereferenceJsonSchema(schema);
19
+ expect(out.properties).toEqual({
20
+ invoice: {
21
+ type: "object",
22
+ properties: { id: { type: "string" } },
23
+ required: ["id"],
24
+ },
25
+ });
26
+ expect(out.definitions).toBeUndefined();
27
+ });
28
+
29
+ test("dereferenceJsonSchema supports $defs", () => {
30
+ const schema = {
31
+ type: "object",
32
+ properties: {
33
+ item: { $ref: "#/$defs/Item" },
34
+ },
35
+ $defs: {
36
+ Item: { type: "string" },
37
+ },
38
+ };
39
+ const out = dereferenceJsonSchema(schema);
40
+ expect(out.properties).toEqual({ item: { type: "string" } });
41
+ expect(out.$defs).toBeUndefined();
42
+ });
43
+
44
+ test("dereferenceJsonSchema merges $ref siblings", () => {
45
+ const schema = {
46
+ type: "object",
47
+ properties: {
48
+ invoice: {
49
+ $ref: "#/definitions/InvoiceData",
50
+ description: "Invoice payload",
51
+ },
52
+ },
53
+ definitions: {
54
+ InvoiceData: { type: "object" },
55
+ },
56
+ };
57
+ const out = dereferenceJsonSchema(schema) as {
58
+ properties: { invoice: { type: string; description: string } };
59
+ };
60
+ expect(out.properties.invoice).toEqual({
61
+ type: "object",
62
+ description: "Invoice payload",
63
+ });
64
+ });
65
+
66
+ test("dereferenceJsonSchema ignores circular refs", () => {
67
+ const schema = {
68
+ type: "object",
69
+ properties: {
70
+ self: { $ref: "#/definitions/Node" },
71
+ },
72
+ definitions: {
73
+ Node: {
74
+ type: "object",
75
+ properties: {
76
+ again: { $ref: "#/definitions/Node" },
77
+ },
78
+ },
79
+ },
80
+ };
81
+ const out = dereferenceJsonSchema(schema) as {
82
+ properties: { self: { type: string; properties: { again: { $ref: string } } } };
83
+ };
84
+ expect(out.properties.self.type).toBe("object");
85
+ expect(out.properties.self.properties.again).toEqual({ $ref: "#/definitions/Node" });
86
+ });
87
+
88
+ test("dereferenceJsonSchema leaves external refs unchanged", () => {
89
+ const schema = {
90
+ type: "object",
91
+ properties: {
92
+ remote: { $ref: "https://example.com/schema.json" },
93
+ },
94
+ };
95
+ const out = dereferenceJsonSchema(schema);
96
+ expect(out.properties).toEqual({
97
+ remote: { $ref: "https://example.com/schema.json" },
98
+ });
99
+ });
@@ -0,0 +1,76 @@
1
+ /*
2
+ Inline JSON Schema $ref dereferencing for OpenAPI embedding.
3
+ */
4
+
5
+ function decodeJsonPointerSegment(segment: string): string {
6
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
7
+ }
8
+
9
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
10
+ return value !== null && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ /** Resolves a same-document JSON Pointer (`#/definitions/Foo`). */
14
+ function resolveJsonPointer(root: Record<string, unknown>, ref: string): unknown {
15
+ if (!ref.startsWith("#/")) {
16
+ return undefined;
17
+ }
18
+ const segments = ref
19
+ .slice(2)
20
+ .split("/")
21
+ .filter((segment) => segment.length > 0)
22
+ .map(decodeJsonPointerSegment);
23
+ let current: unknown = root;
24
+ for (const segment of segments) {
25
+ if (!isPlainObject(current)) {
26
+ return undefined;
27
+ }
28
+ current = current[segment];
29
+ }
30
+ return current;
31
+ }
32
+
33
+ function derefValue(value: unknown, root: Record<string, unknown>, resolving: Set<string>): unknown {
34
+ if (Array.isArray(value)) {
35
+ return value.map((item) => derefValue(item, root, resolving));
36
+ }
37
+ if (!isPlainObject(value)) {
38
+ return value;
39
+ }
40
+
41
+ if (typeof value.$ref === "string") {
42
+ const { $ref, ...siblings } = value;
43
+ if (resolving.has($ref)) {
44
+ return value;
45
+ }
46
+ const target = resolveJsonPointer(root, $ref);
47
+ if (target === undefined) {
48
+ return value;
49
+ }
50
+ resolving.add($ref);
51
+ const resolved = derefValue(structuredClone(target), root, resolving);
52
+ resolving.delete($ref);
53
+ if (!isPlainObject(resolved)) {
54
+ return resolved;
55
+ }
56
+ if (Object.keys(siblings).length === 0) {
57
+ return resolved;
58
+ }
59
+ return { ...resolved, ...siblings };
60
+ }
61
+
62
+ const out: Record<string, unknown> = {};
63
+ for (const [key, child] of Object.entries(value)) {
64
+ if (key === "definitions" || key === "$defs") {
65
+ continue;
66
+ }
67
+ out[key] = derefValue(child, root, resolving);
68
+ }
69
+ return out;
70
+ }
71
+
72
+ /** Inlines internal `$ref` pointers and drops `definitions` / `$defs` from the output. */
73
+ export function dereferenceJsonSchema(schema: Record<string, unknown>): Record<string, unknown> {
74
+ const root = structuredClone(schema);
75
+ return derefValue(root, root, new Set()) as Record<string, unknown>;
76
+ }
@@ -8,7 +8,7 @@ import { $ } from "bun";
8
8
  import { generateOpenApi } from "./api/openapi.ts";
9
9
  import { API_CORS_HEADERS } from "./api/result.ts";
10
10
  import { handleApiRequest } from "./api/server.ts";
11
- import { Cli, CliContext, type CliContext as CliContextType, CliOptionKind } from "./index.ts";
11
+ import { Cli, CliContext, type CliContext as CliContextType, CliOptionKind, cliErrWithHelp } from "./index.ts";
12
12
  import { nestedMcpFixture, testProgram } from "./test-fixtures.ts";
13
13
  import { cliValidateProgram } from "./validate.ts";
14
14
 
@@ -290,6 +290,37 @@ describe("HTTP API routes", () => {
290
290
  expect(res.status).toBe(400);
291
291
  const body = (await res.json()) as { error: string };
292
292
  expect(body.error).toContain("Missing argument: path");
293
+ expect(body).not.toHaveProperty("stderr");
294
+ expect(body.error).not.toContain("\u001B[");
295
+ });
296
+
297
+ test("POST /tools/:name returns plain JSON validation errors", async () => {
298
+ const failProgram = testProgram({
299
+ key: "app",
300
+ description: "Test app",
301
+ apiServer: { enabled: true },
302
+ commands: [
303
+ {
304
+ key: "fail",
305
+ description: "Fails with cliErrWithHelp.",
306
+ handler: (ctx: CliContextType) => {
307
+ cliErrWithHelp(ctx, "bad input");
308
+ },
309
+ },
310
+ ],
311
+ });
312
+ cliValidateProgram(failProgram);
313
+ const res = await apiRequest(
314
+ failProgram,
315
+ new Request("http://127.0.0.1/tools/fail", {
316
+ method: "POST",
317
+ headers: { "content-type": "application/json" },
318
+ body: "{}",
319
+ }),
320
+ );
321
+ expect(res.status).toBe(400);
322
+ const body = (await res.json()) as Record<string, unknown>;
323
+ expect(body).toEqual({ error: "bad input" });
293
324
  });
294
325
 
295
326
  test("GET /openapi.json lists tool paths", async () => {
@@ -304,7 +335,10 @@ describe("HTTP API routes", () => {
304
335
  const res = await apiRequest(program, new Request("http://127.0.0.1/openapi-browser"));
305
336
  expect(res.status).toBe(200);
306
337
  expect(res.headers.get("content-type")).toContain("text/html");
307
- expect(await res.text()).toContain("@scalar/api-reference");
338
+ const html = await res.text();
339
+ expect(html).toContain("@scalar/api-reference");
340
+ expect(html).toContain('orderSchemaPropertiesBy: "preserve"');
341
+ expect(html).toContain("orderRequiredPropertiesFirst: false");
308
342
  });
309
343
  });
310
344
 
@@ -319,6 +353,55 @@ test("generateOpenApi maps binary content types", () => {
319
353
  expect(pdf.schema.format).toBe("binary");
320
354
  });
321
355
 
356
+ test("generateOpenApi dereferences nested inputSchema definitions", () => {
357
+ const program = testProgram({
358
+ key: "app",
359
+ description: "Test app",
360
+ apiServer: { enabled: true },
361
+ commands: [
362
+ {
363
+ key: "render",
364
+ description: "Render a document.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {
368
+ invoice: { $ref: "#/definitions/InvoiceData" },
369
+ },
370
+ definitions: {
371
+ InvoiceData: {
372
+ type: "object",
373
+ properties: {
374
+ id: { type: "string" },
375
+ },
376
+ required: ["id"],
377
+ },
378
+ },
379
+ },
380
+ handler: () => ({ ok: true }),
381
+ },
382
+ ],
383
+ });
384
+ cliValidateProgram(program);
385
+ const doc = generateOpenApi(program) as {
386
+ paths: Record<
387
+ string,
388
+ {
389
+ post: {
390
+ requestBody: {
391
+ content: Record<string, { schema: { properties: { invoice: Record<string, unknown> } } }>;
392
+ };
393
+ };
394
+ }
395
+ >;
396
+ };
397
+ const schema = doc.paths["/tools/render"]?.post.requestBody.content["application/json; charset=utf-8"].schema;
398
+ expect(schema.properties.invoice).toEqual({
399
+ type: "object",
400
+ properties: { id: { type: "string" } },
401
+ required: ["id"],
402
+ });
403
+ });
404
+
322
405
  test("ctx.respond throws when called twice", () => {
323
406
  const program = testProgram({
324
407
  key: "app",
package/src/cli-errors.ts CHANGED
@@ -7,6 +7,9 @@ import type { CliContext } from "./context.ts";
7
7
  import { cliHelpRender } from "./help.ts";
8
8
 
9
9
  export function cliErrWithHelp(ctx: CliContext, msg: string): never {
10
+ if (ctx.invocation === "api" || ctx.invocation === "mcp") {
11
+ throw new Error(msg);
12
+ }
10
13
  const color = process.stderr.isTTY;
11
14
  const line = color ? `\u001B[31m${msg}\u001B[0m` : msg;
12
15
  process.stderr.write(`${line}\n`);
@@ -63,7 +63,8 @@ describe("full-example template", () => {
63
63
 
64
64
  test("status command defines outputSchema", () => {
65
65
  const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
66
- expect(statusSource).toContain("outputSchema:");
66
+ expect(statusSource).toMatch(/outputSchema[,:]/);
67
+ expect(statusSource).toContain('from "./__generated__/index.ts"');
67
68
  });
68
69
 
69
70
  test("resolveCapabilities matches full sink shape", () => {
@@ -41,10 +41,10 @@ export async function runPostCreate(targetDir: string, dryRun: boolean): Promise
41
41
  },
42
42
  },
43
43
  {
44
- label: "bun scripts/schemagen.ts",
44
+ label: "argsbarg schemagen",
45
45
  run: () => {
46
46
  if (dryRun) return;
47
- const proc = Bun.spawnSync(["bun", "scripts/schemagen.ts"], {
47
+ const proc = Bun.spawnSync(["argsbarg", "schemagen"], {
48
48
  cwd: abs,
49
49
  stdout: "inherit",
50
50
  stderr: "inherit",
@@ -103,7 +103,7 @@ export async function runPostCreate(targetDir: string, dryRun: boolean): Promise
103
103
  export function printPostCreatePlan(): void {
104
104
  process.stderr.write("Post-create steps:\n");
105
105
  process.stderr.write(" 1. bun install\n");
106
- process.stderr.write(" 2. bun scripts/schemagen.ts\n");
106
+ process.stderr.write(" 2. just schemagen\n");
107
107
  process.stderr.write(" 3. bun test\n");
108
108
  process.stderr.write(" 4. git init + Initial commit (skipped inside existing git work tree)\n");
109
109
  }
@@ -5,6 +5,7 @@ Argsbarg developer tools — bootstrap consumer CLIs via `create`.
5
5
  import pkg from "../../package.json" with { type: "json" };
6
6
  import { CliOptionKind, type CliProgram } from "../index.ts";
7
7
  import { runCreate } from "./run-create.ts";
8
+ import { runSchemagenCli } from "./run-schemagen.ts";
8
9
 
9
10
  export const program = {
10
11
  key: "argsbarg",
@@ -92,5 +93,39 @@ export const program = {
92
93
  process.exit(code);
93
94
  },
94
95
  },
96
+ {
97
+ key: "schemagen",
98
+ description: "Generate JSON Schema artifacts from src/**/schema.ts into colocated __generated__/ directories.",
99
+ options: [
100
+ {
101
+ name: "root",
102
+ description: "Project root (default: current working directory).",
103
+ kind: CliOptionKind.String,
104
+ },
105
+ {
106
+ name: "src-dir",
107
+ description: "Source directory relative to --root (default: src).",
108
+ kind: CliOptionKind.String,
109
+ },
110
+ {
111
+ name: "tsconfig",
112
+ description: "Path to tsconfig relative to --root (default: tsconfig.json).",
113
+ kind: CliOptionKind.String,
114
+ },
115
+ ],
116
+ handler: (ctx) => {
117
+ try {
118
+ runSchemagenCli({
119
+ root: ctx.stringOpt("root"),
120
+ srcDir: ctx.stringOpt("src-dir"),
121
+ tsconfig: ctx.stringOpt("tsconfig"),
122
+ });
123
+ } catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ process.stderr.write(`${message}\n`);
126
+ process.exit(1);
127
+ }
128
+ },
129
+ },
95
130
  ],
96
131
  } satisfies CliProgram;
@@ -0,0 +1,23 @@
1
+ /** Run argsbarg schemagen in the current working directory (or `--root`). */
2
+
3
+ import { resolve } from "node:path";
4
+ import { runSchemagen } from "./schemagen/run.ts";
5
+
6
+ export interface RunSchemagenCliOptions {
7
+ root?: string;
8
+ srcDir?: string;
9
+ tsconfig?: string;
10
+ }
11
+
12
+ /** Exit 0 on success; throws on failure. */
13
+ export function runSchemagenCli(options: RunSchemagenCliOptions = {}): void {
14
+ const projectRoot = resolve(options.root ?? process.cwd());
15
+ const counts = runSchemagen({
16
+ projectRoot,
17
+ srcDir: options.srcDir,
18
+ tsconfig: options.tsconfig,
19
+ });
20
+ console.log(
21
+ `config roots: ${counts.configRoots}, input roots: ${counts.inputRoots}, output roots: ${counts.outputRoots}`,
22
+ );
23
+ }