@cli-schema/zod 0.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/README.md +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +11 -0
- package/dist/json-schema.d.ts.map +1 -0
- package/dist/json-schema.js +88 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/schema-args.d.ts +24 -0
- package/dist/schema-args.d.ts.map +1 -0
- package/dist/schema-args.js +118 -0
- package/dist/schema-args.js.map +1 -0
- package/package.json +38 -0
- package/src/index.ts +7 -0
- package/src/json-schema.ts +114 -0
- package/src/schema-args.ts +269 -0
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @cli-schema/zod
|
|
2
|
+
|
|
3
|
+
Derives [CLI Schema](https://github.com/cli-schema/cli-schema) parameter definitions from
|
|
4
|
+
[Zod](https://zod.dev) schemas — types, requiredness, defaults, descriptions, and JSON-Schema
|
|
5
|
+
validation constraints, all read directly off the schema you already wrote for validation.
|
|
6
|
+
|
|
7
|
+
Part of the [`cli-schema-js`](https://github.com/cli-schema/cli-schema-js) monorepo. Framework-
|
|
8
|
+
agnostic: it doesn't know about Commander, yargs, or any other CLI library — it only knows Zod
|
|
9
|
+
in, structured parameter data out. [`@cli-schema/commander`](https://www.npmjs.com/package/@cli-schema/commander)
|
|
10
|
+
builds on top of it.
|
|
11
|
+
|
|
12
|
+
## Quickstart
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @cli-schema/zod zod
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { z } from 'zod'
|
|
20
|
+
import { extractSchemaArgs } from '@cli-schema/zod'
|
|
21
|
+
|
|
22
|
+
const SearchInput = z.object({
|
|
23
|
+
index: z.string().describe('Index to search'),
|
|
24
|
+
size: z.number().default(10).describe('Number of results'),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
extractSchemaArgs(SearchInput)
|
|
28
|
+
// [
|
|
29
|
+
// { schemaKey: 'index', cliFlag: 'index', type: 'string', required: true, description: 'Index to search' },
|
|
30
|
+
// { schemaKey: 'size', cliFlag: 'size', type: 'number', required: false, description: 'Number of results', defaultValue: 10 },
|
|
31
|
+
// ]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
That's the core of it: one Zod object schema in, one CLI parameter definition per top-level key
|
|
35
|
+
out.
|
|
36
|
+
|
|
37
|
+
## Purpose
|
|
38
|
+
|
|
39
|
+
If your CLI already validates its input with Zod, that schema *is* a complete, precise
|
|
40
|
+
description of every flag the command accepts — type, requiredness, default, description,
|
|
41
|
+
enum/range/length constraints. Re-describing all of that by hand for help text, documentation, or
|
|
42
|
+
a CLI Schema document is duplicated, drifting work. `@cli-schema/zod` reads it back out.
|
|
43
|
+
|
|
44
|
+
It deliberately knows nothing about any particular CLI's own conventions (routing metadata,
|
|
45
|
+
custom parsing hints, reserved flag names) — those vary per project, so they're exposed as
|
|
46
|
+
extension points (`enrich`, a `reserved` list) rather than baked in.
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- **`extractSchemaArgs`** — the main entry point. Walks a `z.object()`'s top-level shape and
|
|
51
|
+
returns one `SchemaArgDefinition` per key:
|
|
52
|
+
- **Type inference** across `string`, `number`, `integer` (zod's `z.int()`), `boolean`,
|
|
53
|
+
`object`, `array`, `enum` — including through `.optional()`, `.default()`, `z.lazy()`,
|
|
54
|
+
`z.union()` (first member wins), `z.record()`/`z.any()`/`z.unknown()` (→ `object`).
|
|
55
|
+
- **Kebab-case flag names** derived from the schema key (`toKebabCase`) — handles
|
|
56
|
+
`snake_case`, `camelCase`, and strips leading underscores (`_source` → `source`).
|
|
57
|
+
- **Requiredness and defaults** — a field is required only if it's neither optional nor
|
|
58
|
+
defaulted; `defaultValue` is read straight off `.default(...)`.
|
|
59
|
+
- **Descriptions** from `.describe()` / `.meta({ description })`.
|
|
60
|
+
- **`acceptsArrayForm`** detection for the `union(T, array(T))` pattern — a common way to let a
|
|
61
|
+
field accept either a single value or a collection while keeping the type inference on the
|
|
62
|
+
scalar branch for CLI ergonomics.
|
|
63
|
+
- **An `enrich` hook** so your own framework can attach extra fields (routing metadata, parse
|
|
64
|
+
hints, anything) without this package needing to know what they mean.
|
|
65
|
+
- **`validateSchemaArgs`** — fail-fast collision detection: throws if two schema keys would
|
|
66
|
+
produce the same CLI flag, or if a flag collides with your CLI's reserved names.
|
|
67
|
+
- **`buildFlagKeyMap`** — a bidirectional `cliFlag` ↔ `schemaKey` map, for merging parsed CLI
|
|
68
|
+
flags back into the shape your Zod schema expects.
|
|
69
|
+
- **JSON-Schema-derived enrichment** (`zodToJsonSchema`, `extractEnumValues`,
|
|
70
|
+
`extractElementType`, `extractValidations`) — for the detail `extractSchemaArgs` alone can't
|
|
71
|
+
carry (enum values, array element types, range/length/regex/email/url constraints), reusing
|
|
72
|
+
Zod's own `toJSONSchema()` rather than re-implementing schema introspection.
|
|
73
|
+
- **`readMetaField` / `walkWrapperChain`** — the low-level primitives the above are built on,
|
|
74
|
+
exported for CLIs that need their own custom extraction (e.g. a `found_in` routing tag, or a
|
|
75
|
+
named-shape check like "is this schema tagged `Sort` anywhere in its wrapper chain").
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
### Basic extraction
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { z } from 'zod'
|
|
83
|
+
import { extractSchemaArgs } from '@cli-schema/zod'
|
|
84
|
+
|
|
85
|
+
const args = extractSchemaArgs(z.object({
|
|
86
|
+
numShards: z.number().min(1).max(100),
|
|
87
|
+
level: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
88
|
+
tags: z.array(z.string()).optional(),
|
|
89
|
+
}))
|
|
90
|
+
|
|
91
|
+
args.map((a) => a.cliFlag) // ['num-shards', 'level', 'tags']
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Attaching framework-specific extras with `enrich`
|
|
95
|
+
|
|
96
|
+
Say your CLI routes some fields to an HTTP path/query/body via `.meta({ found_in: '...' })`.
|
|
97
|
+
Rather than this package hardcoding that convention, thread it through `enrich`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { z } from 'zod'
|
|
101
|
+
import { extractSchemaArgs, type SchemaArgDefinition } from '@cli-schema/zod'
|
|
102
|
+
|
|
103
|
+
interface MyExtras {
|
|
104
|
+
foundIn?: 'path' | 'query' | 'body'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const schema = z.object({
|
|
108
|
+
index: z.string().meta({ found_in: 'path' }),
|
|
109
|
+
q: z.string().optional().meta({ found_in: 'query' }),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const args: Array<SchemaArgDefinition & MyExtras> = extractSchemaArgs(schema, {
|
|
113
|
+
enrich: (field, base) => {
|
|
114
|
+
const foundIn = (field.meta() as Record<string, unknown> | undefined)?.['found_in']
|
|
115
|
+
return foundIn != null ? { foundIn } : {}
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Validating for flag collisions
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { validateSchemaArgs } from '@cli-schema/zod'
|
|
124
|
+
|
|
125
|
+
// throws: "help" collides with a name your CLI already uses for --help
|
|
126
|
+
validateSchemaArgs(extractSchemaArgs(z.object({ help: z.string() })), ['help', 'json', 'config-file'])
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`reserved` defaults to `['help']` if omitted, since nearly every CLI framework reserves
|
|
130
|
+
`--help`.
|
|
131
|
+
|
|
132
|
+
### Round-tripping flags back to schema keys
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { buildFlagKeyMap } from '@cli-schema/zod'
|
|
136
|
+
|
|
137
|
+
const map = buildFlagKeyMap(args)
|
|
138
|
+
map.toSchemaKey.get('num-shards') // 'numShards'
|
|
139
|
+
map.toCliFlag.get('numShards') // 'num-shards'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Enum values, array element types, and validation constraints
|
|
143
|
+
|
|
144
|
+
`extractSchemaArgs` alone doesn't carry enum values or range/length constraints — those need the
|
|
145
|
+
full JSON Schema, which is a separate (and more expensive) step best done once per command:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { z } from 'zod'
|
|
149
|
+
import { extractSchemaArgs, zodToJsonSchema, extractEnumValues, extractElementType, extractValidations } from '@cli-schema/zod'
|
|
150
|
+
|
|
151
|
+
const schema = z.object({
|
|
152
|
+
level: z.enum(['low', 'medium', 'high']),
|
|
153
|
+
size: z.number().min(1).max(100),
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const root = zodToJsonSchema(schema)
|
|
157
|
+
const properties = root.properties as Record<string, Record<string, unknown>>
|
|
158
|
+
|
|
159
|
+
for (const arg of extractSchemaArgs(schema)) {
|
|
160
|
+
const node = properties[arg.schemaKey]
|
|
161
|
+
console.log(arg.cliFlag, {
|
|
162
|
+
enumValues: extractEnumValues(node, root),
|
|
163
|
+
elementType: extractElementType(node, root), // only meaningful when arg.type === 'array'
|
|
164
|
+
validations: extractValidations(node, root), // e.g. [{ kind: 'range', min: '1', max: '100' }]
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`extractValidations` covers the constraint kinds directly derivable from JSON Schema keywords —
|
|
170
|
+
`range` (`minimum`/`maximum`), `length` (`minLength`/`maxLength`), `regex` (`pattern`), `email`
|
|
171
|
+
and `url` (`format`). Kinds that need domain knowledge beyond what JSON Schema expresses
|
|
172
|
+
(`timeSpanRange`, `count`, `allowed`, `denied`, `uriScheme`, `fileExtensions`, `existing`,
|
|
173
|
+
`nonExisting`, `rejectSymbolicLinks` — see the [spec](https://github.com/cli-schema/cli-schema/blob/main/spec/v1/README.md#62-constraints))
|
|
174
|
+
aren't inferred automatically; attach them via the `enrich` hook if your schema encodes them.
|
|
175
|
+
|
|
176
|
+
## Related packages
|
|
177
|
+
|
|
178
|
+
- [`@cli-schema/spec`](https://www.npmjs.com/package/@cli-schema/spec) — the `Constraint`/`Parameter` types this package's JSON-Schema helpers produce
|
|
179
|
+
- [`@cli-schema/commander`](https://www.npmjs.com/package/@cli-schema/commander) — wires this package's output into a full `CliSchema` document from a live Commander tree
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Constraint, ParameterElementType } from '@cli-schema/spec';
|
|
3
|
+
export type JsonSchemaNode = Record<string, unknown>;
|
|
4
|
+
export declare function zodToJsonSchema(schema: z.ZodType): JsonSchemaNode;
|
|
5
|
+
export declare function resolveRef(node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode;
|
|
6
|
+
export declare function unwrapNullable(node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode;
|
|
7
|
+
export declare function resolveNode(node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode;
|
|
8
|
+
export declare function extractEnumValues(node: JsonSchemaNode, root: JsonSchemaNode): string[] | undefined;
|
|
9
|
+
export declare function extractElementType(node: JsonSchemaNode, root: JsonSchemaNode): ParameterElementType | undefined;
|
|
10
|
+
export declare function extractValidations(node: JsonSchemaNode, root: JsonSchemaNode): Constraint[] | undefined;
|
|
11
|
+
//# sourceMappingURL=json-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAExE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAGpD,wBAAgB,eAAe,CAAE,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,cAAc,CAElE;AAED,wBAAgB,UAAU,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAUtF;AAED,wBAAgB,cAAc,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAS1F;AAED,wBAAgB,WAAW,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,cAAc,CAEvF;AAED,wBAAgB,iBAAiB,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,MAAM,EAAE,GAAG,SAAS,CAKnG;AAID,wBAAgB,kBAAkB,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,oBAAoB,GAAG,SAAS,CAShH;AAaD,wBAAgB,kBAAkB,CAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,cAAc,GAAG,UAAU,EAAE,GAAG,SAAS,CAwCxG"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function zodToJsonSchema(schema) {
|
|
3
|
+
return z.toJSONSchema(schema, { reused: 'ref' });
|
|
4
|
+
}
|
|
5
|
+
export function resolveRef(node, root) {
|
|
6
|
+
const ref = node['$ref'];
|
|
7
|
+
if (typeof ref !== 'string')
|
|
8
|
+
return node;
|
|
9
|
+
const parts = ref.replace(/^#\//, '').split('/');
|
|
10
|
+
let cur = root;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (cur == null || typeof cur !== 'object')
|
|
13
|
+
return node;
|
|
14
|
+
cur = cur[part];
|
|
15
|
+
}
|
|
16
|
+
return (cur != null && typeof cur === 'object') ? cur : node;
|
|
17
|
+
}
|
|
18
|
+
export function unwrapNullable(node, root) {
|
|
19
|
+
const anyOf = node['anyOf'];
|
|
20
|
+
if (!Array.isArray(anyOf))
|
|
21
|
+
return node;
|
|
22
|
+
const nonNull = anyOf.find((b) => {
|
|
23
|
+
if (b == null || typeof b !== 'object')
|
|
24
|
+
return false;
|
|
25
|
+
const branch = b;
|
|
26
|
+
return branch['type'] !== 'null' && branch['const'] !== null;
|
|
27
|
+
});
|
|
28
|
+
return nonNull != null ? resolveRef(nonNull, root) : node;
|
|
29
|
+
}
|
|
30
|
+
export function resolveNode(node, root) {
|
|
31
|
+
return unwrapNullable(resolveRef(node, root), root);
|
|
32
|
+
}
|
|
33
|
+
export function extractEnumValues(node, root) {
|
|
34
|
+
const resolved = resolveNode(node, root);
|
|
35
|
+
const enumVals = resolved['enum'];
|
|
36
|
+
if (!Array.isArray(enumVals) || enumVals.length === 0)
|
|
37
|
+
return undefined;
|
|
38
|
+
return enumVals.filter((v) => v != null).map(String);
|
|
39
|
+
}
|
|
40
|
+
const SCALAR_ELEMENT_TYPES = new Set(['string', 'integer', 'number', 'boolean']);
|
|
41
|
+
export function extractElementType(node, root) {
|
|
42
|
+
const resolved = resolveNode(node, root);
|
|
43
|
+
const items = resolved['items'];
|
|
44
|
+
if (items == null || typeof items !== 'object')
|
|
45
|
+
return undefined;
|
|
46
|
+
const resolvedItems = resolveNode(items, root);
|
|
47
|
+
const t = resolvedItems['type'];
|
|
48
|
+
if (typeof t !== 'string')
|
|
49
|
+
return undefined;
|
|
50
|
+
return SCALAR_ELEMENT_TYPES.has(t) ? t : undefined;
|
|
51
|
+
}
|
|
52
|
+
export function extractValidations(node, root) {
|
|
53
|
+
const resolved = resolveNode(node, root);
|
|
54
|
+
const validations = [];
|
|
55
|
+
const min = resolved['minimum'];
|
|
56
|
+
const max = resolved['maximum'];
|
|
57
|
+
if (min !== undefined || max !== undefined) {
|
|
58
|
+
validations.push({
|
|
59
|
+
kind: 'range',
|
|
60
|
+
...(min !== undefined && { min: String(min) }),
|
|
61
|
+
...(max !== undefined && { max: String(max) }),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const minLength = resolved['minLength'];
|
|
65
|
+
const maxLength = resolved['maxLength'];
|
|
66
|
+
if (minLength !== undefined || maxLength !== undefined) {
|
|
67
|
+
validations.push({
|
|
68
|
+
kind: 'length',
|
|
69
|
+
...(minLength !== undefined && { min: String(minLength) }),
|
|
70
|
+
...(maxLength !== undefined && { max: String(maxLength) }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const format = resolved['format'];
|
|
74
|
+
if (format === 'email') {
|
|
75
|
+
validations.push({ kind: 'email' });
|
|
76
|
+
}
|
|
77
|
+
else if (format === 'uri' || format === 'url') {
|
|
78
|
+
validations.push({ kind: 'url' });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const pattern = resolved['pattern'];
|
|
82
|
+
if (typeof pattern === 'string') {
|
|
83
|
+
validations.push({ kind: 'regex', pattern });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return validations.length > 0 ? validations : undefined;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.js","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
export type SchemaArgType = 'string' | 'integer' | 'number' | 'boolean' | 'object' | 'array' | 'enum';
|
|
3
|
+
export interface SchemaArgDefinition {
|
|
4
|
+
schemaKey: string;
|
|
5
|
+
cliFlag: string;
|
|
6
|
+
type: SchemaArgType;
|
|
7
|
+
required: boolean;
|
|
8
|
+
defaultValue?: unknown;
|
|
9
|
+
description: string;
|
|
10
|
+
acceptsArrayForm?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface FlagKeyMap {
|
|
13
|
+
toSchemaKey: Map<string, string>;
|
|
14
|
+
toCliFlag: Map<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export declare function toKebabCase(key: string): string;
|
|
17
|
+
export declare function walkWrapperChain(field: z.ZodType, predicate: (node: z.ZodType) => boolean): boolean;
|
|
18
|
+
export declare function readMetaField<T = unknown>(field: z.ZodType, key: string): T | undefined;
|
|
19
|
+
export declare function extractSchemaArgs<E extends object = object>(schema: unknown, opts?: {
|
|
20
|
+
enrich?: (field: z.ZodType, base: SchemaArgDefinition) => E;
|
|
21
|
+
}): Array<SchemaArgDefinition & E>;
|
|
22
|
+
export declare function buildFlagKeyMap(args: Array<Pick<SchemaArgDefinition, 'schemaKey' | 'cliFlag'>>): FlagKeyMap;
|
|
23
|
+
export declare function validateSchemaArgs(args: Array<Pick<SchemaArgDefinition, 'schemaKey' | 'cliFlag'>>, reserved?: Iterable<string>): void;
|
|
24
|
+
//# sourceMappingURL=schema-args.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-args.d.ts","sourceRoot":"","sources":["../src/schema-args.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAG5B,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;AAQrG,MAAM,WAAW,mBAAmB;IAElC,SAAS,EAAE,MAAM,CAAA;IAGjB,OAAO,EAAE,MAAM,CAAA;IAGf,IAAI,EAAE,aAAa,CAAA;IAGnB,QAAQ,EAAE,OAAO,CAAA;IAGjB,YAAY,CAAC,EAAE,OAAO,CAAA;IAGtB,WAAW,EAAE,MAAM,CAAA;IAOnB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAKD,MAAM,WAAW,UAAU;IAEzB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAGhC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC/B;AAaD,wBAAgB,WAAW,CAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMhD;AAiED,wBAAgB,gBAAgB,CAAE,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,KAAK,OAAO,GAAG,OAAO,CAmBpG;AAOD,wBAAgB,aAAa,CAAE,CAAC,GAAG,OAAO,EAAG,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAWzF;AAwBD,wBAAgB,iBAAiB,CAAE,CAAC,SAAS,MAAM,GAAG,MAAM,EAC1D,MAAM,EAAE,OAAO,EACf,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,IAAI,EAAE,mBAAmB,KAAK,CAAC,CAAA;CAAE,GACrE,KAAK,CAAC,mBAAmB,GAAG,CAAC,CAAC,CA4BhC;AAMD,wBAAgB,eAAe,CAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,GAAG,SAAS,CAAC,CAAC,GAAG,UAAU,CAQ5G;AAWD,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,mBAAmB,EAAE,WAAW,GAAG,SAAS,CAAC,CAAC,EAC/D,QAAQ,GAAE,QAAQ,CAAC,MAAM,CAAY,GACpC,IAAI,CAYN"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export function toKebabCase(key) {
|
|
2
|
+
return key
|
|
3
|
+
.replace(/^_+/, '')
|
|
4
|
+
.replace(/_/g, '-')
|
|
5
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
function unwrapField(field) {
|
|
9
|
+
const def = field.def;
|
|
10
|
+
if (def.type === 'date') {
|
|
11
|
+
throw new Error('Date cannot be represented in JSON Schema: use z.string() with an ISO-8601 description instead of z.date()');
|
|
12
|
+
}
|
|
13
|
+
if (def.type === 'optional') {
|
|
14
|
+
const inner = unwrapField(def.innerType);
|
|
15
|
+
return { ...inner, isOptional: true };
|
|
16
|
+
}
|
|
17
|
+
if (def.type === 'default') {
|
|
18
|
+
const inner = unwrapField(def.innerType);
|
|
19
|
+
return { ...inner, defaultValue: def.defaultValue, isOptional: false };
|
|
20
|
+
}
|
|
21
|
+
if (def.type === 'lazy' && typeof def.getter === 'function') {
|
|
22
|
+
return unwrapField(def.getter());
|
|
23
|
+
}
|
|
24
|
+
if (def.type === 'record' || def.type === 'any' || def.type === 'unknown') {
|
|
25
|
+
return { typeName: 'object', isOptional: false };
|
|
26
|
+
}
|
|
27
|
+
if (def.type === 'union' && Array.isArray(def.options) && def.options.length > 0) {
|
|
28
|
+
return unwrapField(def.options[0]);
|
|
29
|
+
}
|
|
30
|
+
if (def.type === 'int' || def.type === 'int32' || def.type === 'int64') {
|
|
31
|
+
return { typeName: 'integer', isOptional: false };
|
|
32
|
+
}
|
|
33
|
+
return { typeName: def.type, isOptional: false };
|
|
34
|
+
}
|
|
35
|
+
const CLI_TYPES = new Set(['string', 'integer', 'number', 'boolean', 'object', 'array', 'enum']);
|
|
36
|
+
export function walkWrapperChain(field, predicate) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
function walk(node) {
|
|
39
|
+
if (seen.has(node))
|
|
40
|
+
return false;
|
|
41
|
+
seen.add(node);
|
|
42
|
+
if (predicate(node))
|
|
43
|
+
return true;
|
|
44
|
+
const def = node.def;
|
|
45
|
+
if (def.type === 'optional' || def.type === 'default') {
|
|
46
|
+
return def.innerType != null && walk(def.innerType);
|
|
47
|
+
}
|
|
48
|
+
if (def.type === 'lazy' && typeof def.getter === 'function') {
|
|
49
|
+
return walk(def.getter());
|
|
50
|
+
}
|
|
51
|
+
if (def.type === 'union' && Array.isArray(def.options)) {
|
|
52
|
+
return def.options.some((o) => walk(o));
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return walk(field);
|
|
57
|
+
}
|
|
58
|
+
export function readMetaField(field, key) {
|
|
59
|
+
const outerMeta = field.meta();
|
|
60
|
+
if (outerMeta?.[key] != null)
|
|
61
|
+
return outerMeta[key];
|
|
62
|
+
const innerType = field.def.innerType;
|
|
63
|
+
if (innerType != null) {
|
|
64
|
+
const innerMeta = innerType.meta();
|
|
65
|
+
if (innerMeta?.[key] != null)
|
|
66
|
+
return innerMeta[key];
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function schemaAcceptsArrayForm(field) {
|
|
71
|
+
return walkWrapperChain(field, (n) => n.def.type === 'array');
|
|
72
|
+
}
|
|
73
|
+
export function extractSchemaArgs(schema, opts) {
|
|
74
|
+
const shape = schema?.shape;
|
|
75
|
+
if (shape == null || typeof shape !== 'object')
|
|
76
|
+
return [];
|
|
77
|
+
return Object.entries(shape).map(([key, fieldSchema]) => {
|
|
78
|
+
const field = fieldSchema;
|
|
79
|
+
const { typeName, isOptional, defaultValue } = unwrapField(field);
|
|
80
|
+
const type = (CLI_TYPES.has(typeName) ? typeName : 'string');
|
|
81
|
+
const description = readMetaField(field, 'description') ?? '';
|
|
82
|
+
const acceptsArrayForm = type !== 'array' && schemaAcceptsArrayForm(field);
|
|
83
|
+
const base = {
|
|
84
|
+
schemaKey: key,
|
|
85
|
+
cliFlag: toKebabCase(key),
|
|
86
|
+
type,
|
|
87
|
+
required: !isOptional && defaultValue === undefined,
|
|
88
|
+
defaultValue,
|
|
89
|
+
description,
|
|
90
|
+
...(acceptsArrayForm ? { acceptsArrayForm: true } : {}),
|
|
91
|
+
};
|
|
92
|
+
const extra = opts?.enrich != null ? opts.enrich(field, base) : {};
|
|
93
|
+
return { ...base, ...extra };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function buildFlagKeyMap(args) {
|
|
97
|
+
const toSchemaKey = new Map();
|
|
98
|
+
const toCliFlag = new Map();
|
|
99
|
+
for (const arg of args) {
|
|
100
|
+
toSchemaKey.set(arg.cliFlag, arg.schemaKey);
|
|
101
|
+
toCliFlag.set(arg.schemaKey, arg.cliFlag);
|
|
102
|
+
}
|
|
103
|
+
return { toSchemaKey, toCliFlag };
|
|
104
|
+
}
|
|
105
|
+
export function validateSchemaArgs(args, reserved = ['help']) {
|
|
106
|
+
const reservedSet = new Set(reserved);
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
for (const arg of args) {
|
|
109
|
+
if (reservedSet.has(arg.cliFlag)) {
|
|
110
|
+
throw new Error(`Schema key "${arg.schemaKey}" collides with reserved flag "--${arg.cliFlag}"`);
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(arg.cliFlag)) {
|
|
113
|
+
throw new Error(`Duplicate CLI flag collision: multiple schema keys map to "--${arg.cliFlag}"`);
|
|
114
|
+
}
|
|
115
|
+
seen.add(arg.cliFlag);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=schema-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-args.js","sourceRoot":"","sources":["../src/schema-args.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cli-schema/zod",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Derives CLI Schema parameter definitions from Zod schemas (https://github.com/cli-schema/cli-schema)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -b",
|
|
22
|
+
"test": "node --import tsx/esm --test test/**/*.test.ts"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@cli-schema/spec": "^0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"zod": "^4.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"zod": "^4.3.6"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/cli-schema/cli-schema-js.git",
|
|
36
|
+
"directory": "packages/zod"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright cli-schema contributors
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import type { Constraint, ParameterElementType } from '@cli-schema/spec'
|
|
8
|
+
|
|
9
|
+
export type JsonSchemaNode = Record<string, unknown>
|
|
10
|
+
|
|
11
|
+
/** Converts a Zod schema to JSON Schema, resolving repeated refs to `$ref` (matches `z.toJSONSchema` defaults for this package's needs). */
|
|
12
|
+
export function zodToJsonSchema (schema: z.ZodType): JsonSchemaNode {
|
|
13
|
+
return z.toJSONSchema(schema, { reused: 'ref' }) as JsonSchemaNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveRef (node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode {
|
|
17
|
+
const ref = node['$ref']
|
|
18
|
+
if (typeof ref !== 'string') return node
|
|
19
|
+
const parts = ref.replace(/^#\//, '').split('/')
|
|
20
|
+
let cur: unknown = root
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (cur == null || typeof cur !== 'object') return node
|
|
23
|
+
cur = (cur as JsonSchemaNode)[part]
|
|
24
|
+
}
|
|
25
|
+
return (cur != null && typeof cur === 'object') ? cur as JsonSchemaNode : node
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function unwrapNullable (node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode {
|
|
29
|
+
const anyOf = node['anyOf']
|
|
30
|
+
if (!Array.isArray(anyOf)) return node
|
|
31
|
+
const nonNull = anyOf.find((b: unknown) => {
|
|
32
|
+
if (b == null || typeof b !== 'object') return false
|
|
33
|
+
const branch = b as JsonSchemaNode
|
|
34
|
+
return branch['type'] !== 'null' && branch['const'] !== null
|
|
35
|
+
})
|
|
36
|
+
return nonNull != null ? resolveRef(nonNull as JsonSchemaNode, root) : node
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function resolveNode (node: JsonSchemaNode, root: JsonSchemaNode): JsonSchemaNode {
|
|
40
|
+
return unwrapNullable(resolveRef(node, root), root)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function extractEnumValues (node: JsonSchemaNode, root: JsonSchemaNode): string[] | undefined {
|
|
44
|
+
const resolved = resolveNode(node, root)
|
|
45
|
+
const enumVals = resolved['enum']
|
|
46
|
+
if (!Array.isArray(enumVals) || enumVals.length === 0) return undefined
|
|
47
|
+
return enumVals.filter((v) => v != null).map(String)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const SCALAR_ELEMENT_TYPES: ReadonlySet<string> = new Set(['string', 'integer', 'number', 'boolean'])
|
|
51
|
+
|
|
52
|
+
export function extractElementType (node: JsonSchemaNode, root: JsonSchemaNode): ParameterElementType | undefined {
|
|
53
|
+
const resolved = resolveNode(node, root)
|
|
54
|
+
const items = resolved['items']
|
|
55
|
+
if (items == null || typeof items !== 'object') return undefined
|
|
56
|
+
const resolvedItems = resolveNode(items as JsonSchemaNode, root)
|
|
57
|
+
const t = resolvedItems['type']
|
|
58
|
+
if (typeof t !== 'string') return undefined
|
|
59
|
+
// Spec only allows scalar element types; object/array items are not expressible.
|
|
60
|
+
return SCALAR_ELEMENT_TYPES.has(t) ? t as ParameterElementType : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Derives {@link Constraint} validation rules from a resolved JSON Schema node.
|
|
65
|
+
*
|
|
66
|
+
* Covers the constraint kinds directly derivable from JSON Schema keywords
|
|
67
|
+
* (`range`, `length`, `regex`, `email`, `url`). Kinds that require domain knowledge beyond
|
|
68
|
+
* what JSON Schema expresses (`timeSpanRange`, `count`, `allowed`, `denied`, `uriScheme`,
|
|
69
|
+
* `fileExtensions`, `existing`, `nonExisting`, `rejectSymbolicLinks`) are not inferred here —
|
|
70
|
+
* attach them via {@link extractSchemaArgs}'s `enrich` hook if your schema encodes them.
|
|
71
|
+
*
|
|
72
|
+
* Enum values are surfaced via the dedicated `enumValues` field, not as a constraint.
|
|
73
|
+
*/
|
|
74
|
+
export function extractValidations (node: JsonSchemaNode, root: JsonSchemaNode): Constraint[] | undefined {
|
|
75
|
+
const resolved = resolveNode(node, root)
|
|
76
|
+
const validations: Constraint[] = []
|
|
77
|
+
|
|
78
|
+
const min = resolved['minimum']
|
|
79
|
+
const max = resolved['maximum']
|
|
80
|
+
if (min !== undefined || max !== undefined) {
|
|
81
|
+
validations.push({
|
|
82
|
+
kind: 'range',
|
|
83
|
+
...(min !== undefined && { min: String(min) }),
|
|
84
|
+
...(max !== undefined && { max: String(max) }),
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const minLength = resolved['minLength']
|
|
89
|
+
const maxLength = resolved['maxLength']
|
|
90
|
+
if (minLength !== undefined || maxLength !== undefined) {
|
|
91
|
+
validations.push({
|
|
92
|
+
kind: 'length',
|
|
93
|
+
...(minLength !== undefined && { min: String(minLength) }),
|
|
94
|
+
...(maxLength !== undefined && { max: String(maxLength) }),
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A named format (email, url) implies its own pattern under the hood (e.g. zod's z.email()
|
|
99
|
+
// JSON Schema includes both `format: "email"` and a matching `pattern`); surface only the
|
|
100
|
+
// named constraint in that case rather than a redundant, semantically weaker regex.
|
|
101
|
+
const format = resolved['format']
|
|
102
|
+
if (format === 'email') {
|
|
103
|
+
validations.push({ kind: 'email' })
|
|
104
|
+
} else if (format === 'uri' || format === 'url') {
|
|
105
|
+
validations.push({ kind: 'url' })
|
|
106
|
+
} else {
|
|
107
|
+
const pattern = resolved['pattern']
|
|
108
|
+
if (typeof pattern === 'string') {
|
|
109
|
+
validations.push({ kind: 'regex', pattern })
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return validations.length > 0 ? validations : undefined
|
|
114
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright cli-schema contributors
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
/** Declared type of a schema-derived CLI argument, before mapping to a spec {@link Parameter} type. */
|
|
9
|
+
export type SchemaArgType = 'string' | 'integer' | 'number' | 'boolean' | 'object' | 'array' | 'enum'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a single CLI argument derived from a top-level key in a Zod object schema.
|
|
13
|
+
*
|
|
14
|
+
* Framework-specific extras (e.g. HTTP-transport routing, custom parsing hints) are not part
|
|
15
|
+
* of this type — attach them via the `enrich` option on {@link extractSchemaArgs}.
|
|
16
|
+
*/
|
|
17
|
+
export interface SchemaArgDefinition {
|
|
18
|
+
/** Original key name as defined in the Zod schema (e.g., `num_shards`, `refreshInterval`) */
|
|
19
|
+
schemaKey: string
|
|
20
|
+
|
|
21
|
+
/** Kebab-case flag name derived from `schemaKey` (e.g., `num-shards`, `refresh-interval`) */
|
|
22
|
+
cliFlag: string
|
|
23
|
+
|
|
24
|
+
/** Declared type from schema introspection */
|
|
25
|
+
type: SchemaArgType
|
|
26
|
+
|
|
27
|
+
/** Whether the field is required (no default, not optional) */
|
|
28
|
+
required: boolean
|
|
29
|
+
|
|
30
|
+
/** Default value from the schema, if any */
|
|
31
|
+
defaultValue?: unknown
|
|
32
|
+
|
|
33
|
+
/** Description from the schema's metadata, used in help text */
|
|
34
|
+
description: string
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when the schema accepts both a scalar and an array form (e.g. `Fields = union(Field, array(Field))`).
|
|
38
|
+
* CLI ergonomics typically register a scalar flag; callers that need the array form split
|
|
39
|
+
* comma-separated values themselves where the destination demands it.
|
|
40
|
+
*/
|
|
41
|
+
acceptsArrayForm?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A bidirectional mapping between kebab-case CLI flag names and original schema keys.
|
|
46
|
+
*/
|
|
47
|
+
export interface FlagKeyMap {
|
|
48
|
+
/** Maps `cliFlag` -> `schemaKey` for reverse lookup during merge */
|
|
49
|
+
toSchemaKey: Map<string, string>
|
|
50
|
+
|
|
51
|
+
/** Maps `schemaKey` -> `cliFlag` for registration and help text */
|
|
52
|
+
toCliFlag: Map<string, string>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a schema key to its kebab-case CLI flag name.
|
|
57
|
+
* Handles snake_case, camelCase, and plain lowercase inputs.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* toKebabCase('num_shards') // 'num-shards'
|
|
62
|
+
* toKebabCase('refreshInterval') // 'refresh-interval'
|
|
63
|
+
* toKebabCase('index') // 'index'
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export function toKebabCase (key: string): string {
|
|
67
|
+
return key
|
|
68
|
+
.replace(/^_+/, '') // strip leading underscores (e.g. Elasticsearch's _source, _meta)
|
|
69
|
+
.replace(/_/g, '-')
|
|
70
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Minimal shape of a Zod field def for the properties we need to introspect. */
|
|
75
|
+
interface ZodFieldDef {
|
|
76
|
+
type: string
|
|
77
|
+
innerType?: { def: ZodFieldDef }
|
|
78
|
+
defaultValue?: unknown
|
|
79
|
+
getter?: () => z.ZodType
|
|
80
|
+
options?: z.ZodType[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Unwraps wrapper types from a Zod schema field, resolving lazy thunks,
|
|
85
|
+
* records, unions, and any/unknown to their CLI-appropriate type names.
|
|
86
|
+
*/
|
|
87
|
+
function unwrapField (field: z.ZodType): { typeName: string, isOptional: boolean, defaultValue?: unknown } {
|
|
88
|
+
const def = field.def as ZodFieldDef
|
|
89
|
+
if (def.type === 'date') {
|
|
90
|
+
throw new Error('Date cannot be represented in JSON Schema: use z.string() with an ISO-8601 description instead of z.date()')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (def.type === 'optional') {
|
|
94
|
+
const inner = unwrapField(def.innerType as unknown as z.ZodType)
|
|
95
|
+
return { ...inner, isOptional: true }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (def.type === 'default') {
|
|
99
|
+
const inner = unwrapField(def.innerType as unknown as z.ZodType)
|
|
100
|
+
return { ...inner, defaultValue: def.defaultValue, isOptional: false }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (def.type === 'lazy' && typeof def.getter === 'function') {
|
|
104
|
+
return unwrapField(def.getter())
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (def.type === 'record' || def.type === 'any' || def.type === 'unknown') {
|
|
108
|
+
return { typeName: 'object', isOptional: false }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (def.type === 'union' && Array.isArray(def.options) && def.options.length > 0) {
|
|
112
|
+
return unwrapField(def.options[0] as z.ZodType)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// zod v4 exposes `z.int()` (and `.int()` refinements) as a distinct 'int' def.type
|
|
116
|
+
// in some code paths; treat it as a first-class integer rather than folding into 'number'.
|
|
117
|
+
if (def.type === 'int' || def.type === 'int32' || def.type === 'int64') {
|
|
118
|
+
return { typeName: 'integer', isOptional: false }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { typeName: def.type, isOptional: false }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const CLI_TYPES = new Set<SchemaArgType>(['string', 'integer', 'number', 'boolean', 'object', 'array', 'enum'])
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Walks `field` through `optional`, `default`, `lazy`, and `union` wrappers, returning true
|
|
128
|
+
* if `predicate` matches any visited node. The `seen` set breaks cycles in self-referential
|
|
129
|
+
* lazy schemas (e.g. `z.lazy(() => Foo)` where `Foo` references itself).
|
|
130
|
+
*
|
|
131
|
+
* Exported for consumers that need to detect wrapped conditions this package doesn't model
|
|
132
|
+
* itself (e.g. a framework-specific `.meta({id: 'Sort'})` tag anywhere in the wrapper chain).
|
|
133
|
+
*
|
|
134
|
+
* Note: evaluating `lazy.getter()` forces lazy-schema evaluation; schemas that need predicates
|
|
135
|
+
* depending on a lazy's inner shape can't avoid this.
|
|
136
|
+
*/
|
|
137
|
+
export function walkWrapperChain (field: z.ZodType, predicate: (node: z.ZodType) => boolean): boolean {
|
|
138
|
+
const seen = new Set<z.ZodType>()
|
|
139
|
+
function walk (node: z.ZodType): boolean {
|
|
140
|
+
if (seen.has(node)) return false
|
|
141
|
+
seen.add(node)
|
|
142
|
+
if (predicate(node)) return true
|
|
143
|
+
const def = node.def as ZodFieldDef
|
|
144
|
+
if (def.type === 'optional' || def.type === 'default') {
|
|
145
|
+
return def.innerType != null && walk(def.innerType as unknown as z.ZodType)
|
|
146
|
+
}
|
|
147
|
+
if (def.type === 'lazy' && typeof def.getter === 'function') {
|
|
148
|
+
return walk(def.getter())
|
|
149
|
+
}
|
|
150
|
+
if (def.type === 'union' && Array.isArray(def.options)) {
|
|
151
|
+
return def.options.some((o) => walk(o))
|
|
152
|
+
}
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
return walk(field)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reads a `.meta()` field from `field`, checking the outermost type first and falling back to
|
|
160
|
+
* one wrapper level in (`optional`/`default`) if absent. Zod's `.optional()`/`.default()` wrap
|
|
161
|
+
* the original schema, so metadata attached before wrapping lives on the inner type.
|
|
162
|
+
*/
|
|
163
|
+
export function readMetaField <T = unknown> (field: z.ZodType, key: string): T | undefined {
|
|
164
|
+
const outerMeta = field.meta() as Record<string, unknown> | null | undefined
|
|
165
|
+
if (outerMeta?.[key] != null) return outerMeta[key] as T
|
|
166
|
+
|
|
167
|
+
const innerType = (field.def as { innerType?: z.ZodType }).innerType
|
|
168
|
+
if (innerType != null) {
|
|
169
|
+
const innerMeta = innerType.meta() as Record<string, unknown> | null | undefined
|
|
170
|
+
if (innerMeta?.[key] != null) return innerMeta[key] as T
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns true when the schema (or any branch of its unions) accepts an array form.
|
|
178
|
+
*
|
|
179
|
+
* A common CLI-schema pattern types a field as `union(T, array(T))` (e.g. accept either a
|
|
180
|
+
* single value or a comma-separated / repeated collection), which {@link unwrapField} collapses
|
|
181
|
+
* to the scalar branch for CLI ergonomics. `acceptsArrayForm` surfaces that the array form still
|
|
182
|
+
* exists so callers can register a `repeatable`/`separator` parameter or split values themselves.
|
|
183
|
+
*/
|
|
184
|
+
function schemaAcceptsArrayForm (field: z.ZodType): boolean {
|
|
185
|
+
return walkWrapperChain(field, (n) => (n.def as ZodFieldDef).type === 'array')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extracts CLI argument definitions from a Zod object schema.
|
|
190
|
+
* Each top-level key becomes a `SchemaArgDefinition` with its kebab-case flag name,
|
|
191
|
+
* type, required status, default value, and description.
|
|
192
|
+
*
|
|
193
|
+
* Returns an empty array if `schema` is not a Zod object schema.
|
|
194
|
+
*
|
|
195
|
+
* Pass `enrich` to attach framework-specific extras (read off the original, un-unwrapped Zod
|
|
196
|
+
* field) to each definition — e.g. HTTP-transport routing metadata, custom parse hints.
|
|
197
|
+
*/
|
|
198
|
+
export function extractSchemaArgs <E extends object = object> (
|
|
199
|
+
schema: unknown,
|
|
200
|
+
opts?: { enrich?: (field: z.ZodType, base: SchemaArgDefinition) => E },
|
|
201
|
+
): Array<SchemaArgDefinition & E> {
|
|
202
|
+
const shape = (schema as z.ZodObject<z.ZodRawShape> | null)?.shape
|
|
203
|
+
if (shape == null || typeof shape !== 'object') return []
|
|
204
|
+
|
|
205
|
+
return Object.entries(shape).map(([key, fieldSchema]) => {
|
|
206
|
+
const field = fieldSchema as z.ZodType
|
|
207
|
+
const { typeName, isOptional, defaultValue } = unwrapField(field)
|
|
208
|
+
const type = (CLI_TYPES.has(typeName as SchemaArgType) ? typeName : 'string') as SchemaArgType
|
|
209
|
+
|
|
210
|
+
// Read description from the Zod globalRegistry -- much faster than calling
|
|
211
|
+
// .toJSONSchema() per field, which would force lazy-schema evaluation.
|
|
212
|
+
const description = readMetaField<string>(field, 'description') ?? ''
|
|
213
|
+
|
|
214
|
+
const acceptsArrayForm = type !== 'array' && schemaAcceptsArrayForm(field)
|
|
215
|
+
|
|
216
|
+
const base: SchemaArgDefinition = {
|
|
217
|
+
schemaKey: key,
|
|
218
|
+
cliFlag: toKebabCase(key),
|
|
219
|
+
type,
|
|
220
|
+
required: !isOptional && defaultValue === undefined,
|
|
221
|
+
defaultValue,
|
|
222
|
+
description,
|
|
223
|
+
...(acceptsArrayForm ? { acceptsArrayForm: true } : {}),
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const extra = opts?.enrich != null ? opts.enrich(field, base) : ({} as E)
|
|
227
|
+
return { ...base, ...extra }
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Builds a bidirectional mapping between CLI flag names and schema keys for a command.
|
|
233
|
+
* Created once at registration time; immutable after creation.
|
|
234
|
+
*/
|
|
235
|
+
export function buildFlagKeyMap (args: Array<Pick<SchemaArgDefinition, 'schemaKey' | 'cliFlag'>>): FlagKeyMap {
|
|
236
|
+
const toSchemaKey = new Map<string, string>()
|
|
237
|
+
const toCliFlag = new Map<string, string>()
|
|
238
|
+
for (const arg of args) {
|
|
239
|
+
toSchemaKey.set(arg.cliFlag, arg.schemaKey)
|
|
240
|
+
toCliFlag.set(arg.schemaKey, arg.cliFlag)
|
|
241
|
+
}
|
|
242
|
+
return { toSchemaKey, toCliFlag }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validates schema arguments for naming conflicts.
|
|
247
|
+
* Throws if any `cliFlag` collides with a `reserved` flag or duplicates another arg's flag.
|
|
248
|
+
* Called at command registration time for fail-fast detection.
|
|
249
|
+
*
|
|
250
|
+
* @param reserved - flag names this CLI already uses for framework-level concerns
|
|
251
|
+
* (e.g. `help`, `json`, `config-file`). Defaults to `['help']`, since nearly every CLI
|
|
252
|
+
* framework reserves `--help`.
|
|
253
|
+
*/
|
|
254
|
+
export function validateSchemaArgs (
|
|
255
|
+
args: Array<Pick<SchemaArgDefinition, 'schemaKey' | 'cliFlag'>>,
|
|
256
|
+
reserved: Iterable<string> = ['help'],
|
|
257
|
+
): void {
|
|
258
|
+
const reservedSet = new Set(reserved)
|
|
259
|
+
const seen = new Set<string>()
|
|
260
|
+
for (const arg of args) {
|
|
261
|
+
if (reservedSet.has(arg.cliFlag)) {
|
|
262
|
+
throw new Error(`Schema key "${arg.schemaKey}" collides with reserved flag "--${arg.cliFlag}"`)
|
|
263
|
+
}
|
|
264
|
+
if (seen.has(arg.cliFlag)) {
|
|
265
|
+
throw new Error(`Duplicate CLI flag collision: multiple schema keys map to "--${arg.cliFlag}"`)
|
|
266
|
+
}
|
|
267
|
+
seen.add(arg.cliFlag)
|
|
268
|
+
}
|
|
269
|
+
}
|