argsbarg 6.0.2 → 6.1.0

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,11 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [6.1.0] - 2026-07-22
11
+
12
+ ### Added
13
+
14
+ - **`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.
16
+
17
+ ### Changed
18
+
19
+ - **Schemagen discovery** — role exports (`configType` / `inputType` / `outputType`) live in `src/**/types.ts` instead of thin `schema.ts` manifests.
20
+
10
21
  ## [6.0.2] - 2026-07-22
11
22
 
12
23
  ### Added
13
24
 
14
- - **`argsbarg schemagen`** — centralized JSON Schema generation from `src/**/schema.ts` into colocated `__generated__/` directories (`configSchema.json`, `inputSchema.json`, `outputSchema.json`, plus `index.ts` re-exports). Removes orphan `__generated__/` trees and stale JSON when schema roots or kinds are removed.
25
+ - **`argsbarg schemagen`** — centralized JSON Schema generation from thin `src/**/schema.ts` manifests into colocated `__generated__/` directories. Follows role aliases (`configType` / `inputType` / `outputType`) to types in sibling modules (typically `types.ts`).
15
26
  - **`argsbarg/schemagen`** export — `runSchemagen`, `discoverSchemaRoots`, and naming helpers (`src/cli-tool/schemagen/`).
16
27
 
17
28
  ### Changed
@@ -739,7 +750,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
739
750
  - 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`).
740
751
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
741
752
 
