@optique/zod 1.0.0-dev.908 → 1.0.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 CHANGED
@@ -37,7 +37,9 @@ import { z } from "zod";
37
37
 
38
38
  const cli = run(
39
39
  object({
40
- email: option("--email", zod(z.string().email())),
40
+ email: option("--email",
41
+ zod(z.string().email(), { placeholder: "" }),
42
+ ),
41
43
  }),
42
44
  );
43
45
 
@@ -65,7 +67,9 @@ import { option } from "@optique/core/primitives";
65
67
  import { zod } from "@optique/zod";
66
68
  import { z } from "zod";
67
69
 
68
- const email = option("--email", zod(z.string().email()));
70
+ const email = option("--email",
71
+ zod(z.string().email(), { placeholder: "" }),
72
+ );
69
73
  ~~~~
70
74
 
71
75
  ### URL validation
@@ -75,7 +79,9 @@ import { option } from "@optique/core/primitives";
75
79
  import { zod } from "@optique/zod";
76
80
  import { z } from "zod";
77
81
 
78
- const url = option("--url", zod(z.string().url()));
82
+ const url = option("--url",
83
+ zod(z.string().url(), { placeholder: "" }),
84
+ );
79
85
  ~~~~
80
86
 
81
87
  ### Port numbers with range validation
@@ -90,7 +96,7 @@ import { zod } from "@optique/zod";
90
96
  import { z } from "zod";
91
97
 
92
98
  const port = option("-p", "--port",
93
- zod(z.coerce.number().int().min(1024).max(65535))
99
+ zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
94
100
  );
95
101
  ~~~~
96
102
 
@@ -102,7 +108,7 @@ import { zod } from "@optique/zod";
102
108
  import { z } from "zod";
103
109
 
104
110
  const logLevel = option("--log-level",
105
- zod(z.enum(["debug", "info", "warn", "error"]))
111
+ zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
106
112
  );
107
113
  ~~~~
108
114
 
@@ -114,7 +120,7 @@ import { zod } from "@optique/zod";
114
120
  import { z } from "zod";
115
121
 
116
122
  const startDate = argument(
117
- zod(z.string().transform((s) => new Date(s)))
123
+ zod(z.string().transform((s) => new Date(s)), { placeholder: new Date(0) }),
118
124
  );
119
125
  ~~~~
120
126
 
@@ -131,6 +137,7 @@ import { message } from "@optique/core/message";
131
137
  import { z } from "zod";
132
138
 
