argsbarg 6.0.1 → 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/README.md +4 -3
  3. package/docs/api-server.md +1 -1
  4. package/docs/cli-program.md +28 -0
  5. package/docs/config-schema.md +15 -13
  6. package/docs/output-schema.md +76 -73
  7. package/examples/full-example/README.md +7 -7
  8. package/examples/full-example/bun.lock +0 -29
  9. package/examples/full-example/justfile +6 -3
  10. package/examples/full-example/package.json +0 -2
  11. package/examples/full-example/src/commands/status/__generated__/index.ts +5 -0
  12. package/examples/full-example/src/commands/status/command.ts +2 -2
  13. package/examples/full-example/src/commands/status/types.ts +19 -1
  14. package/examples/full-example/src/config/__generated__/index.ts +5 -0
  15. package/examples/full-example/src/program.ts +4 -4
  16. package/index.d.ts +24 -3
  17. package/package.json +8 -1
  18. package/src/cli-tool/full-example-capabilities.test.ts +2 -1
  19. package/src/cli-tool/post-create.ts +3 -3
  20. package/src/cli-tool/program.ts +36 -0
  21. package/src/cli-tool/run-schemagen.ts +23 -0
  22. package/src/cli-tool/schemagen/cleanup.ts +60 -0
  23. package/src/cli-tool/schemagen/discover-schema-roots.ts +173 -0
  24. package/src/cli-tool/schemagen/index.ts +3 -0
  25. package/src/cli-tool/schemagen/names.ts +22 -0
  26. package/src/cli-tool/schemagen/run.ts +109 -0
  27. package/src/cli-tool/schemagen/schemagen.test.ts +143 -0
  28. package/src/context.ts +20 -2
  29. package/src/help.ts +2 -0
  30. package/src/index.ts +1 -0
  31. package/src/leaf-inputs.test.ts +170 -0
  32. package/src/leaf-inputs.ts +178 -0
  33. package/src/mcp/tools.ts +5 -0
  34. package/src/parse.ts +11 -0
  35. package/src/types.ts +8 -1
  36. package/src/validate.ts +43 -0
  37. package/examples/full-example/schemas/configSchemas.ts +0 -6
  38. package/examples/full-example/schemas/outputSchemas.ts +0 -6
  39. package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +0 -25
  40. package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +0 -148
  41. package/examples/full-example/scripts/schemagen/naming.ts +0 -99
  42. package/examples/full-example/scripts/schemagen.ts +0 -91
  43. package/examples/full-example/src/commands/status/schema-types.ts +0 -14
  44. /package/examples/full-example/{schemas/generated/status.json → src/commands/status/__generated__/outputSchema.json} +0 -0
  45. /package/examples/full-example/{schemas/generated/app-config.json → src/config/__generated__/configSchema.json} +0 -0
  46. /package/examples/full-example/src/config/{schema-types.ts → types.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ 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
+
21
+ ## [6.0.2] - 2026-07-22
22
+
23
+ ### Added
24
+
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`).
26
+ - **`argsbarg/schemagen`** export — `runSchemagen`, `discoverSchemaRoots`, and naming helpers (`src/cli-tool/schemagen/`).
27
+
28
+ ### Changed
29
+
30
+ - **Schemagen convention** — replace per-repo `scripts/schemagen*` copies and `schema-types.ts` bindings with `schema.ts` + gitignored `__generated__/`. Run via `just schemagen` (`argsbarg schemagen` with `node_modules/.bin` on `PATH`).
31
+
10
32
  ## [6.0.1] - 2026-07-22
11
33
 
12
34
  ### Added
@@ -15,7 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
37
 
16
38
  ### Changed
17
39
 
18
- - **Schemagen discovery contract** — `examples/full-example` and docs now use `src/**/schema-types.ts` with `export type configType` / `outputType` / `inputType` instead of JSDoc markers (`Config schema`, `JSON payload`, `Tool input`) on `types.ts`.
40
+ - **Colocated schemagen** — `*.schema.json` beside each `schema-types.ts`; exports `configSchema` / `inputSchema` / `outputSchema` (replaces central `*Schemas.ts` bridges).
19
41
  - **HTTP tool errors** — return a plain `{ "error": "..." }` JSON body without ANSI color codes or appended CLI help text.
20
42
  - **`cliErrWithHelp`** — on `api` / `mcp` invocations, throws a plain error instead of printing contextual help.
21
43
  - **`GET /openapi-browser`** — Scalar config preserves schema property order from the OpenAPI document (`orderSchemaPropertiesBy: "preserve"`).
@@ -728,7 +750,9 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
728
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`).
729
751
  - Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
