argsbarg 3.6.4 → 4.0.0
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 +28 -2
- package/README.md +21 -9
- package/docs/README.md +12 -8
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +62 -2
- package/docs/config-schema.md +192 -0
- package/docs/developing.md +13 -0
- package/docs/install.md +38 -1
- package/docs/mcp.md +43 -19
- package/docs/output-schema.md +74 -52
- package/docs/templates/cursor/rules/cli-program.mdc +10 -5
- package/examples/config-app/main.ts +20 -0
- package/examples/config-app/program.ts +81 -0
- package/examples/config-app/schema.ts +37 -0
- package/examples/config-app/types.ts +19 -0
- package/examples/consumer-app/README.md +56 -0
- package/examples/consumer-app/bun.lock +75 -0
- package/examples/consumer-app/capabilities.test.ts +69 -0
- package/examples/consumer-app/package.json +17 -0
- package/examples/consumer-app/schemas/configSchemas.ts +6 -0
- package/examples/consumer-app/schemas/generated/app-config.json +40 -0
- package/examples/consumer-app/schemas/generated/status.json +28 -0
- package/examples/consumer-app/schemas/outputSchemas.ts +6 -0
- package/examples/consumer-app/scripts/schemagen/discover-schema-roots.test.ts +25 -0
- package/examples/consumer-app/scripts/schemagen/discover-schema-roots.ts +93 -0
- package/examples/consumer-app/scripts/schemagen/naming.ts +82 -0
- package/examples/consumer-app/scripts/schemagen.ts +76 -0
- package/examples/consumer-app/src/commands/status/types.ts +11 -0
- package/examples/consumer-app/src/main.ts +15 -0
- package/examples/consumer-app/src/program.ts +116 -0
- package/examples/consumer-app/src/types.ts +23 -0
- package/examples/consumer-app/tsconfig.json +14 -0
- package/examples/formats.ts +10 -3
- package/examples/mcp-test.ts +27 -8
- package/examples/minimal.ts +4 -3
- package/examples/nested.ts +5 -4
- package/examples/option-required.ts +8 -4
- package/index.d.ts +152 -75
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +13 -0
- package/src/builtins/config.test.ts +82 -0
- package/src/builtins/config.ts +220 -0
- package/src/builtins/dispatch.ts +18 -9
- package/src/builtins/export.ts +8 -33
- package/src/builtins/index.ts +1 -0
- package/src/builtins/install.ts +13 -0
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +2 -17
- package/src/builtins/registry.ts +40 -0
- package/src/capabilities.ts +46 -0
- package/src/cli-errors.ts +15 -0
- package/src/cli.ts +389 -0
- package/src/config/bootstrap.ts +265 -0
- package/src/config/context.test.ts +61 -0
- package/src/config/context.ts +98 -0
- package/src/config/entry.ts +81 -0
- package/src/config/file.test.ts +112 -0
- package/src/config/file.ts +120 -0
- package/src/config/manifest.ts +62 -0
- package/src/config/resolve.test.ts +88 -0
- package/src/config/resolve.ts +167 -0
- package/src/config/schema.ts +101 -0
- package/src/config/validate.test.ts +63 -0
- package/src/config/validate.ts +292 -0
- package/src/config.integration.test.ts +100 -0
- package/src/context.ts +5 -0
- package/src/docs/docs.test.ts +16 -16
- package/src/docs/mcp-guide.ts +35 -11
- package/src/hidden-mcpb.test.ts +40 -2
- package/src/index.ts +4 -3
- package/src/install/index.ts +46 -3
- package/src/install/paths.ts +5 -20
- package/src/install/plan.ts +6 -0
- package/src/install/status.ts +12 -0
- package/src/install/uninstall.ts +11 -0
- package/src/install/update.test.ts +5 -5
- package/src/invoke.test.ts +207 -0
- package/src/mcp/bundle.ts +9 -116
- package/src/mcp/claude.test.ts +73 -0
- package/src/mcp/claude.ts +168 -0
- package/src/mcp/env.ts +3 -37
- package/src/mcp/server.ts +18 -10
- package/src/mcp/tools.ts +3 -7
- package/src/mcp/zip.ts +82 -0
- package/src/mcp.integration.test.ts +502 -0
- package/src/{index.test.ts → parse.test.ts} +24 -935
- package/src/paths/host.ts +40 -0
- package/src/schema.ts +1 -1
- package/src/skill/generate.ts +18 -5
- package/src/skill/hint.ts +18 -0
- package/src/skill/install.ts +1 -5
- package/src/test-fixtures.ts +192 -0
- package/src/types.ts +39 -13
- package/src/validate.ts +70 -0
- package/src/completion.ts +0 -13
- package/src/invoke.ts +0 -217
- package/src/mcp.ts +0 -28
- package/src/runtime.ts +0 -134
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "consumer-app-example",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"argsbarg": "file:../..",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"ts-json-schema-generator": "^2.3.0",
|
|
12
|
+
"typescript": "^5.9.3",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@biomejs/biome": ["@biomejs/biome@2.5.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.1", "@biomejs/cli-darwin-x64": "2.5.1", "@biomejs/cli-linux-arm64": "2.5.1", "@biomejs/cli-linux-arm64-musl": "2.5.1", "@biomejs/cli-linux-x64": "2.5.1", "@biomejs/cli-linux-x64-musl": "2.5.1", "@biomejs/cli-win32-arm64": "2.5.1", "@biomejs/cli-win32-x64": "2.5.1" }, "bin": { "biome": "bin/biome" } }, "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q=="],
|
|
18
|
+
|
|
19
|
+
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w=="],
|
|
20
|
+
|
|
21
|
+
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw=="],
|
|
22
|
+
|
|
23
|
+
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q=="],
|
|
24
|
+
|
|
25
|
+
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw=="],
|
|
26
|
+
|
|
27
|
+
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg=="],
|
|
28
|
+
|
|
29
|
+
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A=="],
|
|
30
|
+
|
|
31
|
+
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ=="],
|
|
32
|
+
|
|
33
|
+
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg=="],
|
|
34
|
+
|
|
35
|
+
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
|
36
|
+
|
|
37
|
+
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
|
38
|
+
|
|
39
|
+
"@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="],
|
|
40
|
+
|
|
41
|
+
"argsbarg": ["argsbarg@file:../..", { "devDependencies": { "@biomejs/biome": "^2.5.0", "@types/bun": "^1.3.12", "typescript": "^5.9.3" }, "bin": { "argsbarg": "src/index.ts" } }],
|
|
42
|
+
|
|
43
|
+
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
|
44
|
+
|
|
45
|
+
"brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
|
|
46
|
+
|
|
47
|
+
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
|
48
|
+
|
|
49
|
+
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
|
50
|
+
|
|
51
|
+
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
|
52
|
+
|
|
53
|
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
|
54
|
+
|
|
55
|
+
"lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
|
|
56
|
+
|
|
57
|
+
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
|
58
|
+
|
|
59
|
+
"minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
|
|
60
|
+
|
|
61
|
+
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
|
62
|
+
|
|
63
|
+
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
|
64
|
+
|
|
65
|
+
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
|
66
|
+
|
|
67
|
+
"ts-json-schema-generator": ["ts-json-schema-generator@2.9.0", "", { "dependencies": { "@types/json-schema": "^7.0.15", "commander": "^14.0.3", "glob": "^13.0.6", "json5": "^2.2.3", "normalize-path": "^3.0.0", "safe-stable-stringify": "^2.5.0", "tslib": "^2.8.1", "typescript": "^5.9.3" }, "bin": { "ts-json-schema-generator": "bin/ts-json-schema-generator.js" } }, "sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q=="],
|
|
68
|
+
|
|
69
|
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
70
|
+
|
|
71
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
72
|
+
|
|
73
|
+
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveCapabilities } from "../../src/capabilities.ts";
|
|
5
|
+
import { CliOptionKind, type CliProgram } from "../../src/types.ts";
|
|
6
|
+
|
|
7
|
+
const programSource = readFileSync(join(import.meta.dir, "src/program.ts"), "utf8");
|
|
8
|
+
|
|
9
|
+
/** Mirror of consumer-app/src/program.ts capability flags (in-repo types only). */
|
|
10
|
+
const sinkProgram = {
|
|
11
|
+
key: "consumer-app",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
description: "Sink reference.",
|
|
14
|
+
appConfig: {
|
|
15
|
+
entries: {
|
|
16
|
+
apiToken: { description: "Token.", env: "CONSUMER_APP_API_TOKEN" },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
docs: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
topics: { readme: { text: "# readme\n" } },
|
|
22
|
+
},
|
|
23
|
+
mcpServer: { enabled: true },
|
|
24
|
+
install: {
|
|
25
|
+
updateGetLatest: async () => ({ path: "/bin/app", version: "1.0.0" }),
|
|
26
|
+
},
|
|
27
|
+
commands: [
|
|
28
|
+
{
|
|
29
|
+
key: "status",
|
|
30
|
+
description: "Status.",
|
|
31
|
+
handler: () => {},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "echo",
|
|
35
|
+
description: "Echo.",
|
|
36
|
+
options: [
|
|
37
|
+
{
|
|
38
|
+
name: "message",
|
|
39
|
+
description: "Message.",
|
|
40
|
+
kind: CliOptionKind.String,
|
|
41
|
+
required: true,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
handler: () => {},
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
} satisfies CliProgram;
|
|
48
|
+
|
|
49
|
+
describe("consumer-app sink", () => {
|
|
50
|
+
test("program source enables every builtin flag", () => {
|
|
51
|
+
expect(programSource).toContain("mcpServer: {");
|
|
52
|
+
expect(programSource).toContain("enabled: true");
|
|
53
|
+
expect(programSource).toContain("docs:");
|
|
54
|
+
expect(programSource).toContain("updateGetLatest");
|
|
55
|
+
expect(programSource).toContain("appConfig:");
|
|
56
|
+
expect(programSource).toContain("outputSchema:");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("resolveCapabilities matches full sink shape", () => {
|
|
60
|
+
expect(resolveCapabilities(sinkProgram)).toEqual({
|
|
61
|
+
completion: true,
|
|
62
|
+
mcp: true,
|
|
63
|
+
install: true,
|
|
64
|
+
docs: true,
|
|
65
|
+
update: true,
|
|
66
|
+
configCommands: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "consumer-app-example",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Argsbarg kitchen-sink reference app (copy template; not published to npm).",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"schemagen": "bun run scripts/schemagen.ts",
|
|
8
|
+
"start": "bun run src/main.ts"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"argsbarg": "file:../.."
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"ts-json-schema-generator": "^2.3.0",
|
|
15
|
+
"typescript": "^5.9.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Auto-generated by scripts/schemagen.ts — do not edit by hand.
|
|
2
|
+
|
|
3
|
+
import app_config from "./generated/app-config.json";
|
|
4
|
+
|
|
5
|
+
/** JSON Schema for program.appConfig.jsonSchema from `AppConfig`. */
|
|
6
|
+
export const APP_CONFIG_JSON_SCHEMA = app_config as Record<string, unknown>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"apiToken": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "API token from the provider dashboard."
|
|
8
|
+
},
|
|
9
|
+
"defaultRegion": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "AWS region (default us-east-1).",
|
|
12
|
+
"default": "us-east-1"
|
|
13
|
+
},
|
|
14
|
+
"maxRetries": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"description": "HTTP retry count (default 3).",
|
|
17
|
+
"default": 3
|
|
18
|
+
},
|
|
19
|
+
"prefs": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"ttl": {
|
|
23
|
+
"type": "number"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"required": [
|
|
27
|
+
"ttl"
|
|
28
|
+
],
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"description": "Local preferences (file-only; not mapped to process.env)."
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": [
|
|
34
|
+
"apiToken",
|
|
35
|
+
"maxRetries"
|
|
36
|
+
],
|
|
37
|
+
"additionalProperties": false,
|
|
38
|
+
"description": "Config schema\n\nApplication settings for `consumer-app` (`program.appConfig`).",
|
|
39
|
+
"definitions": {}
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"defaultRegion": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"description": "Resolved AWS region."
|
|
8
|
+
},
|
|
9
|
+
"maxRetries": {
|
|
10
|
+
"type": "number",
|
|
11
|
+
"description": "Resolved retry count."
|
|
12
|
+
},
|
|
13
|
+
"apiTokenSet": {
|
|
14
|
+
"type": "boolean",
|
|
15
|
+
"description": "Whether apiToken is set (value never included)."
|
|
16
|
+
},
|
|
17
|
+
"version": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "App version from program root."
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": [
|
|
23
|
+
"apiTokenSet",
|
|
24
|
+
"version"
|
|
25
|
+
],
|
|
26
|
+
"description": "JSON payload for `consumer-app status --json`.",
|
|
27
|
+
"definitions": {}
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { discoverSchemaRoots } from "./discover-schema-roots.ts";
|
|
4
|
+
|
|
5
|
+
const projectRoot = join(import.meta.dir, "../..");
|
|
6
|
+
|
|
7
|
+
describe("discover-schema-roots", () => {
|
|
8
|
+
test("finds AppConfig and StatusJsonOutput in src types.ts files", () => {
|
|
9
|
+
const roots = discoverSchemaRoots(projectRoot);
|
|
10
|
+
const config = roots.find((r) => r.kind === "config");
|
|
11
|
+
const output = roots.find((r) => r.kind === "output");
|
|
12
|
+
expect(config).toMatchObject({
|
|
13
|
+
typeName: "AppConfig",
|
|
14
|
+
relFile: "src/types.ts",
|
|
15
|
+
outfile: "app-config.json",
|
|
16
|
+
exportName: "APP_CONFIG_JSON_SCHEMA",
|
|
17
|
+
});
|
|
18
|
+
expect(output).toMatchObject({
|
|
19
|
+
typeName: "StatusJsonOutput",
|
|
20
|
+
relFile: "src/commands/status/types.ts",
|
|
21
|
+
outfile: "status.json",
|
|
22
|
+
exportName: "STATUS_JSON_OUTPUT_SCHEMA",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Discovers schema roots in src (recursive) types.ts files by JSDoc markers.
|
|
3
|
+
Copy per consumer repo — see docs/output-schema.md and docs/config-schema.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { join, relative } from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
configSchemaExportName,
|
|
10
|
+
outfileForConfigType,
|
|
11
|
+
outfileForOutputType,
|
|
12
|
+
outputSchemaExportName,
|
|
13
|
+
} from "./naming.ts";
|
|
14
|
+
|
|
15
|
+
export type SchemaRootKind = "config" | "output";
|
|
16
|
+
|
|
17
|
+
export interface SchemaRoot {
|
|
18
|
+
kind: SchemaRootKind;
|
|
19
|
+
typeName: string;
|
|
20
|
+
/** Path relative to project root (e.g. src/types.ts). */
|
|
21
|
+
relFile: string;
|
|
22
|
+
outfile: string;
|
|
23
|
+
exportName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CONFIG_MARKER = "Config schema";
|
|
27
|
+
const OUTPUT_MARKER = "JSON payload";
|
|
28
|
+
|
|
29
|
+
const INTERFACE_RE = /\/\*\*([\s\S]*?)\*\/\s*export\s+interface\s+(\w+)/g;
|
|
30
|
+
|
|
31
|
+
function listTypesTsFiles(srcDir: string, baseDir: string, out: string[]): void {
|
|
32
|
+
for (const ent of readdirSync(srcDir)) {
|
|
33
|
+
const full = join(srcDir, ent);
|
|
34
|
+
const st = statSync(full);
|
|
35
|
+
if (st.isDirectory()) {
|
|
36
|
+
listTypesTsFiles(full, baseDir, out);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ent === "types.ts") {
|
|
40
|
+
out.push(relative(baseDir, full));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function classifyRoot(jsDoc: string, typeName: string, relFile: string): SchemaRoot | undefined {
|
|
46
|
+
const hasConfig = jsDoc.includes(CONFIG_MARKER);
|
|
47
|
+
const hasOutput = jsDoc.includes(OUTPUT_MARKER);
|
|
48
|
+
if (hasConfig && hasOutput) {
|
|
49
|
+
throw new Error(`${relFile}: ${typeName} has both Config schema and JSON payload markers`);
|
|
50
|
+
}
|
|
51
|
+
if (!hasConfig && !hasOutput) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (hasConfig) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "config",
|
|
57
|
+
typeName,
|
|
58
|
+
relFile,
|
|
59
|
+
outfile: outfileForConfigType(typeName),
|
|
60
|
+
exportName: configSchemaExportName(typeName),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
kind: "output",
|
|
65
|
+
typeName,
|
|
66
|
+
relFile,
|
|
67
|
+
outfile: outfileForOutputType(typeName),
|
|
68
|
+
exportName: outputSchemaExportName(typeName),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Find all schema roots under `src/` in files named types.ts. */
|
|
73
|
+
export function discoverSchemaRoots(projectRoot: string): SchemaRoot[] {
|
|
74
|
+
const srcDir = join(projectRoot, "src");
|
|
75
|
+
const files: string[] = [];
|
|
76
|
+
listTypesTsFiles(srcDir, projectRoot, files);
|
|
77
|
+
const roots: SchemaRoot[] = [];
|
|
78
|
+
for (const relFile of files.sort()) {
|
|
79
|
+
const text = readFileSync(join(projectRoot, relFile), "utf8");
|
|
80
|
+
for (const match of text.matchAll(INTERFACE_RE)) {
|
|
81
|
+
const jsDoc = match[1] ?? "";
|
|
82
|
+
const typeName = match[2];
|
|
83
|
+
if (!typeName) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const root = classifyRoot(jsDoc, typeName, relFile);
|
|
87
|
+
if (root) {
|
|
88
|
+
roots.push(root);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return roots;
|
|
93
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Maps discovered schema root type names to generated filenames and bridge export constants.
|
|
3
|
+
Matches conventions in docs/output-schema.md and docs/config-schema.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** kebab-case from PascalCase segments. */
|
|
7
|
+
export function camelToKebab(name: string): string {
|
|
8
|
+
return name
|
|
9
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
10
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
11
|
+
.toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** SCREAMING_SNAKE from PascalCase. */
|
|
15
|
+
export function camelToScreamingSnake(name: string): string {
|
|
16
|
+
return camelToKebab(name).replace(/-/g, "_").toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Generated JSON filename for an output-schema root type. */
|
|
20
|
+
export function outfileForOutputType(typeName: string): string {
|
|
21
|
+
if (typeName.endsWith("JsonOutput")) {
|
|
22
|
+
return `${camelToKebab(typeName.slice(0, -"JsonOutput".length))}.json`;
|
|
23
|
+
}
|
|
24
|
+
if (typeName.endsWith("OpResult")) {
|
|
25
|
+
return `${camelToKebab(typeName.slice(0, -"OpResult".length))}-op-result.json`;
|
|
26
|
+
}
|
|
27
|
+
if (typeName.endsWith("Output")) {
|
|
28
|
+
return `${camelToKebab(typeName.slice(0, -"Output".length))}.json`;
|
|
29
|
+
}
|
|
30
|
+
if (typeName.endsWith("Result")) {
|
|
31
|
+
return `${camelToKebab(typeName.slice(0, -"Result".length))}.json`;
|
|
32
|
+
}
|
|
33
|
+
return `${camelToKebab(typeName)}.json`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bridge export constant for an output-schema root. */
|
|
37
|
+
export function outputSchemaExportName(typeName: string): string {
|
|
38
|
+
if (typeName.endsWith("JsonOutput")) {
|
|
39
|
+
const base = typeName.slice(0, -"JsonOutput".length);
|
|
40
|
+
return `${camelToScreamingSnake(base)}_JSON_OUTPUT_SCHEMA`;
|
|
41
|
+
}
|
|
42
|
+
if (typeName.endsWith("OpResult")) {
|
|
43
|
+
const base = typeName.slice(0, -"OpResult".length);
|
|
44
|
+
return `${camelToScreamingSnake(base)}_OP_RESULT_OUTPUT_SCHEMA`;
|
|
45
|
+
}
|
|
46
|
+
if (typeName.endsWith("Output")) {
|
|
47
|
+
const base = typeName.slice(0, -"Output".length);
|
|
48
|
+
return `${camelToScreamingSnake(base)}_OUTPUT_SCHEMA`;
|
|
49
|
+
}
|
|
50
|
+
if (typeName.endsWith("Result")) {
|
|
51
|
+
const base = typeName.slice(0, -"Result".length);
|
|
52
|
+
return `${camelToScreamingSnake(base)}_RESULT_OUTPUT_SCHEMA`;
|
|
53
|
+
}
|
|
54
|
+
return `${camelToScreamingSnake(typeName)}_OUTPUT_SCHEMA`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Generated JSON filename for a config-schema root (typically AppConfig → app-config.json). */
|
|
58
|
+
export function outfileForConfigType(typeName: string): string {
|
|
59
|
+
if (typeName.endsWith("Config")) {
|
|
60
|
+
return `${camelToKebab(typeName.slice(0, -"Config".length))}-config.json`;
|
|
61
|
+
}
|
|
62
|
+
return `${camelToKebab(typeName)}-config.json`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bridge export constant for a config-schema root (AppConfig → APP_CONFIG_JSON_SCHEMA). */
|
|
66
|
+
export function configSchemaExportName(typeName: string): string {
|
|
67
|
+
if (typeName.endsWith("Config")) {
|
|
68
|
+
const base = typeName.slice(0, -"Config".length);
|
|
69
|
+
return `${camelToScreamingSnake(base)}_CONFIG_JSON_SCHEMA`;
|
|
70
|
+
}
|
|
71
|
+
return `${camelToScreamingSnake(typeName)}_CONFIG_JSON_SCHEMA`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Import path basename for a generated JSON file (no extension). */
|
|
75
|
+
export function jsonImportBasename(outfile: string): string {
|
|
76
|
+
return outfile.replace(/\.json$/, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Safe import binding for a generated JSON file (no extension, hyphens → underscores). */
|
|
80
|
+
export function jsonImportVar(outfile: string): string {
|
|
81
|
+
return jsonImportBasename(outfile).replace(/-/g, "_");
|
|
82
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Generate JSON Schema artifacts and bridge modules from discovered types.ts roots.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { createGenerator } from "ts-json-schema-generator";
|
|
8
|
+
import { discoverSchemaRoots, type SchemaRoot } from "./schemagen/discover-schema-roots.ts";
|
|
9
|
+
import { jsonImportVar } from "./schemagen/naming.ts";
|
|
10
|
+
|
|
11
|
+
const projectRoot = join(import.meta.dir, "..");
|
|
12
|
+
const generatedDir = join(projectRoot, "schemas", "generated");
|
|
13
|
+
|
|
14
|
+
function generateJson(root: SchemaRoot): Record<string, unknown> {
|
|
15
|
+
const generator = createGenerator({
|
|
16
|
+
path: join(projectRoot, root.relFile),
|
|
17
|
+
type: root.typeName,
|
|
18
|
+
tsconfig: join(projectRoot, "tsconfig.json"),
|
|
19
|
+
topRef: false,
|
|
20
|
+
skipTypeCheck: false,
|
|
21
|
+
jsDoc: "extended",
|
|
22
|
+
additionalProperties: root.kind === "config" ? false : undefined,
|
|
23
|
+
});
|
|
24
|
+
const schema = generator.createSchema(root.typeName) as Record<string, unknown>;
|
|
25
|
+
if (root.kind === "config") {
|
|
26
|
+
schema.additionalProperties = false;
|
|
27
|
+
}
|
|
28
|
+
return schema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeBridge(path: string, scriptName: string, roots: SchemaRoot[], banner: string): void {
|
|
32
|
+
const lines = [`// Auto-generated by scripts/${scriptName} — do not edit by hand.`, ""];
|
|
33
|
+
for (const root of roots) {
|
|
34
|
+
const varName = jsonImportVar(root.outfile);
|
|
35
|
+
lines.push(`import ${varName} from "./generated/${root.outfile}";`);
|
|
36
|
+
}
|
|
37
|
+
if (roots.length > 0) {
|
|
38
|
+
lines.push("");
|
|
39
|
+
}
|
|
40
|
+
for (const root of roots) {
|
|
41
|
+
const varName = jsonImportVar(root.outfile);
|
|
42
|
+
lines.push(`/** ${banner} \`${root.typeName}\`. */`);
|
|
43
|
+
lines.push(`export const ${root.exportName} = ${varName} as Record<string, unknown>;`);
|
|
44
|
+
lines.push("");
|
|
45
|
+
}
|
|
46
|
+
writeFileSync(path, `${lines.join("\n")}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const roots = discoverSchemaRoots(projectRoot);
|
|
50
|
+
const configRoots = roots.filter((r) => r.kind === "config");
|
|
51
|
+
const outputRoots = roots.filter((r) => r.kind === "output");
|
|
52
|
+
|
|
53
|
+
mkdirSync(generatedDir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
for (const root of roots) {
|
|
56
|
+
const schema = generateJson(root);
|
|
57
|
+
const outPath = join(generatedDir, root.outfile);
|
|
58
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
59
|
+
writeFileSync(outPath, `${JSON.stringify(schema, null, 2)}\n`);
|
|
60
|
+
console.log(`wrote schemas/generated/${root.outfile} (${root.typeName})`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
writeBridge(
|
|
64
|
+
join(projectRoot, "schemas", "configSchemas.ts"),
|
|
65
|
+
"schemagen.ts",
|
|
66
|
+
configRoots,
|
|
67
|
+
"JSON Schema for program.appConfig.jsonSchema from",
|
|
68
|
+
);
|
|
69
|
+
writeBridge(
|
|
70
|
+
join(projectRoot, "schemas", "outputSchemas.ts"),
|
|
71
|
+
"schemagen.ts",
|
|
72
|
+
outputRoots,
|
|
73
|
+
"JSON Schema for leaf outputSchema from",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(`config roots: ${configRoots.length}, output roots: ${outputRoots.length}`);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** JSON payload for `consumer-app 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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/*
|
|
3
|
+
Kitchen-sink argsbarg consumer reference.
|
|
4
|
+
|
|
5
|
+
cd examples/consumer-app && bun install && bun run schemagen
|
|
6
|
+
CONSUMER_APP_API_TOKEN=dev bun run start status --json
|
|
7
|
+
CONSUMER_APP_API_TOKEN=dev bun run start config get
|
|
8
|
+
CONSUMER_APP_API_TOKEN=dev bun run start docs readme
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Cli } from "argsbarg";
|
|
12
|
+
import { program } from "./program.ts";
|
|
13
|
+
|
|
14
|
+
const cli = new Cli(program);
|
|
15
|
+
await cli.run();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Kitchen-sink CliProgram — every argsbarg builtin enabled.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type CliAppConfig,
|
|
7
|
+
type CliAppConfigEntry,
|
|
8
|
+
CliOptionKind,
|
|
9
|
+
type CliProgram,
|
|
10
|
+
} from "argsbarg";
|
|
11
|
+
import { APP_CONFIG_JSON_SCHEMA } from "../schemas/configSchemas.ts";
|
|
12
|
+
import { STATUS_JSON_OUTPUT_SCHEMA } from "../schemas/outputSchemas.ts";
|
|
13
|
+
import type { StatusJsonOutput } from "./commands/status/types.ts";
|
|
14
|
+
|
|
15
|
+
const configPath = process.env.CONSUMER_APP_CONFIG_FILE;
|
|
16
|
+
|
|
17
|
+
const configSchema = {
|
|
18
|
+
apiToken: {
|
|
19
|
+
description: "Create at https://example.com/settings/tokens",
|
|
20
|
+
env: "CONSUMER_APP_API_TOKEN",
|
|
21
|
+
sensitive: true,
|
|
22
|
+
},
|
|
23
|
+
defaultRegion: {
|
|
24
|
+
description: "AWS region for API calls.",
|
|
25
|
+
required: false,
|
|
26
|
+
},
|
|
27
|
+
maxRetries: {
|
|
28
|
+
description: "HTTP retry count (0–10).",
|
|
29
|
+
},
|
|
30
|
+
prefs: {
|
|
31
|
+
description: "Local cache preferences (not exported to env).",
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
} as const satisfies Record<string, CliAppConfigEntry>;
|
|
35
|
+
|
|
36
|
+
export const program = {
|
|
37
|
+
key: "consumer-app",
|
|
38
|
+
version: "1.0.0",
|
|
39
|
+
description: "Argsbarg kitchen-sink reference — all builtins, schemagen, ctx.appConfig.",
|
|
40
|
+
appConfig: {
|
|
41
|
+
...(configPath ? { path: configPath } : {}),
|
|
42
|
+
jsonSchema: APP_CONFIG_JSON_SCHEMA,
|
|
43
|
+
entries: configSchema,
|
|
44
|
+
} satisfies CliAppConfig,
|
|
45
|
+
docs: {
|
|
46
|
+
enabled: true,
|
|
47
|
+
topics: {
|
|
48
|
+
readme: {
|
|
49
|
+
text: "# consumer-app\n\nKitchen-sink argsbarg reference. Copy this layout into a new CLI repo.\n",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
mcpServer: {
|
|
54
|
+
enabled: true,
|
|
55
|
+
resources: [
|
|
56
|
+
{
|
|
57
|
+
uri: "consumer-app://readme",
|
|
58
|
+
name: "readme",
|
|
59
|
+
description: "Bundled readme topic.",
|
|
60
|
+
mimeType: "text/plain",
|
|
61
|
+
load: () => "# consumer-app\n\nKitchen-sink reference.\n",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
install: {
|
|
66
|
+
updateGetLatest: async () => ({
|
|
67
|
+
path: process.execPath,
|
|
68
|
+
version: "1.0.0",
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
commands: [
|
|
72
|
+
{
|
|
73
|
+
key: "status",
|
|
74
|
+
description: "Show resolved config and app version.",
|
|
75
|
+
options: [
|
|
76
|
+
{
|
|
77
|
+
name: "json",
|
|
78
|
+
description: "Emit JSON.",
|
|
79
|
+
kind: CliOptionKind.Presence,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
outputSchema: STATUS_JSON_OUTPUT_SCHEMA,
|
|
83
|
+
handler: (ctx) => {
|
|
84
|
+
const out: StatusJsonOutput = {
|
|
85
|
+
defaultRegion: ctx.appConfig.get("defaultRegion") as string | undefined,
|
|
86
|
+
maxRetries: ctx.appConfig.get("maxRetries") as number | undefined,
|
|
87
|
+
apiTokenSet: ctx.appConfig.get("apiToken") !== undefined,
|
|
88
|
+
version: ctx.program.version,
|
|
89
|
+
};
|
|
90
|
+
if (ctx.hasFlag("json")) {
|
|
91
|
+
console.log(JSON.stringify(out, null, 2));
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`version=${out.version}`);
|
|
94
|
+
console.log(`region=${out.defaultRegion ?? "(not set)"}`);
|
|
95
|
+
console.log(`maxRetries=${out.maxRetries ?? "(not set)"}`);
|
|
96
|
+
console.log(`apiToken=${out.apiTokenSet ? "set" : "missing"}`);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
key: "echo",
|
|
102
|
+
description: "Echo a message (MCP-friendly leaf).",
|
|
103
|
+
options: [
|
|
104
|
+
{
|
|
105
|
+
name: "message",
|
|
106
|
+
description: "Text to print.",
|
|
107
|
+
kind: CliOptionKind.String,
|
|
108
|
+
required: true,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
handler: (ctx) => {
|
|
112
|
+
console.log(ctx.stringOpt("message") ?? "");
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
} satisfies CliProgram;
|