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 +14 -2
- package/README.md +3 -2
- package/docs/api-server.md +1 -1
- package/docs/cli-program.md +28 -0
- package/docs/config-schema.md +3 -3
- package/docs/output-schema.md +26 -24
- package/examples/full-example/README.md +6 -6
- package/examples/full-example/src/commands/status/types.ts +19 -1
- package/examples/full-example/src/program.ts +1 -1
- package/index.d.ts +24 -3
- package/package.json +1 -1
- package/src/cli-tool/program.ts +2 -1
- package/src/cli-tool/schemagen/cleanup.ts +3 -3
- package/src/cli-tool/schemagen/discover-schema-roots.ts +85 -15
- package/src/cli-tool/schemagen/index.ts +1 -1
- package/src/cli-tool/schemagen/names.ts +3 -3
- package/src/cli-tool/schemagen/run.ts +2 -1
- package/src/cli-tool/schemagen/schemagen.test.ts +35 -17
- package/src/context.ts +20 -2
- package/src/help.ts +2 -0
- package/src/index.ts +1 -0
- package/src/leaf-inputs.test.ts +170 -0
- package/src/leaf-inputs.ts +178 -0
- package/src/mcp/tools.ts +5 -0
- package/src/parse.ts +11 -0
- package/src/types.ts +8 -1
- package/src/validate.ts +43 -0
- package/examples/full-example/src/commands/status/schema.ts +0 -14
- /package/examples/full-example/src/config/{schema.ts → types.ts} +0 -0
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
|
|
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
|
|
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/
|
|
271
|
-
| `outputSchema` | `src/commands/status/
|
|
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 |
|
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 and read `ctx.
|
|
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`.
|
package/docs/cli-program.md
CHANGED
|
@@ -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.
|
package/docs/config-schema.md
CHANGED
|
@@ -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 [
|
|
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/
|
|
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/
|
|
174
|
+
// src/config/types.ts
|
|
175
175
|
export interface AppConfig {
|
|
176
176
|
apiToken: string;
|
|
177
177
|
}
|
package/docs/output-schema.md
CHANGED
|
@@ -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 [
|
|
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
|
|
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/**/
|
|
78
|
-
| Artifacts | `src/**/__generated__/` — gitignored; run schemagen after clone or when `
|
|
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 **`
|
|
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/
|
|
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
|
-
|
|
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/
|
|
117
|
-
export interface
|
|
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
|
|
123
|
-
|
|
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/
|
|
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**
|
|
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
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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/**/
|
|
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 — `
|
|
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/**/
|
|
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 `
|
|
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`
|
|
68
|
-
| `export type outputType = …` | `__generated__/outputSchema.json` | `{ outputSchema }` from `__generated__/index.ts`
|
|
69
|
-
| `export type inputType = …` | `__generated__/inputSchema.json` | `{ inputSchema }` from `__generated__/index.ts`
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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
package/src/cli-tool/program.ts
CHANGED
|
@@ -95,7 +95,8 @@ export const program = {
|
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
97
|
key: "schemagen",
|
|
98
|
-
description:
|
|
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,
|
|
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
|
|
42
|
-
const roots = existsSync(join(projectRoot,
|
|
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
|
|
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 {
|
|
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 (
|
|
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
|
|
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
|
-
|
|
37
|
+
listTypesManifestFiles(full, baseDir, out);
|
|
34
38
|
continue;
|
|
35
39
|
}
|
|
36
|
-
if (ent
|
|
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
|
|
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
|
|
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
|
-
|
|
133
|
+
const resolved = resolveAliasedTypeSource(projectRoot, path, text, typeName);
|
|
134
|
+
if (!resolved) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
sourcePath = resolved;
|
|
69
138
|
}
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 `
|
|
3
|
+
/** Directory name for generated schema artifacts (next to `types.ts`). */
|
|
4
4
|
export const GENERATED_DIR = "__generated__";
|
|
5
5
|
|
|
6
|
-
/** TypeScript
|
|
7
|
-
export const
|
|
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:
|
|
41
|
+
path: typeFile,
|
|
41
42
|
type: root.typeName,
|
|
42
43
|
tsconfig: tsconfigPath,
|
|
43
44
|
topRef: false,
|