@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 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
@@ -0,0 +1,3 @@
1
+ export * from './schema-args.ts';
2
+ export * from './json-schema.ts';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from "./schema-args.js";
2
+ export * from "./json-schema.js";
3
+ //# sourceMappingURL=index.js.map
@@ -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":"AAiEA,MAAM,UAAU,WAAW,CAAE,GAAW;IACtC,OAAO,GAAG;SACP,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;SAClB,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;SAClB,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC;SACnC,WAAW,EAAE,CAAA;AAClB,CAAC;AAeD,SAAS,WAAW,CAAE,KAAgB;IACpC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAkB,CAAA;IACpC,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,4GAA4G,CAAC,CAAA;IAC/H,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,SAAiC,CAAC,CAAA;QAChE,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAA;IACvC,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,SAAiC,CAAC,CAAA;QAChE,OAAO,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,GAAG,CAAC,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;IACxE,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAC5D,OAAO,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;IAClC,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC1E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;IAClD,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjF,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAc,CAAC,CAAA;IACjD,CAAC;IAID,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACvE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;IACnD,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAA;AAClD,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAgB,CAAC,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAA;AAa/G,MAAM,UAAU,gBAAgB,CAAE,KAAgB,EAAE,SAAuC;IACzF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAa,CAAA;IACjC,SAAS,IAAI,CAAE,IAAe;QAC5B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAA;QAChC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACd,IAAI,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAA;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAkB,CAAA;QACnC,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACtD,OAAO,GAAG,CAAC,SAAS,IAAI,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,SAAiC,CAAC,CAAA;QAC7E,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YAC5D,OAAO,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;QAC3B,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvD,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QACzC,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAOD,MAAM,UAAU,aAAa,CAAgB,KAAgB,EAAE,GAAW;IACxE,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,EAAgD,CAAA;IAC5E,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC,GAAG,CAAM,CAAA;IAExD,MAAM,SAAS,GAAI,KAAK,CAAC,GAAiC,CAAC,SAAS,CAAA;IACpE,IAAI,SAAS,IAAI,IAAI,EAAE,CAAC;QACtB,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,EAAgD,CAAA;QAChF,IAAI,SAAS,EAAE,CAAC,GAAG,CAAC,IAAI,IAAI;YAAE,OAAO,SAAS,CAAC,GAAG,CAAM,CAAA;IAC1D,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAUD,SAAS,sBAAsB,CAAE,KAAgB;IAC/C,OAAO,gBAAgB,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAE,CAAC,CAAC,GAAmB,CAAC,IAAI,KAAK,OAAO,CAAC,CAAA;AAChF,CAAC;AAYD,MAAM,UAAU,iBAAiB,CAC/B,MAAe,EACf,IAAsE;IAEtE,MAAM,KAAK,GAAI,MAA4C,EAAE,KAAK,CAAA;IAClE,IAAI,KAAK,IAAI,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAA;IAEzD,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,EAAE;QACtD,MAAM,KAAK,GAAG,WAAwB,CAAA;QACtC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAA;QACjE,MAAM,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,QAAyB,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAkB,CAAA;QAI9F,MAAM,WAAW,GAAG,aAAa,CAAS,KAAK,EAAE,aAAa,CAAC,IAAI,EAAE,CAAA;QAErE,MAAM,gBAAgB,GAAG,IAAI,KAAK,OAAO,IAAI,sBAAsB,CAAC,KAAK,CAAC,CAAA;QAE1E,MAAM,IAAI,GAAwB;YAChC,SAAS,EAAE,GAAG;YACd,OAAO,EAAE,WAAW,CAAC,GAAG,CAAC;YACzB,IAAI;YACJ,QAAQ,EAAE,CAAC,UAAU,IAAI,YAAY,KAAK,SAAS;YACnD,YAAY;YACZ,WAAW;YACX,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD,CAAA;QAED,MAAM,KAAK,GAAG,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAE,EAAQ,CAAA;QACzE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,EAAE,CAAA;IAC9B,CAAC,CAAC,CAAA;AACJ,CAAC;AAMD,MAAM,UAAU,eAAe,CAAE,IAA+D;IAC9F,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC7C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC3C,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;QAC3C,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAA;AACnC,CAAC;AAWD,MAAM,UAAU,kBAAkB,CAChC,IAA+D,EAC/D,WAA6B,CAAC,MAAM,CAAC;IAErC,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,eAAe,GAAG,CAAC,SAAS,oCAAoC,GAAG,CAAC,OAAO,GAAG,CAAC,CAAA;QACjG,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,gEAAgE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAA;QACjG,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACvB,CAAC;AACH,CAAC"}
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,7 @@
1
+ /*
2
+ * Copyright cli-schema contributors
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './schema-args.ts'
7
+ export * from './json-schema.ts'
@@ -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
+ }