argsbarg 6.0.2 → 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.
@@ -39,10 +39,10 @@ function makeTempProject(): string {
39
39
  return root;
40
40
  }
41
41
 
42
- function writeSchema(root: string, relDir: string, body: string): void {
42
+ function writeTypes(root: string, relDir: string, body: string): void {
43
43
  const dir = join(root, "src", relDir);
44
44
  mkdirSync(dir, { recursive: true });
45
- writeFileSync(join(dir, "schema.ts"), body);
45
+ writeFileSync(join(dir, "types.ts"), body);
46
46
  }
47
47
 
48
48
  describe("schemagen", () => {
@@ -62,14 +62,38 @@ describe("schemagen", () => {
62
62
  expect(counts).toEqual({ configRoots: 1, inputRoots: 0, outputRoots: 1 });
63
63
  });
64
64
 
65
+ test("discovers roots from types.ts with interface and role export", () => {
66
+ const root = makeTempProject();
67
+ const dir = join(root, "src/commands/demo");
68
+ writeTypes(
69
+ root,
70
+ "commands/demo",
71
+ `export interface DemoOutput { ok: boolean; }
72
+
73
+ /** Schemagen root for leaf outputSchema. */
74
+ export type outputType = DemoOutput;
75
+ `,
76
+ );
77
+
78
+ const roots = discoverSchemaRoots(root);
79
+ expect(roots).toEqual([
80
+ {
81
+ kind: "output",
82
+ typeName: "DemoOutput",
83
+ path: "src/commands/demo/types.ts",
84
+ sourcePath: "src/commands/demo/types.ts",
85
+ },
86
+ ]);
87
+ runSchemagen({ projectRoot: root });
88
+ expect(existsSync(join(dir, "__generated__/outputSchema.json"))).toBe(true);
89
+ });
90
+
65
91
  test("removes stale JSON files when a schema kind is dropped", () => {
66
92
  const root = makeTempProject();
67
- writeSchema(
93
+ writeTypes(
68
94
  root,
69
95
  "commands/demo",
70
- `/** Tool input. */
71
- export interface DemoInput { id: string; }
72
- /** Tool output. */
96
+ `export interface DemoInput { id: string; }
73
97
  export interface DemoOutput { ok: boolean; }
74
98
  export type inputType = DemoInput;
75
99
  export type outputType = DemoOutput;
@@ -81,11 +105,10 @@ export type outputType = DemoOutput;
81
105
  expect(existsSync(join(generatedDir, "inputSchema.json"))).toBe(true);
82
106
  expect(existsSync(join(generatedDir, "outputSchema.json"))).toBe(true);
83
107
 
84
- writeSchema(
108
+ writeTypes(
85
109
  root,
86
110
  "commands/demo",
87
- `/** Tool output. */
88
- export interface DemoOutput { ok: boolean; }
111
+ `export interface DemoOutput { ok: boolean; }
89
112
  export type outputType = DemoOutput;
90
113
  `,
91
114
  );
@@ -96,7 +119,7 @@ export type outputType = DemoOutput;
96
119
  expect(existsSync(join(generatedDir, "index.ts"))).toBe(true);
97
120
  });
98
121
 
99
- test("removes orphan __generated__ when schema.ts is gone", () => {
122
+ test("removes orphan __generated__ when types.ts manifest is gone", () => {
100
123
  const root = makeTempProject();
101
124
  const generatedDir = join(root, "src/orphan/__generated__");
102
125
  mkdirSync(generatedDir, { recursive: true });
@@ -107,14 +130,9 @@ export type outputType = DemoOutput;
107
130
  expect(existsSync(generatedDir)).toBe(false);
108
131
  });
109
132
 
110
- test("removes __generated__ when schema.ts has no discoverable roots", () => {
133
+ test("removes __generated__ when types.ts has no discoverable roots", () => {
111
134
  const root = makeTempProject();
112
- writeSchema(
113
- root,
114
- "commands/empty",
115
- `export type outputType = never;
116
- `,
117
- );
135
+ writeTypes(root, "commands/empty", `export type outputType = never;\n`);
118
136
  const generatedDir = join(root, "src/commands/empty/__generated__");
119
137
  mkdirSync(generatedDir, { recursive: true });
120
138
  writeFileSync(join(generatedDir, "outputSchema.json"), "{}\n");
package/src/context.ts CHANGED
@@ -10,6 +10,7 @@ parsed values.
10
10
  import type { AnyAppConfigSnapshot } from "./config/context.ts";
11
11
  import { EmptyAppConfigSnapshot } from "./config/context.ts";
12
12
  import { parseCommaList, parseDate, parseDateTime, parseDurationMs } from "./formats.ts";
13
+ import { readLeafInputsAsync as loadLeafInputsAsync } from "./leaf-inputs.ts";
13
14
  import { collectOptionDefs } from "./parse.ts";
14
15
  import { normalizeRespondOptions, writeRespondBodyToStdout } from "./respond.ts";
15
16
  import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram, CliRespondOptions } from "./types.ts";
@@ -17,7 +18,7 @@ import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.t
17
18
  import { strictParseDouble } from "./utils.ts";
18
19
 
19
20
  /** Coerced leaf inputs keyed by option and positional names. */
20
- export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
21
+ export type CliLeafInputs = Record<string, boolean | number | string | string[] | unknown | undefined>;
21
22
 
22
23
  /**
23
24
  * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
@@ -163,7 +164,15 @@ export class CliContext {
163
164
  return out;
164
165
  }
165
166
 
166
- private _readOptionValue(opt: CliOption): boolean | number | string | string[] | undefined {
167
+ /**
168
+ * Reads coerced leaf inputs, resolving Json options from flags, piped stdin (when `pipable`),
169
+ * or MCP/API toolArgs, and validates against `leaf.inputSchema` when set.
170
+ */
171
+ async readLeafInputsAsync(): Promise<CliLeafInputs> {
172
+ return loadLeafInputsAsync(this);
173
+ }
174
+
175
+ private _readOptionValue(opt: CliOption): boolean | number | string | string[] | unknown | undefined {
167
176
  if (opt.kind === CliOptionKind.Presence) {
168
177
  return this.hasFlag(opt.name);
169
178
  }
@@ -171,6 +180,15 @@ export class CliContext {
171
180
  const n = this.numberOpt(opt.name);
172
181
  return n === null ? undefined : n;
173
182
  }
183
+ if (opt.kind === CliOptionKind.Json) {
184
+ const raw = this.stringOpt(opt.name);
185
+ if (raw === undefined) return undefined;
186
+ try {
187
+ return JSON.parse(raw) as unknown;
188
+ } catch {
189
+ return undefined;
190
+ }
191
+ }
174
192
  if (opt.format === CliValueFormat.Duration) {
175
193
  return this.durationOpt(opt.name);
176
194
  }
package/src/help.ts CHANGED
@@ -178,6 +178,8 @@ function optKindLabel(k: CliOptionKind, o?: CliOption): string {
178
178
  }
179
179
  return ` <${choices.slice(0, 3).join("|")}|…>`;
180
180
  }
181
+ case CliOptionKind.Json:
182
+ return " <json>";
181
183
  }
182
184
  }
183
185
 
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export {
28
28
  shouldRunHeadlessWithYes,
29
29
  wantsExplicitJson,
30
30
  } from "./headless.ts";
31
+ export { LeafInputError, readLeafInputsAsync } from "./leaf-inputs.ts";
31
32
  export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
32
33
  export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
33
34
  export type {
@@ -0,0 +1,170 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { Cli, CliContext, CliOptionKind } from "./index.ts";
3
+ import { LeafInputError, readLeafInputsAsync } from "./leaf-inputs.ts";
4
+ import type { CliProgram } from "./types.ts";
5
+
6
+ const invoiceSchema = {
7
+ type: "object",
8
+ properties: {
9
+ format: { type: "string", enum: ["pdf", "html"] },
10
+ invoice: {
11
+ type: "object",
12
+ properties: {
13
+ id: { type: "string" },
14
+ },
15
+ required: ["id"],
16
+ additionalProperties: false,
17
+ },
18
+ },
19
+ required: ["format", "invoice"],
20
+ additionalProperties: false,
21
+ } as const;
22
+
23
+ function renderProgram(): CliProgram {
24
+ return {
25
+ key: "json-pipe-test",
26
+ version: "1.0.0",
27
+ description: "Json pipable option tests",
28
+ commands: [
29
+ {
30
+ key: "render",
31
+ description: "Render with pipable invoice JSON",
32
+ inputSchema: invoiceSchema,
33
+ options: [
34
+ {
35
+ name: "format",
36
+ description: "Output format",
37
+ kind: CliOptionKind.Enum,
38
+ choices: ["pdf", "html"],
39
+ required: true,
40
+ },
41
+ {
42
+ name: "invoice",
43
+ description: "Invoice JSON (flag or stdin)",
44
+ kind: CliOptionKind.Json,
45
+ pipable: true,
46
+ required: true,
47
+ },
48
+ ],
49
+ handler: async (ctx) => {
50
+ const inputs = await ctx.readLeafInputsAsync();
51
+ return inputs;
52
+ },
53
+ },
54
+ ],
55
+ } satisfies CliProgram;
56
+ }
57
+
58
+ describe("readLeafInputsAsync", () => {
59
+ test("reads Json option from MCP toolArgs", async () => {
60
+ const cli = new Cli(renderProgram());
61
+ const result = await cli.invoke(["render", "--format", "pdf"], {
62
+ invocation: "mcp",
63
+ toolArgs: { format: "pdf", invoice: { id: "INV-1" } },
64
+ });
65
+ expect(result.kind).toBe("ok");
66
+ expect(result.exitCode).toBe(0);
67
+ expect(result.response?.body).toEqual({
68
+ format: "pdf",
69
+ invoice: { id: "INV-1" },
70
+ });
71
+ });
72
+
73
+ test("flag wins over toolArgs for Json option", async () => {
74
+ const cli = new Cli(renderProgram());
75
+ const result = await cli.invoke(["render", "--format", "pdf", "--invoice", '{"id":"from-flag"}'], {
76
+ invocation: "mcp",
77
+ toolArgs: { format: "pdf", invoice: { id: "from-tool-args" } },
78
+ });
79
+ expect(result.kind).toBe("ok");
80
+ expect(result.response?.body).toEqual({
81
+ format: "pdf",
82
+ invoice: { id: "from-flag" },
83
+ });
84
+ });
85
+
86
+ test("rejects invalid Json flag at parse time", async () => {
87
+ const cli = new Cli(renderProgram());
88
+ const result = await cli.invoke(["render", "--format", "pdf", "--invoice", "not-json"], {
89
+ invocation: "mcp",
90
+ toolArgs: {},
91
+ });
92
+ expect(result.kind).toBe("error");
93
+ expect(result.errorMsg).toContain("Invalid JSON");
94
+ });
95
+
96
+ test("validates merged inputs against inputSchema", async () => {
97
+ const cli = new Cli(renderProgram());
98
+ const result = await cli.invoke(["render", "--format", "pdf"], {
99
+ invocation: "mcp",
100
+ toolArgs: { format: "pdf", invoice: { id: 123 } },
101
+ });
102
+ expect(result.kind).toBe("error");
103
+ expect(result.stderr).toContain("invoice.id");
104
+ });
105
+
106
+ test("readLeafInputsAsync throws LeafInputError when required Json is missing", async () => {
107
+ const program = renderProgram();
108
+ const ctx = new CliContext("json-pipe-test", ["render"], [], { format: "pdf" }, program, "mcp", undefined, {});
109
+ await expect(readLeafInputsAsync(ctx)).rejects.toThrow(LeafInputError);
110
+ });
111
+
112
+ test("omits undefined optional properties before inputSchema validation", async () => {
113
+ const schemaWithOptional = {
114
+ type: "object",
115
+ properties: {
116
+ format: { type: "string", enum: ["pdf", "html"] },
117
+ template: { type: "string" },
118
+ invoice: {
119
+ type: "object",
120
+ properties: { id: { type: "string" } },
121
+ required: ["id"],
122
+ additionalProperties: false,
123
+ },
124
+ },
125
+ required: ["format", "invoice"],
126
+ additionalProperties: false,
127
+ };
128
+ const program = {
129
+ key: "json-pipe-test",
130
+ version: "1.0.0",
131
+ description: "optional template",
132
+ commands: [
133
+ {
134
+ key: "render",
135
+ description: "Render",
136
+ inputSchema: schemaWithOptional,
137
+ options: [
138
+ {
139
+ name: "format",
140
+ description: "Output format",
141
+ kind: CliOptionKind.Enum,
142
+ choices: ["pdf", "html"],
143
+ required: true,
144
+ },
145
+ {
146
+ name: "template",
147
+ description: "Template name",
148
+ kind: CliOptionKind.String,
149
+ },
150
+ {
151
+ name: "invoice",
152
+ description: "Invoice JSON",
153
+ kind: CliOptionKind.Json,
154
+ pipable: true,
155
+ required: true,
156
+ },
157
+ ],
158
+ handler: async (ctx) => ctx.readLeafInputsAsync(),
159
+ },
160
+ ],
161
+ } satisfies CliProgram;
162
+ const cli = new Cli(program);
163
+ const result = await cli.invoke(["render", "--format", "pdf"], {
164
+ invocation: "mcp",
165
+ toolArgs: { format: "pdf", invoice: { id: "INV-1" } },
166
+ });
167
+ expect(result.kind).toBe("ok");
168
+ expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
169
+ });
170
+ });
@@ -0,0 +1,178 @@
1
+ /*
2
+ Async leaf input reads: Json options (flag, piped stdin, or toolArgs), optional inputSchema validation.
3
+ */
4
+
5
+ import { validateConfigDocument } from "./config/validate.ts";
6
+ import type { CliContext, CliLeafInputs } from "./context.ts";
7
+ import { collectOptionDefs } from "./parse.ts";
8
+ import type { CliLeaf, CliNode, CliOption } from "./types.ts";
9
+ import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
10
+ import { isInteractiveTty } from "./utils.ts";
11
+
12
+ /** Thrown when {@link CliContext.readLeafInputsAsync} cannot resolve or validate inputs. */
13
+ export class LeafInputError extends Error {
14
+ constructor(message: string) {
15
+ super(message);
16
+ this.name = "LeafInputError";
17
+ }
18
+ }
19
+
20
+ function leafNode(ctx: CliContext): CliLeaf | undefined {
21
+ let node: CliNode = ctx.program;
22
+ for (const seg of ctx.commandPath) {
23
+ if (!isCliRouter(node)) return undefined;
24
+ const child = node.commands.find((c) => c.key === seg);
25
+ if (!child) return undefined;
26
+ node = child;
27
+ }
28
+ return isCliLeaf(node) ? node : undefined;
29
+ }
30
+
31
+ function readSyncOptionValue(
32
+ ctx: CliContext,
33
+ opt: CliOption,
34
+ ): boolean | number | string | string[] | unknown | undefined {
35
+ if (opt.kind === CliOptionKind.Presence) {
36
+ return ctx.hasFlag(opt.name);
37
+ }
38
+ if (opt.kind === CliOptionKind.Number) {
39
+ const n = ctx.numberOpt(opt.name);
40
+ return n === null ? undefined : n;
41
+ }
42
+ if (opt.kind === CliOptionKind.Json) {
43
+ const raw = ctx.stringOpt(opt.name);
44
+ if (raw === undefined) return undefined;
45
+ return parseJsonText(raw, `--${opt.name}`);
46
+ }
47
+ if (opt.format !== undefined) {
48
+ if (opt.format === CliValueFormat.Duration) {
49
+ return ctx.durationOpt(opt.name);
50
+ }
51
+ if (opt.format === CliValueFormat.CommaList) {
52
+ return ctx.commaListOpt(opt.name);
53
+ }
54
+ if (opt.format === CliValueFormat.Date) {
55
+ return ctx.dateOpt(opt.name);
56
+ }
57
+ if (opt.format === CliValueFormat.DateTime) {
58
+ return ctx.dateTimeOpt(opt.name);
59
+ }
60
+ }
61
+ return ctx.stringOpt(opt.name);
62
+ }
63
+
64
+ function parseJsonText(raw: string, label: string): unknown {
65
+ const trimmed = raw.trim();
66
+ if (trimmed.length === 0) {
67
+ throw new LeafInputError(`${label}: JSON value is empty`);
68
+ }
69
+ try {
70
+ return JSON.parse(trimmed);
71
+ } catch {
72
+ throw new LeafInputError(`${label}: invalid JSON`);
73
+ }
74
+ }
75
+
76
+ async function readPipedJsonStdin(): Promise<unknown> {
77
+ const raw = await new Response(Bun.stdin).text();
78
+ const trimmed = raw.trim();
79
+ if (trimmed.length === 0) {
80
+ throw new LeafInputError("stdin is empty; pass JSON via the option flag or pipe a JSON document to stdin");
81
+ }
82
+ try {
83
+ return JSON.parse(trimmed);
84
+ } catch {
85
+ throw new LeafInputError("stdin is not valid JSON");
86
+ }
87
+ }
88
+
89
+ function pipableJsonHelp(opt: CliOption): string {
90
+ return `Missing required option --${opt.name}: pass JSON via --${opt.name} '<json>' or pipe a JSON document to stdin`;
91
+ }
92
+
93
+ function omitUndefinedInputs(out: CliLeafInputs): Record<string, unknown> {
94
+ const stripped: Record<string, unknown> = {};
95
+ for (const [key, value] of Object.entries(out)) {
96
+ if (value !== undefined) {
97
+ stripped[key] = value;
98
+ }
99
+ }
100
+ return stripped;
101
+ }
102
+
103
+ function validateAgainstInputSchema(out: CliLeafInputs, inputSchema: Record<string, unknown>): void {
104
+ const result = validateConfigDocument(omitUndefinedInputs(out), inputSchema);
105
+ if (!result.valid) {
106
+ throw new LeafInputError(result.errors.join("; "));
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Reads coerced leaf inputs, resolving Json options from flags, piped stdin, or toolArgs,
112
+ * and validates against `leaf.inputSchema` when set.
113
+ */
114
+ export async function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs> {
115
+ const leaf = leafNode(ctx);
116
+ if (!leaf) return {};
117
+
118
+ const out: CliLeafInputs = {};
119
+ const options = collectOptionDefs(ctx.program, ctx.commandPath);
120
+ let pipedJson: unknown | undefined;
121
+ let pipedJsonRead = false;
122
+
123
+ for (const opt of options) {
124
+ if (opt.kind === CliOptionKind.Json) {
125
+ const flagValue = ctx.stringOpt(opt.name);
126
+ if (flagValue !== undefined) {
127
+ out[opt.name] = parseJsonText(flagValue, `--${opt.name}`);
128
+ continue;
129
+ }
130
+ if (ctx.toolArgs !== undefined && opt.name in ctx.toolArgs) {
131
+ out[opt.name] = ctx.toolArgs[opt.name];
132
+ continue;
133
+ }
134
+ if (opt.pipable && ctx.invocation === "cli") {
135
+ if (isInteractiveTty) {
136
+ out[opt.name] = undefined;
137
+ continue;
138
+ }
139
+ if (!pipedJsonRead) {
140
+ pipedJson = await readPipedJsonStdin();
141
+ pipedJsonRead = true;
142
+ }
143
+ out[opt.name] = pipedJson;
144
+ continue;
145
+ }
146
+ out[opt.name] = undefined;
147
+ continue;
148
+ }
149
+
150
+ out[opt.name] = readSyncOptionValue(ctx, opt);
151
+ }
152
+
153
+ for (const p of leaf.positionals ?? []) {
154
+ const val = ctx.positional(p.name);
155
+ if (val === undefined) {
156
+ out[p.name] = undefined;
157
+ } else if (Array.isArray(val)) {
158
+ out[p.name] = val;
159
+ } else {
160
+ out[p.name] = val;
161
+ }
162
+ }
163
+
164
+ for (const opt of options) {
165
+ if (opt.required && out[opt.name] === undefined) {
166
+ if (opt.kind === CliOptionKind.Json && opt.pipable && ctx.invocation === "cli" && isInteractiveTty) {
167
+ throw new LeafInputError(pipableJsonHelp(opt));
168
+ }
169
+ throw new LeafInputError(`Missing required option: --${opt.name}`);
170
+ }
171
+ }
172
+
173
+ if (leaf.inputSchema !== undefined) {
174
+ validateAgainstInputSchema(out, leaf.inputSchema);
175
+ }
176
+
177
+ return omitUndefinedInputs(out);
178
+ }
package/src/mcp/tools.ts CHANGED
@@ -116,6 +116,8 @@ function optionProperty(opt: CliOption): Record<string, unknown> {
116
116
  return { type: "number", ...base };
117
117
  case CliOptionKind.Enum:
118
118
  return { type: "string", enum: opt.choices, ...base };
119
+ case CliOptionKind.Json:
120
+ return { type: "object", ...base };
119
121
  }
120
122
  }
121
123
 
@@ -287,6 +289,9 @@ export function mcpToolCallToArgv(
287
289
  const argv = [...tool.path];
288
290
 
289
291
  for (const opt of collectOptionDefs(root, tool.path)) {
292
+ if (opt.kind === CliOptionKind.Json) {
293
+ continue;
294
+ }
290
295
  const val = args[opt.name];
291
296
  if (val === undefined) {
292
297
  continue;
package/src/parse.ts CHANGED
@@ -583,6 +583,9 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
583
583
 
584
584
  for (const d of defs) {
585
585
  if (d.required && !(d.name in opts)) {
586
+ if (d.kind === CliOptionKind.Json) {
587
+ continue;
588
+ }
586
589
  return errorResult(`Missing required option: --${d.name}`, pr.path);
587
590
  }
588
591
  }
@@ -592,6 +595,14 @@ export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
592
595
  if (!d) {
593
596
  return errorResult(`Unknown option key: ${k}`, pr.path);
594
597
  }
598
+ if (d.kind === CliOptionKind.Json) {
599
+ try {
600
+ JSON.parse(v);
601
+ } catch {
602
+ return errorResult(`Invalid JSON for option --${k}`, pr.path);
603
+ }
604
+ continue;
605
+ }
595
606
  if (d.kind === CliOptionKind.Number) {
596
607
  if (!fullStringIsDouble(v)) {
597
608
  return errorResult(`Invalid number for option --${k}: ${v}`, pr.path);
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ import type { CliContext } from "./context.ts";
12
12
  export type CliInvocation = "cli" | "mcp" | "api";
13
13
 
14
14
  /**
15
- * Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
15
+ * Option kinds: presence (boolean flag), string (free-form text), number (strict double), enum (fixed choices), or json (parsed JSON object/array).
16
16
  */
17
17
  export enum CliOptionKind {
18
18
  /** Boolean flag: no value token (may be implicit `"1"` when set). */
@@ -23,6 +23,8 @@ export enum CliOptionKind {
23
23
  Number = "number",
24
24
  /** Fixed set of allowed string values. Requires non-empty `choices` on the option. */
25
25
  Enum = "enum",
26
+ /** JSON object or array (parsed from `--name '<json>'`, piped stdin when `pipable`, or MCP/API tool body). */
27
+ Json = "json",
26
28
  }
27
29
 
28
30
  /**
@@ -89,6 +91,11 @@ export interface CliOption {
89
91
  default?: string;
90
92
  /** Regex pattern for string options. Mutually exclusive with `format`. */
91
93
  pattern?: string;
94
+ /**
95
+ * When `true` on a `Json` option, CLI may omit `--name` and supply JSON via stdin instead.
96
+ * If `--name` is set, the flag value wins and stdin is not read.
97
+ */
98
+ pipable?: boolean;
92
99
  }
93
100
 
94
101
  /**
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,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;