133
139
  const email = option("--email", zod(z.string().email(), {
140
+ placeholder: "",
134
141
  metavar: "EMAIL",
135
142
  errors: {
136
143
  zodError: (error, input) =>
@@ -154,7 +161,7 @@ import { zod } from "@optique/zod";
154
161
  import { z } from "zod";
155
162
 
156
163
  // ✅ Correct
157
- const port = option("-p", zod(z.coerce.number()));
164
+ const port = option("-p", zod(z.coerce.number(), { placeholder: 0 }));
158
165
 
159
166
  // ❌ Won't work (CLI arguments are always strings)
160
167
  // const port = option("-p", zod(z.number()));
@@ -172,7 +179,8 @@ import { z } from "zod";
172
179
 
173
180
  // ❌ Not supported
174
181
  const email = option("--email",
175
- zod(z.string().refine(async (val) => await checkDB(val)))
182
+ zod(z.string().refine(async (val) => await checkDB(val)),
183
+ { placeholder: "" }),
176
184
  );
177
185
  ~~~~
178
186
 
package/dist/index.cjs CHANGED
@@ -22,9 +22,136 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
 
23
23
  //#endregion
24
24
  const __optique_core_message = __toESM(require("@optique/core/message"));
25
+ const __optique_core_nonempty = __toESM(require("@optique/core/nonempty"));
26
+ const zod = __toESM(require("zod"));
25
27
 
26
28
  //#region src/index.ts
27
29
  /**
30
+ * Checks whether the given error is a Zod async-parse error.
31
+ *
32
+ * - **Zod v4** throws a dedicated `$ZodAsyncError` class.
33
+ * - **Zod v3** (3.25+) throws a plain `Error` whose message starts with
34
+ * `"Async refinement encountered during synchronous parse operation"` for
35
+ * async refinements, or `"Asynchronous transform encountered during
36
+ * synchronous parse operation"` for async transforms.
37
+ */
38
+ function isZodAsyncError(error) {
39
+ if (error.constructor.name === "$ZodAsyncError") return true;
40
+ if (error.message === "Async refinement encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Synchronous parse encountered promise.") return true;
41
+ return false;
42
+ }
43
+ const BOOL_TRUE_LITERALS = [
44
+ "true",
45
+ "1",
46
+ "yes",
47
+ "on"
48
+ ];
49
+ const BOOL_FALSE_LITERALS = [
50
+ "false",
51
+ "0",
52
+ "no",
53
+ "off"
54
+ ];
55
+ /**
56
+ * Analyzes whether the given Zod schema represents a boolean type,
57
+ * unwrapping all known Zod wrappers. Also determines whether it is
58
+ * safe to expose `choices` and `suggest()` — wrappers that can narrow
59
+ * the accepted domain (effects, catch) suppress choice exposure.
60
+ */
61
+ function analyzeBooleanSchema(schema) {
62
+ const result = analyzeBooleanInner(schema, true, /* @__PURE__ */ new WeakSet());
63
+ if (!result.isBoolean) return {
64
+ isBoolean: false,
65
+ exposeChoices: false,
66
+ isCoerced: false
67
+ };
68
+ return result;
69
+ }
70
+ function analyzeBooleanInner(schema, canExposeChoices, visited) {
71
+ if (visited.has(schema)) return {
72
+ isBoolean: false,
73
+ exposeChoices: false,
74
+ isCoerced: false
75
+ };
76
+ visited.add(schema);
77
+ const def = schema._def;
78
+ if (!def) return {
79
+ isBoolean: false,
80
+ exposeChoices: false,
81
+ isCoerced: false
82
+ };
83
+ const typeName = def.typeName ?? def.type;
84
+ if (typeName === "ZodBoolean" || typeName === "boolean") {
85
+ const hasCustomChecks = Array.isArray(def.checks) && def.checks.some((c) => c.kind === "custom" || c.type === "custom" || c._zod?.def?.check === "custom");
86
+ return {
87
+ isBoolean: true,
88
+ exposeChoices: canExposeChoices && !hasCustomChecks,
89
+ isCoerced: def.coerce === true
90
+ };
91
+ }
92
+ if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default" || typeName === "ZodReadonly" || typeName === "readonly" || typeName === "prefault" || typeName === "nonoptional") {
93
+ const innerType = def.innerType;
94
+ if (innerType != null) return analyzeBooleanInner(innerType, canExposeChoices, visited);
95
+ }
96
+ if (typeName === "ZodLazy" || typeName === "lazy") {
97
+ if (typeof def.getter === "function") return analyzeBooleanInner(def.getter(), canExposeChoices, visited);
98
+ }
99
+ if (typeName === "ZodEffects" || typeName === "effects") {
100
+ if (def.effect?.type === "preprocess") return {
101
+ isBoolean: false,
102
+ exposeChoices: false,
103
+ isCoerced: false
104
+ };
105
+ const innerSchema = def.schema;
106
+ if (innerSchema != null) return analyzeBooleanInner(innerSchema, false, visited);
107
+ }
108
+ if (typeName === "ZodCatch" || typeName === "catch") {
109
+ const innerType = def.innerType;
110
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
111
+ }
112
+ if (typeName === "ZodBranded" || typeName === "branded") {
113
+ const innerType = def.innerType ?? (typeof def.type === "object" && def.type != null ? def.type : void 0);
114
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
115
+ }
116
+ if (typeName === "pipe" || typeName === "ZodPipeline") {
117
+ const inSchema = def.in;
118
+ if (inSchema != null) return analyzeBooleanInner(inSchema, false, visited);
119
+ const innerType = def.innerType;
120
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
121
+ }
122
+ if (typeName === "pipeline") {
123
+ const innerType = def.innerType;
124
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
125
+ }
126
+ return {
127
+ isBoolean: false,
128
+ exposeChoices: false,
129
+ isCoerced: false
130
+ };
131
+ }
132
+ /**
133
+ * Pre-converts a CLI string input to an actual boolean value using
134
+ * CLI-friendly literals (true/false, 1/0, yes/no, on/off).
135
+ */
136
+ function preConvertBoolean(input) {
137
+ const normalized = input.trim().toLowerCase();
138
+ if (BOOL_TRUE_LITERALS.includes(normalized)) return {
139
+ success: true,
140
+ value: true
141
+ };
142
+ if (BOOL_FALSE_LITERALS.includes(normalized)) return {
143
+ success: true,
144
+ value: false
145
+ };
146
+ return {
147
+ success: false,
148
+ error: __optique_core_message.message`Invalid Boolean value: ${input}. Expected one of ${(0, __optique_core_message.valueSet)([...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS], {
149
+ fallback: "",
150
+ locale: "en-US"
151
+ })}.`
152
+ };
153
+ }
154
+ /**
28
155
  * Infers an appropriate metavar string from a Zod schema.
29
156
  *
30
157
  * This function analyzes the Zod schema's internal structure to determine
@@ -47,7 +174,7 @@ const __optique_core_message = __toESM(require("@optique/core/message"));
47
174
  function inferMetavar(schema) {
48
175
  const def = schema._def;
49
176
  if (!def) return "VALUE";
50
- const typeName = def.typeName || def.type;
177
+ const typeName = def.typeName ?? def.type;
51
178
  if (typeName === "ZodString" || typeName === "string") {
52
179
  if (Array.isArray(def.checks)) for (const check of def.checks) {
53
180
  const kind = check.kind || check.format;
@@ -76,8 +203,15 @@ function inferMetavar(schema) {
76
203
  }
77
204
  if (typeName === "ZodBoolean" || typeName === "boolean") return "BOOLEAN";
78
205
  if (typeName === "ZodDate" || typeName === "date") return "DATE";
79
- if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return "CHOICE";
80
- if (typeName === "ZodUnion" || typeName === "union" || typeName === "ZodLiteral" || typeName === "literal") return "VALUE";
206
+ if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return inferChoices(schema) != null ? "CHOICE" : "VALUE";
207
+ if (typeName === "ZodLiteral" || typeName === "literal") {
208
+ if (inferChoices(schema) != null) return "CHOICE";
209
+ return "VALUE";
210
+ }
211
+ if (typeName === "ZodUnion" || typeName === "union") {
212
+ if (inferChoices(schema) != null) return "CHOICE";
213
+ return "VALUE";
214
+ }
81
215
  if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable") {
82
216
  const innerType = def.innerType;
83
217
  if (innerType != null) return inferMetavar(innerType);
@@ -91,6 +225,70 @@ function inferMetavar(schema) {
91
225
  return "VALUE";
92
226
  }
93
227
  /**
228
+ * Extracts valid choices from a Zod schema that represents a fixed set of
229
+ * values (enum, literal, or union of literals).
230
+ *
231
+ * @param schema A Zod schema to analyze.
232
+ * @returns An array of string representations of valid choices, or `undefined`
233
+ * if the schema does not represent a fixed set of values.
234
+ */
235
+ function inferChoices(schema) {
236
+ const def = schema._def;
237
+ if (!def) return void 0;
238
+ const typeName = def.typeName ?? def.type;
239
+ if (typeName === "ZodEnum" || typeName === "enum") {
240
+ const values = def.values;
241
+ if (Array.isArray(values)) return values.map(String);
242
+ const entries = def.entries;
243
+ if (entries != null && typeof entries === "object") {
244
+ const result = /* @__PURE__ */ new Set();
245
+ for (const val of Object.values(entries)) if (typeof val === "string") result.add(val);
246
+ else return void 0;
247
+ return result.size > 0 ? [...result] : void 0;
248
+ }
249
+ return void 0;
250
+ }
251
+ if (typeName === "ZodNativeEnum" || typeName === "nativeEnum") {
252
+ const values = def.values;
253
+ if (values != null && typeof values === "object" && !Array.isArray(values)) {
254
+ const result = /* @__PURE__ */ new Set();
255
+ for (const val of Object.values(values)) if (typeof val === "string") result.add(val);
256
+ else return void 0;
257
+ return result.size > 0 ? [...result] : void 0;
258
+ }
259
+ return void 0;
260
+ }
261
+ if (typeName === "ZodLiteral" || typeName === "literal") {
262
+ const value = def.value;
263
+ if (typeof value === "string") return [value];
264
+ const values = def.values;
265
+ if (Array.isArray(values)) {
266
+ const result = [];
267
+ for (const v of values) if (typeof v === "string") result.push(v);
268
+ else return void 0;
269
+ return result.length > 0 ? result : void 0;
270
+ }
271
+ return void 0;
272
+ }
273
+ if (typeName === "ZodUnion" || typeName === "union") {
274
+ const options = def.options;
275
+ if (!Array.isArray(options)) return void 0;
276
+ const allChoices = /* @__PURE__ */ new Set();
277
+ for (const opt of options) {
278
+ const sub = inferChoices(opt);
279
+ if (sub == null) return void 0;
280
+ for (const choice of sub) allChoices.add(choice);
281
+ }
282
+ return allChoices.size > 0 ? [...allChoices] : void 0;
283
+ }
284
+ if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default") {
285
+ const innerType = def.innerType;
286
+ if (innerType != null) return inferChoices(innerType);
287
+ return void 0;
288
+ }
289
+ return void 0;
290
+ }
291
+ /**
94
292
  * Creates a value parser from a Zod schema.
95
293
  *
96
294
  * This parser validates CLI argument strings using Zod schemas, enabling
@@ -107,7 +305,8 @@ function inferMetavar(schema) {
107
305
  *
108
306
  * @template T The output type of the Zod schema.
109
307
  * @param schema A Zod schema to validate input against.
110
- * @param options Optional configuration for the parser.
308
+ * @param options Configuration for the parser, including a required
309
+ * `placeholder` value used during deferred prompt resolution.
111
310
  * @returns A value parser that validates inputs using the provided schema.
112
311
  *
113
312
  * @example Basic string validation
@@ -116,7 +315,9 @@ function inferMetavar(schema) {
116
315
  * import { zod } from "@optique/zod";
117
316
  * import { option } from "@optique/core/primitives";
118
317
  *
119
- * const email = option("--email", zod(z.string().email()));
318
+ * const email = option("--email",
319
+ * zod(z.string().email(), { placeholder: "" }),
320
+ * );
120
321
  * ```
121
322
  *
122
323
  * @example Number validation with coercion
@@ -127,7 +328,7 @@ function inferMetavar(schema) {
127
328
  *
128
329
  * // Use z.coerce for non-string types since CLI args are always strings
129
330
  * const port = option("-p", "--port",
130
- * zod(z.coerce.number().int().min(1024).max(65535))
331
+ * zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
131
332
  * );
132
333
  * ```
133
334
  *
@@ -138,7 +339,7 @@ function inferMetavar(schema) {
138
339
  * import { option } from "@optique/core/primitives";
139
340
  *
140
341
  * const logLevel = option("--log-level",
141
- * zod(z.enum(["debug", "info", "warn", "error"]))
342
+ * zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
142
343
  * );
143
344
  * ```
144
345
  *
@@ -149,50 +350,140 @@ function inferMetavar(schema) {
149
350
  * import { message } from "@optique/core/message";
150
351
  * import { option } from "@optique/core/primitives";
151
352
  *
152
- * const email = option("--email", zod(z.string().email(), {
153
- * metavar: "EMAIL",
154
- * errors: {
155
- * zodError: (error, input) =>
156
- * message`Please provide a valid email address, got ${input}.`
157
- * }
158
- * }));
353
+ * const email = option("--email",
354
+ * zod(z.string().email(), {
355
+ * placeholder: "",
356
+ * metavar: "EMAIL",
357
+ * errors: {
358
+ * zodError: (error, input) =>
359
+ * message`Please provide a valid email address, got ${input}.`
360
+ * },
361
+ * }),
362
+ * );
159
363
  * ```
160
364
  *
365
+ * @throws {TypeError} If `options` is missing, not an object, or does not
366
+ * include `placeholder`.
367
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
368
+ * @throws {TypeError} If the schema contains async refinements or other async
369
+ * operations that cannot be executed synchronously.
161
370
  * @since 0.7.0
162
371
  */
163
- function zod(schema, options = {}) {
164
- return {
165
- $mode: "sync",
166
- metavar: options.metavar ?? inferMetavar(schema),
167
- parse(input) {
168
- const result = schema.safeParse(input);
169
- if (result.success) return {
170
- success: true,
171
- value: result.data
372
+ function zod$1(schema, options) {
373
+ if (options == null || typeof options !== "object") throw new TypeError("zod() requires an options object with a placeholder property.");
374
+ if (!("placeholder" in options)) throw new TypeError("zod() options must include a placeholder property.");
375
+ const choices = inferChoices(schema);
376
+ const boolInfo = analyzeBooleanSchema(schema);
377
+ const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
378
+ (0, __optique_core_nonempty.ensureNonEmptyString)(metavar);
379
+ function doSafeParse(input, rawInput) {
380
+ let result;
381
+ try {
382
+ result = schema.safeParse(input);
383
+ } catch (error) {
384
+ if (error instanceof Error && isZodAsyncError(error)) throw new TypeError("Async Zod schemas (e.g., async refinements) are not supported by zod(). Use synchronous schemas instead.");
385
+ throw error;
386
+ }
387
+ if (result.success) return {
388
+ success: true,
389
+ value: result.data
390
+ };
391
+ if (options.errors?.zodError) return {
392
+ success: false,
393
+ error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
394
+ };
395
+ const zodModule = schema;
396
+ if (typeof zodModule.constructor?.prettifyError === "function") try {
397
+ const pretty = zodModule.constructor.prettifyError(result.error);
398
+ return {
399
+ success: false,
400
+ error: __optique_core_message.message`${pretty}`
172
401
  };
173
- if (options.errors?.zodError) return {
402
+ } catch {}
403
+ const firstError = result.error.issues[0];
404
+ return {
405
+ success: false,
406
+ error: __optique_core_message.message`${firstError?.message ?? "Validation failed"}`
407
+ };
408
+ }
409
+ /**
410
+ * Handles a failed boolean literal pre-conversion.
411
+ *
412
+ * - *Non-coerced* (`z.boolean()`): falls through to `doSafeParse`
413
+ * so that catch/default, custom errors, and async detection all
414
+ * work. This is safe because `safeParse(string)` fails at the
415
+ * type level before any refinements execute.
416
+ * - *Coerced* (`z.coerce.boolean()`): runs the lazy async probe
417
+ * (if not yet completed), then returns the pre-conversion error
418
+ * or delegates to the custom `zodError` callback.
419
+ */
420
+ function handleBooleanLiteralError(boolResult, rawInput) {
421
+ if (!boolInfo.isCoerced) return doSafeParse(rawInput, rawInput);
422
+ if (options.errors?.zodError) {
423
+ if (typeof options.errors.zodError !== "function") return {
174
424
  success: false,
175
- error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, input) : options.errors.zodError
425
+ error: options.errors.zodError
176
426
  };
177
- const zodModule = schema;
178
- if (typeof zodModule.constructor?.prettifyError === "function") try {
179
- const pretty = zodModule.constructor.prettifyError(result.error);
180
- return {
181
- success: false,
182
- error: __optique_core_message.message`${pretty}`
183
- };
184
- } catch {}
185
- const firstError = result.error.issues[0];
427
+ const zodError = new zod.ZodError([{
428
+ code: "invalid_type",
429
+ expected: "boolean",
430
+ message: `Invalid Boolean value: ${rawInput}`,
431
+ path: []
432
+ }]);
186
433
  return {
187
434
  success: false,
188
- error: __optique_core_message.message`${firstError?.message ?? "Validation failed"}`
435
+ error: options.errors.zodError(zodError, rawInput)
189
436
  };
437
+ }
438
+ return boolResult;
439
+ }
440
+ const parser = {
441
+ mode: "sync",
442
+ metavar,
443
+ placeholder: options.placeholder,
444
+ ...boolInfo.exposeChoices ? {
445
+ choices: Object.freeze([true, false]),
446
+ suggest(prefix) {
447
+ const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
448
+ const normalizedPrefix = prefix.toLowerCase();
449
+ return allLiterals.filter((lit) => lit.startsWith(normalizedPrefix)).map((lit) => ({
450
+ kind: "literal",
451
+ text: lit
452
+ }));
453
+ }
454
+ } : choices != null && choices.length > 0 ? {
455
+ choices: Object.freeze(choices),
456
+ *suggest(prefix) {
457
+ for (const c of choices) if (c.startsWith(prefix)) yield {
458
+ kind: "literal",
459
+ text: c
460
+ };
461
+ }
462
+ } : {},
463
+ parse(input) {
464
+ if (boolInfo.isBoolean) {
465
+ const boolResult = preConvertBoolean(input);
466
+ if (!boolResult.success) return handleBooleanLiteralError(boolResult, input);
467
+ return doSafeParse(boolResult.value, input);
468
+ }
469
+ return doSafeParse(input, input);
190
470
  },
191
471
  format(value) {
192
- return String(value);
472
+ if (options.format) return options.format(value);
473
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
474
+ if (typeof value !== "object" || value === null) return String(value);
475
+ if (Array.isArray(value)) return String(value);
476
+ const str = String(value);
477
+ if (str !== "[object Object]") return str;
478
+ const proto = Object.getPrototypeOf(value);
479
+ if (proto === Object.prototype || proto === null) try {
480
+ return JSON.stringify(value) ?? str;
481
+ } catch {}
482
+ return str;
193
483
  }
194
484
  };
485
+ return parser;
195
486
  }
196
487
 
197
488
  //#endregion
198
- exports.zod = zod;
489
+ exports.zod = zod$1;
package/dist/index.d.cts CHANGED
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  * Options for creating a Zod value parser.
9
9
  * @since 0.7.0
10
10
  */
11
- interface ZodParserOptions {
11
+ interface ZodParserOptions<T = unknown> {
12
12
  /**
13
13
  * The metavariable name for this parser. This is used in help messages to
14
14
  * indicate what kind of value this parser expects. Usually a single
@@ -16,6 +16,25 @@ interface ZodParserOptions {
16
16
  * @default `"VALUE"`
17
17
  */
18
18
  readonly metavar?: NonEmptyString;
19
+ /**
20
+ * A phase-one stand-in value of type `T` used during deferred prompt
21
+ * resolution. Because the output type of a Zod schema cannot be
22
+ * inferred to a concrete default, callers must provide this explicitly.
23
+ * @since 1.0.0
24
+ */
25
+ readonly placeholder: T;
26
+ /**
27
+ * Custom formatter for displaying parsed values in help messages.
28
+ * When not provided, the default formatter is used: primitives use
29
+ * `String()`, valid `Date` values use `.toISOString()`, and plain
30
+ * objects use `JSON.stringify()`. All other objects (arrays, class
31
+ * instances, etc.) use `String()`.
32
+ *
33
+ * @param value The parsed value to format.
34
+ * @returns A string representation of the value.
35
+ * @since 1.0.0
36
+ */
37
+ readonly format?: (value: T) => string;
19
38
  /**
20
39
  * Custom error messages for Zod validation failures.
21
40
  */
@@ -46,7 +65,8 @@ interface ZodParserOptions {
46
65
  *
47
66
  * @template T The output type of the Zod schema.
48
67
  * @param schema A Zod schema to validate input against.
49
- * @param options Optional configuration for the parser.
68
+ * @param options Configuration for the parser, including a required
69
+ * `placeholder` value used during deferred prompt resolution.
50
70
  * @returns A value parser that validates inputs using the provided schema.
51
71
  *
52
72
  * @example Basic string validation
@@ -55,7 +75,9 @@ interface ZodParserOptions {
55
75
  * import { zod } from "@optique/zod";
56
76
  * import { option } from "@optique/core/primitives";
57
77
  *
58
- * const email = option("--email", zod(z.string().email()));
78
+ * const email = option("--email",
79
+ * zod(z.string().email(), { placeholder: "" }),
80
+ * );
59
81
  * ```
60
82
  *
61
83
  * @example Number validation with coercion
@@ -66,7 +88,7 @@ interface ZodParserOptions {
66
88
  *
67
89
  * // Use z.coerce for non-string types since CLI args are always strings
68
90
  * const port = option("-p", "--port",
69
- * zod(z.coerce.number().int().min(1024).max(65535))
91
+ * zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
70
92
  * );
71
93
  * ```
72
94
  *
@@ -77,7 +99,7 @@ interface ZodParserOptions {
77
99
  * import { option } from "@optique/core/primitives";
78
100
  *
79
101
  * const logLevel = option("--log-level",
80
- * zod(z.enum(["debug", "info", "warn", "error"]))
102
+ * zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
81
103
  * );
82
104
  * ```
83
105
  *
@@ -88,17 +110,25 @@ interface ZodParserOptions {
88
110
  * import { message } from "@optique/core/message";
89
111
  * import { option } from "@optique/core/primitives";
90
112
  *
91
- * const email = option("--email", zod(z.string().email(), {
92
- * metavar: "EMAIL",
93
- * errors: {
94
- * zodError: (error, input) =>
95
- * message`Please provide a valid email address, got ${input}.`
96
- * }
97
- * }));
113
+ * const email = option("--email",
114
+ * zod(z.string().email(), {
115
+ * placeholder: "",
116
+ * metavar: "EMAIL",
117
+ * errors: {
118
+ * zodError: (error, input) =>
119
+ * message`Please provide a valid email address, got ${input}.`
120
+ * },
121
+ * }),
122
+ * );
98
123
  * ```
99
124
  *
125
+ * @throws {TypeError} If `options` is missing, not an object, or does not
126
+ * include `placeholder`.
127
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
128
+ * @throws {TypeError} If the schema contains async refinements or other async
129
+ * operations that cannot be executed synchronously.
100
130
  * @since 0.7.0
101
131
  */
102
- declare function zod<T>(schema: z.Schema<T>, options?: ZodParserOptions): ValueParser<"sync", T>;
132
+ declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
103
133
  //#endregion
104
134
  export { ZodParserOptions, zod };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Message } from "@optique/core/message";
2
- import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
3
2
  import { z } from "zod";
3
+ import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
4
4
 
5
5
  //#region src/index.d.ts
6
6
 
@@ -8,7 +8,7 @@ import { z } from "zod";
8
8
  * Options for creating a Zod value parser.
9
9
  * @since 0.7.0
10
10
  */
11
- interface ZodParserOptions {
11
+ interface ZodParserOptions<T = unknown> {
12
12
  /**
13
13
  * The metavariable name for this parser. This is used in help messages to
14
14
  * indicate what kind of value this parser expects. Usually a single
@@ -16,6 +16,25 @@ interface ZodParserOptions {
16
16
  * @default `"VALUE"`
17
17
  */
18
18
  readonly metavar?: NonEmptyString;
19
+ /**
20
+ * A phase-one stand-in value of type `T` used during deferred prompt
21
+ * resolution. Because the output type of a Zod schema cannot be
22
+ * inferred to a concrete default, callers must provide this explicitly.
23
+ * @since 1.0.0
24
+ */
25
+ readonly placeholder: T;
26
+ /**
27
+ * Custom formatter for displaying parsed values in help messages.
28
+ * When not provided, the default formatter is used: primitives use
29
+ * `String()`, valid `Date` values use `.toISOString()`, and plain
30
+ * objects use `JSON.stringify()`. All other objects (arrays, class
31
+ * instances, etc.) use `String()`.
32
+ *
33
+ * @param value The parsed value to format.
34
+ * @returns A string representation of the value.
35
+ * @since 1.0.0
36
+ */
37
+ readonly format?: (value: T) => string;
19
38
  /**
20
39
  * Custom error messages for Zod validation failures.
21
40
  */
@@ -46,7 +65,8 @@ interface ZodParserOptions {
46
65
  *
47
66
  * @template T The output type of the Zod schema.
48
67
  * @param schema A Zod schema to validate input against.
49
- * @param options Optional configuration for the parser.
68
+ * @param options Configuration for the parser, including a required
69
+ * `placeholder` value used during deferred prompt resolution.
50
70
  * @returns A value parser that validates inputs using the provided schema.
51
71
  *
52
72
  * @example Basic string validation
@@ -55,7 +75,9 @@ interface ZodParserOptions {
55
75
  * import { zod } from "@optique/zod";
56
76
  * import { option } from "@optique/core/primitives";
57
77
  *
58
- * const email = option("--email", zod(z.string().email()));
78
+ * const email = option("--email",
79
+ * zod(z.string().email(), { placeholder: "" }),
80
+ * );
59
81
  * ```
60
82
  *
61
83
  * @example Number validation with coercion
@@ -66,7 +88,7 @@ interface ZodParserOptions {
66
88
  *
67
89
  * // Use z.coerce for non-string types since CLI args are always strings
68
90
  * const port = option("-p", "--port",
69
- * zod(z.coerce.number().int().min(1024).max(65535))
91
+ * zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
70
92
  * );
71
93
  * ```
72
94
  *
@@ -77,7 +99,7 @@ interface ZodParserOptions {
77
99
  * import { option } from "@optique/core/primitives";
78
100
  *
79
101
  * const logLevel = option("--log-level",
80
- * zod(z.enum(["debug", "info", "warn", "error"]))
102
+ * zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
81
103
  * );
82
104
  * ```
83
105
  *
@@ -88,17 +110,25 @@ interface ZodParserOptions {
88
110
  * import { message } from "@optique/core/message";
89
111
  * import { option } from "@optique/core/primitives";
90
112
  *
91
- * const email = option("--email", zod(z.string().email(), {
92
- * metavar: "EMAIL",
93
- * errors: {
94
- * zodError: (error, input) =>
95
- * message`Please provide a valid email address, got ${input}.`
96
- * }
97
- * }));
113
+ * const email = option("--email",
114
+ * zod(z.string().email(), {
115
+ * placeholder: "",
116
+ * metavar: "EMAIL",
117
+ * errors: {
118
+ * zodError: (error, input) =>
119
+ * message`Please provide a valid email address, got ${input}.`
120
+ * },
121
+ * }),
122
+ * );
98
123
  * ```
99
124
  *
125
+ * @throws {TypeError} If `options` is missing, not an object, or does not
126
+ * include `placeholder`.
127
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
128
+ * @throws {TypeError} If the schema contains async refinements or other async
129
+ * operations that cannot be executed synchronously.
100
130
  * @since 0.7.0
101
131
  */
102
- declare function zod<T>(schema: z.Schema<T>, options?: ZodParserOptions): ValueParser<"sync", T>;
132
+ declare function zod<T>(schema: z.Schema<T>, options: ZodParserOptions<T>): ValueParser<"sync", T>;
103
133
  //#endregion
104
134
  export { ZodParserOptions, zod };
package/dist/index.js CHANGED
@@ -1,7 +1,134 @@
1
- import { message } from "@optique/core/message";
1
+ import { message, valueSet } from "@optique/core/message";
2
+ import { ensureNonEmptyString } from "@optique/core/nonempty";
3
+ import { ZodError } from "zod";
2
4
 
3
5
  //#region src/index.ts
4
6
  /**
7
+ * Checks whether the given error is a Zod async-parse error.
8
+ *
9
+ * - **Zod v4** throws a dedicated `$ZodAsyncError` class.
10
+ * - **Zod v3** (3.25+) throws a plain `Error` whose message starts with
11
+ * `"Async refinement encountered during synchronous parse operation"` for
12
+ * async refinements, or `"Asynchronous transform encountered during
13
+ * synchronous parse operation"` for async transforms.
14
+ */
15
+ function isZodAsyncError(error) {
16
+ if (error.constructor.name === "$ZodAsyncError") return true;
17
+ if (error.message === "Async refinement encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead." || error.message === "Synchronous parse encountered promise.") return true;
18
+ return false;
19
+ }
20
+ const BOOL_TRUE_LITERALS = [
21
+ "true",
22
+ "1",
23
+ "yes",
24
+ "on"
25
+ ];
26
+ const BOOL_FALSE_LITERALS = [
27
+ "false",
28
+ "0",
29
+ "no",
30
+ "off"
31
+ ];
32
+ /**
33
+ * Analyzes whether the given Zod schema represents a boolean type,
34
+ * unwrapping all known Zod wrappers. Also determines whether it is
35
+ * safe to expose `choices` and `suggest()` — wrappers that can narrow
36
+ * the accepted domain (effects, catch) suppress choice exposure.
37
+ */
38
+ function analyzeBooleanSchema(schema) {
39
+ const result = analyzeBooleanInner(schema, true, /* @__PURE__ */ new WeakSet());
40
+ if (!result.isBoolean) return {
41
+ isBoolean: false,
42
+ exposeChoices: false,
43
+ isCoerced: false
44
+ };
45
+ return result;
46
+ }
47
+ function analyzeBooleanInner(schema, canExposeChoices, visited) {
48
+ if (visited.has(schema)) return {
49
+ isBoolean: false,
50
+ exposeChoices: false,
51
+ isCoerced: false
52
+ };
53
+ visited.add(schema);
54
+ const def = schema._def;
55
+ if (!def) return {
56
+ isBoolean: false,
57
+ exposeChoices: false,
58
+ isCoerced: false
59
+ };
60
+ const typeName = def.typeName ?? def.type;
61
+ if (typeName === "ZodBoolean" || typeName === "boolean") {
62
+ const hasCustomChecks = Array.isArray(def.checks) && def.checks.some((c) => c.kind === "custom" || c.type === "custom" || c._zod?.def?.check === "custom");
63
+ return {
64
+ isBoolean: true,
65
+ exposeChoices: canExposeChoices && !hasCustomChecks,
66
+ isCoerced: def.coerce === true
67
+ };
68
+ }
69
+ if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default" || typeName === "ZodReadonly" || typeName === "readonly" || typeName === "prefault" || typeName === "nonoptional") {
70
+ const innerType = def.innerType;
71
+ if (innerType != null) return analyzeBooleanInner(innerType, canExposeChoices, visited);
72
+ }
73
+ if (typeName === "ZodLazy" || typeName === "lazy") {
74
+ if (typeof def.getter === "function") return analyzeBooleanInner(def.getter(), canExposeChoices, visited);
75
+ }
76
+ if (typeName === "ZodEffects" || typeName === "effects") {
77
+ if (def.effect?.type === "preprocess") return {
78
+ isBoolean: false,
79
+ exposeChoices: false,
80
+ isCoerced: false
81
+ };
82
+ const innerSchema = def.schema;
83
+ if (innerSchema != null) return analyzeBooleanInner(innerSchema, false, visited);
84
+ }
85
+ if (typeName === "ZodCatch" || typeName === "catch") {
86
+ const innerType = def.innerType;
87
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
88
+ }
89
+ if (typeName === "ZodBranded" || typeName === "branded") {
90
+ const innerType = def.innerType ?? (typeof def.type === "object" && def.type != null ? def.type : void 0);
91
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
92
+ }
93
+ if (typeName === "pipe" || typeName === "ZodPipeline") {
94
+ const inSchema = def.in;
95
+ if (inSchema != null) return analyzeBooleanInner(inSchema, false, visited);
96
+ const innerType = def.innerType;
97
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
98
+ }
99
+ if (typeName === "pipeline") {
100
+ const innerType = def.innerType;
101
+ if (innerType != null) return analyzeBooleanInner(innerType, false, visited);
102
+ }
103
+ return {
104
+ isBoolean: false,
105
+ exposeChoices: false,
106
+ isCoerced: false
107
+ };
108
+ }
109
+ /**
110
+ * Pre-converts a CLI string input to an actual boolean value using
111
+ * CLI-friendly literals (true/false, 1/0, yes/no, on/off).
112
+ */
113
+ function preConvertBoolean(input) {
114
+ const normalized = input.trim().toLowerCase();
115
+ if (BOOL_TRUE_LITERALS.includes(normalized)) return {
116
+ success: true,
117
+ value: true
118
+ };
119
+ if (BOOL_FALSE_LITERALS.includes(normalized)) return {
120
+ success: true,
121
+ value: false
122
+ };
123
+ return {
124
+ success: false,
125
+ error: message`Invalid Boolean value: ${input}. Expected one of ${valueSet([...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS], {
126
+ fallback: "",
127
+ locale: "en-US"
128
+ })}.`
129
+ };
130
+ }
131
+ /**
5
132
  * Infers an appropriate metavar string from a Zod schema.
6
133
  *
7
134
  * This function analyzes the Zod schema's internal structure to determine
@@ -24,7 +151,7 @@ import { message } from "@optique/core/message";
24
151
  function inferMetavar(schema) {
25
152
  const def = schema._def;
26
153
  if (!def) return "VALUE";
27
- const typeName = def.typeName || def.type;
154
+ const typeName = def.typeName ?? def.type;
28
155
  if (typeName === "ZodString" || typeName === "string") {
29
156
  if (Array.isArray(def.checks)) for (const check of def.checks) {
30
157
  const kind = check.kind || check.format;
@@ -53,8 +180,15 @@ function inferMetavar(schema) {
53
180
  }
54
181
  if (typeName === "ZodBoolean" || typeName === "boolean") return "BOOLEAN";
55
182
  if (typeName === "ZodDate" || typeName === "date") return "DATE";
56
- if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return "CHOICE";
57
- if (typeName === "ZodUnion" || typeName === "union" || typeName === "ZodLiteral" || typeName === "literal") return "VALUE";
183
+ if (typeName === "ZodEnum" || typeName === "enum" || typeName === "ZodNativeEnum" || typeName === "nativeEnum") return inferChoices(schema) != null ? "CHOICE" : "VALUE";
184
+ if (typeName === "ZodLiteral" || typeName === "literal") {
185
+ if (inferChoices(schema) != null) return "CHOICE";
186
+ return "VALUE";
187
+ }
188
+ if (typeName === "ZodUnion" || typeName === "union") {
189
+ if (inferChoices(schema) != null) return "CHOICE";
190
+ return "VALUE";
191
+ }
58
192
  if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable") {
59
193
  const innerType = def.innerType;
60
194
  if (innerType != null) return inferMetavar(innerType);
@@ -68,6 +202,70 @@ function inferMetavar(schema) {
68
202
  return "VALUE";
69
203
  }
70
204
  /**
205
+ * Extracts valid choices from a Zod schema that represents a fixed set of
206
+ * values (enum, literal, or union of literals).
207
+ *
208
+ * @param schema A Zod schema to analyze.
209
+ * @returns An array of string representations of valid choices, or `undefined`
210
+ * if the schema does not represent a fixed set of values.
211
+ */
212
+ function inferChoices(schema) {
213
+ const def = schema._def;
214
+ if (!def) return void 0;
215
+ const typeName = def.typeName ?? def.type;
216
+ if (typeName === "ZodEnum" || typeName === "enum") {
217
+ const values = def.values;
218
+ if (Array.isArray(values)) return values.map(String);
219
+ const entries = def.entries;
220
+ if (entries != null && typeof entries === "object") {
221
+ const result = /* @__PURE__ */ new Set();
222
+ for (const val of Object.values(entries)) if (typeof val === "string") result.add(val);
223
+ else return void 0;
224
+ return result.size > 0 ? [...result] : void 0;
225
+ }
226
+ return void 0;
227
+ }
228
+ if (typeName === "ZodNativeEnum" || typeName === "nativeEnum") {
229
+ const values = def.values;
230
+ if (values != null && typeof values === "object" && !Array.isArray(values)) {
231
+ const result = /* @__PURE__ */ new Set();
232
+ for (const val of Object.values(values)) if (typeof val === "string") result.add(val);
233
+ else return void 0;
234
+ return result.size > 0 ? [...result] : void 0;
235
+ }
236
+ return void 0;
237
+ }
238
+ if (typeName === "ZodLiteral" || typeName === "literal") {
239
+ const value = def.value;
240
+ if (typeof value === "string") return [value];
241
+ const values = def.values;
242
+ if (Array.isArray(values)) {
243
+ const result = [];
244
+ for (const v of values) if (typeof v === "string") result.push(v);
245
+ else return void 0;
246
+ return result.length > 0 ? result : void 0;
247
+ }
248
+ return void 0;
249
+ }
250
+ if (typeName === "ZodUnion" || typeName === "union") {
251
+ const options = def.options;
252
+ if (!Array.isArray(options)) return void 0;
253
+ const allChoices = /* @__PURE__ */ new Set();
254
+ for (const opt of options) {
255
+ const sub = inferChoices(opt);
256
+ if (sub == null) return void 0;
257
+ for (const choice of sub) allChoices.add(choice);
258
+ }
259
+ return allChoices.size > 0 ? [...allChoices] : void 0;
260
+ }
261
+ if (typeName === "ZodOptional" || typeName === "optional" || typeName === "ZodNullable" || typeName === "nullable" || typeName === "ZodDefault" || typeName === "default") {
262
+ const innerType = def.innerType;
263
+ if (innerType != null) return inferChoices(innerType);
264
+ return void 0;
265
+ }
266
+ return void 0;
267
+ }
268
+ /**
71
269
  * Creates a value parser from a Zod schema.
72
270
  *
73
271
  * This parser validates CLI argument strings using Zod schemas, enabling
@@ -84,7 +282,8 @@ function inferMetavar(schema) {
84
282
  *
85
283
  * @template T The output type of the Zod schema.
86
284
  * @param schema A Zod schema to validate input against.
87
- * @param options Optional configuration for the parser.
285
+ * @param options Configuration for the parser, including a required
286
+ * `placeholder` value used during deferred prompt resolution.
88
287
  * @returns A value parser that validates inputs using the provided schema.
89
288
  *
90
289
  * @example Basic string validation
@@ -93,7 +292,9 @@ function inferMetavar(schema) {
93
292
  * import { zod } from "@optique/zod";
94
293
  * import { option } from "@optique/core/primitives";
95
294
  *
96
- * const email = option("--email", zod(z.string().email()));
295
+ * const email = option("--email",
296
+ * zod(z.string().email(), { placeholder: "" }),
297
+ * );
97
298
  * ```
98
299
  *
99
300
  * @example Number validation with coercion
@@ -104,7 +305,7 @@ function inferMetavar(schema) {
104
305
  *
105
306
  * // Use z.coerce for non-string types since CLI args are always strings
106
307
  * const port = option("-p", "--port",
107
- * zod(z.coerce.number().int().min(1024).max(65535))
308
+ * zod(z.coerce.number().int().min(1024).max(65535), { placeholder: 1024 }),
108
309
  * );
109
310
  * ```
110
311
  *
@@ -115,7 +316,7 @@ function inferMetavar(schema) {
115
316
  * import { option } from "@optique/core/primitives";
116
317
  *
117
318
  * const logLevel = option("--log-level",
118
- * zod(z.enum(["debug", "info", "warn", "error"]))
319
+ * zod(z.enum(["debug", "info", "warn", "error"]), { placeholder: "debug" }),
119
320
  * );
120
321
  * ```
121
322
  *
@@ -126,49 +327,139 @@ function inferMetavar(schema) {
126
327
  * import { message } from "@optique/core/message";
127
328
  * import { option } from "@optique/core/primitives";
128
329
  *
129
- * const email = option("--email", zod(z.string().email(), {
130
- * metavar: "EMAIL",
131
- * errors: {
132
- * zodError: (error, input) =>
133
- * message`Please provide a valid email address, got ${input}.`
134
- * }
135
- * }));
330
+ * const email = option("--email",
331
+ * zod(z.string().email(), {
332
+ * placeholder: "",
333
+ * metavar: "EMAIL",
334
+ * errors: {
335
+ * zodError: (error, input) =>
336
+ * message`Please provide a valid email address, got ${input}.`
337
+ * },
338
+ * }),
339
+ * );
136
340
  * ```
137
341
  *
342
+ * @throws {TypeError} If `options` is missing, not an object, or does not
343
+ * include `placeholder`.
344
+ * @throws {TypeError} If the resolved `metavar` is an empty string.
345
+ * @throws {TypeError} If the schema contains async refinements or other async
346
+ * operations that cannot be executed synchronously.
138
347
  * @since 0.7.0
139
348
  */
140
- function zod(schema, options = {}) {
141
- return {
142
- $mode: "sync",
143
- metavar: options.metavar ?? inferMetavar(schema),
144
- parse(input) {
145
- const result = schema.safeParse(input);
146
- if (result.success) return {
147
- success: true,
148
- value: result.data
349
+ function zod(schema, options) {
350
+ if (options == null || typeof options !== "object") throw new TypeError("zod() requires an options object with a placeholder property.");
351
+ if (!("placeholder" in options)) throw new TypeError("zod() options must include a placeholder property.");
352
+ const choices = inferChoices(schema);
353
+ const boolInfo = analyzeBooleanSchema(schema);
354
+ const metavar = options.metavar ?? (boolInfo.isBoolean ? "BOOLEAN" : inferMetavar(schema));
355
+ ensureNonEmptyString(metavar);
356
+ function doSafeParse(input, rawInput) {
357
+ let result;
358
+ try {
359
+ result = schema.safeParse(input);
360
+ } catch (error) {
361
+ if (error instanceof Error && isZodAsyncError(error)) throw new TypeError("Async Zod schemas (e.g., async refinements) are not supported by zod(). Use synchronous schemas instead.");
362
+ throw error;
363
+ }
364
+ if (result.success) return {
365
+ success: true,
366
+ value: result.data
367
+ };
368
+ if (options.errors?.zodError) return {
369
+ success: false,
370
+ error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, rawInput) : options.errors.zodError
371
+ };
372
+ const zodModule = schema;
373
+ if (typeof zodModule.constructor?.prettifyError === "function") try {
374
+ const pretty = zodModule.constructor.prettifyError(result.error);
375
+ return {
376
+ success: false,
377
+ error: message`${pretty}`
149
378
  };
150
- if (options.errors?.zodError) return {
379
+ } catch {}
380
+ const firstError = result.error.issues[0];
381
+ return {
382
+ success: false,
383
+ error: message`${firstError?.message ?? "Validation failed"}`
384
+ };
385
+ }
386
+ /**
387
+ * Handles a failed boolean literal pre-conversion.
388
+ *
389
+ * - *Non-coerced* (`z.boolean()`): falls through to `doSafeParse`
390
+ * so that catch/default, custom errors, and async detection all
391
+ * work. This is safe because `safeParse(string)` fails at the
392
+ * type level before any refinements execute.
393
+ * - *Coerced* (`z.coerce.boolean()`): runs the lazy async probe
394
+ * (if not yet completed), then returns the pre-conversion error
395
+ * or delegates to the custom `zodError` callback.
396
+ */
397
+ function handleBooleanLiteralError(boolResult, rawInput) {
398
+ if (!boolInfo.isCoerced) return doSafeParse(rawInput, rawInput);
399
+ if (options.errors?.zodError) {
400
+ if (typeof options.errors.zodError !== "function") return {
151
401
  success: false,
152
- error: typeof options.errors.zodError === "function" ? options.errors.zodError(result.error, input) : options.errors.zodError
402
+ error: options.errors.zodError
153
403
  };
154
- const zodModule = schema;
155
- if (typeof zodModule.constructor?.prettifyError === "function") try {
156
- const pretty = zodModule.constructor.prettifyError(result.error);
157
- return {
158
- success: false,
159
- error: message`${pretty}`
160
- };
161
- } catch {}
162
- const firstError = result.error.issues[0];
404
+ const zodError = new ZodError([{
405
+ code: "invalid_type",
406
+ expected: "boolean",
407
+ message: `Invalid Boolean value: ${rawInput}`,
408
+ path: []
409
+ }]);
163
410
  return {
164
411
  success: false,
165
- error: message`${firstError?.message ?? "Validation failed"}`
412
+ error: options.errors.zodError(zodError, rawInput)
166
413
  };
414
+ }
415
+ return boolResult;
416
+ }
417
+ const parser = {
418
+ mode: "sync",
419
+ metavar,
420
+ placeholder: options.placeholder,
421
+ ...boolInfo.exposeChoices ? {
422
+ choices: Object.freeze([true, false]),
423
+ suggest(prefix) {
424
+ const allLiterals = [...BOOL_TRUE_LITERALS, ...BOOL_FALSE_LITERALS];
425
+ const normalizedPrefix = prefix.toLowerCase();
426
+ return allLiterals.filter((lit) => lit.startsWith(normalizedPrefix)).map((lit) => ({
427
+ kind: "literal",
428
+ text: lit
429
+ }));
430
+ }
431
+ } : choices != null && choices.length > 0 ? {
432
+ choices: Object.freeze(choices),
433
+ *suggest(prefix) {
434
+ for (const c of choices) if (c.startsWith(prefix)) yield {
435
+ kind: "literal",
436
+ text: c
437
+ };
438
+ }
439
+ } : {},
440
+ parse(input) {
441
+ if (boolInfo.isBoolean) {
442
+ const boolResult = preConvertBoolean(input);
443
+ if (!boolResult.success) return handleBooleanLiteralError(boolResult, input);
444
+ return doSafeParse(boolResult.value, input);
445
+ }
446
+ return doSafeParse(input, input);
167
447
  },
168
448
  format(value) {
169
- return String(value);
449
+ if (options.format) return options.format(value);
450
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
451
+ if (typeof value !== "object" || value === null) return String(value);
452
+ if (Array.isArray(value)) return String(value);
453
+ const str = String(value);
454
+ if (str !== "[object Object]") return str;
455
+ const proto = Object.getPrototypeOf(value);
456
+ if (proto === Object.prototype || proto === null) try {
457
+ return JSON.stringify(value) ?? str;
458
+ } catch {}
459
+ return str;
170
460
  }
171
461
  };
462
+ return parser;
172
463
  }
173
464
 
174
465
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/zod",
3
- "version": "1.0.0-dev.908+f60f037d",
3
+ "version": "1.0.0",
4
4
  "description": "Zod value parsers for Optique",
5
5
  "keywords": [
6
6
  "CLI",
@@ -57,7 +57,7 @@
57
57
  "zod": "^3.25.0 || ^4.0.0"
58
58
  },
59
59
  "dependencies": {
60
- "@optique/core": "1.0.0-dev.908+f60f037d"
60
+ "@optique/core": "1.0.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/node": "^20.19.9",