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.
- package/CHANGELOG.md +27 -1
- package/README.md +4 -4
- package/docs/api-server.md +1 -1
- package/docs/config-schema.md +27 -15
- package/docs/output-schema.md +107 -75
- package/examples/full-example/README.md +8 -7
- package/examples/full-example/bun.lock +0 -29
- package/examples/full-example/justfile +6 -3
- package/examples/full-example/package.json +0 -2
- package/examples/full-example/src/commands/status/__generated__/index.ts +5 -0
- package/examples/full-example/{schemas/generated/status.json → src/commands/status/__generated__/outputSchema.json} +1 -1
- package/examples/full-example/src/commands/status/command.ts +2 -2
- package/examples/full-example/src/commands/status/schema.ts +14 -0
- package/examples/full-example/src/commands/status/types.ts +1 -11
- package/examples/full-example/{schemas/generated/app-config.json → src/config/__generated__/configSchema.json} +1 -1
- package/examples/full-example/src/config/__generated__/index.ts +5 -0
- package/examples/full-example/src/{types.ts → config/schema.ts} +3 -2
- package/examples/full-example/src/program.ts +4 -4
- package/package.json +24 -17
- package/src/api/openapi.ts +4 -2
- package/src/api/result.ts +23 -1
- package/src/api/schema-deref.test.ts +99 -0
- package/src/api/schema-deref.ts +76 -0
- package/src/api.integration.test.ts +85 -2
- package/src/cli-errors.ts +3 -0
- package/src/cli-tool/full-example-capabilities.test.ts +2 -1
- package/src/cli-tool/post-create.ts +3 -3
- package/src/cli-tool/program.ts +35 -0
- package/src/cli-tool/run-schemagen.ts +23 -0
- package/src/cli-tool/schemagen/cleanup.ts +60 -0
- package/src/cli-tool/schemagen/discover-schema-roots.ts +103 -0
- package/src/cli-tool/schemagen/index.ts +3 -0
- package/src/cli-tool/schemagen/names.ts +22 -0
- package/src/cli-tool/schemagen/run.ts +108 -0
- package/src/cli-tool/schemagen/schemagen.test.ts +125 -0
- package/src/docs/mcp-resources.ts +5 -2
- package/src/headless/tool-call.ts +16 -6
- package/examples/full-example/schemas/configSchemas.ts +0 -6
- package/examples/full-example/schemas/outputSchemas.ts +0 -6
- package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +0 -25
- package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +0 -93
- package/examples/full-example/scripts/schemagen/naming.ts +0 -82
- 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
|
-
|
|
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": "
|
|
38
|
+
"description": "Application settings for `full-example` (`program.appConfig`).",
|
|
39
39
|
"definitions": {}
|
|
40
40
|
}
|
|
@@ -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 {
|
|
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
|
|
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:
|
|
38
|
-
entries:
|
|
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.
|
|
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
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
40
|
+
"scripts": {
|
|
41
|
+
"//just": "echo this app uses justfile for development tasks"
|
|
42
|
+
},
|
|
43
|
+
"type": "module",
|
|
44
|
+
"types": "./index.d.ts"
|
|
38
45
|
}
|
package/src/api/openapi.ts
CHANGED
|
@@ -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:
|
|
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
|
-
<
|
|
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
|
-
|
|
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).
|
|
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: "
|
|
44
|
+
label: "argsbarg schemagen",
|
|
45
45
|
run: () => {
|
|
46
46
|
if (dryRun) return;
|
|
47
|
-
const proc = Bun.spawnSync(["
|
|
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.
|
|
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
|
}
|
package/src/cli-tool/program.ts
CHANGED
|
@@ -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
|
+
}
|