argsbarg 6.1.0 → 6.1.2

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,12 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.1.2] - 2026-07-23
11
+
12
+ ### Added
13
+
14
+ - **`kind: "json"` on `CliLeaf`** — pure JSON body leaves with no CLI flags. Requires `inputSchema`; forbids `options` and `positionals`. CLI accepts one JSON positional or piped stdin; MCP/HTTP use the tool args object directly. **`isJsonLeaf()`** helper exported.
15
+
16
+ ## [6.1.1] - 2026-07-23
17
+
18
+ ### Added
19
+
20
+ - **`ctx.inputs`** — coerced, pre-validated leaf inputs (getter; preferred over `readLeafInputs()`).
21
+ - **`ctx.inputsAs<T>()`** — `ctx.inputs` cast to a schemagen or app input type (`T` unconstrained; consumer-asserted).
22
+
23
+ ### Changed
24
+
25
+ - **`leaf.inputSchema` validation** — runs before the leaf handler in `Cli.run()` and `Cli.invoke()` (same JSON Schema subset as `program.appConfig`). `LeafInputError` prints contextual help on CLI. `ctx.inputs` returns the cached, pre-validated result.
26
+ - **`ctx.readLeafInputs()`** — deprecated; use `ctx.inputs` or `ctx.inputsAs()`.
27
+
10
28
  ## [6.1.0] - 2026-07-22
11
29
 
12
30
  ### Added
13
31
 
14
32
  - **`CliOptionKind.Json`** and **`pipable`** — JSON object options with CLI `--name '<json>'` or piped stdin when the flag is omitted (flag wins if set). MCP/API values merge from `toolArgs`.
15
- - **`ctx.readLeafInputsAsync()`** — resolves Json options (flag, stdin, or `toolArgs`) and validates against `leaf.inputSchema` when set.
33
+ - **`ctx.jsonOpt(name)`** — parsed Json option from flag, preloaded stdin, or MCP/API `toolArgs`.
34
+ - **`ctx.readLeafInputs()`** — all coerced inputs; validates against `leaf.inputSchema` when set (stdin preloaded before handler).
35
+ - **`ctx.readLeafInputsAsync()`** — deprecated alias for sync `readLeafInputs()`.
16
36
 
17
37
  ### Changed
18
38
 
@@ -750,7 +770,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
750
770
  - 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`).
751
771
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
752
772
 
753
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.0...HEAD
773
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.2...HEAD
774
+ [6.1.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.2
775
+ [6.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.1
754
776
  [6.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.0
755
777
  [6.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.2
756
778
  [6.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.1
package/README.md CHANGED
@@ -207,8 +207,11 @@ Add `CliPositional` entries to the command’s `positionals` list (separate from
207
207
  - `ctx.durationOpt("timeout")` — duration options (`format: CliValueFormat.Duration`) as milliseconds.
208
208
  - `ctx.commaListOpt("services")` — comma-list options as `string[] | undefined`.
209
209
  - `ctx.dateOpt("on")` / `ctx.dateTimeOpt("since")` — ISO date / date-time options.
210
- - `ctx.readLeafInputs()` — coerced option and positional values for the current leaf (schema-driven).
211
- - `ctx.readLeafInputsAsync()` — like `readLeafInputs()`, plus Json options (flag, piped stdin, MCP `toolArgs`) and `inputSchema` validation.
210
+ - `ctx.inputs` — coerced option and positional values for the current leaf; when `inputSchema` is set, validated before the handler runs and cached on `ctx`.
211
+ - `ctx.inputsAs<T>()` — `ctx.inputs` cast to a schemagen or app input type.
212
+ - `ctx.readLeafInputs()` — deprecated; use `ctx.inputs` or `ctx.inputsAs()`.
213
+ - `ctx.jsonOpt(name)` — parsed Json option (flag, preloaded stdin, or MCP/API `toolArgs`).
214
+ - `ctx.readLeafInputsAsync()` — deprecated alias for `readLeafInputs()`.
212
215
  - `ctx.typedOpt<T>("custom", parseFn)` — custom parsing for type-safe option resolution.
213
216
  - `ctx.args` — positional words in order as `string[]`.
214
217
  - `ctx.positional("name")` — named positional lookup; varargs slots return `string[]`, single slots return `string | undefined`.
@@ -138,4 +138,4 @@ Call `generateOpenApi(program)` or `openApiJson(program)` from the package, fetc
138
138
 
139
139
  ## Complex tool inputs
140
140
 
141
- For nested request bodies (e.g. invoice template data), set `inputSchema` on the leaf, declare a matching `kind: Json` option (optionally `pipable: true` for CLI stdin), and read inputs with `await ctx.readLeafInputsAsync()` — it merges flag values, piped stdin, and the original flat JSON body from `POST /tools/:name`.
141
+ For nested request bodies (e.g. invoice template data), set `inputSchema` on the leaf, declare a matching `kind: Json` option (optionally `pipable: true` for CLI stdin), and read inputs with `ctx.jsonOpt(...)` or `ctx.readLeafInputs()` — piped stdin is loaded before the handler runs.
@@ -203,14 +203,17 @@ handler: async (ctx) => {
203
203
 
204
204
  Read varargs with `ctx.positional("uids")` (returns `string[]`) or `ctx.args`. Do not comma-split argv tokens or use `format` on positionals.
205
205
 
206
- **`readLeafInputs()`** — for leaves with several flags, one schema-driven read instead of hand-rolled `hasFlag` / `stringOpt` lines:
206
+ **`ctx.inputs`** — coerced option and positional values for the current leaf. When `leaf.inputSchema` is set, argsbarg validates **before the handler runs** and caches the result on `ctx`:
207
207
 
208
208
  ```typescript
