argsbarg 7.0.7 → 7.0.9

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 CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.0.9] - 2026-09-16
11
+
12
+ ### Added
13
+
14
+ - **`kind: "document"` leaf commands with YAML and JSON support** — introduced `kind: "document"` as the primary naming for structured payload leaves, while retaining `kind: "json"` and `isJsonLeaf` as fully backward-compatible aliases. Both `"document"` and `"json"` leaves now accept YAML input in addition to JSON via command positional arguments and piped stdin.
15
+ - **YAML request body support in HTTP server** — the HTTP API server now accepts YAML request bodies in addition to JSON for structured document endpoints.
16
+ - **`isDocumentLeaf` and `parseDocumentText` exports** — exported `isDocumentLeaf` type guard and `parseDocumentText` utility from framework root and CLI exports.
17
+
18
+ ### Changed
19
+
20
+ - **Help rendering for document leaves** — usage lines for `kind: "document"` leaves render `[DOCUMENT]` (retaining `[JSON]` for legacy `kind: "json"` leaves) and describe inputs as `"Pass a JSON or YAML document as an argument or pipe to stdin."`
21
+
22
+ ### Added
23
+
24
+ - **Non-TTY in-band schema discovery in `--help`** — when `--help` is invoked in non-TTY environments (such as pipes, scripts, and AI agent subprocesses), argsbarg automatically outputs full, untruncated YAML `Output Schema` (and `Input Schema` on `kind: "json"` commands) below command options and arguments. Enables zero-drift contract discovery for AI agents in a single turn without reading external documentation.
25
+ - **Unboxed plain-text help in non-TTY** — strips Unicode box borders, vertical bars, and trailing whitespace padding when output is not a TTY, outputting clean, indented plain text that optimizes token usage and prevents parsing artifacts in automated tooling. TTY sessions retain compact, rounded UTF-8 boxes without schema bloat by default.
26
+ - **`schemaToYamlLines` helper** — exported utility converting JSON Schema definitions (with `$ref` resolution, property JSDoc comments, optional `?` markers, and enums) into clean, human- and agent-readable YAML representation.
27
+
10
28
  ## [7.0.7] - 2026-09-15
11
29
 
12
30
  ### Removed
@@ -984,7 +1002,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
984
1002
  - Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
985
1003
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
986
1004
 
987
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.7...HEAD
1005
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v7.0.9...HEAD
1006
+ [7.0.9]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.9
988
1007
  [7.0.7]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.7
989
1008
  [7.0.6]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.6
990
1009
  [7.0.5]: https://github.com/bdombro/bun-argsbarg/releases/tag/v7.0.5
package/README.md CHANGED
@@ -148,7 +148,7 @@ ArgsBarg automatically integrates several core features into your application. T
148
148
 
149
149
  ### Core Capabilities (Stable)
150
150
 
151
- - `-h` / `--help` — Highly-formatted, terminal-width scoped help at any routing depth.
151
+ - `-h` / `--help` — Highly-formatted, terminal-width scoped help at any routing depth. Rounded UTF-8 boxes in TTY; unboxed plain text with in-band YAML input and output schemas in non-TTY for zero-drift agent discovery.
152
152
  - `version` — Print the program's version (e.g., `myapp version`).
153
153
  - `http` — Launch the high-performance HTTP REST server (injected when `httpServer.enabled` is `true`).
154
154
  - `completion bash` / `zsh` / `fish` — Generate shell completion scripts to stdout for deployment and packaging.
