argsbarg 6.1.0 → 6.1.1
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 +17 -2
- package/README.md +5 -2
- package/docs/api-server.md +1 -1
- package/docs/cli-program.md +11 -8
- package/docs/output-schema.md +1 -1
- package/index.d.ts +38 -8
- package/package.json +1 -1
- package/src/cli.ts +54 -1
- package/src/context.ts +40 -57
- package/src/index.ts +8 -1
- package/src/leaf-inputs.test.ts +118 -9
- package/src/leaf-inputs.ts +85 -68
package/CHANGELOG.md
CHANGED
|
@@ -7,12 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [6.1.1] - 2026-07-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`ctx.inputs`** — coerced, pre-validated leaf inputs (getter; preferred over `readLeafInputs()`).
|
|
15
|
+
- **`ctx.inputsAs<T>()`** — `ctx.inputs` cast to a schemagen or app input type (`T` unconstrained; consumer-asserted).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **`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.
|
|
20
|
+
- **`ctx.readLeafInputs()`** — deprecated; use `ctx.inputs` or `ctx.inputsAs()`.
|
|
21
|
+
|
|
10
22
|
## [6.1.0] - 2026-07-22
|
|
11
23
|
|
|
12
24
|
### Added
|
|
13
25
|
|
|
14
26
|
- **`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.
|
|
27
|
+
- **`ctx.jsonOpt(name)`** — parsed Json option from flag, preloaded stdin, or MCP/API `toolArgs`.
|
|
28
|
+
- **`ctx.readLeafInputs()`** — all coerced inputs; validates against `leaf.inputSchema` when set (stdin preloaded before handler).
|
|
29
|
+
- **`ctx.readLeafInputsAsync()`** — deprecated alias for sync `readLeafInputs()`.
|
|
16
30
|
|
|
17
31
|
### Changed
|
|
18
32
|
|
|
@@ -750,7 +764,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
750
764
|
- 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
765
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
752
766
|
|
|
753
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.
|
|
767
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.1...HEAD
|
|
768
|
+
[6.1.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.1
|
|
754
769
|
[6.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.0
|
|
755
770
|
[6.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.2
|
|
756
771
|
[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,12 +246,12 @@ 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
|
|
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;
|
|
@@ -596,6 +613,8 @@ export declare class Cli {
|
|
|
596
613
|
}): Promise<CliInvokeResult>;
|
|
597
614
|
serveMcp(): Promise<never>;
|
|
598
615
|
serveApi(): Promise<never>;
|
|
616
|
+
private ensureValidatedLeafInputs;
|
|
617
|
+
private exitLeafInputError;
|
|
599
618
|
private prepareDispatch;
|
|
600
619
|
private buildAppConfigSnapshot;
|
|
601
620
|
}
|
|
@@ -643,14 +662,25 @@ dryRun?: boolean,
|
|
|
643
662
|
interactive?: boolean): void;
|
|
644
663
|
/** Prefixes a success message when running in dry-run mode. */
|
|
645
664
|
export declare function formatDryRunMessage(message: string, dryRun: boolean): string;
|
|
646
|
-
/** Thrown when
|
|
665
|
+
/** Thrown when leaf input resolution or validation fails. */
|
|
647
666
|
export declare class LeafInputError extends Error {
|
|
648
667
|
constructor(message: string);
|
|
649
668
|
}
|
|
669
|
+
/** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
|
|
670
|
+
export declare function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined;
|
|
671
|
+
/**
|
|
672
|
+
* Reads piped stdin for a pipable Json option when the flag is omitted (CLI only).
|
|
673
|
+
* Call from {@link Cli.run} before constructing the handler context.
|
|
674
|
+
*/
|
|
675
|
+
export declare function preloadPipableJson(program: CliProgram, commandPath: string[], opts: Record<string, string>, invocation: CliInvocation): Promise<Record<string, unknown>>;
|
|
650
676
|
/**
|
|
651
|
-
*
|
|
652
|
-
*
|
|
677
|
+
* Loads coerced leaf inputs and validates against `leaf.inputSchema` when set.
|
|
678
|
+
* Used by {@link CliContext.inputs}; prefer `ctx.inputs` or `ctx.inputsAs()` in handlers.
|
|
653
679
|
*/
|
|
680
|
+
export declare function loadLeafInputs(ctx: CliContext): CliLeafInputs;
|
|
681
|
+
/** @deprecated Use {@link CliContext.inputs} or {@link loadLeafInputs} via `ctx.inputs`. */
|
|
682
|
+
export declare function readLeafInputs(ctx: CliContext): CliLeafInputs;
|
|
683
|
+
/** @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs. */
|
|
654
684
|
export declare function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs>;
|
|
655
685
|
/** Resolved paths for `mcp bundle`. */
|
|
656
686
|
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");
|
|
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/index.ts
CHANGED
|
@@ -28,7 +28,14 @@ export {
|
|
|
28
28
|
shouldRunHeadlessWithYes,
|
|
29
29
|
wantsExplicitJson,
|
|
30
30
|
} from "./headless.ts";
|
|
31
|
-
export {
|
|
31
|
+
export {
|
|
32
|
+
LeafInputError,
|
|
33
|
+
loadLeafInputs,
|
|
34
|
+
preloadPipableJson,
|
|
35
|
+
readJsonOptionValue,
|
|
36
|
+
readLeafInputs,
|
|
37
|
+
readLeafInputsAsync,
|
|
38
|
+
} from "./leaf-inputs.ts";
|
|
32
39
|
export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
|
|
33
40
|
export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
|
|
34
41
|
export type {
|
package/src/leaf-inputs.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { Cli, CliContext, CliOptionKind } from "./index.ts";
|
|
3
|
-
import { LeafInputError,
|
|
3
|
+
import { LeafInputError, readLeafInputs } from "./leaf-inputs.ts";
|
|
4
4
|
import type { CliProgram } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
const invoiceSchema = {
|
|
@@ -46,16 +46,13 @@ function renderProgram(): CliProgram {
|
|
|
46
46
|
required: true,
|
|
47
47
|
},
|
|
48
48
|
],
|
|
49
|
-
handler:
|
|
50
|
-
const inputs = await ctx.readLeafInputsAsync();
|
|
51
|
-
return inputs;
|
|
52
|
-
},
|
|
49
|
+
handler: (ctx) => ctx.readLeafInputs(),
|
|
53
50
|
},
|
|
54
51
|
],
|
|
55
52
|
} satisfies CliProgram;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
describe("
|
|
55
|
+
describe("readLeafInputs / jsonOpt", () => {
|
|
59
56
|
test("reads Json option from MCP toolArgs", async () => {
|
|
60
57
|
const cli = new Cli(renderProgram());
|
|
61
58
|
const result = await cli.invoke(["render", "--format", "pdf"], {
|
|
@@ -83,6 +80,33 @@ describe("readLeafInputsAsync", () => {
|
|
|
83
80
|
});
|
|
84
81
|
});
|
|
85
82
|
|
|
83
|
+
test("jsonOpt reads from preloadedJson", () => {
|
|
84
|
+
const program = renderProgram();
|
|
85
|
+
const ctx = new CliContext(
|
|
86
|
+
"json-pipe-test",
|
|
87
|
+
["render"],
|
|
88
|
+
[],
|
|
89
|
+
{ format: "pdf" },
|
|
90
|
+
program,
|
|
91
|
+
"cli",
|
|
92
|
+
undefined,
|
|
93
|
+
undefined,
|
|
94
|
+
{ invoice: { id: "piped" } },
|
|
95
|
+
);
|
|
96
|
+
expect(ctx.jsonOpt("invoice")).toEqual({ id: "piped" });
|
|
97
|
+
expect(ctx.inputs).toEqual({ format: "pdf", invoice: { id: "piped" } });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("inputsAs returns schemagen-shaped inputs", () => {
|
|
101
|
+
type RenderInput = { format: "pdf" | "html"; invoice: { id: string } };
|
|
102
|
+
const program = renderProgram();
|
|
103
|
+
const ctx = new CliContext("json-pipe-test", ["render"], [], { format: "pdf" }, program, "mcp", undefined, {
|
|
104
|
+
format: "pdf",
|
|
105
|
+
invoice: { id: "INV-1" },
|
|
106
|
+
});
|
|
107
|
+
expect(ctx.inputsAs<RenderInput>()).toEqual({ format: "pdf", invoice: { id: "INV-1" } });
|
|
108
|
+
});
|
|
109
|
+
|
|
86
110
|
test("rejects invalid Json flag at parse time", async () => {
|
|
87
111
|
const cli = new Cli(renderProgram());
|
|
88
112
|
const result = await cli.invoke(["render", "--format", "pdf", "--invoice", "not-json"], {
|
|
@@ -103,10 +127,95 @@ describe("readLeafInputsAsync", () => {
|
|
|
103
127
|
expect(result.stderr).toContain("invoice.id");
|
|
104
128
|
});
|
|
105
129
|
|
|
106
|
-
test("
|
|
130
|
+
test("validates inputSchema before handler runs", async () => {
|
|
131
|
+
let handlerCalled = false;
|
|
132
|
+
const program = {
|
|
133
|
+
key: "json-pipe-test",
|
|
134
|
+
version: "1.0.0",
|
|
135
|
+
description: "pre-handler validation",
|
|
136
|
+
commands: [
|
|
137
|
+
{
|
|
138
|
+
key: "render",
|
|
139
|
+
description: "Render",
|
|
140
|
+
inputSchema: invoiceSchema,
|
|
141
|
+
options: [
|
|
142
|
+
{
|
|
143
|
+
name: "format",
|
|
144
|
+
description: "Output format",
|
|
145
|
+
kind: CliOptionKind.Enum,
|
|
146
|
+
choices: ["pdf", "html"],
|
|
147
|
+
required: true,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "invoice",
|
|
151
|
+
description: "Invoice JSON",
|
|
152
|
+
kind: CliOptionKind.Json,
|
|
153
|
+
pipable: true,
|
|
154
|
+
required: true,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
handler: () => {
|
|
158
|
+
handlerCalled = true;
|
|
159
|
+
return { ok: true };
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
} satisfies CliProgram;
|
|
164
|
+
const cli = new Cli(program);
|
|
165
|
+
const result = await cli.invoke(["render", "--format", "pdf"], {
|
|
166
|
+
invocation: "mcp",
|
|
167
|
+
toolArgs: { format: "pdf", invoice: { id: 123 } },
|
|
168
|
+
});
|
|
169
|
+
expect(result.kind).toBe("error");
|
|
170
|
+
expect(handlerCalled).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("inputs returns cached inputs after pre-handler validation", async () => {
|
|
174
|
+
const inputs: unknown[] = [];
|
|
175
|
+
const program = {
|
|
176
|
+
key: "json-pipe-test",
|
|
177
|
+
version: "1.0.0",
|
|
178
|
+
description: "cached inputs",
|
|
179
|
+
commands: [
|
|
180
|
+
{
|
|
181
|
+
key: "render",
|
|
182
|
+
description: "Render",
|
|
183
|
+
inputSchema: invoiceSchema,
|
|
184
|
+
options: [
|
|
185
|
+
{
|
|
186
|
+
name: "format",
|
|
187
|
+
description: "Output format",
|
|
188
|
+
kind: CliOptionKind.Enum,
|
|
189
|
+
choices: ["pdf", "html"],
|
|
190
|
+
required: true,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "invoice",
|
|
194
|
+
description: "Invoice JSON",
|
|
195
|
+
kind: CliOptionKind.Json,
|
|
196
|
+
required: true,
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
handler: (ctx) => {
|
|
200
|
+
inputs.push(ctx.inputs);
|
|
201
|
+
inputs.push(ctx.inputs);
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
} satisfies CliProgram;
|
|
206
|
+
const cli = new Cli(program);
|
|
207
|
+
await cli.invoke(["render", "--format", "pdf"], {
|
|
208
|
+
invocation: "mcp",
|
|
209
|
+
toolArgs: { format: "pdf", invoice: { id: "INV-1" } },
|
|
210
|
+
});
|
|
211
|
+
expect(inputs).toHaveLength(2);
|
|
212
|
+
expect(inputs[0]).toEqual(inputs[1]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("readLeafInputs throws LeafInputError when required Json is missing", () => {
|
|
107
216
|
const program = renderProgram();
|
|
108
217
|
const ctx = new CliContext("json-pipe-test", ["render"], [], { format: "pdf" }, program, "mcp", undefined, {});
|
|
109
|
-
|
|
218
|
+
expect(() => readLeafInputs(ctx)).toThrow(LeafInputError);
|
|
110
219
|
});
|
|
111
220
|
|
|
112
221
|
test("omits undefined optional properties before inputSchema validation", async () => {
|
|
@@ -155,7 +264,7 @@ describe("readLeafInputsAsync", () => {
|
|
|
155
264
|
required: true,
|
|
156
265
|
},
|
|
157
266
|
],
|
|
158
|
-
handler:
|
|
267
|
+
handler: (ctx) => ctx.readLeafInputs(),
|
|
159
268
|
},
|
|
160
269
|
],
|
|
161
270
|
} satisfies CliProgram;
|
package/src/leaf-inputs.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
2
|
+
Leaf input reads: Json options (flag, preloaded stdin, or toolArgs), optional inputSchema validation.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { validateConfigDocument } from "./config/validate.ts";
|
|
6
6
|
import type { CliContext, CliLeafInputs } from "./context.ts";
|
|
7
7
|
import { collectOptionDefs } from "./parse.ts";
|
|
8
|
-
import type { CliLeaf, CliNode, CliOption } from "./types.ts";
|
|
8
|
+
import type { CliInvocation, CliLeaf, CliNode, CliOption, CliProgram } from "./types.ts";
|
|
9
9
|
import { CliOptionKind, CliValueFormat, isCliLeaf, isCliRouter } from "./types.ts";
|
|
10
10
|
import { isInteractiveTty } from "./utils.ts";
|
|
11
11
|
|
|
12
|
-
/** Thrown when
|
|
12
|
+
/** Thrown when leaf input resolution or validation fails. */
|
|
13
13
|
export class LeafInputError extends Error {
|
|
14
14
|
constructor(message: string) {
|
|
15
15
|
super(message);
|
|
@@ -28,40 +28,8 @@ function leafNode(ctx: CliContext): CliLeaf | undefined {
|
|
|
28
28
|
return isCliLeaf(node) ? node : undefined;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
opt: CliOption,
|
|
34
|
-
): boolean | number | string | string[] | unknown | undefined {
|
|
35
|
-
if (opt.kind === CliOptionKind.Presence) {
|
|
36
|
-
return ctx.hasFlag(opt.name);
|
|
37
|
-
}
|
|
38
|
-
if (opt.kind === CliOptionKind.Number) {
|
|
39
|
-
const n = ctx.numberOpt(opt.name);
|
|
40
|
-
return n === null ? undefined : n;
|
|
41
|
-
}
|
|
42
|
-
if (opt.kind === CliOptionKind.Json) {
|
|
43
|
-
const raw = ctx.stringOpt(opt.name);
|
|
44
|
-
if (raw === undefined) return undefined;
|
|
45
|
-
return parseJsonText(raw, `--${opt.name}`);
|
|
46
|
-
}
|
|
47
|
-
if (opt.format !== undefined) {
|
|
48
|
-
if (opt.format === CliValueFormat.Duration) {
|
|
49
|
-
return ctx.durationOpt(opt.name);
|
|
50
|
-
}
|
|
51
|
-
if (opt.format === CliValueFormat.CommaList) {
|
|
52
|
-
return ctx.commaListOpt(opt.name);
|
|
53
|
-
}
|
|
54
|
-
if (opt.format === CliValueFormat.Date) {
|
|
55
|
-
return ctx.dateOpt(opt.name);
|
|
56
|
-
}
|
|
57
|
-
if (opt.format === CliValueFormat.DateTime) {
|
|
58
|
-
return ctx.dateTimeOpt(opt.name);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return ctx.stringOpt(opt.name);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseJsonText(raw: string, label: string): unknown {
|
|
31
|
+
/** Parses a JSON string from a `--name` flag value. */
|
|
32
|
+
export function parseJsonText(raw: string, label: string): unknown {
|
|
65
33
|
const trimmed = raw.trim();
|
|
66
34
|
if (trimmed.length === 0) {
|
|
67
35
|
throw new LeafInputError(`${label}: JSON value is empty`);
|
|
@@ -107,46 +75,85 @@ function validateAgainstInputSchema(out: CliLeafInputs, inputSchema: Record<stri
|
|
|
107
75
|
}
|
|
108
76
|
}
|
|
109
77
|
|
|
78
|
+
/** Resolves a Json option from argv, preloaded stdin, or toolArgs (flag wins). */
|
|
79
|
+
export function readJsonOptionValue(ctx: CliContext, name: string): unknown | undefined {
|
|
80
|
+
const flagValue = ctx.stringOpt(name);
|
|
81
|
+
if (flagValue !== undefined) {
|
|
82
|
+
return parseJsonText(flagValue, `--${name}`);
|
|
83
|
+
}
|
|
84
|
+
if (name in ctx.preloadedJson) {
|
|
85
|
+
return ctx.preloadedJson[name];
|
|
86
|
+
}
|
|
87
|
+
if (ctx.toolArgs !== undefined && name in ctx.toolArgs) {
|
|
88
|
+
return ctx.toolArgs[name];
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reads piped stdin for a pipable Json option when the flag is omitted (CLI only).
|
|
95
|
+
* Call from {@link Cli.run} before constructing the handler context.
|
|
96
|
+
*/
|
|
97
|
+
export async function preloadPipableJson(
|
|
98
|
+
program: CliProgram,
|
|
99
|
+
commandPath: string[],
|
|
100
|
+
opts: Record<string, string>,
|
|
101
|
+
invocation: CliInvocation,
|
|
102
|
+
): Promise<Record<string, unknown>> {
|
|
103
|
+
if (invocation !== "cli" || isInteractiveTty) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
for (const opt of collectOptionDefs(program, commandPath)) {
|
|
107
|
+
if (opt.kind === CliOptionKind.Json && opt.pipable && !(opt.name in opts)) {
|
|
108
|
+
return { [opt.name]: await readPipedJsonStdin() };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readSyncOptionValue(
|
|
115
|
+
ctx: CliContext,
|
|
116
|
+
opt: CliOption,
|
|
117
|
+
): boolean | number | string | string[] | unknown | undefined {
|
|
118
|
+
if (opt.kind === CliOptionKind.Presence) {
|
|
119
|
+
return ctx.hasFlag(opt.name);
|
|
120
|
+
}
|
|
121
|
+
if (opt.kind === CliOptionKind.Number) {
|
|
122
|
+
const n = ctx.numberOpt(opt.name);
|
|
123
|
+
return n === null ? undefined : n;
|
|
124
|
+
}
|
|
125
|
+
if (opt.kind === CliOptionKind.Json) {
|
|
126
|
+
return readJsonOptionValue(ctx, opt.name);
|
|
127
|
+
}
|
|
128
|
+
if (opt.format !== undefined) {
|
|
129
|
+
if (opt.format === CliValueFormat.Duration) {
|
|
130
|
+
return ctx.durationOpt(opt.name);
|
|
131
|
+
}
|
|
132
|
+
if (opt.format === CliValueFormat.CommaList) {
|
|
133
|
+
return ctx.commaListOpt(opt.name);
|
|
134
|
+
}
|
|
135
|
+
if (opt.format === CliValueFormat.Date) {
|
|
136
|
+
return ctx.dateOpt(opt.name);
|
|
137
|
+
}
|
|
138
|
+
if (opt.format === CliValueFormat.DateTime) {
|
|
139
|
+
return ctx.dateTimeOpt(opt.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return ctx.stringOpt(opt.name);
|
|
143
|
+
}
|
|
144
|
+
|
|
110
145
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
146
|
+
* Loads coerced leaf inputs and validates against `leaf.inputSchema` when set.
|
|
147
|
+
* Used by {@link CliContext.inputs}; prefer `ctx.inputs` or `ctx.inputsAs()` in handlers.
|
|
113
148
|
*/
|
|
114
|
-
export
|
|
149
|
+
export function loadLeafInputs(ctx: CliContext): CliLeafInputs {
|
|
115
150
|
const leaf = leafNode(ctx);
|
|
116
151
|
if (!leaf) return {};
|
|
117
152
|
|
|
118
153
|
const out: CliLeafInputs = {};
|
|
119
154
|
const options = collectOptionDefs(ctx.program, ctx.commandPath);
|
|
120
|
-
let pipedJson: unknown | undefined;
|
|
121
|
-
let pipedJsonRead = false;
|
|
122
155
|
|
|
123
156
|
for (const opt of options) {
|
|
124
|
-
if (opt.kind === CliOptionKind.Json) {
|
|
125
|
-
const flagValue = ctx.stringOpt(opt.name);
|
|
126
|
-
if (flagValue !== undefined) {
|
|
127
|
-
out[opt.name] = parseJsonText(flagValue, `--${opt.name}`);
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (ctx.toolArgs !== undefined && opt.name in ctx.toolArgs) {
|
|
131
|
-
out[opt.name] = ctx.toolArgs[opt.name];
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
if (opt.pipable && ctx.invocation === "cli") {
|
|
135
|
-
if (isInteractiveTty) {
|
|
136
|
-
out[opt.name] = undefined;
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (!pipedJsonRead) {
|
|
140
|
-
pipedJson = await readPipedJsonStdin();
|
|
141
|
-
pipedJsonRead = true;
|
|
142
|
-
}
|
|
143
|
-
out[opt.name] = pipedJson;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
out[opt.name] = undefined;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
157
|
out[opt.name] = readSyncOptionValue(ctx, opt);
|
|
151
158
|
}
|
|
152
159
|
|
|
@@ -176,3 +183,13 @@ export async function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInput
|
|
|
176
183
|
|
|
177
184
|
return omitUndefinedInputs(out);
|
|
178
185
|
}
|
|
186
|
+
|
|
187
|
+
/** @deprecated Use {@link CliContext.inputs} or {@link loadLeafInputs} via `ctx.inputs`. */
|
|
188
|
+
export function readLeafInputs(ctx: CliContext): CliLeafInputs {
|
|
189
|
+
return ctx.inputs;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @deprecated Use sync {@link readLeafInputs} — stdin is preloaded before the handler runs. */
|
|
193
|
+
export function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs> {
|
|
194
|
+
return Promise.resolve(readLeafInputs(ctx));
|
|
195
|
+
}
|