209
- const { limit, "skip-readiness": skipReadiness, timeout } = ctx.readLeafInputs();
210
- // duration → number (ms); comma-list → string[]; presence → boolean; number → number
209
+ const { limit, "skip-readiness": skipReadiness, timeout } = ctx.inputs;
210
+ // or, with a schemagen type:
211
+ const { format, invoice } = ctx.inputsAs<RenderInvoiceInput>();
211
212
  ```
212
213
 
213
- **`CliLeafInputs`** — return type of `readLeafInputs()` (exported from `"argsbarg"`). A flat record keyed by **schema option and positional names** (hyphens preserved, e.g. `"skip-readiness"`). Values are coerced per kind/format:
214
+ **`readLeafInputs()`** — deprecated alias for `ctx.inputs`.
215
+
216
+ **`CliLeafInputs`** — return type of `ctx.inputs` (exported from `"argsbarg"`). A flat record keyed by **schema option and positional names** (hyphens preserved, e.g. `"skip-readiness"`). Values are coerced per kind/format:
214
217
 
215
218
  | Schema | Value in `CliLeafInputs` |
216
219
  | --- | --- |
@@ -223,9 +226,9 @@ const { limit, "skip-readiness": skipReadiness, timeout } = ctx.readLeafInputs()
223
226
  | `format: date-time` | `string` (normalized UTC ISO) |
224
227
  | Single positional | `string` or `undefined` |
225
228
  | Varargs positional | `string[]` or `undefined` |
226
- | `kind: json` | parsed object/array or `undefined` (use `readLeafInputsAsync()` for pipable stdin and MCP merge) |
229
+ | `kind: json` | parsed object/array or `undefined` (`ctx.jsonOpt(name)`; piped stdin preloaded before handler) |
227
230
 
228
- Omitted options appear as `undefined` (not absent keys). Options with `default` are filled in post-parse before handlers run, so `readLeafInputs()` and `durationOpt` see defaults. **`ctx.opts` always holds raw strings** — use typed accessors or `readLeafInputs()` for coerced values.
231
+ Omitted options appear as `undefined` (not absent keys). Options with `default` are filled in post-parse before handlers run, so `ctx.inputs` and `durationOpt` see defaults. **`ctx.opts` always holds raw strings** — use typed accessors or `ctx.inputs` for coerced values.
229
232
 
230
233
  ### Json options and piped stdin
231
234
 
@@ -243,15 +246,39 @@ For nested tool bodies (e.g. invoice template data), declare a matching property
243
246
 
244
247
  | Surface | How `invoice` is supplied |
245
248
  | --- | --- |
246
- | CLI | `--invoice '<json>'` **or** omit the flag and pipe JSON to stdin |
249
+ | CLI | `--invoice '<json>'` **or** omit the flag and pipe JSON to stdin (preloaded before the handler) |
247
250
  | MCP / HTTP | `invoice` object in the tool JSON body (`ctx.toolArgs`) |
248
251
 
249
252
  **Precedence:** if `--invoice` is set, the flag value wins and stdin is not read.
250
253
 
251
- Use **`await ctx.readLeafInputsAsync()`** (not sync `readLeafInputs()`) so Json options resolve from flags, piped stdin, or `toolArgs`. When `leaf.inputSchema` is set, argsbarg validates the merged inputs (same JSON Schema subset as `program.appConfig`).
254
+ Use **`ctx.jsonOpt("invoice")`**, **`ctx.inputs`**, or **`ctx.inputsAs<MyInput>()`** — all synchronous. Argsbarg reads piped stdin before calling the handler when a `pipable` Json flag is omitted. When `leaf.inputSchema` is set, argsbarg validates merged inputs **before the handler runs** (same JSON Schema subset as `program.appConfig`); `ctx.inputs` returns the cached result.
252
255
 
253
256
  At most one `pipable` Json option per leaf. Json option names must appear in `inputSchema.properties` when a custom `inputSchema` is set.
254
257
 
258
+ ### Pure JSON leaves (`kind: "json"`)
259
+
260
+ When the entire tool body is JSON (no CLI flags), set **`kind: "json"`** on the leaf with **`inputSchema`** and **no `options` or `positionals`**:
261
+
262
+ ```typescript
263
+ {
264
+ key: "render-invoice",
265
+ description: "Render an invoice from template data",
266
+ kind: "json",
267
+ inputSchema,
268
+ handler: (ctx) => {
269
+ const { format, invoice } = ctx.inputsAs<RenderInvoiceInput>();
270
+ // ...
271
+ },
272
+ }
273
+ ```
274
+
275
+ | Surface | How input is supplied |
276
+ | --- | --- |
277
+ | CLI | One JSON positional **or** pipe a JSON document to stdin |
278
+ | MCP / HTTP | Full tool args object (`ctx.toolArgs` / POST body) |
279
+
280
+ Example CLI: `jq '{format:"pdf", invoice:.}' data.json | myapp render-invoice`
281
+
255
282
  See [output-schema.md](output-schema.md) for schemagen `inputType` and [api-server.md](api-server.md) for HTTP tool bodies.
256
283
 
257
284
  `CliLeafInputs` is intentionally untyped at the framework level. Narrow in your app (`read*Flags(ctx)` returning a typed struct) rather than expecting inference from `satisfies CliLeaf`.
@@ -133,7 +133,7 @@ export type outputType = RenderInvoiceOutput;
133
133
 
134
134
  **Domain helpers** and **role exports** live in `types.ts`. Commands without structured JSON omit role exports. Shared shapes (e.g. `HeadlessOpResult`) are defined once in `types.ts` with a single `outputType`; other commands import `{ outputSchema }` from that module’s `__generated__/index.ts`.
135
135
 
136
- For nested MCP/HTTP bodies, add a `kind: Json` option (same name as the `inputType` property) and use `await ctx.readLeafInputsAsync()` — see [cli-program.md](cli-program.md#json-options-and-piped-stdin).
136
+ For nested MCP/HTTP bodies, add a `kind: Json` option (same name as the `inputType` property) and use `ctx.jsonOpt(...)` or `ctx.readLeafInputs()` — see [cli-program.md](cli-program.md#json-options-and-piped-stdin).
137
137
 
138
138
  When you do **not** set `inputSchema`, argsbarg builds tool input from CLI `options` + `positionals`.
139
139
 
package/index.d.ts CHANGED
@@ -59,9 +59,12 @@ export declare class CliContext {
59
59
  readonly appConfig: AnyAppConfigSnapshot;
60
60
  /** Original flat tool arguments for API/MCP invocations (when provided). */
61
61
  readonly toolArgs?: Record<string, unknown>;
62
+ /** Pipable Json option values read from stdin before the handler (CLI only). */
63
+ readonly preloadedJson: Record<string, unknown>;
62
64
  private response?;
65
+ private leafInputsCache?;
63
66
  /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
64
- constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation, appConfig?: AnyAppConfigSnapshot, toolArgs?: Record<string, unknown>);
67
+ constructor(appName: string, commandPath: string[], args: string[], opts: Record<string, string>, program: CliProgram, invocation?: CliInvocation, appConfig?: AnyAppConfigSnapshot, toolArgs?: Record<string, unknown>, preloadedJson?: Record<string, unknown>);
65
68
  /**
66
69
  * Sets the machine-readable response for API/MCP invocations, or writes to stdout in CLI mode.
67
70
  * May only be called once per invocation.
@@ -88,16 +91,30 @@ export declare class CliContext {
88
91
  dateOpt(name: string): string | undefined;
89
92
  /** Date-time option as normalized ISO 8601 UTC (post-parse validated). */
90
93
  dateTimeOpt(name: string): string | undefined;
94
+ /**
95
+ * Parsed Json option: `--name '<json>'`, preloaded piped stdin (when `pipable`), or MCP/API toolArgs.
96
+ * Flag wins over stdin and toolArgs.
97
+ */
98
+ jsonOpt(name: string): unknown | undefined;
91
99
  /** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
92
100
  positional(name: string): string | string[] | undefined;
93
- /** Reads coerced option and positional values for the current leaf from schema metadata. */
101
+ /**
102
+ * Coerced option and positional values for the current leaf.
103
+ * When `leaf.inputSchema` is set, argsbarg validates before the handler runs; this returns the cached result.
104
+ */
105
+ get inputs(): CliLeafInputs;
106
+ /**
107
+ * {@link inputs} cast to a schemagen or app-defined input type (consumer-asserted; not inferred from `inputSchema`).
108
+ */
109
+ inputsAs<T = CliLeafInputs>(): T;
110
+ /**
111
+ * @deprecated Use {@link inputs} or {@link inputsAs}.
112
+ */
94
113
  readLeafInputs(): CliLeafInputs;
95
114
  /**
96
- * Reads coerced leaf inputs, resolving Json options from flags, piped stdin (when `pipable`),
97
- * or MCP/API toolArgs, and validates against `leaf.inputSchema` when set.
115
+ * @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs.
98
116
  */
99
117
  readLeafInputsAsync(): Promise<CliLeafInputs>;
100
- private _readOptionValue;
101
118
  private _leafNode;
102
119
  private _posMap;
103
120
  private _positionalMap;
@@ -473,10 +490,17 @@ export interface CliNodeBase {
473
490
  /** Global or command-level flags/options. */
474
491
  options?: CliOption[];
475
492
  }
493
+ /** Leaf input mode: `json` = pure JSON body (no CLI flags). */
494
+ export type CliLeafKind = "json";
476
495
  /**
477
496
  * A leaf command node with a handler and optional positionals.
478
497
  */
479
498
  export type CliLeaf = CliNodeBase & {
499
+ /**
500
+ * When `"json"`, the leaf accepts a single JSON document (CLI positional or piped stdin;
501
+ * MCP/HTTP tool args = body). Requires `inputSchema`; forbids `options` and `positionals`.
502
+ */
503
+ kind?: CliLeafKind;
480
504
  /** Handler function for leaf commands. */
481
505
  handler: CliHandler;
482
506
  /** Positional argument definitions. */
@@ -528,6 +552,8 @@ export type CliProgram = CliNode & {
528
552
  /** When set with `enabled: true`, enables the `docs` built-in command group. */
529
553
  docs?: CliDocsConfig;
530
554
  };
555
+ /** True when the leaf accepts a pure JSON body (no CLI flags). */
556
+ export declare function isJsonLeaf(leaf: CliLeaf): boolean;
531
557
  /**
532
558
  * Handler closure type for leaf commands.
533
559
  * Supports sync and async handlers; non-undefined return values become implicit JSON responses for headless invocations.
@@ -596,6 +622,8 @@ export declare class Cli {
596
622
  }): Promise<CliInvokeResult>;
597
623
  serveMcp(): Promise<never>;
598
624
  serveApi(): Promise<never>;
625
+ private ensureValidatedLeafInputs;
626
+ private exitLeafInputError;
599
627
  private prepareDispatch;
600
628
  private buildAppConfigSnapshot;
601
629
  }
@@ -643,14 +671,25 @@ dryRun?: boolean,
643
671
  interactive?: boolean): void;
644
672
  /** Prefixes a success message when running in dry-run mode. */
645
673
  export declare function formatDryRunMessage(message: string, dryRun: boolean): string;
646
- /** Thrown when {@link CliContext.readLeafInputsAsync} cannot resolve or validate inputs. */
674
+ /** Thrown when leaf input resolution or validation fails. */
647
675
  export declare class LeafInputError extends Error {
648
676
  constructor(message: string);
649
677
  }
678
+ /** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
679
+ export declare function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined;
680
+ /**
681
+ * Reads piped stdin for a pipable Json option when the flag is omitted (CLI only).
682
+ * Call from {@link Cli.run} before constructing the handler context.
683
+ */
684
+ export declare function preloadPipableJson(program: CliProgram, commandPath: string[], opts: Record<string, string>, invocation: CliInvocation, args?: string[]): Promise<Record<string, unknown>>;
650
685
  /**
651
- * Reads coerced leaf inputs, resolving Json options from flags, piped stdin, or toolArgs,
652
- * and validates against `leaf.inputSchema` when set.
686
+ * Loads coerced leaf inputs and validates against `leaf.inputSchema` when set.
687
+ * Used by {@link CliContext.inputs}; prefer `ctx.inputs` or `ctx.inputsAs()` in handlers.
653
688
  */
689
+ export declare function loadLeafInputs(ctx: CliContext): CliLeafInputs;
690
+ /** @deprecated Use {@link CliContext.inputs} or {@link loadLeafInputs} via `ctx.inputs`. */
691
+ export declare function readLeafInputs(ctx: CliContext): CliLeafInputs;
692
+ /** @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs. */
654
693
  export declare function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs>;
655
694
  /** Resolved paths for `mcp bundle`. */
656
695
  export interface McpBundlePaths {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "6.1.0",
3
+ "version": "6.1.2",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
package/src/cli.ts CHANGED
@@ -18,6 +18,7 @@ import { readAppConfigFileRaw, resolveAppConfigPath } from "./config/file.ts";
18
18
  import { effectiveJsonSchema } from "./config/schema.ts";
19
19
  import { CliContext } from "./context.ts";
20
20
  import { cliHelpRender } from "./help.ts";
21
+ import { LeafInputError, preloadPipableJson } from "./leaf-inputs.ts";
21
22
  import { bootstrapMcpEnv } from "./mcp/env.ts";
22
23
  import { mcpServeStdioLoop } from "./mcp/server.ts";
23
24
  import { ParseKind, type ParseResult, parse, postParseValidate } from "./parse.ts";
@@ -124,14 +125,41 @@ export class Cli {
124
125
  exitOnMissing: !skipRequiredConfig,
125
126
  });
126
127
 
127
- const ctx = new CliContext(this.program.key, pr.path, pr.args, pr.opts, this.program, "cli", snapshot);
128
+ let preloadedJson: Record<string, unknown> = {};
128
129
  try {
130
+ preloadedJson = await preloadPipableJson(this.program, pr.path, pr.opts, "cli", pr.args);
131
+ } catch (err) {
132
+ if (err instanceof LeafInputError) {
133
+ this.exitLeafInputError(err, pr.path);
134
+ }
135
+ const msg = err instanceof Error ? err.message : String(err);
136
+ const color = process.stderr.isTTY;
137
+ process.stderr.write(color ? `\u001B[31m${msg}\u001B[0m\n` : `${msg}\n`);
138
+ process.exit(1);
139
+ }
140
+
141
+ const ctx = new CliContext(
142
+ this.program.key,
143
+ pr.path,
144
+ pr.args,
145
+ pr.opts,
146
+ this.program,
147
+ "cli",
148
+ snapshot,
149
+ undefined,
150
+ preloadedJson,
151
+ );
152
+ try {
153
+ this.ensureValidatedLeafInputs(ctx, leaf);
129
154
  const handlerResult = await Promise.resolve(leaf.handler(ctx));
130
155
  if (handlerResult !== undefined && ctx.getResponse() === undefined) {
131
156
  ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
132
157
  }
133
158
  process.exit(0);
134
159
  } catch (err) {
160
+ if (err instanceof LeafInputError) {
161
+ this.exitLeafInputError(err, pr.path);
162
+ }
135
163
  if (err instanceof Error) {
136
164
  process.stderr.write(`${err.message}\n`);
137
165
  }
@@ -232,6 +260,7 @@ export class Cli {
232
260
  });
233
261
  }
234
262
 
263
+ this.ensureValidatedLeafInputs(ctx, leaf);
235
264
  const handlerResult = await Promise.resolve(leaf.handler(ctx));
236
265
  if (handlerResult !== undefined && ctx.getResponse() === undefined) {
237
266
  ctx.respond({ body: handlerResult as CliRespondOptions["body"] });
@@ -260,6 +289,15 @@ export class Cli {
260
289
  const msg = stderr.trim() || `Exit code ${err.code}`;
261
290
  return { kind: "error", exitCode: err.code, stdout, stderr, errorMsg: msg };
262
291
  }
292
+ if (err instanceof LeafInputError) {
293
+ return {
294
+ kind: "error",
295
+ exitCode: 1,
296
+ stdout,
297
+ stderr: `${err.message}\n`,
298
+ errorMsg: err.message,
299
+ };
300
+ }
263
301
  if (err instanceof Error) {
264
302
  return {
265
303
  kind: "error",
@@ -320,6 +358,21 @@ export class Cli {
320
358
  }
321
359
  }
322
360
 
361
+ private ensureValidatedLeafInputs(ctx: CliContext, leaf: CliLeaf): void {
362
+ if (leaf.inputSchema === undefined) {
363
+ return;
364
+ }
365
+ ctx.inputs;
366
+ }
367
+
368
+ private exitLeafInputError(err: LeafInputError, helpPath: string[]): never {
369
+ const color = process.stderr.isTTY;
370
+ const msg = color ? `\u001B[31m${err.message}\u001B[0m` : err.message;
371
+ process.stderr.write(`${msg}\n`);
372
+ process.stderr.write(cliHelpRender(this.presentationRoot, helpPath, true));
373
+ process.exit(1);
374
+ }
375
+
323
376
  private prepareDispatch(
324
377
  argv: string[],
325
378
  opts?: { presentationFallback?: boolean },
package/src/context.ts CHANGED
@@ -10,11 +10,10 @@ 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";
14
- import { collectOptionDefs } from "./parse.ts";
13
+ import { loadLeafInputs, readJsonOptionValue } from "./leaf-inputs.ts";
15
14
  import { normalizeRespondOptions, writeRespondBodyToStdout } from "./respond.ts";
16
- import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram, CliRespondOptions } from "./types.ts";
17
- import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
15
+ import type { CliInvocation, CliLeaf, CliNode, CliProgram, CliRespondOptions } from "./types.ts";
16
+ import { isCliLeaf, isCliRouter } from "./types.ts";
18
17
  import { strictParseDouble } from "./utils.ts";
19
18
 
20
19
  /** Coerced leaf inputs keyed by option and positional names. */
@@ -33,8 +32,11 @@ export class CliContext {
33
32
  readonly appConfig: AnyAppConfigSnapshot;
34
33
  /** Original flat tool arguments for API/MCP invocations (when provided). */
35
34
  readonly toolArgs?: Record<string, unknown>;
35
+ /** Pipable Json option values read from stdin before the handler (CLI only). */
36
+ readonly preloadedJson: Record<string, unknown>;
36
37
 
37
38
  private response?: CliRespondOptions;
39
+ private leafInputsCache?: CliLeafInputs;
38
40
 
39
41
  /** Captures the program root, routed path, positional words, and option map for a leaf handler. */
40
42
  constructor(
@@ -46,6 +48,7 @@ export class CliContext {
46
48
  invocation: CliInvocation = "cli",
47
49
  appConfig: AnyAppConfigSnapshot = new EmptyAppConfigSnapshot(program),
48
50
  toolArgs?: Record<string, unknown>,
51
+ preloadedJson: Record<string, unknown> = {},
49
52
  ) {
50
53
  this.appName = appName;
51
54
  this.commandPath = commandPath;
@@ -55,6 +58,7 @@ export class CliContext {
55
58
  this.invocation = invocation;
56
59
  this.appConfig = appConfig;
57
60
  this.toolArgs = toolArgs;
61
+ this.preloadedJson = preloadedJson;
58
62
  }
59
63
 
60
64
  /**
@@ -137,71 +141,50 @@ export class CliContext {
137
141
  return parseDateTime(s);
138
142
  }
139
143
 
144
+ /**
145
+ * Parsed Json option: `--name '<json>'`, preloaded piped stdin (when `pipable`), or MCP/API toolArgs.
146
+ * Flag wins over stdin and toolArgs.
147
+ */
148
+ jsonOpt(name: string): unknown | undefined {
149
+ return readJsonOptionValue(this, name);
150
+ }
151
+
140
152
  /** Returns the value(s) for a named positional slot. Varargs slots return string[]; single slots return string | undefined. */
141
153
  positional(name: string): string | string[] | undefined {
142
154
  return this._positionalMap()[name];
143
155
  }
144
156
 
145
- /** Reads coerced option and positional values for the current leaf from schema metadata. */
146
- readLeafInputs(): CliLeafInputs {
147
- const leaf = this._leafNode();
148
- if (!leaf) return {};
149
-
150
- const out: CliLeafInputs = {};
151
- for (const opt of collectOptionDefs(this.program, this.commandPath)) {
152
- out[opt.name] = this._readOptionValue(opt);
153
- }
154
- for (const p of leaf.positionals ?? []) {
155
- const val = this.positional(p.name);
156
- if (val === undefined) {
157
- out[p.name] = undefined;
158
- } else if (Array.isArray(val)) {
159
- out[p.name] = val;
160
- } else {
161
- out[p.name] = val;
162
- }
157
+ /**
158
+ * Coerced option and positional values for the current leaf.
159
+ * When `leaf.inputSchema` is set, argsbarg validates before the handler runs; this returns the cached result.
160
+ */
161
+ get inputs(): CliLeafInputs {
162
+ if (this.leafInputsCache !== undefined) {
163
+ return this.leafInputsCache;
163
164
  }
164
- return out;
165
+ this.leafInputsCache = loadLeafInputs(this);
166
+ return this.leafInputsCache;
165
167
  }
166
168
 
167
169
  /**
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
+ * {@link inputs} cast to a schemagen or app-defined input type (consumer-asserted; not inferred from `inputSchema`).
170
171
  */
171
- async readLeafInputsAsync(): Promise<CliLeafInputs> {
172
- return loadLeafInputsAsync(this);
172
+ inputsAs<T = CliLeafInputs>(): T {
173
+ return this.inputs as T;
173
174
  }
174
175
 
175
- private _readOptionValue(opt: CliOption): boolean | number | string | string[] | unknown | undefined {
176
- if (opt.kind === CliOptionKind.Presence) {
177
- return this.hasFlag(opt.name);
178
- }
179
- if (opt.kind === CliOptionKind.Number) {
180
- const n = this.numberOpt(opt.name);
181
- return n === null ? undefined : n;
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
- }
192
- if (opt.format === CliValueFormat.Duration) {
193
- return this.durationOpt(opt.name);
194
- }
195
- if (opt.format === CliValueFormat.CommaList) {
196
- return this.commaListOpt(opt.name);
197
- }
198
- if (opt.format === CliValueFormat.Date) {
199
- return this.dateOpt(opt.name);
200
- }
201
- if (opt.format === CliValueFormat.DateTime) {
202
- return this.dateTimeOpt(opt.name);
203
- }
204
- return this.stringOpt(opt.name);
176
+ /**
177
+ * @deprecated Use {@link inputs} or {@link inputsAs}.
178
+ */
179
+ readLeafInputs(): CliLeafInputs {
180
+ return this.inputs;
181
+ }
182
+
183
+ /**
184
+ * @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs.
185
+ */
186
+ readLeafInputsAsync(): Promise<CliLeafInputs> {
187
+ return Promise.resolve(this.readLeafInputs());
205
188
  }
206
189
 
207
190
  private _leafNode(): CliLeaf | undefined {
package/src/help.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  type CliRouter,
17
17
  isCliLeaf,
18
18
  isCliRouter,
19
+ isJsonLeaf,
19
20
  } from "./types.ts";
20
21
 
21
22
  // ── ANSI Style Helpers ────────────────────────────────────────────────────────
@@ -328,6 +329,7 @@ function usageLines(
328
329
  helpPath: string[],
329
330
  hasCommands: boolean,
330
331
  hasArgs: boolean,
332
+ jsonLeaf: boolean,
331
333
  color: boolean,
332
334
  ): string[] {
333
335
  let fullPath = appName;
@@ -337,6 +339,7 @@ function usageLines(
337
339
  const usageOpts = color ? style.aquaBold("[OPTIONS]") : "[OPTIONS]";
338
340
  const usageCmd = color ? style.aquaBold("COMMAND") : "COMMAND";
339
341
  const usageArgs = color ? style.aquaBold("[ARGS]...") : "[ARGS]...";
342
+ const usageJson = color ? style.aquaBold("[JSON]") : "[JSON]";
340
343
 
341
344
  const out: string[] = [];
342
345
  if (helpPath.length === 0) {
@@ -347,6 +350,10 @@ function usageLines(
347
350
  }
348
351
  return out;
349
352
  }
353
+ if (jsonLeaf) {
354
+ out.push(`${fullPath} ${usageJson}`);
355
+ return out;
356
+ }
350
357
  out.push(`${fullPath} ${usageOpts}${hasArgs ? ` ${usageArgs}` : ""}`);
351
358
  if (hasCommands) {
352
359
  out.push(`${fullPath} ${usageCmd} ${usageArgs}`);
@@ -354,6 +361,25 @@ function usageLines(
354
361
  return out;
355
362
  }
356
363
 
364
+ /** Table rows for `kind: "json"` leaf input (schema properties + stdin hint). */
365
+ function rowsForJsonInput(inputSchema: Record<string, unknown> | undefined): HelpRow[] {
366
+ const hint = "Pass a JSON document as an argument or pipe to stdin.";
367
+ const rows: HelpRow[] = [{ label: "JSON", description: hint }];
368
+ const props = inputSchema?.properties;
369
+ if (!props || typeof props !== "object" || Array.isArray(props)) {
370
+ return rows;
371
+ }
372
+ const required = new Set(Array.isArray(inputSchema?.required) ? inputSchema.required.map((k) => String(k)) : []);
373
+ for (const [name, prop] of Object.entries(props as Record<string, { description?: string }>)) {
374
+ const desc = prop.description ?? "";
375
+ rows.push({
376
+ label: name,
377
+ description: required.has(name) ? `(required) ${desc}` : desc,
378
+ });
379
+ }
380
+ return rows;
381
+ }
382
+
357
383
  /** Table rows for named options, including synthetic built-in rows. */
358
384
  function rowsForOptions(defs: CliOption[], color: boolean): HelpRow[] {
359
385
  const rows: HelpRow[] = [];
@@ -407,7 +433,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
407
433
  lines.push(
408
434
  renderTextBox(
409
435
  "Usage",
410
- usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, color),
436
+ usageLines(schema.key, helpPath, (schema.commands ?? []).length > 0, false, false, color),
411
437
  hw,
412
438
  color,
413
439
  ).join("\n"),
@@ -446,6 +472,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
446
472
  lines.push(color ? style.white(node.description) : node.description);
447
473
  lines.push("");
448
474
  }
475
+ const nodeIsJsonLeaf = isCliLeaf(node) && isJsonLeaf(node);
449
476
  lines.push(
450
477
  renderTextBox(
451
478
  "Usage",
@@ -454,6 +481,7 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
454
481
  helpPath,
455
482
  isCliRouter(node) && node.commands.length > 0,
456
483
  isCliLeaf(node) && (node.positionals ?? []).length > 0,
484
+ nodeIsJsonLeaf,
457
485
  color,
458
486
  ),
459
487
  hw,
@@ -461,21 +489,29 @@ export function cliHelpRender(schema: CliRouter, helpPath: string[], _useStderr:
461
489
  ).join("\n"),
462
490
  );
463
491
 
464
- const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
465
- if (optBox.length > 0) {
466
- lines.push("");
467
- lines.push(optBox.join("\n"));
468
- }
492
+ if (nodeIsJsonLeaf && isCliLeaf(node)) {
493
+ const inputBox = renderTableBox("Input", rowsForJsonInput(node.inputSchema), hw, color);
494
+ if (inputBox.length > 0) {
495
+ lines.push("");
496
+ lines.push(inputBox.join("\n"));
497
+ }
498
+ } else {
499
+ const optBox = renderTableBox("Options", rowsForOptions(visibleOptions(node.options), color), hw, color);
500
+ if (optBox.length > 0) {
501
+ lines.push("");
502
+ lines.push(optBox.join("\n"));
503
+ }
469
504
 
470
- const posBox = renderTableBox(
471
- "Arguments",
472
- rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
473
- hw,
474
- color,
475
- );
476
- if (posBox.length > 0) {
477
- lines.push("");
478
- lines.push(posBox.join("\n"));
505
+ const posBox = renderTableBox(
506
+ "Arguments",
507
+ rowsForPositionals(isCliLeaf(node) ? (node.positionals ?? []) : [], color),
508
+ hw,
509
+ color,
510
+ );
511
+ if (posBox.length > 0) {
512
+ lines.push("");
513
+ lines.push(posBox.join("\n"));
514
+ }
479
515
  }
480
516
 
481
517
  const subcmds = isCliRouter(node) ? node.commands : [];