argsbarg 6.0.1 → 6.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/README.md +4 -3
  3. package/docs/api-server.md +1 -1
  4. package/docs/cli-program.md +28 -0
  5. package/docs/config-schema.md +15 -13
  6. package/docs/output-schema.md +76 -73
  7. package/examples/full-example/README.md +7 -7
  8. package/examples/full-example/bun.lock +0 -29
  9. package/examples/full-example/justfile +6 -3
  10. package/examples/full-example/package.json +0 -2
  11. package/examples/full-example/src/commands/status/__generated__/index.ts +5 -0
  12. package/examples/full-example/src/commands/status/command.ts +2 -2
  13. package/examples/full-example/src/commands/status/types.ts +19 -1
  14. package/examples/full-example/src/config/__generated__/index.ts +5 -0
  15. package/examples/full-example/src/program.ts +4 -4
  16. package/index.d.ts +24 -3
  17. package/package.json +8 -1
  18. package/src/cli-tool/full-example-capabilities.test.ts +2 -1
  19. package/src/cli-tool/post-create.ts +3 -3
  20. package/src/cli-tool/program.ts +36 -0
  21. package/src/cli-tool/run-schemagen.ts +23 -0
  22. package/src/cli-tool/schemagen/cleanup.ts +60 -0
  23. package/src/cli-tool/schemagen/discover-schema-roots.ts +173 -0
  24. package/src/cli-tool/schemagen/index.ts +3 -0
  25. package/src/cli-tool/schemagen/names.ts +22 -0
  26. package/src/cli-tool/schemagen/run.ts +109 -0
  27. package/src/cli-tool/schemagen/schemagen.test.ts +143 -0
  28. package/src/context.ts +20 -2
  29. package/src/help.ts +2 -0
  30. package/src/index.ts +1 -0
  31. package/src/leaf-inputs.test.ts +170 -0
  32. package/src/leaf-inputs.ts +178 -0
  33. package/src/mcp/tools.ts +5 -0
  34. package/src/parse.ts +11 -0
  35. package/src/types.ts +8 -1
  36. package/src/validate.ts +43 -0
  37. package/examples/full-example/schemas/configSchemas.ts +0 -6
  38. package/examples/full-example/schemas/outputSchemas.ts +0 -6
  39. package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +0 -25
  40. package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +0 -148
  41. package/examples/full-example/scripts/schemagen/naming.ts +0 -99
  42. package/examples/full-example/scripts/schemagen.ts +0 -91
  43. package/examples/full-example/src/commands/status/schema-types.ts +0 -14
  44. /package/examples/full-example/{schemas/generated/status.json → src/commands/status/__generated__/outputSchema.json} +0 -0
  45. /package/examples/full-example/{schemas/generated/app-config.json → src/config/__generated__/configSchema.json} +0 -0
  46. /package/examples/full-example/src/config/{schema-types.ts → types.ts} +0 -0
package/src/validate.ts CHANGED
@@ -247,6 +247,31 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
247
247
  if (resolved !== undefined && (typeof resolved !== "object" || resolved === null || Array.isArray(resolved))) {
248
248
  throw new CliSchemaValidationError("outputSchema must be a JSON Schema object (not null or an array)");
249
249
  }
