argsbarg 7.0.8 → 7.0.10

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.
@@ -0,0 +1,208 @@
1
+ /*
2
+ Tests for structured document leaves (kind: "document") supporting JSON and YAML document input.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { Cli } from "../index.ts";
7
+ import { LeafInputError, parseDocumentText } from "./leaf-inputs.ts";
8
+ import { ParseKind, parse } from "./parse.ts";
9
+ import { CliOptionKind, type CliProgram, CliSchemaValidationError } from "./types.ts";
10
+ import { cliValidateProgram } from "./validate.ts";
11
+
12
+ /** JSON Schema for the deployment test document body. */
13
+ const deploySchema = {
14
+ type: "object",
15
+ properties: {
16
+ target: { type: "string", enum: ["staging", "production"] },
17
+ config: {
18
+ type: "object",
19
+ properties: { replicas: { type: "integer" } },
20
+ required: ["replicas"],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ required: ["target", "config"],
25
+ additionalProperties: false,
26
+ } as const;
27
+
28
+ /** Creates a test program with a single `kind: "document"` leaf command. */
29
+ function documentLeafProgram() {
30
+ return {
31
+ key: "document-leaf-test",
32
+ version: "1.0.0",
33
+ description: "document leaf tests",
34
+ commands: [
35
+ {
36
+ key: "deploy",
37
+ description: "Deploy from document body",
38
+ kind: "document",
39
+ inputSchema: deploySchema,
40
+ handler: (ctx) => ctx.inputsAs<{ target: string; config: { replicas: number } }>(),
41
+ },
42
+ ],
43
+ } satisfies CliProgram;
44
+ }
45
+
46
+ /** Tests for `kind: "document"` leaf commands. */
47
+ describe("kind: document leaf", () => {
48
+ /** Tests validation rules for document leaves. */
49
+ test("validate requires inputSchema and forbids options/positionals", () => {
50
+ expect(() =>
51
+ cliValidateProgram({
52
+ key: "bad",
53
+ version: "1",
54
+ description: "bad",
55
+ commands: [{ key: "x", description: "x", kind: "document", handler: () => {} }],
56
+ }),
57
+ ).toThrow(CliSchemaValidationError);
58
+
59
+ expect(() =>
60
+ cliValidateProgram({
61
+ key: "bad",
62
+ version: "1",
63
+ description: "bad",
64
+ commands: [
65
+ {
66
+ key: "x",
67
+ description: "x",
68
+ kind: "document",
69
+ inputSchema: deploySchema,
70
+ options: [{ name: "f", description: "f", kind: CliOptionKind.String }],
71
+ handler: () => {},
72
+ },
73
+ ],
74
+ }),
75
+ ).toThrow(CliSchemaValidationError);
76
+
77
+ expect(() =>
78
+ cliValidateProgram({
79
+ key: "bad",
80
+ version: "1",
81
+ description: "bad",
82
+ commands: [
83
+ {
84
+ key: "x",
85
+ description: "x",
86
+ kind: "document",
87
+ inputSchema: deploySchema,
88
+ positionals: [{ name: "file", description: "file", kind: CliOptionKind.String }],
89
+ handler: () => {},
90
+ },
91
+ ],
92
+ }),
93
+ ).toThrow(CliSchemaValidationError);
94
+ });
95
+
96
+ /** Tests that flags on document leaves are rejected. */
97
+ test("parse rejects CLI flags on document leaf", () => {
98
+ const root = documentLeafProgram();
99
+ const pr = parse(root, ["deploy", "--target", "staging"]);
100
+ expect(pr.kind).toBe(ParseKind.Error);
101
+ expect(pr.errorMsg).toContain("Document commands do not accept options");
102
+ });
103
+
104
+ /** Tests that document leaf accepts positional argument tokens. */
105
+ test("parse accepts document positional token", () => {
106
+ const root = documentLeafProgram();
107
+ const pr = parse(root, ["deploy", "target: staging"]);
108
+ expect(pr.kind).toBe(ParseKind.Ok);
109
+ expect(pr.args).toEqual(["target: staging"]);
110
+ });
111
+
112
+ /** Tests reading body from YAML positional argv. */
113
+ test("invoke reads body from YAML positional argv", async () => {
114
+ const cli = new Cli(documentLeafProgram());
115
+ const yaml = "target: staging\nconfig:\n replicas: 3";
116
+ const result = await cli.invoke(["deploy", yaml], { invocation: "mcp" });
117
+ expect(result.kind).toBe("ok");
118
+ expect(result.response?.body).toEqual({ target: "staging", config: { replicas: 3 } });
119
+
120
+ const cliResult = await cli.invoke(["deploy", yaml], { invocation: "cli" });
121
+ expect(cliResult.kind).toBe("ok");
122
+ expect(JSON.parse(cliResult.stdout)).toEqual({ target: "staging", config: { replicas: 3 } });
123
+ });
124
+
125
+ /** Tests reading body from JSON positional argv. */
126
+ test("invoke reads body from JSON positional argv", async () => {
127
+ const cli = new Cli(documentLeafProgram());
128
+ const json = '{"target":"production","config":{"replicas":5}}';
129
+ const result = await cli.invoke(["deploy", json], { invocation: "mcp" });
130
+ expect(result.kind).toBe("ok");
131
+ expect(result.response?.body).toEqual({ target: "production", config: { replicas: 5 } });
132
+ });
133
+
134
+ /** Tests reading body from toolArgs. */
135
+ test("invoke reads body from toolArgs", async () => {
136
+ const cli = new Cli(documentLeafProgram());
137
+ const result = await cli.invoke(["deploy"], {
138
+ invocation: "http",
139
+ toolArgs: { target: "production", config: { replicas: 10 } },
140
+ });
141
+ expect(result.kind).toBe("ok");
142
+ expect(result.response?.body).toEqual({ target: "production", config: { replicas: 10 } });
143
+ });
144
+
145
+ /** Tests error message when document body is missing. */
146
+ test("invoke errors when body is missing", async () => {
147
+ const cli = new Cli(documentLeafProgram());
148
+ const result = await cli.invoke(["deploy"], { invocation: "cli" });
149
+ expect(result.kind).toBe("error");
150
+ expect(result.errorMsg).toContain("Missing document input");
151
+ });
152
+
153
+ /** Tests inputSchema validation error handling. */
154
+ test("invoke validates inputSchema before handler runs", async () => {
155
+ let handlerRan = false;
156
+ const base = documentLeafProgram();
157
+ const program = {
158
+ ...base,
159
+ commands: [
160
+ {
161
+ ...base.commands[0],
162
+ handler: () => {
163
+ handlerRan = true;
164
+ },
165
+ },
166
+ ],
167
+ } satisfies CliProgram;
168
+ const cli = new Cli(program);
169
+ const result = await cli.invoke(["deploy", "target: invalid-target\nconfig:\n replicas: 1"], {
170
+ invocation: "cli",
171
+ });
172
+ expect(result.kind).toBe("error");
173
+ expect(handlerRan).toBe(false);
174
+ });
175
+
176
+ /** Tests non-object document body returns error. */
177
+ test("non-object document body returns error", async () => {
178
+ const cli = new Cli(documentLeafProgram());
179
+ const result = await cli.invoke(["deploy", '"just-a-string"'], { invocation: "cli" });
180
+ expect(result.kind).toBe("error");
181
+ expect(result.errorMsg).toContain("Document input must be a JSON or YAML object");
182
+ });
183
+ });
184
+
185
+ /** Tests for parseDocumentText helper function. */
186
+ describe("parseDocumentText", () => {
187
+ /** Tests parsing valid JSON. */
188
+ test("parses valid JSON object", () => {
189
+ const parsed = parseDocumentText('{"key":"value"}', "test");
190
+ expect(parsed).toEqual({ key: "value" });
191
+ });
192
+
193
+ /** Tests parsing valid YAML. */
194
+ test("parses valid YAML object", () => {
195
+ const parsed = parseDocumentText("key: value\nnested:\n count: 2", "test");
196
+ expect(parsed).toEqual({ key: "value", nested: { count: 2 } });
197
+ });
198
+
199
+ /** Tests empty string throws error. */
200
+ test("throws on empty string", () => {
201
+ expect(() => parseDocumentText(" ", "test")).toThrow(LeafInputError);
202
+ });
203
+
204
+ /** Tests invalid syntax throws error. */
205
+ test("throws on invalid syntax", () => {
206
+ expect(() => parseDocumentText("{bad json", "test")).toThrow(LeafInputError);
207
+ });
208
+ });
@@ -1,9 +1,14 @@
1
+ /*
2
+ Tests for pure JSON leaves (kind: "json") including backward-compatibility and YAML document input.
3
+ */
4
+
1
5
  import { describe, expect, test } from "bun:test";
2
6
  import { Cli } from "../index.ts";
3
7
  import { ParseKind, parse } from "./parse.ts";
4
8
  import { CliOptionKind, type CliProgram, CliSchemaValidationError } from "./types.ts";
5
9
  import { cliValidateProgram } from "./validate.ts";
6
10
 
11
+ /** JSON Schema for the invoice render test body. */
7
12
  const bodySchema = {
8
13
  type: "object",
9
14
  properties: {
@@ -19,6 +24,7 @@ const bodySchema = {
19
24
  additionalProperties: false,
20
25
  } as const;
21
26
 
27
+ /** Creates a test program with a single `kind: "json"` leaf command. */
22
28
  function jsonLeafProgram() {
23
29
  return {
24
30
  key: "json-leaf-test",
@@ -36,7 +42,9 @@ function jsonLeafProgram() {
36
42
  } satisfies CliProgram;
37
43
  }
38
44
 
45
+ /** Tests for `kind: "json"` leaf commands. */
39
46
  describe("kind: json leaf", () => {
47
+ /** Tests validation rules for json leaves. */
40
48
  test("validate requires inputSchema and forbids options/positionals", () => {
41
49
  expect(() =>
42
50
  cliValidateProgram({
@@ -84,6 +92,7 @@ describe("kind: json leaf", () => {
84
92
  ).toThrow(CliSchemaValidationError);
85
93
  });
86
94
 
95
+ /** Tests that flags on json leaves are rejected. */
87
96
  test("parse rejects CLI flags on json leaf", () => {
88
97
  const root = jsonLeafProgram();
89
98
  const pr = parse(root, ["render", "--format", "pdf"]);
@@ -91,6 +100,7 @@ describe("kind: json leaf", () => {
91
100
  expect(pr.errorMsg).toContain("JSON commands do not accept options");
92
101
  });
93
102
 
103
+ /** Tests that json leaf accepts JSON positional arguments. */
94
104
  test("parse accepts JSON positional", () => {
95
105
  const root = jsonLeafProgram();
96
106
  const pr = parse(root, ["render", '{"format":"pdf","invoice":{"id":"1"}}']);
@@ -98,6 +108,7 @@ describe("kind: json leaf", () => {
98
108
  expect(pr.args).toEqual(['{"format":"pdf","invoice":{"id":"1"}}']);
99
109
  });
100
110
 
111
+ /** Tests reading body from toolArgs. */
101
112
  test("invoke reads body from toolArgs", async () => {
102
113
  const cli = new Cli(jsonLeafProgram());
103
114
  const result = await cli.invoke(["render"], {
@@ -108,6 +119,7 @@ describe("kind: json leaf", () => {
108
119
  expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
109
120
  });
110
121
 
122
+ /** Tests reading body from JSON positional argv. */
111
123
  test("invoke reads body from JSON positional argv", async () => {
112
124
  const cli = new Cli(jsonLeafProgram());
113
125
  const result = await cli.invoke(["render", '{"format":"html","invoice":{"id":"2"}}'], {
@@ -117,6 +129,18 @@ describe("kind: json leaf", () => {
117
129
  expect(result.response?.body).toEqual({ format: "html", invoice: { id: "2" } });
118
130
  });
119
131
 
132
+ /** Tests reading body from YAML positional argv for kind: "json" leaf. */
133
+ test("invoke reads body from YAML positional argv", async () => {
134
+ const cli = new Cli(jsonLeafProgram());
135
+ const yamlBody = "format: pdf\ninvoice:\n id: 'INV-YAML-1'";
136
+ const result = await cli.invoke(["render", yamlBody], {
137
+ invocation: "mcp",
138
+ });
139
+ expect(result.kind).toBe("ok");
140
+ expect(result.response?.body).toEqual({ format: "pdf", invoice: { id: "INV-YAML-1" } });
141
+ });
142
+
143
+ /** Tests error when body is missing. */
120
144
  test("invoke errors when body missing", async () => {
121
145
  const cli = new Cli(jsonLeafProgram());
122
146
  const result = await cli.invoke(["render"], { invocation: "http" });
@@ -124,6 +148,7 @@ describe("kind: json leaf", () => {
124
148
  expect(result.errorMsg).toContain("Missing JSON input");
125
149
  });
126
150
 
151
+ /** Tests inputSchema validation before handler runs. */
127
152
  test("invoke validates inputSchema before handler", async () => {
128
153
  let called = false;
129
154
  const base = jsonLeafProgram();
@@ -147,6 +172,7 @@ describe("kind: json leaf", () => {
147
172
  expect(called).toBe(false);
148
173
  });
149
174
 
175
+ /** Tests error on non-object JSON body. */
150
176
  test("non-object JSON body returns error", async () => {
151
177
  const cli = new Cli(jsonLeafProgram());
152
178
  const result = await cli.invoke(["render", '"not-an-object"'], { invocation: "cli" });
@@ -7,7 +7,7 @@ import { isInteractiveTty } from "../utils.ts";
7
7
  import type { CliContext, CliLeafInputs } from "./context.ts";
8
8
  import { collectOptionDefs } from "./parse.ts";
9
9
  import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
10
- import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter, isJsonLeaf } from "./types.ts";
10
+ import { type CliLeafKind, CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter, isDocumentLeaf } from "./types.ts";
11
11
 
12
12
  /** Thrown when leaf input resolution or validation fails. */
13
13
  export class LeafInputError extends Error {
@@ -17,8 +17,11 @@ export class LeafInputError extends Error {
17
17
  }
18
18
  }
19
19
 
20
- /** Internal key for piped stdin on `kind: "json"` leaves. */
21
- export const JSON_LEAF_BODY_KEY = "__jsonLeafBody";
20
+ /** Internal key for piped stdin on `kind: "document"` or `kind: "json"` leaves. */
21
+ export const DOCUMENT_LEAF_BODY_KEY = "__documentLeafBody";
22
+
23
+ /** Internal key for piped stdin on `kind: "json"` leaves (backward-compatible alias). */
24
+ export const JSON_LEAF_BODY_KEY = DOCUMENT_LEAF_BODY_KEY;
22
25
 
23
26
  function resolveLeaf(program: CliProgram, commandPath: string[]): CliLeaf | undefined {
24
27
  let node: CliNode = program;
@@ -36,7 +39,12 @@ function leafNode(ctx: CliContext): CliLeaf | undefined {
36
39
  }
37
40
 
38
41
  /** Parses a JSON string from a `--name` flag value. */
39
- export function parseJsonText(raw: string, label: string): unknown {
42
+ export function parseJsonText(
43
+ /** Raw text to parse as JSON. */
44
+ raw: string,
45
+ /** Field or argument label for error reporting. */
46
+ label: string,
47
+ ): unknown {
40
48
  const trimmed = raw.trim();
41
49
  if (trimmed.length === 0) {
42
50
  throw new LeafInputError(`${label}: JSON value is empty`);
@@ -48,6 +56,31 @@ export function parseJsonText(raw: string, label: string): unknown {
48
56
  }
49
57
  }
50
58
 
59
+ /** Parses a JSON or YAML string from a command argument or document body. */
60
+ export function parseDocumentText(
61
+ /** Raw text containing a JSON or YAML document. */
62
+ raw: string,
63
+ /** Field or argument label for error reporting. */
64
+ label: string,
65
+ ): unknown {
66
+ const trimmed = raw.trim();
67
+ if (trimmed.length === 0) {
68
+ throw new LeafInputError(`${label}: value is empty`);
69
+ }
70
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
71
+ try {
72
+ return JSON.parse(trimmed);
73
+ } catch {
74
+ // Fall through to YAML if JSON parse fails
75
+ }
76
+ }
77
+ try {
78
+ return Bun.YAML.parse(trimmed);
79
+ } catch {
80
+ throw new LeafInputError(`${label}: invalid JSON or YAML`);
81
+ }
82
+ }
83
+
51
84
  async function readPipedJsonStdin(): Promise<unknown> {
52
85
  const raw = await new Response(Bun.stdin).text();
53
86
  const trimmed = raw.trim();
@@ -61,20 +94,38 @@ async function readPipedJsonStdin(): Promise<unknown> {
61
94
  }
62
95
  }
63
96
 
64
- function jsonLeafBodyHelp(): string {
97
+ /** Returns the error message when document input is missing. */
98
+ function jsonLeafBodyHelp(
99
+ /** Leaf kind: document or json. */
100
+ kind: CliLeafKind = "json",
101
+ ): string {
102
+ if (kind === "document") {
103
+ return "Missing document input: pass a JSON or YAML document as an argument or pipe to stdin";
104
+ }
65
105
  return "Missing JSON input: pass a JSON document as an argument or pipe to stdin";
66
106
  }
67
107
 
68
- async function readPipedJsonStdinForJsonLeaf(): Promise<unknown> {
108
+ /** Reads piped stdin for a document or json leaf command. */
109
+ async function readPipedJsonStdinForJsonLeaf(
110
+ /** Leaf kind: document or json. */
111
+ kind: CliLeafKind = "json",
112
+ ): Promise<unknown> {
69
113
  const raw = await new Response(Bun.stdin).text();
70
114
  const trimmed = raw.trim();
71
115
  if (trimmed.length === 0) {
72
- throw new LeafInputError(jsonLeafBodyHelp());
116
+ throw new LeafInputError(jsonLeafBodyHelp(kind));
117
+ }
118
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
119
+ try {
120
+ return JSON.parse(trimmed);
121
+ } catch {
122
+ // Fall through to YAML
123
+ }
73
124
  }
74
125
  try {
75
- return JSON.parse(trimmed);
126
+ return Bun.YAML.parse(trimmed);
76
127
  } catch {
77
- throw new LeafInputError("stdin is not valid JSON");
128
+ throw new LeafInputError("stdin is not valid JSON or YAML");
78
129
  }
79
130
  }
80
131
 
@@ -130,8 +181,8 @@ export async function preloadPipableJson(
130
181
  }
131
182
 
132
183
  const leaf = resolveLeaf(program, commandPath);
133
- if (leaf && isJsonLeaf(leaf) && args.length === 0) {
134
- return { [JSON_LEAF_BODY_KEY]: await readPipedJsonStdinForJsonLeaf() };
184
+ if (leaf && isDocumentLeaf(leaf) && args.length === 0) {
185
+ return { [JSON_LEAF_BODY_KEY]: await readPipedJsonStdinForJsonLeaf(leaf.kind) };
135
186
  }
136
187
 
137
188
  for (const opt of collectOptionDefs(program, commandPath)) {
@@ -181,22 +232,26 @@ export function loadLeafInputs(ctx: CliContext): CliLeafInputs {
181
232
  const leaf = leafNode(ctx);
182
233
  if (!leaf) return {};
183
234
 
184
- if (isJsonLeaf(leaf)) {
235
+ if (isDocumentLeaf(leaf)) {
185
236
  let body: unknown;
186
237
  if (ctx.toolArgs !== undefined) {
187
238
  body = ctx.toolArgs;
188
239
  } else if (ctx.args.length > 0) {
189
240
  const [arg0] = ctx.args;
190
241
  if (arg0 === undefined) {
191
- throw new LeafInputError(jsonLeafBodyHelp());
242
+ throw new LeafInputError(jsonLeafBodyHelp(leaf.kind));
192
243
  }
193
- body = parseJsonText(arg0, "JSON argument");
244
+ const label = leaf.kind === "document" ? "Document argument" : "JSON argument";
245
+ body = parseDocumentText(arg0, label);
194
246
  } else if (JSON_LEAF_BODY_KEY in ctx.preloadedJson) {
195
247
  body = ctx.preloadedJson[JSON_LEAF_BODY_KEY];
196
248
  } else {
197
- throw new LeafInputError(jsonLeafBodyHelp());
249
+ throw new LeafInputError(jsonLeafBodyHelp(leaf.kind));
198
250
  }
199
251
  if (typeof body !== "object" || body === null || Array.isArray(body)) {
252
+ if (leaf.kind === "document") {
253
+ throw new LeafInputError("Document input must be a JSON or YAML object");
254
+ }
200
255
  throw new LeafInputError("JSON input must be a JSON object");
201
256
  }
202
257
  const out = body as CliLeafInputs;
package/src/core/parse.ts CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  type CliRouter,
20
20
  isCliLeaf,
21
21
  isCliRouter,
22
- isJsonLeaf,
22
+ isDocumentLeaf,
23
23
  } from "./types.ts";
24
24
 
25
25
  // ── Parse Result ──────────────────────────────────────────────────────────────
@@ -292,9 +292,9 @@ export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
292
292
  return [...(node.options ?? [])];
293
293
  }
294
294
 
295
- /** Fills `args` for a json leaf from `startIdx` (0 or 1 JSON string positional). */
295
+ /** Fills `args` for a document / json leaf from `startIdx` (0 or 1 JSON or YAML string positional). */
296
296
  function finishJsonLeaf(
297
- _node: CliLeaf,
297
+ node: CliLeaf,
298
298
  startIdx: number,
299
299
  argv: string[],
300
300
  path: string[],
@@ -313,7 +313,8 @@ function finishJsonLeaf(
313
313
  return errorResult("Unexpected extra arguments", path, [], pathParams);
314
314
  }
315
315
  if (tok.startsWith("-")) {
316
- return errorResult(`JSON commands do not accept options: ${tok}`, path, [], pathParams);
316
+ const kindLabel = node.kind === "document" ? "Document" : "JSON";
317
+ return errorResult(`${kindLabel} commands do not accept options: ${tok}`, path, [], pathParams);
317
318
  }
318
319
  args.push(tok);
319
320
  idx += 1;
@@ -552,7 +553,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
552
553
  let node: CliNode | undefined;
553
554
 
554
555
  if (isCliLeaf(root)) {
555
- if (isJsonLeaf(root)) {
556
+ if (isDocumentLeaf(root)) {
556
557
  return finishJsonLeaf(root, i, argv, path, opts, pathParams);
557
558
  }
558
559
  return finishLeaf(root, i, argv, path, opts, root.options ?? [], forcePositionals, pathParams);
@@ -645,7 +646,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
645
646
 
646
647
  // Walk the command tree
647
648
  while (true) {
648
- if (isCliLeaf(current) && isJsonLeaf(current)) {
649
+ if (isCliLeaf(current) && isDocumentLeaf(current)) {
649
650
  return finishJsonLeaf(current, i, argv, path, opts, pathParams);
650
651
  }
651
652
 
@@ -6,6 +6,7 @@ import { type CliSchemaExport, exportPresentationBuiltins } from "../builtins/ex
6
6
  import { cliResolveNotes } from "../help.ts";
7
7
  import { isCliSchemaHidden, visibleOptions } from "../runtime/exposure.ts";
8
8
  import { type CliNode, type CliProgram, isCliLeaf, isCliRouter, leafOutputSchema } from "./types.ts";
9
+ import { buildLeafInputSchema } from "./wire-schema.ts";
9
10
 
10
11
  const RESERVED = new Set(["http", "completion", "configure", "docs", "mcp", "version"]);
11
12
 
@@ -32,6 +33,7 @@ function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
32
33
  if ((cmd.positionals ?? []).length > 0) {
33
34
  out.positionals = cmd.positionals;
34
35
  }
36
+ out.inputSchema = buildLeafInputSchema(cmd);
35
37
  const outputSchema = leafOutputSchema(cmd);
36
38
  if (outputSchema !== undefined) {
37
39
  out.outputSchema = outputSchema;
package/src/core/types.ts CHANGED
@@ -500,16 +500,17 @@ export interface CliNodeBase {
500
500
  options?: CliOption[];
501
501
  }
502
502
 
503
- /** Leaf input mode: `json` = pure JSON body (no CLI flags). */
504
- export type CliLeafKind = "json";
503
+ /** Leaf input mode: `document` (or legacy `json`) = structured JSON or YAML document body (no CLI flags). */
504
+ export type CliLeafKind = "document" | "json";
505
505
 
506
506
  /**
507
507
  * A leaf command node with a handler and optional positionals.
508
508
  */
509
509
  export type CliLeaf = CliNodeBase & {
510
510
  /**
511
- * When `"json"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
512
- * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
511
+ * When `"document"` (or legacy `"json"`), the leaf accepts a single JSON or YAML document
512
+ * (CLI positional or piped stdin; MCP/HTTP tool args = body). Requires `inputSchema`;
513
+ * forbids `options` and `positionals`.
513
514
  */
514
515
  kind?: CliLeafKind;
515
516
  /** Handler function for leaf commands. */
@@ -691,9 +692,20 @@ export function isCliLeaf(node: CliNode): node is CliLeaf {
691
692
  return "handler" in node && typeof node.handler === "function";
692
693
  }
693
694
 
694
- /** True when the leaf accepts a pure JSON body (no CLI flags). */
695
- export function isJsonLeaf(leaf: CliLeaf): boolean {
696
- return leaf.kind === "json";
695
+ /** True when the leaf accepts a structured JSON or YAML document body (no CLI flags). */
696
+ export function isDocumentLeaf(
697
+ /** Leaf command node to inspect. */
698
+ leaf: CliLeaf,
699
+ ): boolean {
700
+ return leaf.kind === "document" || leaf.kind === "json";
701
+ }
702
+
703
+ /** True when the leaf accepts a structured document body (backward-compatible alias for `isDocumentLeaf`). */
704
+ export function isJsonLeaf(
705
+ /** Leaf command node to inspect. */
706
+ leaf: CliLeaf,
707
+ ): boolean {
708
+ return isDocumentLeaf(leaf);
697
709
  }
698
710
 
699
711
  /** True when the node is a router (has subcommands). */
@@ -17,7 +17,7 @@ import {
17
17
  CliValueFormat,
18
18
  isCliLeaf,
19
19
  isCliRouter,
20
- isJsonLeaf,
20
+ isDocumentLeaf,
21
21
  } from "./types.ts";
22
22
 
23
23
  /** Validates `docs` configuration on the program root. */
@@ -264,15 +264,16 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
264
264
  if (isRoot && node.mcpTool !== undefined) {
265
265
  throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
266
266
  }
267
- if (isJsonLeaf(node)) {
267
+ if (isDocumentLeaf(node)) {
268
+ const kindStr = `kind: "${node.kind ?? "document"}"`;
268
269
  if (node.inputSchema === undefined) {
269
- throw new CliSchemaValidationError(`kind: "json" requires inputSchema on ${node.key}`);
270
+ throw new CliSchemaValidationError(`${kindStr} requires inputSchema on ${node.key}`);
270
271
  }
271
272
  if ((node.options ?? []).length > 0) {
272
- throw new CliSchemaValidationError(`kind: "json" forbids options on ${node.key}`);
273
+ throw new CliSchemaValidationError(`${kindStr} forbids options on ${node.key}`);
273
274
  }
274
275
  if ((node.positionals ?? []).length > 0) {
275
- throw new CliSchemaValidationError(`kind: "json" forbids positionals on ${node.key}`);
276
+ throw new CliSchemaValidationError(`${kindStr} forbids positionals on ${node.key}`);
276
277
  }
277
278
  }
278
279
  const outputSchema = node.outputSchema;