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 +24 -2
- package/README.md +5 -2
- package/docs/api-server.md +1 -1
- package/docs/cli-program.md +35 -8
- package/docs/output-schema.md +1 -1
- package/index.d.ts +47 -8
- package/package.json +1 -1
- package/src/cli.ts +54 -1
- package/src/context.ts +40 -57
- package/src/help.ts +51 -15
- package/src/index.ts +10 -1
- package/src/json-leaf.test.ts +156 -0
- package/src/leaf-inputs.test.ts +118 -9
- package/src/leaf-inputs.ts +143 -70
- package/src/mcp/tools.ts +9 -0
- package/src/parse.ts +50 -0
- package/src/types.ts +13 -0
- package/src/validate.ts +12 -0
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.
|
|
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.
|
|
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.
|
|
211
|
-
- `ctx.
|
|
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`.
|
package/docs/api-server.md
CHANGED
|
@@ -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 `
|
|
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.
|
package/docs/cli-program.md
CHANGED
|
@@ -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
|
-
**`
|
|
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.
|
|
210
|
-
//
|
|
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
|
-
**`
|
|
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` (
|
|
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 `
|
|
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 **`
|
|
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`.
|
package/docs/output-schema.md
CHANGED
|
@@ -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 `
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
652
|
-
*
|
|
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
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
|
-
|
|
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 {
|
|
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,
|
|
17
|
-
import {
|
|
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
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
165
|
+
this.leafInputsCache = loadLeafInputs(this);
|
|
166
|
+
return this.leafInputsCache;
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
/**
|
|
168
|
-
*
|
|
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
|
-
|
|
172
|
-
return
|
|
172
|
+
inputsAs<T = CliLeafInputs>(): T {
|
|
173
|
+
return this.inputs as T;
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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 : [];
|