730
752
 
731
- [Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v6.0.1...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
755
+ [6.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.2
732
756
  [6.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.1
733
757
  [6.0.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v6.0.0
734
758
  [5.1.16]: https://github.com/bdombro/bun-argsbarg/releases/tag/v5.1.16
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,9 +268,9 @@ 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-types.ts` (`AppConfig`) → `schemas/configSchemas.ts` |
271
- | `outputSchema` | `src/commands/status/schema-types.ts` → `schemas/outputSchemas.ts` |
272
- | Schemagen | `scripts/schemagen.ts` + `scripts/schemagen/discover-schema-roots.ts` |
271
+ | `program.appConfig` | `src/config/types.ts` → `configSchema` from `__generated__/` |
272
+ | `outputSchema` | `src/commands/status/types.ts` → `outputSchema` from `__generated__/` |
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 |
275
276
  | Package import | `from "argsbarg"` (not relative to argsbarg `src/`) |
@@ -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.
@@ -136,42 +136,42 @@ Interactive `configure` does not persist values supplied only by env or `resolve
136
136
  | **Omit `jsonSchema`** | Simple apps; all values stored as strings; use `entry.default` |
137
137
  | **Codegen from TypeScript** | Typed config, nested objects, shared with JSON Schema CI |
138
138
 
139
- ## Recommended pipeline (copy per repo)
139
+ ## Recommended pipeline (argsbarg schemagen)
140
140
 
141
141
  Mirror the [output-schema.md](output-schema.md) pattern for config:
142
142
 
143
143
  ```mermaid
144
144
  flowchart LR
145
- subgraph types [schema-types.ts]
145
+ subgraph types [types.ts]
146
146
  Marker["export type configType = AppConfig"]
147
147
  end
148
- subgraph gen [just schemagen]
149
- Script["scripts/schemagen.ts"]
148
+ subgraph gen [argsbarg schemagen]
149
+ Script["argsbarg schemagen"]
150
150
  Gen["ts-json-schema-generator"]
151
151
  end
152
- subgraph artifacts [Committed]
153
- Json["schemas/generated/app-config.json"]
154
- Bridge["schemas/configSchemas.ts"]
152
+ subgraph artifacts [Gitignored __generated__]
153
+ Json["configSchema.json"]
154
+ Index["index.ts"]
155
155
  end
156
156
  subgraph runtime [Runtime]
157
157
  Program["program.appConfig.jsonSchema"]
158
158
  Validate["argsbarg runtime subset validator"]
159
159
  end
160
160
  types --> Script --> Gen --> Json
161
- Script --> Bridge --> Program --> Validate
161
+ Gen --> Index --> Program --> Validate
162
162
  ```
163
163
 
164
164
  | Piece | Convention |
165
165
  | --- | --- |
166
- | Generator | [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) |
167
- | Discovery | `export type configType = …` in `src/config/schema-types.ts` (type defined in same file) |
168
- | Artifacts | Commit `app-config.json` and `configSchemas.ts` bridge exporting `APP_CONFIG_JSON_SCHEMA` |
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/types.ts` |
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-types.ts
174
+ // src/config/types.ts
175
175
  export interface AppConfig {
176
176
  apiToken: string;
177
177
  }
@@ -179,6 +179,8 @@ export interface AppConfig {
179
179
  export type configType = AppConfig;
180
180
  ```
181
181
 
182
+ Wire on the program root: `import { configSchema } from "./config/__generated__/index.ts"`.
183
+
182
184
  ### Supported AppConfig shapes (argsbarg runtime validator)
183
185
 
184
186
  | Supported (v1) | Deferred |
@@ -220,7 +222,7 @@ Object/array/`$ref` properties require `--json` on `configure set` when comma-se
220
222
 
221
223
  | Example | Role |
222
224
  | --- | --- |
223
- | [`examples/full-example/`](../examples/full-example/) | **Copy template** — schemagen discovery, `APP_CONFIG_JSON_SCHEMA` bridge, `program.appConfig`, built-in `configure get`/`set` |
225
+ | [`examples/full-example/`](../examples/full-example/) | **Copy template** — `argsbarg schemagen`, `program.appConfig`, built-in `configure get`/`set` |
224
226
 
225
227
  ```bash
226
228
  cd examples/full-example && just setup && just schemagen
@@ -1,18 +1,18 @@
1
1
  # Output schemas (`outputSchema`)
2
2
 
3
- How to describe JSON stdout on leaf commands — and a **recommended codegen pipeline** used in production argsbarg apps.
3
+ How to describe JSON stdout on leaf commands — and the **argsbarg schemagen** pipeline used in production apps.
4
4
 
5
5
  ## Argsbarg contract
6
6
 
7
7
  On **leaf commands**, set `outputSchema` to a JSON Schema object when the handler emits JSON (typically with `--json`, always for JSON-only commands, or on the MCP headless path).
8
8
 
9
9
  ```typescript
10
- import { STATUS_JSON_OUTPUT_SCHEMA } from "../schemas/outputSchemas.js";
10
+ import { outputSchema } from "./__generated__/index.ts";
11
11
 
12
12
  export const status = {
13
13
  key: "status",
14
14
  description: "Show environment status.",
15
- outputSchema: STATUS_JSON_OUTPUT_SCHEMA,
15
+ outputSchema,
16
16
  handler: async (ctx) => { /* writes JSON to stdout */ },
17
17
  } satisfies CliLeaf;
18
18
  ```
@@ -42,58 +42,52 @@ See [cli-program.md — Structured stdout](cli-program.md#structured-stdout) for
42
42
 
43
43
  Production CLIs with several JSON commands tend to use **codegen** so types, handlers, and schemas stay aligned.
44
44
 
45
- ## Recommended pipeline (copy per repo)
45
+ ## Schemagen pipeline (built into argsbarg)
46
46
 
47
- No shared npm package — each app copies the same **contract**. Reference implementations: **sqsp-qa-manager-poc**, **sqsp-workspaces**, **sqsp-i18n-tools-poc**, **pdf-gen** (see each repo’s `docs/architecture.md` for which commands use which schema root).
47
+ No per-repo scripts to copy — run **`argsbarg schemagen`** (or `import { runSchemagen } from "argsbarg/schemagen"`).
48
+
49
+ Reference implementations: **sqsp-qa-manager-poc**, **sqsp-workspaces**, **sqsp-i18n-tools-poc**, **pdf-gen** (see each repo’s `docs/architecture.md` for which commands use which schema root).
48
50
 
49
51
  ```mermaid
50
52
  flowchart LR
51
- subgraph types [schema-types.ts]
53
+ subgraph types [types.ts]
52
54
  Config["export type configType = AppConfig"]
53
55
  Output["export type outputType = StatusJsonOutput"]
54
56
  Input["export type inputType = ToolInput (optional)"]
55
57
  end
56
- subgraph gen [just schemagen]
57
- Discover["discover-schema-roots.ts"]
58
- Script["schemagen.ts / generate-output-schemas.ts"]
58
+ subgraph gen [argsbarg schemagen]
59
+ Discover["discover types.ts roots"]
59
60
  Gen["ts-json-schema-generator"]
60
61
  end
61
- subgraph artifacts [Committed]
62
- Json["schemas/generated/*.json"]
63
- Bridge["outputSchemas.ts / inputSchemas.ts"]
62
+ subgraph artifacts [Gitignored __generated__]
63
+ Json["configSchema.json / inputSchema.json / outputSchema.json"]
64
+ Index["index.ts re-exports"]
64
65
  end
65
66
  subgraph runtime [Runtime]
66
- Leaves["leaf outputSchema / inputSchema"]
67
+ Leaves["import { outputSchema } from ./__generated__/index.ts"]
67
68
  Docgen["just docgen"]
68
69
  end
69
- types --> Discover --> Script --> Gen --> Json
70
- Script --> Bridge --> Leaves --> Docgen
70
+ types --> Discover --> Gen --> Json
71
+ Gen --> Index --> Leaves --> Docgen
71
72
  ```
72
73
 
73
74
  | Piece | Convention |
74
75
  | --- | --- |
75
- | Generator | [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) (`createGenerator` with `jsDoc: "extended"`) |
76
- | Config | `tsconfig: "tsconfig.json"`, `topRef: false`, `skipTypeCheck: false` |
77
- | Discovery | Walk `src/**/schema-types.ts`; generate from `export type outputType = …` / `inputType` / `configType` when the target type is **defined in that file** |
78
- | Artifacts | Commit `schemas/generated/*.json` **and** auto-generated bridge modules |
76
+ | Generator | [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) (bundled as an argsbarg dependency) |
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
+ | Invocation | `just schemagen` — justfile exports `node_modules/.bin` on `PATH` for local `argsbarg` |
79
80
  | tsconfig | `"resolveJsonModule": true` |
80
- | CI | `just check`: `schemagen` → `git diff --exit-code schemas/` → typecheck |
81
+ | CI | `just check`: `schemagen` → typecheck (no git diff on generated files) |
82
+ | Cleanup | Schemagen removes orphan `__generated__/` dirs and stale JSON when roots or kinds are removed |
81
83
  | Docgen | `docgen` depends on `schemagen` so saved `./docs/api.md` and `./docs/cli-schema.json` are fresh |
82
84
 
83
- Copy these scripts into each consumer repo (they are intentionally duplicated, not published):
84
-
85
- - `scripts/schemagen.ts` or `scripts/generate-output-schemas.ts` — generate JSON + rewrite bridges
86
- - `scripts/schemagen/discover-schema-roots.ts` — find roots and map names → filenames / export constants
87
- - `scripts/schemagen/discover-schema-roots.test.ts` — lock discovery and naming per app
88
-
89
85
  ### Declaring a schema root
90
86
 
91
- Put schema-facing interfaces in **`schema-types.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):
92
88
 
93
89
  ```typescript
94
- // src/commands/status/schema-types.ts
95
- import type { WorkspaceStatus } from "./types.ts";
96
-
90
+ // src/commands/status/types.ts
97
91
  /** JSON stdout for `myapp status --json`. */
98
92
  export interface StatusJsonOutput {
99
93
  workspaces: WorkspaceStatus[];
@@ -103,10 +97,10 @@ export interface StatusJsonOutput {
103
97
  export type outputType = StatusJsonOutput;
104
98
  ```
105
99
 
106
- ```typescript
107
- // src/ui/runHeadless/schema-types.ts — shared by many mutating commands
108
- import type { HeadlessTaskResult } from "./types.ts";
100
+ Handlers and other modules import from the same **`types.ts`** module.
109
101
 
102
+ ```typescript
103
+ // src/ui/runHeadless/types.ts — shared by many mutating commands
110
104
  export interface HeadlessOpResult {
111
105
  command: string;
112
106
  exitCode: number;
@@ -117,70 +111,80 @@ export type outputType = HeadlessOpResult;
117
111
  ```
118
112
 
119
113
  ```typescript
120
- // src/commands/render-invoice/schema-types.ts — custom HTTP/MCP body (pdf-gen)
121
- export interface RenderInvoiceToolInput {
114
+ // src/commands/render-invoice/types.ts — custom HTTP/MCP body (pdf-gen)
115
+ export interface RenderInvoiceInput {
122
116
  format: "pdf" | "html";
123
117
  invoice: InvoiceData;
124
118
  }
125
119
 
126
- export type inputType = RenderInvoiceToolInput;
127
- export type outputType = RenderInvoiceWrittenOutput;
120
+ export interface RenderInvoiceOutput {
121
+ bytes: number;
122
+ }
123
+
124
+ export type inputType = RenderInvoiceInput;
125
+ export type outputType = RenderInvoiceOutput;
128
126
  ```
129
127
 
130
128
  | Export | Role |
131
129
  | --- | --- |
132
- | `export type configType = AppConfig` | `program.appConfig.jsonSchema` (one per repo, typically `src/config/schema-types.ts`) |
130
+ | `export type configType = AppConfig` | `program.appConfig.jsonSchema` (one per repo, typically `src/config/types.ts`) |
133
131
  | `export type outputType = …` | `leaf.outputSchema` |
134
132
  | `export type inputType = …` | `leaf.inputSchema` (only when the tool body is not flat CLI flags) |
135
133
 
136
- **Domain helpers** stay in `types.ts` (or `core/types.ts`). Discovery scans only `schema-types.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`.
137
135
 
138
- Commands without structured JSON omit `schema-types.ts` and import a shared bridge constant (e.g. `HEADLESS_OP_RESULT_OUTPUT_SCHEMA`).
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).
139
137
 
140
138
  When you do **not** set `inputSchema`, argsbarg builds tool input from CLI `options` + `positionals`.
141
139
 
142
- ### Stable naming (outfile + bridge export)
140
+ ### Generated artifacts
143
141
 
144
- Discovery maps each root type name to a generated filename and bridge constant. Suffix conventions (implemented in `outfileForOutputType` / `outputSchemaExportName`):
142
+ Schemagen writes under `__generated__/` beside each `types.ts` with role exports:
145
143
 
146
- | Type suffix | Example type | Generated file | Bridge export |
147
- | --- | --- | --- | --- |
148
- | `JsonOutput` | `StatusJsonOutput` | `status.json` | `STATUS_JSON_OUTPUT_SCHEMA` |
149
- | `OpResult` | `HeadlessOpResult` | `headless-op-result.json` | `HEADLESS_OP_RESULT_OUTPUT_SCHEMA` |
150
- | `Output` | `OpenUrlOutput` | `open-url.json` | `OPEN_URL_OUTPUT_SCHEMA` |
151
- | `Result` | `PrResult` | `pr.json` | `PR_RESULT_OUTPUT_SCHEMA` |
152
- | `ToolInput` | `RenderInvoiceToolInput` | `render-invoice-tool-input.json` | `RENDER_INVOICE_TOOL_INPUT_SCHEMA` |
144
+ | Kind | Generated file | Exported const (from `__generated__/index.ts`) |
145
+ | --- | --- | --- |
146
+ | config | `configSchema.json` | `configSchema` |
147
+ | output | `outputSchema.json` | `outputSchema` |
148
+ | input | `inputSchema.json` | `inputSchema` |
153
149
 
154
- Prefer these suffixes for new roots so filenames and import constants stay predictable across repos.
150
+ Wire on the leaf:
155
151
 
156
- ### Generated bridge
152
+ ```typescript
153
+ import { outputSchema } from "./__generated__/index.ts";
157
154
 
158
- `scripts/schemagen.ts` rewrites `schemas/outputSchemas.ts` on every run:
155
+ export const statusCommand = {
156
+ outputSchema,
157
+ // …
158
+ } satisfies CliLeaf;
159
+ ```
160
+
161
+ Shared mutators:
159
162
 
160
163
  ```typescript
161
- // Auto-generated by scripts/schemagen.ts — do not edit by hand.
164
+ import { outputSchema } from "../../../ui/runHeadless/__generated__/index.ts";
165
+ ```
162
166
 
163
- import status from "./generated/status.json";
167
+ App config:
164
168
 
165
- /** JSON Schema for leaf outputSchema from `StatusJsonOutput`. */
166
- export const STATUS_JSON_OUTPUT_SCHEMA = status as Record<string, unknown>;
167
- ```
169
+ ```typescript
170
+ import { configSchema } from "./config/__generated__/index.ts";
168
171
 
169
- Wire the constant on each leaf that emits that shape (several commands may share one schema, e.g. mutating ops sharing `HeadlessOpResult`).
172
+ appConfig: { jsonSchema: configSchema, entries: { … } },
173
+ ```
170
174
 
171
175
  ## Schema-facing types
172
176
 
173
177
  **Goal:** generated schemas match what handlers actually print, with descriptions agents can read in `docs api`.
174
178
 
175
- 1. **Schema roots** — `export interface` in `schema-types.ts`, with `export type outputType = …` (or `inputType` / `configType`).
179
+ 1. **Schema roots** — `export interface` in `types.ts`, with `export type outputType = …` (or `inputType` / `configType`).
176
180
  2. **Per property** — `/** … */` on every field that should appear in JSON Schema `properties` (including nested named types).
177
181
  3. **Unions / enums** — document the alias; generator emits `enum` / `anyOf` with type-level description.
178
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"`.
179
- 5. **Do not hand-edit** `schemas/generated/` or bridge modules — change types/JSDoc, run `just schemagen`, commit both.
183
+ 5. **Do not hand-edit** `__generated__/` — change types/JSDoc in `types.ts`, run `just schemagen`.
180
184
 
181
185
  ### Narrowing when runtime ≠ stdout
182
186
 
183
- When a shared runtime type is **wider** than one command’s JSON, add a **schema-facing** root in `schema-types.ts`:
187
+ When a shared runtime type is **wider** than one command’s JSON, add a **schema-facing** root in `types.ts`:
184
188
 
185
189
  ```typescript
186
190
  /** JSON stdout for `myapp pr` and `myapp file`. */
@@ -201,27 +205,26 @@ Handlers keep using runtime types; only discovered roots (and their type graph)
201
205
 
202
206
  ## Tests
203
207
 
204
- Per repo:
208
+ In argsbarg: `src/cli-tool/schemagen/schemagen.test.ts` locks discovery and generation against `examples/full-example/`.
209
+
210
+ Per consumer repo (optional):
205
211
 
206
- - **`scripts/schemagen/discover-schema-roots.test.ts`** — asserts which roots are discovered and stable outfile / export-name mapping.
207
- - **`schemas/outputSchemas.test.ts`** (optional) — schema shape smoke tests: object root, key `description` fields, enums, `@format date-time`.
212
+ - **`src/generated-schemas.test.ts`** — smoke-test that key `outputSchema` objects have expected shape.
208
213
 
209
214
  ## Contributor workflow
210
215
 
211
- 1. Add or edit schema roots in `src/**/schema-types.ts` with `outputType` / `inputType` / `configType` and per-field JSDoc.
212
- 2. `just schemagen` — refresh `schemas/generated/` and bridge modules.
213
- 3. Import the bridge constant on the relevant leaf `outputSchema` / `inputSchema` fields.
214
- 4. Commit generated JSON and bridges with the type changes.
215
- 5. `just docgen` / `myapp docs api --save` — refresh consumer docs.
216
- 6. Document which commands use which roots in **your** `docs/architecture.md` (argsbarg does not maintain per-app tables).
216
+ 1. Add or edit schema roots in `src/**/types.ts` with `outputType` / `inputType` / `configType` and per-field JSDoc.
217
+ 2. `just schemagen` — refresh `src/**/__generated__/`.
218
+ 3. Import `{ outputSchema }` / `{ inputSchema }` / `{ configSchema }` from the relevant `__generated__/index.ts`.
219
+ 4. `just docgen` / `myapp docs api --save` — refresh consumer docs.
220
+ 5. Document which commands use which roots in **your** `docs/architecture.md` (argsbarg does not maintain per-app tables).
217
221
 
218
- Add a bullet under your app’s `**… conventions:**` block in `.cursor/rules/cli-program.mdc` pointing at `node_modules/argsbarg/docs/output-schema.md` and your `schemas/` layout.
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`.
219
223
 
220
- **Reference implementation:** [`examples/full-example/`](../examples/full-example/) in this repo (shipped in npm as `node_modules/argsbarg/examples/full-example/`) — discovery script, bridges, 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`.
221
225
 
222
226
  ## Out of scope
223
227
 
224
- - Shared codegen package or monorepo tooling
225
228
  - Runtime Zod / `.parse()` on stdout in argsbarg
226
229
  - `outputSchema` for plain-text, streaming, or Ink-only commands
227
230
 
@@ -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-types.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-types.ts` | Artifact |
66
- | --- | --- |
67
- | `export type configType = …` | `schemas/configSchemas.ts` + `schemas/generated/*-config.json` |
68
- | `export type outputType = …` | `schemas/outputSchemas.ts` + `schemas/generated/*.json` |
69
- | `export type inputType = …` | `schemas/inputSchemas.ts` + `schemas/generated/*-tool-input.json` |
65
+ | Export in `types.ts` | Generated artifact | Import on leaf / program |
66
+ | --- | --- | --- |
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-types.ts` only. Domain helpers stay in sibling `types.ts`.
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
 
@@ -10,7 +10,6 @@
10
10
  "devDependencies": {
11
11
  "@biomejs/biome": "^2.5.0",
12
12
  "@types/bun": "^1.3.12",
13
- "ts-json-schema-generator": "^2.3.0",
14
13
  "typescript": "^5.9.3",
15
14
  },
16
15
  },
@@ -36,40 +35,12 @@
36
35
 
37
36
  "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
38
37
 
39
- "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
40
-
41
38
  "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="],
42
39
 
43
40
  "argsbarg": ["argsbarg@file:../..", { "devDependencies": { "@biomejs/biome": "^2.5.0", "@types/bun": "^1.3.12", "typescript": "^5.9.3" }, "bin": { "argsbarg": "src/index.ts" } }],
44
41
 
45
- "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
46
-
47
- "brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
48
-
49
42
  "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
50
43
 
51
- "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
52
-
53
- "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
54
-
55
- "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
56
-
57
- "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="],
58
-
59
- "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
60
-
61
- "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
62
-
63
- "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
64
-
65
- "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
66
-
67
- "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
68
-
69
- "ts-json-schema-generator": ["ts-json-schema-generator@2.9.0", "", { "dependencies": { "@types/json-schema": "^7.0.15", "commander": "^14.0.3", "glob": "^13.0.6", "json5": "^2.2.3", "normalize-path": "^3.0.0", "safe-stable-stringify": "^2.5.0", "tslib": "^2.8.1", "typescript": "^5.9.3" }, "bin": { "ts-json-schema-generator": "bin/ts-json-schema-generator.js" } }, "sha512-NR5ZE108uiPtBHBJNGnhwoUaUx5vWTDJzDFG9YlRoqxPU76n+5FClRh92dcGgysbe1smRmYalM9Saj97GW1J4Q=="],
70
-
71
- "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
72
-
73
44
  "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
74
45
 
75
46
  "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
@@ -1,5 +1,7 @@
1
1
  set shell := ["bash", "-eu", "-o", "pipefail", "-c"]
2
2
 
3
+ export PATH := justfile_directory() + "/node_modules/.bin:" + env_var("PATH")
4
+
3
5
  cli_key := `bun scripts/print-identity.ts key`
4
6
  tap_org := `bun scripts/print-identity.ts tapOrg`
5
7
  tap_repo := `bun scripts/print-identity.ts tapRepo`
@@ -19,9 +21,8 @@ build:
19
21
  bun build ./src/index.ts --compile --outfile=dist/{{cli_key}}
20
22
  @rm -f .*.bun-build
21
23
 
22
- # Run schemagen, git diff check, typecheck, and format
24
+ # Run schemagen, typecheck, and format
23
25
  check: schemagen format typecheck
24
- git diff --exit-code schemas/
25
26
 
26
27
  # Run the CLI from source with optional args; restarts on file changes
27
28
  dev *ARGS:
@@ -84,11 +85,13 @@ run *ARGS:
84
85
 
85
86
  # Generate JSON Schema artifacts from TypeScript types
86
87
  schemagen:
87
- bun run schemagen
88
+ argsbarg schemagen
88
89
 
89
90
  # Install bun/npm dependencies
91
+ # Install bun/npm dependencies and generate schemas
90
92
  setup:
91
93
  bun install
94
+ just schemagen
92
95
 
93
96
  # Run unit tests (after check)
94
97
  test: check
@@ -9,7 +9,6 @@
9
9
  },
10
10
  "scripts": {
11
11
  "biome": "biome",
12
- "schemagen": "bun run scripts/schemagen.ts",
13
12
  "start": "bun run src/index.ts"
14
13
  },
15
14
  "dependencies": {
@@ -18,7 +17,6 @@
18
17
  "devDependencies": {
19
18
  "@biomejs/biome": "^2.5.0",
20
19
  "@types/bun": "^1.3.12",
21
- "ts-json-schema-generator": "^2.3.0",
22
20
  "typescript": "^5.9.3"
23
21
  }
24
22
  }
@@ -0,0 +1,5 @@
1
+ // Auto-generated by argsbarg schemagen — do not edit by hand.
2
+
3
+ import outputSchemaJson from "./outputSchema.json";
4
+
5
+ export const outputSchema = outputSchemaJson as Record<string, unknown>;
@@ -3,7 +3,7 @@ Status leaf — demonstrates outputSchema and ctx.appConfig.
3
3
  */
4
4
 
5
5
  import { type CliLeaf, CliOptionKind } from "argsbarg";
6
- import { STATUS_JSON_OUTPUT_SCHEMA } from "../../../schemas/outputSchemas.ts";
6
+ import { outputSchema } from "./__generated__/index.ts";
7
7
  import type { StatusJsonOutput } from "./types.ts";
8
8
 
9
9
  export const statusCommand = {
@@ -16,7 +16,7 @@ export const statusCommand = {
16
16
  kind: CliOptionKind.Presence,
17
17
  },
18
18
  ],
19
- outputSchema: STATUS_JSON_OUTPUT_SCHEMA,
19
+ outputSchema,
20
20
  handler: (ctx) => {
21
21
  const out: StatusJsonOutput = {
22
22
  defaultRegion: ctx.appConfig.get("defaultRegion") as string | undefined,