250
+ const inputSchema = node.inputSchema;
251
+ if (
252
+ inputSchema !== undefined &&
253
+ (typeof inputSchema !== "object" || inputSchema === null || Array.isArray(inputSchema))
254
+ ) {
255
+ throw new CliSchemaValidationError("inputSchema must be a JSON Schema object (not null or an array)");
256
+ }
257
+ if (inputSchema !== undefined) {
258
+ const properties = inputSchema.properties;
259
+ if (
260
+ properties !== undefined &&
261
+ (typeof properties !== "object" || properties === null || Array.isArray(properties))
262
+ ) {
263
+ throw new CliSchemaValidationError(`inputSchema.properties must be an object on ${node.key}`);
264
+ }
265
+ if (properties) {
266
+ for (const opt of node.options ?? []) {
267
+ if (opt.kind === CliOptionKind.Json && !(opt.name in properties)) {
268
+ throw new CliSchemaValidationError(
269
+ `Json option '${opt.name}' is missing from inputSchema.properties on ${node.key}`,
270
+ );
271
+ }
272
+ }
273
+ }
274
+ }
250
275
  } else {
251
276
  const rogue = node as unknown as CliLeaf;
252
277
  if (rogue.mcpTool !== undefined) {
@@ -301,7 +326,22 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
301
326
 
302
327
  function validateOptions(scopeKey: string, options: import("./types.ts").CliOption[]): void {
303
328
  const seenShorts = new Set<string>();
329
+ let pipableCount = 0;
304
330
  for (const opt of options) {
331
+ if (opt.pipable) {
332
+ pipableCount++;
333
+ if (opt.kind !== CliOptionKind.Json) {
334
+ throw new CliSchemaValidationError(`pipable is only valid on Json kind: ${scopeKey}/${opt.name}`);
335
+ }
336
+ }
337
+ if (opt.kind === CliOptionKind.Json) {
338
+ if (opt.format !== undefined || opt.pattern !== undefined || opt.default !== undefined) {
339
+ throw new CliSchemaValidationError(
340
+ `Json option cannot use format, pattern, or default: ${scopeKey}/${opt.name}`,
341
+ );
342
+ }
343
+ }
344
+
305
345
  if (opt.required && opt.kind === CliOptionKind.Presence) {
306
346
  throw new CliSchemaValidationError(`Presence option cannot be required: ${scopeKey}/${opt.name}`);
307
347
  }
@@ -340,6 +380,9 @@ function validateOptions(scopeKey: string, options: import("./types.ts").CliOpti
340
380
  validateOptionValueMetadata(scopeKey, opt);
341
381
  }
342
382
  }
383
+ if (pipableCount > 1) {
384
+ throw new CliSchemaValidationError(`At most one pipable Json option per command: ${scopeKey}`);
385
+ }
343
386
  }
344
387
 
345
388
  function validateOptionValueMetadata(scopeKey: string, opt: import("./types.ts").CliOption): void {
@@ -1,6 +0,0 @@
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>;
@@ -1,6 +0,0 @@
1
- // Auto-generated by scripts/schemagen.ts — do not edit by hand.
2
-
3
- import status from "./generated/status.json";
4
-
5
- /** JSON Schema for leaf outputSchema from `StatusJsonOutput`. */
6
- export const STATUS_JSON_OUTPUT_SCHEMA = status as Record<string, unknown>;
@@ -1,25 +0,0 @@
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 schema-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
- path: "src/config/schema-types.ts",
15
- outfile: "app-config.json",
16
- exportName: "APP_CONFIG_JSON_SCHEMA",
17
- });
18
- expect(output).toMatchObject({
19
- typeName: "StatusJsonOutput",
20
- path: "src/commands/status/schema-types.ts",
21
- outfile: "status.json",
22
- exportName: "STATUS_JSON_OUTPUT_SCHEMA",
23
- });
24
- });
25
- });
@@ -1,148 +0,0 @@
1
- /*
2
- Discovers schema roots in schema-types.ts files via configType / inputType / outputType exports.
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
- inputSchemaExportName,
11
- outfileForConfigType,
12
- outfileForInputType,
13
- outfileForOutputType,
14
- outputSchemaExportName,
15
- } from "./naming.ts";
16
-
17
- export type SchemaRootKind = "config" | "input" | "output";
18
-
19
- export type SchemaRole = "configType" | "inputType" | "outputType";
20
-
21
- export interface SchemaRoot {
22
- kind: SchemaRootKind;
23
- typeName: string;
24
- /** Path relative to project root (e.g. src/config/schema-types.ts). */
25
- path: string;
26
- outfile: string;
27
- exportName: string;
28
- }
29
-
30
- const SCHEMA_TYPES_FILE = "schema-types.ts";
31
-
32
- const ROLE_EXPORT_RE = /export\s+type\s+(configType|inputType|outputType)\s*=\s*(\w+)/g;
33
-
34
- const ROLE_TO_KIND: Record<SchemaRole, SchemaRootKind> = {
35
- configType: "config",
36
- inputType: "input",
37
- outputType: "output",
38
- };
39
-
40
- function listSchemaTypesFiles(srcDir: string, baseDir: string, out: string[]): void {
41
- for (const ent of readdirSync(srcDir)) {
42
- const full = join(srcDir, ent);
43
- const st = statSync(full);
44
- if (st.isDirectory()) {
45
- listSchemaTypesFiles(full, baseDir, out);
46
- continue;
47
- }
48
- if (ent === SCHEMA_TYPES_FILE) {
49
- out.push(relative(baseDir, full));
50
- }
51
- }
52
- }
53
-
54
- /** True when `typeName` is declared in this file (not a re-export alias to another module). */
55
- function isTypeDefinedInFile(text: string, typeName: string): boolean {
56
- if (new RegExp(`export\\s+interface\\s+${typeName}\\b`).test(text)) {
57
- return true;
58
- }
59
- if (new RegExp(`export\\s+type\\s+${typeName}\\s*=`).test(text)) {
60
- return !["configType", "inputType", "outputType"].includes(typeName);
61
- }
62
- return false;
63
- }
64
-
65
- function rootForRole(role: SchemaRole, typeName: string, path: string): SchemaRoot {
66
- const kind = ROLE_TO_KIND[role];
67
- if (kind === "config") {
68
- return {
69
- kind,
70
- typeName,
71
- path,
72
- outfile: outfileForConfigType(typeName),
73
- exportName: configSchemaExportName(typeName),
74
- };
75
- }
76
- if (kind === "input") {
77
- return {
78
- kind,
79
- typeName,
80
- path,
81
- outfile: outfileForInputType(typeName),
82
- exportName: inputSchemaExportName(typeName),
83
- };
84
- }
85
- return {
86
- kind,
87
- typeName,
88
- path,
89
- outfile: outfileForOutputType(typeName),
90
- exportName: outputSchemaExportName(typeName),
91
- };
92
- }
93
-
94
- function discoverFromFile(path: string, text: string): SchemaRoot[] {
95
- const rolesSeen = new Set<SchemaRole>();
96
- const roots: SchemaRoot[] = [];
97
-
98
- for (const match of text.matchAll(ROLE_EXPORT_RE)) {
99
- const role = match[1] as SchemaRole | undefined;
100
- const typeName = match[2];
101
- if (!role || !typeName) {
102
- continue;
103
- }
104
- if (rolesSeen.has(role)) {
105
- throw new Error(`${path}: duplicate export type ${role}`);
106
- }
107
- rolesSeen.add(role);
108
- if (!isTypeDefinedInFile(text, typeName)) {
109
- continue;
110
- }
111
- roots.push(rootForRole(role, typeName, path));
112
- }
113
-
114
- return roots;
115
- }
116
-
117
- /** Find all schema roots under `src/` in files named schema-types.ts. */
118
- export function discoverSchemaRoots(projectRoot: string): SchemaRoot[] {
119
- const srcDir = join(projectRoot, "src");
120
- const files: string[] = [];
121
- listSchemaTypesFiles(srcDir, projectRoot, files);
122
-
123
- const roots: SchemaRoot[] = [];
124
- const typeOwners = new Map<string, string>();
125
-
126
- for (const relPath of files.sort()) {
127
- const text = readFileSync(join(projectRoot, relPath), "utf8");
128
- for (const root of discoverFromFile(relPath, text)) {
129
- const prev = typeOwners.get(root.typeName);
130
- if (prev) {
131
- throw new Error(
132
- `${relPath}: duplicate schema root type ${root.typeName} (already declared in ${prev})`,
133
- );
134
- }
135
- typeOwners.set(root.typeName, relPath);
136
- roots.push(root);
137
- }
138
- }
139
-
140
- const configRoots = roots.filter((r) => r.kind === "config");
141
- if (configRoots.length > 1) {
142
- throw new Error(
143
- `multiple config schema roots: ${configRoots.map((r) => `${r.typeName} (${r.path})`).join(", ")}`,
144
- );
145
- }
146
-
147
- return roots;
148
- }
@@ -1,99 +0,0 @@
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 a tool-input schema root. */
20
- export function outfileForInputType(typeName: string): string {
21
- if (typeName.endsWith("ToolInput")) {
22
- return `${camelToKebab(typeName.slice(0, -"ToolInput".length))}-tool-input.json`;
23
- }
24
- return `${camelToKebab(typeName)}-tool-input.json`;
25
- }
26
-
27
- /** Bridge export constant for a tool-input schema root. */
28
- export function inputSchemaExportName(typeName: string): string {
29
- if (typeName.endsWith("ToolInput")) {
30
- const base = typeName.slice(0, -"ToolInput".length);
31
- return `${camelToScreamingSnake(base)}_TOOL_INPUT_SCHEMA`;
32
- }
33
- return `${camelToScreamingSnake(typeName)}_TOOL_INPUT_SCHEMA`;
34
- }
35
-
36
- /** Generated JSON filename for an output-schema root type. */
37
- export function outfileForOutputType(typeName: string): string {
38
- if (typeName.endsWith("JsonOutput")) {
39
- return `${camelToKebab(typeName.slice(0, -"JsonOutput".length))}.json`;
40
- }
41
- if (typeName.endsWith("OpResult")) {
42
- return `${camelToKebab(typeName.slice(0, -"OpResult".length))}-op-result.json`;
43
- }
44
- if (typeName.endsWith("Output")) {
45
- return `${camelToKebab(typeName.slice(0, -"Output".length))}.json`;
46
- }
47
- if (typeName.endsWith("Result")) {
48
- return `${camelToKebab(typeName.slice(0, -"Result".length))}.json`;
49
- }
50
- return `${camelToKebab(typeName)}.json`;
51
- }
52
-
53
- /** Bridge export constant for an output-schema root. */
54
- export function outputSchemaExportName(typeName: string): string {
55
- if (typeName.endsWith("JsonOutput")) {
56
- const base = typeName.slice(0, -"JsonOutput".length);
57
- return `${camelToScreamingSnake(base)}_JSON_OUTPUT_SCHEMA`;
58
- }
59
- if (typeName.endsWith("OpResult")) {
60
- const base = typeName.slice(0, -"OpResult".length);
61
- return `${camelToScreamingSnake(base)}_OP_RESULT_OUTPUT_SCHEMA`;
62
- }
63
- if (typeName.endsWith("Output")) {
64
- const base = typeName.slice(0, -"Output".length);
65
- return `${camelToScreamingSnake(base)}_OUTPUT_SCHEMA`;
66
- }
67
- if (typeName.endsWith("Result")) {
68
- const base = typeName.slice(0, -"Result".length);
69
- return `${camelToScreamingSnake(base)}_RESULT_OUTPUT_SCHEMA`;
70
- }
71
- return `${camelToScreamingSnake(typeName)}_OUTPUT_SCHEMA`;
72
- }
73
-
74
- /** Generated JSON filename for a config-schema root (typically AppConfig → app-config.json). */
75
- export function outfileForConfigType(typeName: string): string {
76
- if (typeName.endsWith("Config")) {
77
- return `${camelToKebab(typeName.slice(0, -"Config".length))}-config.json`;
78
- }
79
- return `${camelToKebab(typeName)}-config.json`;
80
- }
81
-
82
- /** Bridge export constant for a config-schema root (AppConfig → APP_CONFIG_JSON_SCHEMA). */
83
- export function configSchemaExportName(typeName: string): string {
84
- if (typeName.endsWith("Config")) {
85
- const base = typeName.slice(0, -"Config".length);
86
- return `${camelToScreamingSnake(base)}_CONFIG_JSON_SCHEMA`;
87
- }
88
- return `${camelToScreamingSnake(typeName)}_CONFIG_JSON_SCHEMA`;
89
- }
90
-
91
- /** Import path basename for a generated JSON file (no extension). */
92
- export function jsonImportBasename(outfile: string): string {
93
- return outfile.replace(/\.json$/, "");
94
- }
95
-
96
- /** Safe import binding for a generated JSON file (no extension, hyphens → underscores). */
97
- export function jsonImportVar(outfile: string): string {
98
- return jsonImportBasename(outfile).replace(/-/g, "_");
99
- }
@@ -1,91 +0,0 @@
1
- /*
2
- Generate JSON Schema artifacts and bridge modules from discovered schema-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.path),
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
- /** Sort by import binding so Biome `organizeImports` does not reorder the bridge file. */
33
- const sorted = [...roots].sort((left, right) =>
34
- jsonImportVar(left.outfile).localeCompare(jsonImportVar(right.outfile)),
35
- );
36
- const lines = [`// Auto-generated by scripts/${scriptName} — do not edit by hand.`, ""];
37
- for (const root of sorted) {
38
- const varName = jsonImportVar(root.outfile);
39
- lines.push(`import ${varName} from "./generated/${root.outfile}";`);
40
- }
41
- if (sorted.length > 0) {
42
- lines.push("");
43
- }
44
- for (const root of sorted) {
45
- const varName = jsonImportVar(root.outfile);
46
- lines.push(`/** ${banner} \`${root.typeName}\`. */`);
47
- lines.push(`export const ${root.exportName} = ${varName} as Record<string, unknown>;`);
48
- lines.push("");
49
- }
50
- writeFileSync(path, `${lines.join("\n")}`);
51
- }
52
-
53
- const roots = discoverSchemaRoots(projectRoot);
54
- const configRoots = roots.filter((r) => r.kind === "config");
55
- const inputRoots = roots.filter((r) => r.kind === "input");
56
- const outputRoots = roots.filter((r) => r.kind === "output");
57
-
58
- mkdirSync(generatedDir, { recursive: true });
59
-
60
- for (const root of roots) {
61
- const schema = generateJson(root);
62
- const outPath = join(generatedDir, root.outfile);
63
- mkdirSync(dirname(outPath), { recursive: true });
64
- writeFileSync(outPath, `${JSON.stringify(schema, null, 2)}\n`);
65
- console.log(`wrote schemas/generated/${root.outfile} (${root.typeName})`);
66
- }
67
-
68
- writeBridge(
69
- join(projectRoot, "schemas", "configSchemas.ts"),
70
- "schemagen.ts",
71
- configRoots,
72
- "JSON Schema for program.appConfig.jsonSchema from",
73
- );
74
- if (inputRoots.length > 0) {
75
- writeBridge(
76
- join(projectRoot, "schemas", "inputSchemas.ts"),
77
- "schemagen.ts",
78
- inputRoots,
79
- "JSON Schema for leaf inputSchema from",
80
- );
81
- }
82
- writeBridge(
83
- join(projectRoot, "schemas", "outputSchemas.ts"),
84
- "schemagen.ts",
85
- outputRoots,
86
- "JSON Schema for leaf outputSchema from",
87
- );
88
-
89
- console.log(
90
- `config roots: ${configRoots.length}, input roots: ${inputRoots.length}, output roots: ${outputRoots.length}`,
91
- );
@@ -1,14 +0,0 @@
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;