@@ -108,7 +108,7 @@ Use root **`notes`** for cross-cutting hints shown in help (install commands, do
108
108
  Descriptions and schemas are copied into MCP tools and HTTP OpenAPI — optimize for smaller, clearer agent payloads:
109
109
 
110
110
  - **Declare options on the leaf command** that uses them — routing groups cannot declare options (program root may). Wire schemas (MCP, OpenAPI) expose leaf-local options only.
111
- - Prefer **`kind: "json"`** leaves with schemagen `inputSchema` for complex tool bodies (one nested object beats many flat flags).
111
+ - Prefer **`kind: "document"`** (or legacy `kind: "json"`) leaves with schemagen `inputSchema` for complex tool bodies (one nested object beats many flat flags).
112
112
  - Keep **`description`** strings short and action-oriented; put examples in **`notes`**, not duplicated in every option.
113
113
  - Use **`hidden: true`** or **`mcpTool.enabled: false`** for debug/internal commands.
114
114
  - For shape discovery: HTTP agents load **`docs openapi`** or `GET /openapi.json`; MCP agents use **`docs cli-schema`**; load full **`docs cli`** only when prose is needed.
@@ -300,15 +300,15 @@ Use **`ctx.jsonOpt("invoice")`**, **`ctx.inputs`**, or **`ctx.inputsAs<MyInput>(
300
300
 
301
301
  At most one `pipable` Json option per leaf. Json option names must appear in `inputSchema.properties` when a custom `inputSchema` is set.
302
302
 
303
- ### Pure JSON leaves (`kind: "json"`)
303
+ ### Structured document leaves (`kind: "document"`)
304
304
 
305
- When the entire tool body is JSON (no CLI flags), set **`kind: "json"`** on the leaf with **`inputSchema`** and **no `options` or `positionals`**:
305
+ When the entire tool body is a structured document (JSON or YAML, no CLI flags), set **`kind: "document"`** (or legacy `"json"`) on the leaf with **`inputSchema`** and **no `options` or `positionals`**:
306
306
 
307
307
  ```typescript
308
308
  {
309
309
  key: "render-invoice",
310
310
  description: "Render an invoice from template data",
311
- kind: "json",
311
+ kind: "document",
312
312
  inputSchema,
313
313
  handler: (ctx) => {
314
314
  const { format, invoice } = ctx.inputsAs<RenderInvoiceInput>();
@@ -319,10 +319,25 @@ When the entire tool body is JSON (no CLI flags), set **`kind: "json"`** on the
319
319
 
320
320
  | Surface | How input is supplied |
321
321
  | --- | --- |
322
- | CLI | One JSON positional **or** pipe a JSON document to stdin |
323
- | MCP / HTTP | Full tool args object (`ctx.toolArgs` / POST body) |
322
+ | CLI | One JSON or YAML positional **or** pipe a JSON/YAML document to stdin |
323
+ | MCP / HTTP | Full tool args object (`ctx.toolArgs` / JSON or YAML request body) |
324
324
 
325
- Example CLI: `jq '{format:"pdf", invoice:.}' data.json | myapp render-invoice`
325
+ Example CLI:
326
+ ```bash
327
+ # JSON positional or pipe
328
+ jq '{format:"pdf", invoice:.}' data.json | myapp render-invoice
329
+ myapp render-invoice '{"format":"pdf","invoice":{"id":"INV-1"}}'
330
+
331
+ # YAML positional or pipe
332
+ myapp render-invoice 'format: pdf
333
+ invoice:
334
+ id: INV-1'
335
+ cat << 'EOF' | myapp render-invoice
336
+ format: pdf
337
+ invoice:
338
+ id: INV-1
339
+ EOF
340
+ ```
326
341
 
327
342
  See [output-schema.md](output-schema.md) for schemagen `inputType`, [http-server.md](http-server.md) for HTTP tool bodies, and [json-schema-subset.md](json-schema-subset.md) for validation drafts, Zod interop, and keyword notes.
328
343
 
@@ -23,6 +23,7 @@ export const status = {
23
23
  | `myapp docs cli` | Markdown per-command **Output** section |
24
24
  | MCP `tools/list` | Optional `outputSchema` on each tool |
25
25
  | HTTP `GET /openapi.json` | Response schema per tool |
26
+ | CLI `--help` (non-TTY) | YAML output schema for zero-drift in-band agent discovery |
26
27
 
27
28
  **Not validated at runtime** — argsbarg does not parse or reject handler stdout against the schema today. The schema is documentation and MCP/HTTP metadata.
28
29
 
package/index.d.ts CHANGED
@@ -618,15 +618,16 @@ export interface CliNodeBase {
618
618
  /** Global or command-level flags/options. */
619
619
  options?: CliOption[];
620
620
  }
621
- /** Leaf input mode: `json` = pure JSON body (no CLI flags). */
622
- export type CliLeafKind = "json";
621
+ /** Leaf input mode: `document` (or legacy `json`) = structured JSON or YAML document body (no CLI flags). */
622
+ export type CliLeafKind = "document" | "json";
623
623
  /**
624
624
  * A leaf command node with a handler and optional positionals.
625
625
  */
626
626
  export type CliLeaf = CliNodeBase & {
627
627
  /**
628
- * When `"json"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
629
- * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
628
+ * When `"document"` (or legacy `"json"`), the leaf accepts a single JSON or YAML document
629
+ * (CLI positional or piped stdin; MCP/HTTP tool args = body). Requires `inputSchema`;
630
+ * forbids `options` and `positionals`.
630
631
  */
631
632
  kind?: CliLeafKind;
632
633
  /** Handler function for leaf commands. */
@@ -814,8 +815,14 @@ export type CliProgram = CliNode & {
814
815
  /** Program version (printed by the `version` built-in and MCP serverInfo). */
815
816
  version: string;
816
817
  };
817
- /** True when the leaf accepts a pure JSON body (no CLI flags). */
818
- export declare function isJsonLeaf(leaf: CliLeaf): boolean;
818
+ /** True when the leaf accepts a structured JSON or YAML document body (no CLI flags). */
819
+ export declare function isDocumentLeaf(
820
+ /** Leaf command node to inspect. */
821
+ leaf: CliLeaf): boolean;
822
+ /** True when the leaf accepts a structured document body (backward-compatible alias for `isDocumentLeaf`). */
823
+ export declare function isJsonLeaf(
824
+ /** Leaf command node to inspect. */
825
+ leaf: CliLeaf): boolean;
819
826
  /**
820
827
  * Handler closure type for leaf commands.
821
828
  * Supports sync and async handlers; non-undefined return values become implicit JSON responses for headless invocations.
@@ -844,6 +851,12 @@ export declare function parseDateTime(s: string): string;
844
851
  export declare class LeafInputError extends Error {
845
852
  constructor(message: string);
846
853
  }
854
+ /** Parses a JSON or YAML string from a command argument or document body. */
855
+ export declare function parseDocumentText(
856
+ /** Raw text containing a JSON or YAML document. */
857
+ raw: string,
858
+ /** Field or argument label for error reporting. */
859
+ label: string): unknown;
847
860
  /** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
848
861
  export declare function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined;
849
862
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "7.0.7",
3
+ "version": "7.0.9",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -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;