742
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.0.2...HEAD
753
+ [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.1.0...HEAD
754
+ [6.1.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.1.0
743
755
  [6.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.2
744
756
  [6.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.1
745
757
  [6.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.0
package/README.md CHANGED
@@ -208,6 +208,7 @@ Add `CliPositional` entries to the command’s `positionals` list (separate from
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
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.
211
212
  - `ctx.typedOpt<T>("custom", parseFn)` — custom parsing for type-safe option resolution.
212
213
  - `ctx.args` — positional words in order as `string[]`.
213
214
  - `ctx.positional("name")` — named positional lookup; varargs slots return `string[]`, single slots return `string | undefined`.
@@ -267,8 +268,8 @@ To refresh the Cursor rule in an existing consumer: `bun scripts/merge-cli-progr
267
268
  | Area | Files / wiring |
268
269
  | --------------------- | -------------------------------------------------------------------------------------- |
269
270
  | All builtins | `completion`, `version`, `configure`, `docs`, `mcp`, `api`, `configure get`/`set` |
270
- | `program.appConfig` | `src/config/schema.ts` → `configSchema` from `__generated__/` |
271
- | `outputSchema` | `src/commands/status/schema.ts` → `outputSchema` from `__generated__/` |
271
+ | `program.appConfig` | `src/config/types.ts` → `configSchema` from `__generated__/` |
272
+ | `outputSchema` | `src/commands/status/types.ts` → `outputSchema` from `__generated__/` |
272
273
  | Schemagen | `just schemagen` → `argsbarg schemagen` (justfile exports `node_modules/.bin` on `PATH`) |
273
274
  | Command layout | `src/commands/<name>/command.ts`; registration in `src/program.ts` |
274
275
  | MCP doc topics | `docs.topics` auto-exposed as `<key>://docs/<topic>` resources when docs + MCP enabled |
@@ -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 and read `ctx.toolArgs` in the handler — it contains 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 `await ctx.readLeafInputsAsync()` — it merges flag values, piped stdin, and the original flat JSON body from `POST /tools/:name`.
@@ -223,9 +223,37 @@ const { limit, "skip-readiness": skipReadiness, timeout } = ctx.readLeafInputs()
223
223
  | `format: date-time` | `string` (normalized UTC ISO) |
224
224
  | Single positional | `string` or `undefined` |
225
225
  | Varargs positional | `string[]` or `undefined` |
226
+ | `kind: json` | parsed object/array or `undefined` (use `readLeafInputsAsync()` for pipable stdin and MCP merge) |
226
227
 
227
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.
228
229
 
230
+ ### Json options and piped stdin
231
+
232
+ For nested tool bodies (e.g. invoice template data), declare a matching property in schemagen `inputType`, wire `inputSchema` on the leaf, and add a **`kind: Json`** option with the same name:
233
+
234
+ ```typescript
235
+ {
236
+ name: "invoice",
237
+ description: "Invoice template data. Pass JSON via --invoice or pipe to stdin.",
238
+ kind: CliOptionKind.Json,
239
+ pipable: true,
240
+ required: true,
241
+ }
242
+ ```
243
+
244
+ | Surface | How `invoice` is supplied |
245
+ | --- | --- |
246
+ | CLI | `--invoice '<json>'` **or** omit the flag and pipe JSON to stdin |
247
+ | MCP / HTTP | `invoice` object in the tool JSON body (`ctx.toolArgs`) |
248
+
249
+ **Precedence:** if `--invoice` is set, the flag value wins and stdin is not read.
250
+
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`).
252
+
253
+ At most one `pipable` Json option per leaf. Json option names must appear in `inputSchema.properties` when a custom `inputSchema` is set.
254
+
255
+ See [output-schema.md](output-schema.md) for schemagen `inputType` and [api-server.md](api-server.md) for HTTP tool bodies.
256
+
229
257
  `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`.
230
258
 
231
259
  See [examples/formats.ts](../examples/formats.ts) for a runnable demo.
@@ -142,7 +142,7 @@ Mirror the [output-schema.md](output-schema.md) pattern for config:
142
142
 
143
143
  ```mermaid
144
144
  flowchart LR
145
- subgraph types [schema.ts]
145
+ subgraph types [types.ts]
146
146
  Marker["export type configType = AppConfig"]
147
147
  end
148
148
  subgraph gen [argsbarg schemagen]
@@ -164,14 +164,14 @@ flowchart LR
164
164
  | Piece | Convention |
165
165
  | --- | --- |
166
166
  | Generator | [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) (bundled with argsbarg) |
167
- | Discovery | `export type configType = …` in `src/config/schema.ts` (type defined in same file) |
167
+ | Discovery | `export type configType = …` in `src/config/types.ts` |
168
168
  | Artifacts | `src/config/__generated__/` — gitignored; run `just schemagen` after clone |
169
169
  | Consumer CI | Optional: `ajv` + `ajv-formats` against the same committed JSON (not an argsbarg runtime dep) |
170
170
 
171
171
  Example:
172
172
 
173
173
  ```typescript
174
- // src/config/schema.ts
174
+ // src/config/types.ts
175
175
  export interface AppConfig {
176
176
  apiToken: string;
177
177
  }
@@ -50,13 +50,13 @@ Reference implementations: **sqsp-qa-manager-poc**, **sqsp-workspaces**, **sqsp-
50
50
 
51
51
  ```mermaid
52
52
  flowchart LR
53
- subgraph types [schema.ts]
53
+ subgraph types [types.ts]
54
54
  Config["export type configType = AppConfig"]
55
55
  Output["export type outputType = StatusJsonOutput"]
56
56
  Input["export type inputType = ToolInput (optional)"]
57
57
  end
58
58
  subgraph gen [argsbarg schemagen]
59
- Discover["discover schema.ts roots"]
59
+ Discover["discover types.ts roots"]
60
60
  Gen["ts-json-schema-generator"]
61
61
  end
62
62
  subgraph artifacts [Gitignored __generated__]
@@ -74,8 +74,8 @@ flowchart LR
74
74
  | Piece | Convention |
75
75
  | --- | --- |
76
76
  | Generator | [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) (bundled as an argsbarg dependency) |
77
- | Discovery | Walk `src/**/schema.ts`; generate from `export type outputType = …` / `inputType` / `configType` when the target type is **defined in that file** |
78
- | Artifacts | `src/**/__generated__/` — gitignored; run schemagen after clone or when `schema.ts` changes |
77
+ | Discovery | Walk `src/**/types.ts` for `export type outputType` / `inputType` / `configType`; generate from the aliased type in the same file |
78
+ | Artifacts | `src/**/__generated__/` — gitignored; run schemagen after clone or when `types.ts` changes |
79
79
  | Invocation | `just schemagen` — justfile exports `node_modules/.bin` on `PATH` for local `argsbarg` |
80
80
  | tsconfig | `"resolveJsonModule": true` |
81
81
  | CI | `just check`: `schemagen` → typecheck (no git diff on generated files) |
@@ -84,12 +84,10 @@ flowchart LR
84
84
 
85
85
  ### Declaring a schema root
86
86
 
87
- Put schema-facing interfaces in **`schema.ts`** next to the command (or in a shared module when several commands reuse one shape). Export a role alias:
87
+ Put schema-facing interfaces and role exports in **`types.ts`** (or `core/types.ts` for shared shapes):
88
88
 
89
89
  ```typescript
90
- // src/commands/status/schema.ts
91
- import type { WorkspaceStatus } from "./types.ts";
92
-
90
+ // src/commands/status/types.ts
93
91
  /** JSON stdout for `myapp status --json`. */
94
92
  export interface StatusJsonOutput {
95
93
  workspaces: WorkspaceStatus[];
@@ -99,10 +97,10 @@ export interface StatusJsonOutput {
99
97
  export type outputType = StatusJsonOutput;
100
98
  ```
101
99
 
102
- ```typescript
103
- // src/ui/runHeadless/schema.ts — shared by many mutating commands
104
- import type { HeadlessTaskResult } from "./types.ts";
100
+ Handlers and other modules import from the same **`types.ts`** module.
105
101
 
102
+ ```typescript
103
+ // src/ui/runHeadless/types.ts — shared by many mutating commands
106
104
  export interface HeadlessOpResult {
107
105
  command: string;
108
106
  exitCode: number;
@@ -113,31 +111,35 @@ export type outputType = HeadlessOpResult;
113
111
  ```
114
112
 
115
113
  ```typescript
116
- // src/commands/render-invoice/schema.ts — custom HTTP/MCP body (pdf-gen)
117
- export interface RenderInvoiceToolInput {
114
+ // src/commands/render-invoice/types.ts — custom HTTP/MCP body (pdf-gen)
115
+ export interface RenderInvoiceInput {
118
116
  format: "pdf" | "html";
119
117
  invoice: InvoiceData;
120
118
  }
121
119
 
122
- export type inputType = RenderInvoiceToolInput;
123
- export type outputType = RenderInvoiceWrittenOutput;
120
+ export interface RenderInvoiceOutput {
121
+ bytes: number;
122
+ }
123
+
124
+ export type inputType = RenderInvoiceInput;
125
+ export type outputType = RenderInvoiceOutput;
124
126
  ```
125
127
 
126
128
  | Export | Role |
127
129
  | --- | --- |
128
- | `export type configType = AppConfig` | `program.appConfig.jsonSchema` (one per repo, typically `src/config/schema.ts`) |
130
+ | `export type configType = AppConfig` | `program.appConfig.jsonSchema` (one per repo, typically `src/config/types.ts`) |
129
131
  | `export type outputType = …` | `leaf.outputSchema` |
130
132
  | `export type inputType = …` | `leaf.inputSchema` (only when the tool body is not flat CLI flags) |
131
133
 
132
- **Domain helpers** stay in `types.ts` (or `core/types.ts`). Discovery scans only `schema.ts`. Re-export-only files (`export type outputType = HeadlessOpResult` pointing at another module) are **not** generation roots — generate once from the canonical definition file.
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`.
133
135
 
134
- Commands without structured JSON omit `schema.ts`. Shared shapes (e.g. `HeadlessOpResult`) live in one canonical `schema.ts`; other commands import `{ outputSchema }` from that module’s `__generated__/index.ts`.
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).
135
137
 
136
138
  When you do **not** set `inputSchema`, argsbarg builds tool input from CLI `options` + `positionals`.
137
139
 
138
140
  ### Generated artifacts
139
141
 
140
- Schemagen writes under `__generated__/` beside each `schema.ts`:
142
+ Schemagen writes under `__generated__/` beside each `types.ts` with role exports:
141
143
 
142
144
  | Kind | Generated file | Exported const (from `__generated__/index.ts`) |
143
145
  | --- | --- | --- |
@@ -174,15 +176,15 @@ appConfig: { jsonSchema: configSchema, entries: { … } },
174
176
 
175
177
  **Goal:** generated schemas match what handlers actually print, with descriptions agents can read in `docs api`.
176
178
 
177
- 1. **Schema roots** — `export interface` in `schema.ts`, with `export type outputType = …` (or `inputType` / `configType`).
179
+ 1. **Schema roots** — `export interface` in `types.ts`, with `export type outputType = …` (or `inputType` / `configType`).
178
180
  2. **Per property** — `/** … */` on every field that should appear in JSON Schema `properties` (including nested named types).
179
181
  3. **Unions / enums** — document the alias; generator emits `enum` / `anyOf` with type-level description.
180
182
  4. **Formats** — property JSDoc can include `@format date-time` for ISO timestamps; add a smoke test that the generated property has `format: "date-time"`.
181
- 5. **Do not hand-edit** `__generated__/` — change types/JSDoc in `schema.ts`, run `just schemagen`.
183
+ 5. **Do not hand-edit** `__generated__/` — change types/JSDoc in `types.ts`, run `just schemagen`.
182
184
 
183
185
  ### Narrowing when runtime ≠ stdout
184
186
 
185
- When a shared runtime type is **wider** than one command’s JSON, add a **schema-facing** root in `schema.ts`:
187
+ When a shared runtime type is **wider** than one command’s JSON, add a **schema-facing** root in `types.ts`:
186
188
 
187
189
  ```typescript
188
190
  /** JSON stdout for `myapp pr` and `myapp file`. */
@@ -211,7 +213,7 @@ Per consumer repo (optional):
211
213
 
212
214
  ## Contributor workflow
213
215
 
214
- 1. Add or edit schema roots in `src/**/schema.ts` with `outputType` / `inputType` / `configType` and per-field JSDoc.
216
+ 1. Add or edit schema roots in `src/**/types.ts` with `outputType` / `inputType` / `configType` and per-field JSDoc.
215
217
  2. `just schemagen` — refresh `src/**/__generated__/`.
216
218
  3. Import `{ outputSchema }` / `{ inputSchema }` / `{ configSchema }` from the relevant `__generated__/index.ts`.
217
219
  4. `just docgen` / `myapp docs api --save` — refresh consumer docs.
@@ -219,7 +221,7 @@ Per consumer repo (optional):
219
221
 
220
222
  Add a bullet under your app’s `**… conventions:**` block in `.cursor/rules/cli-program.mdc` pointing at `node_modules/argsbarg/docs/output-schema.md`.
221
223
 
222
- **Reference implementation:** [`examples/full-example/`](../examples/full-example/) in this repo — `schema.ts` roots, `__generated__/`, and `status` leaf with `outputSchema`.
224
+ **Reference implementation:** [`examples/full-example/`](../examples/full-example/) in this repo — `types.ts` roots, `__generated__/`, and `status` leaf with `outputSchema`.
223
225
 
224
226
  ## Out of scope
225
227
 
@@ -9,7 +9,7 @@ From a git checkout at this directory (requires [Homebrew](https://brew.sh), [ju
9
9
  ```bash
10
10
  brew install just bun
11
11
  just setup
12
- just schemagen # after changing src/**/schema.ts
12
+ just schemagen # after changing src/**/types.ts
13
13
  just run status --json
14
14
  just run docs readme
15
15
  ```
@@ -62,13 +62,13 @@ Undo a local dev install: `just uninstall` (formula + agent artifacts; app confi
62
62
 
63
63
  ## Schemagen roots
64
64
 
65
- | Export in `schema.ts` | Generated artifact | Import on leaf / program |
65
+ | Export in `types.ts` | Generated artifact | Import on leaf / program |
66
66
  | --- | --- | --- |
67
- | `export type configType = …` | `__generated__/configSchema.json` | `{ configSchema }` from `config/__generated__/index.ts` → `program.appConfig.jsonSchema` |
68
- | `export type outputType = …` | `__generated__/outputSchema.json` | `{ outputSchema }` from `__generated__/index.ts` → `leaf.outputSchema` |
69
- | `export type inputType = …` | `__generated__/inputSchema.json` | `{ inputSchema }` from `__generated__/index.ts` → `leaf.inputSchema` |
67
+ | `export type configType = …` | `config/__generated__/configSchema.json` | `{ configSchema }` from `config/__generated__/index.ts` |
68
+ | `export type outputType = …` | `__generated__/outputSchema.json` | `{ outputSchema }` from `__generated__/index.ts` |
69
+ | `export type inputType = …` | `__generated__/inputSchema.json` | `{ inputSchema }` from `__generated__/index.ts` |
70
70
 
71
- Discovery walks `src/**/schema.ts` only. Domain helpers stay in sibling `types.ts`. `__generated__/` is gitignored — run `argsbarg schemagen` (via `just schemagen` or `just setup`).
71
+ Type definitions and schemagen role exports live in `types.ts`. Run `argsbarg schemagen` (via `just schemagen` or `just setup`).
72
72
 
73
73
  ## Consumer docs
74
74
 
@@ -1 +1,19 @@
1
- export type { StatusJsonOutput } from "./schema.ts";
1
+ /** JSON stdout for `full-example status --json`. */
2
+ export interface StatusJsonOutput {
3
+ /** Resolved AWS region. */
4
+ defaultRegion?: string;
5
+ /** Resolved retry count. */
6
+ maxRetries?: number;
7
+ /** Whether apiToken is set (value never included). */
8
+ apiTokenSet: boolean;
9
+ /** App version from program root. */
10
+ version: string;
11
+ }
12
+
13
+ /** Returns status JSON (identity helper for schema generation). */
14
+ export function buildStatusJson(output: StatusJsonOutput): StatusJsonOutput {
15
+ return output;
16
+ }
17
+
18
+ /** Schemagen root for leaf outputSchema. */
19
+ export type outputType = StatusJsonOutput;
@@ -4,10 +4,10 @@ Kitchen-sink CliProgram — every argsbarg builtin enabled; command registration
4
4
 
5
5
  import type { CliAppConfig, CliAppConfigEntry, CliProgram } from "argsbarg";
6
6
  import readmeText from "../README.md" with { type: "text" };
7
- import { configSchema } from "./config/__generated__/index.ts";
8
7
  import { createIdentity } from "../scripts/create-identity.ts";
9
8
  import { echoCommand } from "./commands/echo/command.ts";
10
9
  import { statusCommand } from "./commands/status/command.ts";
10
+ import { configSchema } from "./config/__generated__/index.ts";
11
11
 
12
12
  const configEntries = {
13
13
  apiToken: {
package/index.d.ts CHANGED
@@ -45,7 +45,7 @@ declare class AppConfigSnapshot {
45
45
  }
46
46
  export type AnyAppConfigSnapshot = AppConfigSnapshot | EmptyAppConfigSnapshot;
47
47
  /** Coerced leaf inputs keyed by option and positional names. */
48
- export type CliLeafInputs = Record<string, boolean | number | string | string[] | undefined>;
48
+ export type CliLeafInputs = Record<string, boolean | number | string | string[] | unknown | undefined>;
49
49
  /**
50
50
  * Values passed to a leaf command handler after parsing: app name, routed path, args, and merged options.
51
51
  */
@@ -92,6 +92,11 @@ export declare class CliContext {
92
92
  positional(name: string): string | string[] | undefined;
93
93
  /** Reads coerced option and positional values for the current leaf from schema metadata. */
94
94
  readLeafInputs(): CliLeafInputs;
95
+ /**
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.
98
+ */
99
+ readLeafInputsAsync(): Promise<CliLeafInputs>;
95
100
  private _readOptionValue;
96
101
  private _leafNode;
97
102
  private _posMap;
@@ -102,7 +107,7 @@ export declare class CliContext {
102
107
  */
103
108
  export type CliInvocation = "cli" | "mcp" | "api";
104
109
  /**
105
- * Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
110
+ * Option kinds: presence (boolean flag), string (free-form text), number (strict double), enum (fixed choices), or json (parsed JSON object/array).
106
111
  */
107
112
  export declare enum CliOptionKind {
108
113
  /** Boolean flag: no value token (may be implicit `"1"` when set). */
@@ -112,7 +117,9 @@ export declare enum CliOptionKind {
112
117
  /** Strict floating-point value (parsed at validation time). */
113
118
  Number = "number",
114
119
  /** Fixed set of allowed string values. Requires non-empty `choices` on the option. */
115
- Enum = "enum"
120
+ Enum = "enum",
121
+ /** JSON object or array (parsed from `--name '<json>'`, piped stdin when `pipable`, or MCP/API tool body). */
122
+ Json = "json"
116
123
  }
117
124
  /**
118
125
  * Named validation/coercion for string options (`format` on `CliOption`).
@@ -176,6 +183,11 @@ export interface CliOption {
176
183
  default?: string;
177
184
  /** Regex pattern for string options. Mutually exclusive with `format`. */
178
185
  pattern?: string;
186
+ /**
187
+ * When `true` on a `Json` option, CLI may omit `--name` and supply JSON via stdin instead.
188
+ * If `--name` is set, the flag value wins and stdin is not read.
189
+ */
190
+ pipable?: boolean;
179
191
  }
180
192
  /**
181
193
  * An ordered positional argument slot, listed on leaf `positionals`.
@@ -631,6 +643,15 @@ dryRun?: boolean,
631
643
  interactive?: boolean): void;
632
644
  /** Prefixes a success message when running in dry-run mode. */
633
645
  export declare function formatDryRunMessage(message: string, dryRun: boolean): string;
646
+ /** Thrown when {@link CliContext.readLeafInputsAsync} cannot resolve or validate inputs. */
647
+ export declare class LeafInputError extends Error {
648
+ constructor(message: string);
649
+ }
650
+ /**
651
+ * Reads coerced leaf inputs, resolving Json options from flags, piped stdin, or toolArgs,
652
+ * and validates against `leaf.inputSchema` when set.
653
+ */
654
+ export declare function readLeafInputsAsync(ctx: CliContext): Promise<CliLeafInputs>;
634
655
  /** Resolved paths for `mcp bundle`. */
635
656
  export interface McpBundlePaths {
636
657
  binaryPath: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "6.0.2",
3
+ "version": "6.1.0",
4
4
  "main": "./src/index.ts",
5
5
  "module": "./src/index.ts",
6
6
  "dependencies": {
@@ -95,7 +95,8 @@ export const program = {
95
95
  },
96
96
  {
97
97
  key: "schemagen",
98
- description: "Generate JSON Schema artifacts from src/**/schema.ts into colocated __generated__/ directories.",
98
+ description:
99
+ "Generate JSON Schema artifacts from src/**/types.ts role exports into colocated __generated__/ directories.",
99
100
  options: [
100
101
  {
101
102
  name: "root",
@@ -5,7 +5,7 @@ Remove stale __generated__/ directories and files after schemagen runs.
5
5
  import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
6
6
  import { dirname, join, relative } from "node:path";
7
7
  import type { SchemaRoot } from "./discover-schema-roots.ts";
8
- import { GENERATED_DIR, SCHEMA_FILE, schemaJsonBasename } from "./names.ts";
8
+ import { GENERATED_DIR, schemaJsonBasename, TYPES_FILE } from "./names.ts";
9
9
 
10
10
  function listGeneratedDirs(srcDir: string, projectRoot: string, out: string[]): void {
11
11
  for (const ent of readdirSync(srcDir)) {
@@ -38,8 +38,8 @@ export function cleanStaleGenerated(
38
38
 
39
39
  for (const relGeneratedDir of generatedDirs) {
40
40
  const generatedDir = join(projectRoot, relGeneratedDir);
41
- const relSchemaPath = relative(projectRoot, join(dirname(generatedDir), SCHEMA_FILE));
42
- const roots = existsSync(join(projectRoot, relSchemaPath)) ? (activeBySchemaFile.get(relSchemaPath) ?? []) : [];
41
+ const relTypesPath = relative(projectRoot, join(dirname(generatedDir), TYPES_FILE));
42
+ const roots = existsSync(join(projectRoot, relTypesPath)) ? (activeBySchemaFile.get(relTypesPath) ?? []) : [];
43
43
 
44
44
  if (roots.length === 0) {
45
45
  rmSync(generatedDir, { recursive: true, force: true });
@@ -1,10 +1,10 @@
1
1
  /*
2
- Discovers schema roots in schema.ts files via configType / inputType / outputType exports.
2
+ Discovers schema roots in types.ts files via configType / inputType / outputType exports.
3
3
  */
4
4
 
5
- import { readdirSync, readFileSync, statSync } from "node:fs";
6
- import { join, relative } from "node:path";
7
- import { SCHEMA_FILE } from "./names.ts";
5
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
6
+ import { dirname, join, relative } from "node:path";
7
+ import { TYPES_FILE } from "./names.ts";
8
8
 
9
9
  export type SchemaRootKind = "config" | "input" | "output";
10
10
 
@@ -13,11 +13,15 @@ export type SchemaRole = "configType" | "inputType" | "outputType";
13
13
  export interface SchemaRoot {
14
14
  kind: SchemaRootKind;
15
15
  typeName: string;
16
- /** Path relative to project root (e.g. src/config/schema.ts). */
16
+ /** Path to types.ts relative to project root (anchors __generated__/ output). */
17
17
  path: string;
18
+ /** Path to the file that defines `typeName` (defaults to `path`). */
19
+ sourcePath: string;
18
20
  }
19
21
 
20
22
  const ROLE_EXPORT_RE = /export\s+type\s+(configType|inputType|outputType)\s*=\s*(\w+)/g;
23
+ const HAS_ROLE_EXPORT_RE = /export\s+type\s+(configType|inputType|outputType)\s*=/;
24
+ const IMPORT_RE = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
21
25
 
22
26
  const ROLE_TO_KIND: Record<SchemaRole, SchemaRootKind> = {
23
27
  configType: "config",
@@ -25,21 +29,25 @@ const ROLE_TO_KIND: Record<SchemaRole, SchemaRootKind> = {
25
29
  outputType: "output",
26
30
  };
27
31
 
28
- function listSchemaFiles(srcDir: string, baseDir: string, out: string[]): void {
32
+ function listTypesManifestFiles(srcDir: string, baseDir: string, out: string[]): void {
29
33
  for (const ent of readdirSync(srcDir)) {
30
34
  const full = join(srcDir, ent);
31
35
  const st = statSync(full);
32
36
  if (st.isDirectory()) {
33
- listSchemaFiles(full, baseDir, out);
37
+ listTypesManifestFiles(full, baseDir, out);
34
38
  continue;
35
39
  }
36
- if (ent === SCHEMA_FILE) {
40
+ if (ent !== TYPES_FILE) {
41
+ continue;
42
+ }
43
+ const text = readFileSync(full, "utf8");
44
+ if (HAS_ROLE_EXPORT_RE.test(text)) {
37
45
  out.push(relative(baseDir, full));
38
46
  }
39
47
  }
40
48
  }
41
49
 
42
- /** True when `typeName` is declared in this file (not a re-export alias to another module). */
50
+ /** True when `typeName` is declared in this file (not a schemagen role alias). */
43
51
  function isTypeDefinedInFile(text: string, typeName: string): boolean {
44
52
  if (new RegExp(`export\\s+interface\\s+${typeName}\\b`).test(text)) {
45
53
  return true;
@@ -50,7 +58,62 @@ function isTypeDefinedInFile(text: string, typeName: string): boolean {
50
58
  return false;
51
59
  }
52
60
 
53
- function discoverFromFile(path: string, text: string): SchemaRoot[] {
61
+ function parseLocalTypeImports(text: string): Map<string, string> {
62
+ const imports = new Map<string, string>();
63
+ for (const match of text.matchAll(IMPORT_RE)) {
64
+ const names = match[1];
65
+ const from = match[2];
66
+ if (!names || !from?.startsWith(".")) {
67
+ continue;
68
+ }
69
+ for (const part of names.split(",")) {
70
+ const trimmed = part.trim();
71
+ const nameMatch = trimmed.match(/^(?:type\s+)?(\w+)(?:\s+as\s+(\w+))?$/);
72
+ if (!nameMatch?.[1]) {
73
+ continue;
74
+ }
75
+ const localName = nameMatch[2] ?? nameMatch[1];
76
+ imports.set(localName, from);
77
+ }
78
+ }
79
+ return imports;
80
+ }
81
+
82
+ function resolveModuleFile(manifestFile: string, specifier: string): string | null {
83
+ const base = join(dirname(manifestFile), specifier);
84
+ const candidates = [base, `${base}.ts`, join(base, "index.ts")];
85
+ for (const candidate of candidates) {
86
+ if (existsSync(candidate)) {
87
+ return candidate;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /** Resolve a role alias to the module that defines `typeName`, when imported from a relative path. */
94
+ function resolveAliasedTypeSource(
95
+ projectRoot: string,
96
+ manifestRelPath: string,
97
+ manifestText: string,
98
+ typeName: string,
99
+ ): string | null {
100
+ const manifestFile = join(projectRoot, manifestRelPath);
101
+ const specifier = parseLocalTypeImports(manifestText).get(typeName);
102
+ if (!specifier) {
103
+ return null;
104
+ }
105
+ const moduleFile = resolveModuleFile(manifestFile, specifier);
106
+ if (!moduleFile) {
107
+ return null;
108
+ }
109
+ const moduleText = readFileSync(moduleFile, "utf8");
110
+ if (!isTypeDefinedInFile(moduleText, typeName)) {
111
+ return null;
112
+ }
113
+ return relative(projectRoot, moduleFile);
114
+ }
115
+
116
+ function discoverFromFile(projectRoot: string, path: string, text: string): SchemaRoot[] {
54
117
  const rolesSeen = new Set<SchemaRole>();
55
118
  const roots: SchemaRoot[] = [];
56
119
 
@@ -64,27 +127,34 @@ function discoverFromFile(path: string, text: string): SchemaRoot[] {
64
127
  throw new Error(`${path}: duplicate export type ${role}`);
65
128
  }
66
129
  rolesSeen.add(role);
130
+
131
+ let sourcePath = path;
67
132
  if (!isTypeDefinedInFile(text, typeName)) {
68
- continue;
133
+ const resolved = resolveAliasedTypeSource(projectRoot, path, text, typeName);
134
+ if (!resolved) {
135
+ continue;
136
+ }
137
+ sourcePath = resolved;
69
138
  }
70
- roots.push({ kind: ROLE_TO_KIND[role], typeName, path });
139
+
140
+ roots.push({ kind: ROLE_TO_KIND[role], typeName, path, sourcePath });
71
141
  }
72
142
 
73
143
  return roots;
74
144
  }
75
145
 
76
- /** Find all schema roots under `srcDir` in files named `schema.ts`. */
146
+ /** Find all schema roots under `srcDir` in `types.ts` files with role exports. */
77
147
  export function discoverSchemaRoots(projectRoot: string, srcDir = "src"): SchemaRoot[] {
78
148
  const srcPath = join(projectRoot, srcDir);
79
149
  const files: string[] = [];
80
- listSchemaFiles(srcPath, projectRoot, files);
150
+ listTypesManifestFiles(srcPath, projectRoot, files);
81
151
 
82
152
  const roots: SchemaRoot[] = [];
83
153
  const typeOwners = new Map<string, string>();
84
154
 
85
155
  for (const relPath of files.sort()) {
86
156
  const text = readFileSync(join(projectRoot, relPath), "utf8");
87
- for (const root of discoverFromFile(relPath, text)) {
157
+ for (const root of discoverFromFile(projectRoot, relPath, text)) {
88
158
  const prev = typeOwners.get(root.typeName);
89
159
  if (prev) {
90
160
  throw new Error(`${relPath}: duplicate schema root type ${root.typeName} (already declared in ${prev})`);
@@ -1,3 +1,3 @@
1
1
  export { discoverSchemaRoots, type SchemaRoot, type SchemaRootKind } from "./discover-schema-roots.ts";
2
- export { GENERATED_DIR, SCHEMA_FILE, schemaExportName, schemaJsonBasename } from "./names.ts";
2
+ export { GENERATED_DIR, schemaExportName, schemaJsonBasename, TYPES_FILE } from "./names.ts";
3
3
  export { type RunSchemagenOptions, type RunSchemagenResult, runSchemagen } from "./run.ts";
@@ -1,10 +1,10 @@
1
1
  import type { SchemaRootKind } from "./discover-schema-roots.ts";
2
2
 
3
- /** Directory name for generated schema artifacts (next to `schema.ts`). */
3
+ /** Directory name for generated schema artifacts (next to `types.ts`). */
4
4
  export const GENERATED_DIR = "__generated__";
5
5
 
6
- /** TypeScript schema root filename under `src/`. */
7
- export const SCHEMA_FILE = "schema.ts";
6
+ /** TypeScript schemagen manifest filename under `src/`. */
7
+ export const TYPES_FILE = "types.ts";
8
8
 
9
9
  /** JSON basename for a schema root kind (`outputSchema.json`, etc.). */
10
10
  export function schemaJsonBasename(kind: SchemaRootKind): string {
@@ -36,8 +36,9 @@ function resolveTsconfig(projectRoot: string, tsconfig: string): string {
36
36
  }
37
37
 
38
38
  function generateJson(projectRoot: string, tsconfigPath: string, root: SchemaRoot): Record<string, unknown> {
39
+ const typeFile = join(projectRoot, root.sourcePath);
39
40
  const generator = createGenerator({
40
- path: join(projectRoot, root.path),
41
+ path: typeFile,
41
42
  type: root.typeName,
42
43
  tsconfig: tsconfigPath,
43
44
  topRef: false,