@crustjs/validate 0.0.1

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,178 @@
1
+ # @crustjs/validate
2
+
3
+ > Experimental: API may change between minor versions.
4
+
5
+ Validation support for the [Crust](https://crustjs.com) CLI framework.
6
+
7
+ ## Entry points
8
+
9
+ | Entry | Import | Purpose |
10
+ | --- | --- | --- |
11
+ | Shared contracts | `@crustjs/validate` | Provider-agnostic types (`ValidatedContext`, `ValidationIssue`) |
12
+ | Effect provider | `@crustjs/validate/effect` | Schema-first command API (`defineEffectCommand`, `arg`, `flag`) |
13
+ | Zod provider | `@crustjs/validate/zod` | Schema-first command API (`defineZodCommand`, `arg`, `flag`) |
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ bun add @crustjs/validate
19
+
20
+ # choose one or both providers
21
+ bun add zod
22
+ bun add effect
23
+ ```
24
+
25
+ ## Effect schema-first mode (`defineEffectCommand`)
26
+
27
+ Define schemas once and let Crust `args`/`flags` definitions be generated automatically.
28
+
29
+ ```ts
30
+ import { runMain } from "@crustjs/core";
31
+ import {
32
+ arg,
33
+ defineEffectCommand,
34
+ flag,
35
+ } from "@crustjs/validate/effect";
36
+ import * as Schema from "effect/Schema";
37
+
38
+ const serve = defineEffectCommand({
39
+ meta: { name: "serve", description: "Start dev server" },
40
+ args: [
41
+ arg("port", Schema.Number.annotations({ description: "Port to listen on" })),
42
+ arg("host", Schema.UndefinedOr(
43
+ Schema.String.annotations({ description: "Host to bind" }),
44
+ )),
45
+ ],
46
+ flags: {
47
+ verbose: flag(
48
+ Schema.Boolean.annotations({ description: "Enable verbose logging" }),
49
+ { alias: "v" },
50
+ ),
51
+ format: flag(
52
+ Schema.Literal("json", "text").annotations({ description: "Output format" }),
53
+ { alias: "f" },
54
+ ),
55
+ },
56
+ run({ args, flags, input }) {
57
+ // args: { port: number; host: string | undefined }
58
+ // flags: { verbose: boolean; format: "json" | "text" }
59
+ console.log(args.port, args.host, flags.verbose, flags.format);
60
+ console.log(input.args, input.flags); // original parser output
61
+ },
62
+ });
63
+
64
+ runMain(serve);
65
+ ```
66
+
67
+ ### Effect schema support
68
+
69
+ - Primitive schemas: `Schema.String`, `Schema.Number`, `Schema.Boolean`
70
+ - Enums/literals: `Schema.Enums(...)`, `Schema.Literal(...)`
71
+ - Arrays: `Schema.Array(...)` and array-like tuple rest schemas
72
+ - Wrappers: refinement/transformation/suspend wrappers are unwrapped for parser-shape analysis
73
+ - Descriptions: `schema.annotations({ description: "..." })` — auto-extracted through wrappers
74
+
75
+ For optional args/flags, use schemas whose encoded input allows `undefined` (for example `Schema.UndefinedOr(Schema.String)`).
76
+
77
+ ## Zod schema-first mode (`defineZodCommand`)
78
+
79
+ Define schemas once and let Crust `args`/`flags` definitions be generated automatically.
80
+
81
+ ```ts
82
+ import { runMain } from "@crustjs/core";
83
+ import { arg, defineZodCommand, flag } from "@crustjs/validate/zod";
84
+ import { z } from "zod";
85
+
86
+ const serve = defineZodCommand({
87
+ meta: { name: "serve", description: "Start dev server" },
88
+ args: [
89
+ arg("port", z.number().int().min(1).max(65535).describe("Port to listen on")),
90
+ arg("host", z.string().default("localhost").describe("Host to bind")),
91
+ ],
92
+ flags: {
93
+ verbose: flag(
94
+ z.boolean().default(false).describe("Enable verbose logging"),
95
+ { alias: "v" },
96
+ ),
97
+ format: flag(
98
+ z.enum(["json", "text"]).default("text").describe("Output format"),
99
+ { alias: "f" },
100
+ ),
101
+ },
102
+ run({ args, flags, input }) {
103
+ // args: { port: number; host: string }
104
+ // flags: { verbose: boolean; format: "json" | "text" }
105
+ console.log(args.port, args.host, flags.verbose, flags.format);
106
+ console.log(input.args, input.flags); // original parser output
107
+ },
108
+ });
109
+
110
+ runMain(serve);
111
+ ```
112
+
113
+ ### Zod schema support
114
+
115
+ - Primitive schemas: `z.string()`, `z.number()`, `z.boolean()`
116
+ - Enums/literals: `z.enum(...)`, `z.literal(...)`
117
+ - Arrays: `z.array(...)` for flags with `multiple: true`
118
+ - Wrappers: `.optional()`, `.default()`, `.nullable()`, `.transform()`, `.pipe()`
119
+ - Descriptions: `.describe("...")` — auto-extracted through wrappers
120
+
121
+ ### Positional args
122
+
123
+ - Use ordered `arg(name, schema, options?)` entries.
124
+ - Optional/default schemas become optional CLI args (`[name]`).
125
+ - Variadic args use `{ variadic: true }` and must be last.
126
+
127
+ ```ts
128
+ args: [
129
+ arg("mode", z.string()),
130
+ arg("files", z.string(), { variadic: true }),
131
+ ];
132
+ ```
133
+
134
+ ### Flags
135
+
136
+ - Pass plain Zod schemas or `flag(schema, options?)` wrappers.
137
+ - Use `flag(..., { alias })` for short aliases.
138
+ - Use `.describe("...")` on the schema for help text.
139
+
140
+ ```ts
141
+ flags: {
142
+ debug: z.boolean().default(false).describe("Enable debug mode"),
143
+ outDir: flag(z.string().default("dist").describe("Output directory"), { alias: "o" }),
144
+ };
145
+ ```
146
+
147
+ ### Help plugin compatibility
148
+
149
+ Generated definitions are compatible with `helpPlugin`.
150
+
151
+ ```ts
152
+ import { runMain } from "@crustjs/core";
153
+ import { helpPlugin } from "@crustjs/plugins";
154
+
155
+ runMain(serve, { plugins: [helpPlugin()] });
156
+ ```
157
+
158
+ ### Lifecycle hooks
159
+
160
+ `defineZodCommand` supports `preRun` and `postRun` passthrough hooks.
161
+
162
+ - Both hooks receive the core `CommandContext` (raw parser output).
163
+ - Schema validation/transforms run inside `run`, so validated values are only
164
+ available in the schema-first `run` handler.
165
+
166
+ ## Validation errors
167
+
168
+ Failures throw `CrustError("VALIDATION")`.
169
+
170
+ - Message: bullet-list output with dot paths (for example `args.port`, `flags.verbose`).
171
+ - Structured issues: available on both `error.details.issues` and `error.cause`.
172
+
173
+ ## v1 constraints
174
+
175
+ - Args and flags only (no env/config validation).
176
+ - Effect mode currently supports context-free schemas only (`R = never`).
177
+ - Zod mode requires Zod 4+.
178
+ - No automatic schema inheritance across subcommands.
@@ -0,0 +1,148 @@
1
+ import { AnyCommand as AnyCommand2, ValidateFlagAliases, ValidateVariadicArgs } from "@crustjs/core";
2
+ import { CommandContext, CommandDef } from "@crustjs/core";
3
+ import * as schema from "effect/Schema";
4
+ import { AnyCommand } from "@crustjs/core";
5
+ /**
6
+ * Extended command context passed to validated handlers.
7
+ *
8
+ * After validation, `args` and `flags` contain the transformed schema output.
9
+ * The original pre-validation parsed values are preserved in `input` for
10
+ * advanced or debug use.
11
+ */
12
+ interface ValidatedContext<
13
+ ArgsOut,
14
+ FlagsOut
15
+ > {
16
+ /** Transformed positional arguments after schema validation */
17
+ args: ArgsOut;
18
+ /** Transformed flags after schema validation */
19
+ flags: FlagsOut;
20
+ /** Raw arguments that appeared after the `--` separator */
21
+ rawArgs: string[];
22
+ /** The resolved command being executed */
23
+ command: AnyCommand;
24
+ /** Original pre-validation parsed values from the Crust parser */
25
+ input: {
26
+ /** Original parsed args before schema transformation */
27
+ args: Record<string, unknown>;
28
+ /** Original parsed flags before schema transformation */
29
+ flags: Record<string, unknown>;
30
+ };
31
+ }
32
+ /**
33
+ * An Effect schema used by the Effect entrypoint.
34
+ *
35
+ * v1 intentionally supports context-free schemas only (`R = never`).
36
+ * Schemas must also be **synchronous** — async combinators such as
37
+ * `Schema.filterEffect` or async `Schema.transformOrFail` will cause
38
+ * `Effect.runSync` to throw at runtime.
39
+ */
40
+ type EffectSchemaLike = schema.Schema.AnyNoContext;
41
+ /** Infer output type from an Effect schema. */
42
+ type InferSchemaOutput<S> = S extends schema.Schema<infer A, infer _I, infer _R> ? A : never;
43
+ /** Optional metadata for a positional argument declared with `arg()`. */
44
+ interface ArgOptions {
45
+ /** Collect remaining positionals into this arg as an array. */
46
+ readonly variadic?: true;
47
+ }
48
+ /** A single positional argument spec produced by `arg()`. */
49
+ interface ArgSpec<
50
+ Name extends string = string,
51
+ SchemaType extends EffectSchemaLike = EffectSchemaLike,
52
+ Variadic extends true | undefined = true | undefined
53
+ > {
54
+ readonly kind: "arg";
55
+ readonly name: Name;
56
+ readonly schema: SchemaType;
57
+ readonly variadic: Variadic;
58
+ }
59
+ /** Ordered positional argument specs. */
60
+ type ArgSpecs = readonly ArgSpec[];
61
+ /** Output type for one ArgSpec in the validated handler context. */
62
+ type InferArgValue<S extends ArgSpec> = S["variadic"] extends true ? InferSchemaOutput<S["schema"]>[] : InferSchemaOutput<S["schema"]>;
63
+ /** Flattens an intersection of objects for readable inferred types. */
64
+ type Simplify<T> = { [K in keyof T] : T[K] };
65
+ /** Recursively maps ordered ArgSpec entries to a named output object type. */
66
+ type InferArgsFromTuple<A extends readonly ArgSpec[]> = A extends readonly [infer Head extends ArgSpec, ...infer Tail extends readonly ArgSpec[]] ? { [K in Head["name"]] : InferArgValue<Head> } & InferArgsFromTuple<Tail> : {};
67
+ /** Infer validated args object type from ordered ArgSpec entries. */
68
+ type InferArgsFromSpecs<A extends ArgSpecs> = Simplify<InferArgsFromTuple<A>>;
69
+ /** Optional metadata for a flag declared with `flag()`. */
70
+ interface FlagOptions {
71
+ /** Short alias or array of aliases (e.g. `"v"` or `["v", "V"]`). */
72
+ readonly alias?: string | readonly string[];
73
+ }
74
+ /** A named flag schema wrapper produced by `flag()`. */
75
+ interface FlagSpec<
76
+ SchemaType extends EffectSchemaLike = EffectSchemaLike,
77
+ Alias extends string | readonly string[] | undefined = string | readonly string[] | undefined
78
+ > {
79
+ readonly kind: "flag";
80
+ readonly schema: SchemaType;
81
+ readonly alias: Alias;
82
+ }
83
+ /** Allowed value shape for `flags` in `defineEffectCommand()`. */
84
+ type FlagShape = Record<string, EffectSchemaLike | FlagSpec>;
85
+ /** Extract the schema from a flag shape value (plain schema or `flag()` wrapper). */
86
+ type ExtractFlagSchema<V> = V extends FlagSpec<infer S> ? S : V extends EffectSchemaLike ? V : never;
87
+ /** Infer validated flags object type from the flags shape. */
88
+ type InferFlagsFromShape<F extends FlagShape> = { [K in keyof F] : InferSchemaOutput<ExtractFlagSchema<F[K]>> };
89
+ /** Handler type for `defineEffectCommand()` with validated/transformed context. */
90
+ type EffectCommandRunHandler<
91
+ ArgsOut,
92
+ FlagsOut
93
+ > = (context: ValidatedContext<ArgsOut, FlagsOut>) => void | Promise<void>;
94
+ /** Infer args output type from command config args. */
95
+ type InferArgsFromConfig<A> = A extends ArgSpecs ? InferArgsFromSpecs<A> : Record<string, never>;
96
+ /** Infer flags output type from command config flags. */
97
+ type InferFlagsFromConfig<F> = F extends FlagShape ? InferFlagsFromShape<F> : Record<string, never>;
98
+ type EffectOverriddenKeys = "args" | "flags" | "run" | "preRun" | "postRun";
99
+ /** Config for `defineEffectCommand()` using `arg()` + `flag()` schema-first DSL. */
100
+ interface EffectCommandDef<
101
+ A extends ArgSpecs | undefined = undefined,
102
+ F extends FlagShape | undefined = undefined
103
+ > extends Omit<CommandDef, EffectOverriddenKeys> {
104
+ /** Ordered positional args as `arg()` specs. */
105
+ readonly args?: A;
106
+ /** Named flags as plain schemas or `flag()` wrappers. */
107
+ readonly flags?: F;
108
+ /** Optional setup hook before schema validation runs. */
109
+ readonly preRun?: (context: CommandContext) => void | Promise<void>;
110
+ /** Main handler with validated/transformed args and flags. */
111
+ readonly run?: EffectCommandRunHandler<InferArgsFromConfig<A>, InferFlagsFromConfig<F>>;
112
+ /** Optional teardown hook after command execution. */
113
+ readonly postRun?: (context: CommandContext) => void | Promise<void>;
114
+ }
115
+ /**
116
+ * Define a Crust command where Effect schemas are the source of truth.
117
+ *
118
+ * Only context-free (`R = never`), synchronous schemas are supported.
119
+ * Async combinators like `Schema.filterEffect` or async `Schema.transformOrFail`
120
+ * will throw at runtime.
121
+ */
122
+ declare function defineEffectCommand<
123
+ const A extends readonly ArgSpec[] | undefined,
124
+ const F extends FlagShape | undefined
125
+ >(config: EffectCommandDef<A, F> & {
126
+ args?: A extends readonly object[] ? ValidateVariadicArgs<A> : A;
127
+ flags?: F extends Record<string, unknown> ? ValidateFlagAliases<F> : F;
128
+ }): AnyCommand2;
129
+ /**
130
+ * Define a named positional argument schema for `defineEffectCommand()`.
131
+ */
132
+ declare function arg<
133
+ Name extends string,
134
+ SchemaType extends EffectSchemaLike,
135
+ const Variadic extends true | undefined = undefined
136
+ >(name: Name, schema: SchemaType, options?: ArgOptions & {
137
+ variadic?: Variadic;
138
+ }): ArgSpec<Name, SchemaType, Variadic>;
139
+ /**
140
+ * Define a flag schema for `defineEffectCommand()` with optional alias metadata.
141
+ */
142
+ declare function flag<
143
+ SchemaType extends EffectSchemaLike,
144
+ const Alias extends string | readonly string[] | undefined = undefined
145
+ >(schema: SchemaType, options?: FlagOptions & {
146
+ alias?: Alias;
147
+ }): FlagSpec<SchemaType, Alias>;
148
+ export { flag, defineEffectCommand, arg, InferSchemaOutput, InferFlagsFromShape, InferFlagsFromConfig, InferArgsFromSpecs, InferArgsFromConfig, FlagSpec, FlagShape, FlagOptions, EffectSchemaLike, EffectCommandRunHandler, EffectCommandDef, ArgSpecs, ArgSpec, ArgOptions };
@@ -0,0 +1,274 @@
1
+ // @bun
2
+ import {
3
+ buildArgDefinitions,
4
+ buildFlagDefinitions,
5
+ buildRunHandler,
6
+ normalizeIssues
7
+ } from "../shared/chunk-zfhm7pmv.js";
8
+
9
+ // src/effect/command.ts
10
+ import { defineCommand } from "@crustjs/core";
11
+ import { either, runSync } from "effect/Effect";
12
+ import * as Either from "effect/Either";
13
+ import * as ParseResult from "effect/ParseResult";
14
+ import { decodeUnknown } from "effect/Schema";
15
+
16
+ // src/effect/definitions.ts
17
+ import { CrustError as CrustError2 } from "@crustjs/core";
18
+ import { encodedSchema } from "effect/Schema";
19
+
20
+ // src/effect/schema.ts
21
+ import { CrustError } from "@crustjs/core";
22
+ import { isSome } from "effect/Option";
23
+ import { isSchema } from "effect/Schema";
24
+ import { getDescriptionAnnotation } from "effect/SchemaAST";
25
+ function arg(name, schema, options) {
26
+ if (!name.trim()) {
27
+ throw new CrustError("DEFINITION", "arg(): name is required and must be a non-empty string");
28
+ }
29
+ if (!isSchema(schema)) {
30
+ throw new CrustError("DEFINITION", `arg("${name}"): schema must be an Effect schema`);
31
+ }
32
+ return {
33
+ kind: "arg",
34
+ name,
35
+ schema,
36
+ variadic: options?.variadic
37
+ };
38
+ }
39
+ function flag(schema, options) {
40
+ if (!isSchema(schema)) {
41
+ throw new CrustError("DEFINITION", "flag(): schema must be an Effect schema");
42
+ }
43
+ return {
44
+ kind: "flag",
45
+ schema,
46
+ alias: options?.alias
47
+ };
48
+ }
49
+ function resolveDescription(schema) {
50
+ return resolveDescriptionFromAst(schema.ast);
51
+ }
52
+ function resolveDescriptionFromAst(ast) {
53
+ const seen = new Set;
54
+ let current = ast;
55
+ for (;; ) {
56
+ if (seen.has(current)) {
57
+ return;
58
+ }
59
+ seen.add(current);
60
+ const annotated = getDescriptionAnnotation(current);
61
+ if (isSome(annotated) && typeof annotated.value === "string") {
62
+ return annotated.value;
63
+ }
64
+ if (current._tag === "Transformation") {
65
+ current = current.from;
66
+ continue;
67
+ }
68
+ if (current._tag === "Refinement") {
69
+ current = current.from;
70
+ continue;
71
+ }
72
+ if (current._tag === "Suspend") {
73
+ current = current.f();
74
+ continue;
75
+ }
76
+ if (current._tag === "Union") {
77
+ for (const member of current.types) {
78
+ if (member._tag === "UndefinedKeyword") {
79
+ continue;
80
+ }
81
+ const desc = resolveDescriptionFromAst(member);
82
+ if (desc !== undefined) {
83
+ return desc;
84
+ }
85
+ }
86
+ return;
87
+ }
88
+ return;
89
+ }
90
+ }
91
+
92
+ // src/effect/definitions.ts
93
+ function unwrapInputAst(ast) {
94
+ let current = ast;
95
+ const seen = new Set;
96
+ for (;; ) {
97
+ if (seen.has(current)) {
98
+ return current;
99
+ }
100
+ seen.add(current);
101
+ if (current._tag === "Refinement") {
102
+ current = current.from;
103
+ continue;
104
+ }
105
+ if (current._tag === "Transformation") {
106
+ current = current.from;
107
+ continue;
108
+ }
109
+ if (current._tag === "Suspend") {
110
+ current = current.f();
111
+ continue;
112
+ }
113
+ return current;
114
+ }
115
+ }
116
+ function resolveEnumInputType(enums) {
117
+ let kind;
118
+ for (const [, value] of enums) {
119
+ if (typeof value !== "string" && typeof value !== "number") {
120
+ return;
121
+ }
122
+ const current = typeof value === "string" ? "string" : "number";
123
+ if (kind === undefined) {
124
+ kind = current;
125
+ continue;
126
+ }
127
+ if (kind !== current) {
128
+ return;
129
+ }
130
+ }
131
+ return kind;
132
+ }
133
+ function resolvePrimitiveInputType(ast) {
134
+ const unwrapped = unwrapInputAst(ast);
135
+ if (unwrapped._tag === "StringKeyword" || unwrapped._tag === "TemplateLiteral") {
136
+ return "string";
137
+ }
138
+ if (unwrapped._tag === "NumberKeyword") {
139
+ return "number";
140
+ }
141
+ if (unwrapped._tag === "BooleanKeyword") {
142
+ return "boolean";
143
+ }
144
+ if (unwrapped._tag === "Literal") {
145
+ if (typeof unwrapped.literal === "string")
146
+ return "string";
147
+ if (typeof unwrapped.literal === "number")
148
+ return "number";
149
+ if (typeof unwrapped.literal === "boolean")
150
+ return "boolean";
151
+ return;
152
+ }
153
+ if (unwrapped._tag === "Enums") {
154
+ return resolveEnumInputType(unwrapped.enums);
155
+ }
156
+ if (unwrapped._tag === "Union") {
157
+ let kind;
158
+ for (const member of unwrapped.types) {
159
+ const resolved = resolvePrimitiveInputType(member);
160
+ if (resolved === undefined) {
161
+ if (unwrapInputAst(member)._tag === "UndefinedKeyword") {
162
+ continue;
163
+ }
164
+ return;
165
+ }
166
+ if (kind === undefined) {
167
+ kind = resolved;
168
+ continue;
169
+ }
170
+ if (kind !== resolved) {
171
+ return;
172
+ }
173
+ }
174
+ return kind;
175
+ }
176
+ return;
177
+ }
178
+ function resolveTupleArrayShape(ast, label) {
179
+ if (ast._tag !== "TupleType") {
180
+ return;
181
+ }
182
+ if (ast.elements.length > 0) {
183
+ throw new CrustError2("DEFINITION", `${label}: tuple schemas with fixed elements are not supported for CLI parsing. Use Schema.Array(T) for repeatable arguments.`);
184
+ }
185
+ if (ast.rest.length !== 1) {
186
+ throw new CrustError2("DEFINITION", `${label}: tuple schemas are not supported for CLI parsing. Use scalar schemas or array schemas with a single element type.`);
187
+ }
188
+ const rest = ast.rest[0];
189
+ if (!rest) {
190
+ throw new CrustError2("DEFINITION", `${label}: unable to inspect array element schema`);
191
+ }
192
+ const primitive = resolvePrimitiveInputType(rest.type);
193
+ if (!primitive) {
194
+ throw new CrustError2("DEFINITION", `${label}: array element type must be string, number, or boolean`);
195
+ }
196
+ return { type: primitive, multiple: true };
197
+ }
198
+ function resolveInputShape(schema, label) {
199
+ const ast = unwrapInputAst(encodedSchema(schema).ast);
200
+ const tupleShape = resolveTupleArrayShape(ast, label);
201
+ if (tupleShape) {
202
+ return tupleShape;
203
+ }
204
+ const primitive = resolvePrimitiveInputType(ast);
205
+ if (primitive) {
206
+ return { type: primitive, multiple: false };
207
+ }
208
+ throw new CrustError2("DEFINITION", `${label}: unsupported schema type for CLI parsing. Use string, number, boolean, enum/literal, or array of these.`);
209
+ }
210
+ function acceptsUndefined(ast) {
211
+ const unwrapped = unwrapInputAst(ast);
212
+ if (unwrapped._tag === "UndefinedKeyword") {
213
+ return true;
214
+ }
215
+ if (unwrapped._tag === "Union") {
216
+ return unwrapped.types.some((member) => acceptsUndefined(member));
217
+ }
218
+ return false;
219
+ }
220
+ function isOptionalInputSchema(schema) {
221
+ return acceptsUndefined(encodedSchema(schema).ast);
222
+ }
223
+ var effectAdapter = {
224
+ resolveInputShape,
225
+ isOptionalInputSchema,
226
+ resolveDescription,
227
+ commandLabel: "defineEffectCommand",
228
+ arrayHint: "Schema.Array(...)"
229
+ };
230
+ function argsToDefinitions(args) {
231
+ return buildArgDefinitions(args, effectAdapter);
232
+ }
233
+ function flagsToDefinitions(flags) {
234
+ return buildFlagDefinitions(flags, effectAdapter);
235
+ }
236
+
237
+ // src/effect/command.ts
238
+ function validateValue(schema, value, prefix) {
239
+ const result = runSync(either(decodeUnknown(schema)(value)));
240
+ if (Either.isRight(result)) {
241
+ return { ok: true, value: result.right };
242
+ }
243
+ const flattened = ParseResult.ArrayFormatter.formatErrorSync(result.left);
244
+ const prefixed = flattened.map((issue) => ({
245
+ message: issue.message,
246
+ path: [...prefix, ...issue.path]
247
+ }));
248
+ return { ok: false, issues: normalizeIssues(prefixed) };
249
+ }
250
+ function defineEffectCommand(config) {
251
+ const {
252
+ args: effectArgs,
253
+ flags: effectFlags,
254
+ run: userRun,
255
+ ...passthrough
256
+ } = config;
257
+ const argSpecs = effectArgs ?? [];
258
+ const generatedArgs = argsToDefinitions(argSpecs);
259
+ const generatedFlags = flagsToDefinitions(effectFlags);
260
+ const command = defineCommand({
261
+ ...passthrough,
262
+ ...generatedArgs.length > 0 && { args: generatedArgs },
263
+ ...Object.keys(generatedFlags).length > 0 && { flags: generatedFlags },
264
+ ...userRun && {
265
+ run: buildRunHandler(argSpecs, effectFlags, userRun, validateValue)
266
+ }
267
+ });
268
+ return command;
269
+ }
270
+ export {
271
+ flag,
272
+ defineEffectCommand,
273
+ arg
274
+ };
@@ -0,0 +1,42 @@
1
+ import { AnyCommand } from "@crustjs/core";
2
+ /**
3
+ * A normalized validation issue used internally across both entrypoints.
4
+ *
5
+ * Provider issues may use path arrays. This type normalizes them to a
6
+ * flat string-based dot-path for consistent rendering
7
+ * and programmatic consumption.
8
+ */
9
+ interface ValidationIssue {
10
+ /** Human-readable error message for this issue. */
11
+ readonly message: string;
12
+ /** Dot-path string describing the location of the issue (e.g. `"flags.verbose"`, `"args[0]"`). Empty string for root-level issues. */
13
+ readonly path: string;
14
+ }
15
+ /**
16
+ * Extended command context passed to validated handlers.
17
+ *
18
+ * After validation, `args` and `flags` contain the transformed schema output.
19
+ * The original pre-validation parsed values are preserved in `input` for
20
+ * advanced or debug use.
21
+ */
22
+ interface ValidatedContext<
23
+ ArgsOut,
24
+ FlagsOut
25
+ > {
26
+ /** Transformed positional arguments after schema validation */
27
+ args: ArgsOut;
28
+ /** Transformed flags after schema validation */
29
+ flags: FlagsOut;
30
+ /** Raw arguments that appeared after the `--` separator */
31
+ rawArgs: string[];
32
+ /** The resolved command being executed */
33
+ command: AnyCommand;
34
+ /** Original pre-validation parsed values from the Crust parser */
35
+ input: {
36
+ /** Original parsed args before schema transformation */
37
+ args: Record<string, unknown>;
38
+ /** Original parsed flags before schema transformation */
39
+ flags: Record<string, unknown>;
40
+ };
41
+ }
42
+ export { ValidationIssue, ValidatedContext };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ // @bun
@@ -0,0 +1,196 @@
1
+ // @bun
2
+ // src/validation.ts
3
+ import { CrustError } from "@crustjs/core";
4
+ function formatPath(path) {
5
+ let result = "";
6
+ for (const segment of path) {
7
+ if (typeof segment === "number") {
8
+ result += `[${String(segment)}]`;
9
+ } else {
10
+ const str = String(segment);
11
+ if (result.length > 0) {
12
+ result += `.${str}`;
13
+ } else {
14
+ result = str;
15
+ }
16
+ }
17
+ }
18
+ return result;
19
+ }
20
+ function normalizeIssue(issue) {
21
+ return {
22
+ message: issue.message,
23
+ path: issue.path ? formatPath(issue.path) : ""
24
+ };
25
+ }
26
+ function normalizeIssues(issues) {
27
+ return issues.map(normalizeIssue);
28
+ }
29
+ function renderIssueLine(issue) {
30
+ if (issue.path) {
31
+ return ` - ${issue.path}: ${issue.message}`;
32
+ }
33
+ return ` - ${issue.message}`;
34
+ }
35
+ function renderBulletList(prefix, issues) {
36
+ if (issues.length === 0)
37
+ return prefix;
38
+ const lines = issues.map(renderIssueLine);
39
+ return `${prefix}
40
+ ${lines.join(`
41
+ `)}`;
42
+ }
43
+ function throwValidationError(issues, prefix = "Validation failed") {
44
+ const message = renderBulletList(prefix, issues);
45
+ throw new CrustError("VALIDATION", message, { issues }).withCause(issues);
46
+ }
47
+
48
+ // src/flagSpec.ts
49
+ function isFlagSpec(value) {
50
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "flag";
51
+ }
52
+ function getFlagSchema(value) {
53
+ return isFlagSpec(value) ? value.schema : value;
54
+ }
55
+
56
+ // src/runner.ts
57
+ async function validateArgs(argSpecs, context, issues, validateValue) {
58
+ const output = {};
59
+ for (const spec of argSpecs) {
60
+ const input = context.args[spec.name];
61
+ if (spec.variadic) {
62
+ const items = Array.isArray(input) ? input : input === undefined ? [] : [input];
63
+ const transformed = [];
64
+ for (let i = 0;i < items.length; i++) {
65
+ const value = items[i];
66
+ const validated2 = await validateValue(spec.schema, value, [
67
+ "args",
68
+ spec.name,
69
+ i
70
+ ]);
71
+ if (!validated2.ok) {
72
+ issues.push(...validated2.issues);
73
+ continue;
74
+ }
75
+ transformed.push(validated2.value);
76
+ }
77
+ output[spec.name] = transformed;
78
+ continue;
79
+ }
80
+ const validated = await validateValue(spec.schema, input, [
81
+ "args",
82
+ spec.name
83
+ ]);
84
+ if (!validated.ok) {
85
+ issues.push(...validated.issues);
86
+ continue;
87
+ }
88
+ output[spec.name] = validated.value;
89
+ }
90
+ return output;
91
+ }
92
+ async function validateFlags(flags, context, issues, validateValue) {
93
+ if (!flags) {
94
+ return {};
95
+ }
96
+ const output = {};
97
+ for (const [name, rawValue] of Object.entries(flags)) {
98
+ const schema = getFlagSchema(rawValue);
99
+ const input = context.flags[name];
100
+ const validated = await validateValue(schema, input, ["flags", name]);
101
+ if (!validated.ok) {
102
+ issues.push(...validated.issues);
103
+ continue;
104
+ }
105
+ output[name] = validated.value;
106
+ }
107
+ return output;
108
+ }
109
+ function buildRunHandler(argSpecs, flags, userRun, validateValue) {
110
+ return async (context) => {
111
+ const issues = [];
112
+ const validatedArgs = await validateArgs(argSpecs, context, issues, validateValue);
113
+ const validatedFlags = await validateFlags(flags, context, issues, validateValue);
114
+ if (issues.length > 0) {
115
+ throwValidationError(issues);
116
+ }
117
+ const validatedContext = {
118
+ args: validatedArgs,
119
+ flags: validatedFlags,
120
+ rawArgs: context.rawArgs,
121
+ command: context.command,
122
+ input: {
123
+ args: context.args,
124
+ flags: context.flags
125
+ }
126
+ };
127
+ return userRun(validatedContext);
128
+ };
129
+ }
130
+
131
+ // src/definitionBuilders.ts
132
+ import { CrustError as CrustError2 } from "@crustjs/core";
133
+ function buildArgDefinitions(args, adapter) {
134
+ const seen = new Set;
135
+ for (let i = 0;i < args.length; i++) {
136
+ const spec = args[i];
137
+ if (!spec)
138
+ continue;
139
+ if (seen.has(spec.name)) {
140
+ throw new CrustError2("DEFINITION", `${adapter.commandLabel}: duplicate arg name "${spec.name}"`);
141
+ }
142
+ seen.add(spec.name);
143
+ if (spec.variadic && i !== args.length - 1) {
144
+ throw new CrustError2("DEFINITION", `${adapter.commandLabel}: only the last arg can be variadic (arg "${spec.name}")`);
145
+ }
146
+ }
147
+ return args.map((spec) => {
148
+ const shape = adapter.resolveInputShape(spec.schema, `arg "${spec.name}"`);
149
+ if (spec.variadic && shape.multiple) {
150
+ throw new CrustError2("DEFINITION", `arg "${spec.name}": variadic args must use a scalar schema; do not wrap the schema in ${adapter.arrayHint}`);
151
+ }
152
+ if (!spec.variadic && shape.multiple) {
153
+ throw new CrustError2("DEFINITION", `arg "${spec.name}": array schema requires { variadic: true }`);
154
+ }
155
+ const description = adapter.resolveDescription(spec.schema);
156
+ const required = !adapter.isOptionalInputSchema(spec.schema);
157
+ const def = {
158
+ name: spec.name,
159
+ type: shape.type,
160
+ ...description !== undefined && { description },
161
+ ...spec.variadic && { variadic: true },
162
+ ...required && { required: true }
163
+ };
164
+ return def;
165
+ });
166
+ }
167
+ function getFlagMetadata(value) {
168
+ if (isFlagSpec(value)) {
169
+ return { schema: value.schema, alias: value.alias };
170
+ }
171
+ return { schema: value };
172
+ }
173
+ function buildFlagDefinitions(flags, adapter) {
174
+ if (!flags) {
175
+ return {};
176
+ }
177
+ const result = {};
178
+ for (const [name, value] of Object.entries(flags)) {
179
+ const metadata = getFlagMetadata(value);
180
+ const { schema } = metadata;
181
+ const shape = adapter.resolveInputShape(schema, `flag "--${name}"`);
182
+ const required = !adapter.isOptionalInputSchema(schema);
183
+ const description = adapter.resolveDescription(schema);
184
+ const alias = metadata.alias === undefined ? undefined : typeof metadata.alias === "string" ? metadata.alias : [...metadata.alias];
185
+ result[name] = {
186
+ type: shape.type,
187
+ ...shape.multiple && { multiple: true },
188
+ ...alias !== undefined && { alias },
189
+ ...description !== undefined && { description },
190
+ ...required && { required: true }
191
+ };
192
+ }
193
+ return result;
194
+ }
195
+
196
+ export { normalizeIssues, buildRunHandler, buildArgDefinitions, buildFlagDefinitions };
@@ -0,0 +1,212 @@
1
+ import { AnyCommand as AnyCommand2, ValidateFlagAliases, ValidateVariadicArgs } from "@crustjs/core";
2
+ import { CommandContext, CommandDef } from "@crustjs/core";
3
+ import * as z from "zod/v4/core";
4
+ import { AnyCommand } from "@crustjs/core";
5
+ /**
6
+ * Extended command context passed to validated handlers.
7
+ *
8
+ * After validation, `args` and `flags` contain the transformed schema output.
9
+ * The original pre-validation parsed values are preserved in `input` for
10
+ * advanced or debug use.
11
+ */
12
+ interface ValidatedContext<
13
+ ArgsOut,
14
+ FlagsOut
15
+ > {
16
+ /** Transformed positional arguments after schema validation */
17
+ args: ArgsOut;
18
+ /** Transformed flags after schema validation */
19
+ flags: FlagsOut;
20
+ /** Raw arguments that appeared after the `--` separator */
21
+ rawArgs: string[];
22
+ /** The resolved command being executed */
23
+ command: AnyCommand;
24
+ /** Original pre-validation parsed values from the Crust parser */
25
+ input: {
26
+ /** Original parsed args before schema transformation */
27
+ args: Record<string, unknown>;
28
+ /** Original parsed flags before schema transformation */
29
+ flags: Record<string, unknown>;
30
+ };
31
+ }
32
+ /** A Zod schema used by the Zod entrypoint. */
33
+ type ZodSchemaLike<
34
+ Input = unknown,
35
+ Output = Input
36
+ > = z.$ZodType<Output, Input>;
37
+ /** Infer output type from a Zod schema. */
38
+ type InferSchemaOutput<S> = S extends z.$ZodType ? z.output<S> : never;
39
+ /** Optional metadata for a positional argument declared with `arg()`. */
40
+ interface ArgOptions {
41
+ /** Collect remaining positionals into this arg as an array. */
42
+ readonly variadic?: true;
43
+ }
44
+ /**
45
+ * A single positional argument spec produced by `arg()`.
46
+ *
47
+ * The `Variadic` generic parameter preserves the literal `true` from
48
+ * `arg()` calls so `ValidateVariadicArgs` can distinguish variadic args
49
+ * from non-variadic ones at compile time.
50
+ *
51
+ * - `ArgSpec<N, S, true>` — variadic arg (only valid in last position)
52
+ * - `ArgSpec<N, S, undefined>` — normal positional arg
53
+ * - `ArgSpec<N, S>` (default) — type-erased form used in constraints
54
+ */
55
+ interface ArgSpec<
56
+ Name extends string = string,
57
+ Schema extends ZodSchemaLike = ZodSchemaLike,
58
+ Variadic extends true | undefined = true | undefined
59
+ > {
60
+ readonly kind: "arg";
61
+ readonly name: Name;
62
+ readonly schema: Schema;
63
+ readonly variadic: Variadic;
64
+ }
65
+ /** Ordered positional argument specs. */
66
+ type ArgSpecs = readonly ArgSpec[];
67
+ /** Output type for one ArgSpec in the validated handler context. */
68
+ type InferArgValue<S extends ArgSpec> = S["variadic"] extends true ? InferSchemaOutput<S["schema"]>[] : InferSchemaOutput<S["schema"]>;
69
+ /** Flattens an intersection of objects for readable inferred types. */
70
+ type Simplify<T> = { [K in keyof T] : T[K] };
71
+ /** Recursively maps ordered ArgSpec entries to a named output object type. */
72
+ type InferArgsFromTuple<A extends readonly ArgSpec[]> = A extends readonly [infer Head extends ArgSpec, ...infer Tail extends readonly ArgSpec[]] ? { [K in Head["name"]] : InferArgValue<Head> } & InferArgsFromTuple<Tail> : {};
73
+ /** Infer validated args object type from ordered ArgSpec entries. */
74
+ type InferArgsFromSpecs<A extends ArgSpecs> = Simplify<InferArgsFromTuple<A>>;
75
+ /** Optional metadata for a flag declared with `flag()`. */
76
+ interface FlagOptions {
77
+ /** Short alias or array of aliases (e.g. `"v"` or `["v", "V"]`). */
78
+ readonly alias?: string | readonly string[];
79
+ }
80
+ /**
81
+ * A named flag schema wrapper produced by `flag()`.
82
+ *
83
+ * The `Alias` generic parameter preserves alias literals (e.g. `"v"` or
84
+ * `readonly ["v", "V"]`) from `flag()` calls so `ValidateFlagAliases` can
85
+ * detect collisions at compile time.
86
+ *
87
+ * - `FlagSpec<S, "v">` — flag with alias `"v"` (collision-detectable)
88
+ * - `FlagSpec<S, undefined>` — flag without an alias
89
+ * - `FlagSpec<S>` (default) — type-erased form used in constraints
90
+ */
91
+ interface FlagSpec<
92
+ Schema extends ZodSchemaLike = ZodSchemaLike,
93
+ Alias extends string | readonly string[] | undefined = string | readonly string[] | undefined
94
+ > {
95
+ readonly kind: "flag";
96
+ readonly schema: Schema;
97
+ readonly alias: Alias;
98
+ }
99
+ /** Allowed value shape for `flags` in `defineZodCommand()`. */
100
+ type FlagShape = Record<string, ZodSchemaLike | FlagSpec>;
101
+ /** Extract the schema from a flag shape value (plain schema or `flag()` wrapper). */
102
+ type ExtractFlagSchema<V> = V extends FlagSpec<infer S> ? S : V extends ZodSchemaLike ? V : never;
103
+ /** Infer validated flags object type from the flags shape. */
104
+ type InferFlagsFromShape<F extends FlagShape> = { [K in keyof F] : InferSchemaOutput<ExtractFlagSchema<F[K]>> };
105
+ /** Handler type for `defineZodCommand()` with validated/transformed context. */
106
+ type ZodCommandRunHandler<
107
+ ArgsOut,
108
+ FlagsOut
109
+ > = (context: ValidatedContext<ArgsOut, FlagsOut>) => void | Promise<void>;
110
+ /** Infer args output type from command config args. */
111
+ type InferArgsFromConfig<A> = A extends ArgSpecs ? InferArgsFromSpecs<A> : Record<string, never>;
112
+ /** Infer flags output type from command config flags. */
113
+ type InferFlagsFromConfig<F> = F extends FlagShape ? InferFlagsFromShape<F> : Record<string, never>;
114
+ /**
115
+ * Keys from `CommandDef` that `ZodCommandDef` redefines with different types.
116
+ *
117
+ * - `args` / `flags`: Zod schema-based definitions replace core's `ArgsDef`/`FlagsDef`
118
+ * - `run`: receives `ValidatedContext` instead of raw `CommandContext`
119
+ * - `preRun` / `postRun`: use raw `CommandContext` (no `NoInfer` wrapper)
120
+ *
121
+ * All remaining `CommandDef` keys (e.g. `meta`, `subCommands`) are inherited
122
+ * automatically via `Omit`. If a new passthrough field is added to `CommandDef`,
123
+ * it propagates here without changes. The compile-time key exhaustiveness
124
+ * assertion in `command.test.ts` will fail, forcing a review.
125
+ */
126
+ type ZodOverriddenKeys = "args" | "flags" | "run" | "preRun" | "postRun";
127
+ /**
128
+ * Config for `defineZodCommand()` using `arg()` + `flag()` schema-first DSL.
129
+ *
130
+ * Extends `CommandDef` (minus overridden keys) so passthrough fields like
131
+ * `meta` and `subCommands` stay in sync automatically. If a new field is
132
+ * added to `CommandDef`, the key exhaustiveness assertion in
133
+ * `command.test.ts` fails at compile time, forcing a review.
134
+ */
135
+ interface ZodCommandDef<
136
+ A extends ArgSpecs | undefined = undefined,
137
+ F extends FlagShape | undefined = undefined
138
+ > extends Omit<CommandDef, ZodOverriddenKeys> {
139
+ /** Ordered positional args as `arg()` specs. */
140
+ readonly args?: A;
141
+ /** Named flags as plain schemas or `flag()` wrappers. */
142
+ readonly flags?: F;
143
+ /**
144
+ * Optional setup hook before schema validation runs.
145
+ *
146
+ * Receives raw parser output (`CommandContext`), not schema-transformed values.
147
+ */
148
+ readonly preRun?: (context: CommandContext) => void | Promise<void>;
149
+ /** Main handler with validated/transformed args and flags. */
150
+ readonly run?: ZodCommandRunHandler<InferArgsFromConfig<A>, InferFlagsFromConfig<F>>;
151
+ /**
152
+ * Optional teardown hook after command execution.
153
+ *
154
+ * Receives raw parser output (`CommandContext`), not schema-transformed values.
155
+ */
156
+ readonly postRun?: (context: CommandContext) => void | Promise<void>;
157
+ }
158
+ /**
159
+ * Define a Crust command where schemas are the source of truth.
160
+ *
161
+ * Positional args are declared with `arg(name, schema)` in an ordered array,
162
+ * flags are declared as plain schemas or `flag(schema, meta)` wrappers.
163
+ *
164
+ * The factory generates Crust parser/help definitions and runs schema
165
+ * validation after parsing but before user handler execution.
166
+ *
167
+ * Compile-time validation (via intersection branding) catches:
168
+ * - Variadic args that aren't in the last position
169
+ * - Flag alias collisions (alias→name or alias→alias)
170
+ */
171
+ declare function defineZodCommand<
172
+ const A extends readonly ArgSpec[] | undefined,
173
+ const F extends FlagShape | undefined
174
+ >(config: ZodCommandDef<A, F> & {
175
+ args?: A extends readonly object[] ? ValidateVariadicArgs<A> : A;
176
+ flags?: F extends Record<string, unknown> ? ValidateFlagAliases<F> : F;
177
+ }): AnyCommand2;
178
+ /**
179
+ * Define a named positional argument schema for `defineZodCommand()`.
180
+ *
181
+ * The `const Variadic` parameter preserves `{ variadic: true }` as a
182
+ * literal in the return type, enabling compile-time variadic position
183
+ * validation in `defineZodCommand()`.
184
+ *
185
+ * @param name - Positional arg name used in parser output and help text
186
+ * @param schema - Zod schema
187
+ * @param options - Optional CLI metadata (description, variadic)
188
+ */
189
+ declare function arg<
190
+ Name extends string,
191
+ Schema extends ZodSchemaLike,
192
+ const Variadic extends true | undefined = undefined
193
+ >(name: Name, schema: Schema, options?: ArgOptions & {
194
+ variadic?: Variadic;
195
+ }): ArgSpec<Name, Schema, Variadic>;
196
+ /**
197
+ * Define a flag schema for `defineZodCommand()` with optional alias/description.
198
+ *
199
+ * The `const Alias` parameter preserves alias literals (e.g. `"v"` or
200
+ * `readonly ["v", "V"]`) in the return type, enabling compile-time alias
201
+ * collision detection in `defineZodCommand()`.
202
+ *
203
+ * @param schema - Zod schema
204
+ * @param options - Optional flag metadata
205
+ */
206
+ declare function flag<
207
+ Schema extends ZodSchemaLike,
208
+ const Alias extends string | readonly string[] | undefined = undefined
209
+ >(schema: Schema, options?: FlagOptions & {
210
+ alias?: Alias;
211
+ }): FlagSpec<Schema, Alias>;
212
+ export { flag, defineZodCommand, arg, ZodSchemaLike, ZodCommandRunHandler, ZodCommandDef, InferSchemaOutput, InferFlagsFromShape, InferFlagsFromConfig, InferArgsFromSpecs, InferArgsFromConfig, FlagSpec, FlagShape, FlagOptions, ArgSpecs, ArgSpec, ArgOptions };
@@ -0,0 +1,255 @@
1
+ // @bun
2
+ import {
3
+ buildArgDefinitions,
4
+ buildFlagDefinitions,
5
+ buildRunHandler,
6
+ normalizeIssues
7
+ } from "../shared/chunk-zfhm7pmv.js";
8
+
9
+ // src/zod/command.ts
10
+ import { defineCommand } from "@crustjs/core";
11
+ import { safeParseAsync } from "zod/v4/core";
12
+
13
+ // src/zod/definitions.ts
14
+ import { CrustError as CrustError2 } from "@crustjs/core";
15
+
16
+ // src/zod/schema.ts
17
+ import { CrustError } from "@crustjs/core";
18
+ function isZodSchema(value) {
19
+ if (typeof value !== "object" || value === null) {
20
+ return false;
21
+ }
22
+ if (!("_zod" in value)) {
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ function arg(name, schema, options) {
28
+ if (!name.trim()) {
29
+ throw new CrustError("DEFINITION", "arg(): name is required and must be a non-empty string");
30
+ }
31
+ if (!isZodSchema(schema)) {
32
+ throw new CrustError("DEFINITION", `arg("${name}"): schema must be a Zod schema`);
33
+ }
34
+ return {
35
+ kind: "arg",
36
+ name,
37
+ schema,
38
+ variadic: options?.variadic
39
+ };
40
+ }
41
+ function flag(schema, options) {
42
+ if (!isZodSchema(schema)) {
43
+ throw new CrustError("DEFINITION", "flag(): schema must be a Zod schema");
44
+ }
45
+ return {
46
+ kind: "flag",
47
+ schema,
48
+ alias: options?.alias
49
+ };
50
+ }
51
+ function resolveDescription(schema) {
52
+ let current = schema;
53
+ const seen = new Set;
54
+ for (;; ) {
55
+ if (current === undefined || current === null || seen.has(current)) {
56
+ return;
57
+ }
58
+ seen.add(current);
59
+ if (typeof current !== "object") {
60
+ return;
61
+ }
62
+ if ("description" in current && typeof current.description === "string") {
63
+ return current.description;
64
+ }
65
+ const type = "type" in current ? current.type : undefined;
66
+ if (typeof type !== "string") {
67
+ return;
68
+ }
69
+ if (type === "pipe" || type === "transform") {
70
+ const input = current.in;
71
+ if (input !== undefined) {
72
+ current = input;
73
+ continue;
74
+ }
75
+ return;
76
+ }
77
+ if (type === "optional" || type === "nullable" || type === "default" || type === "prefault" || type === "nonoptional" || type === "readonly" || type === "catch") {
78
+ const unwrap = current.unwrap;
79
+ if (typeof unwrap === "function") {
80
+ current = unwrap();
81
+ continue;
82
+ }
83
+ return;
84
+ }
85
+ return;
86
+ }
87
+ }
88
+
89
+ // src/zod/definitions.ts
90
+ function asRuntimeSchema(value) {
91
+ if (typeof value !== "object" || value === null) {
92
+ return;
93
+ }
94
+ return value;
95
+ }
96
+ function getSchemaType(schema) {
97
+ const runtime = asRuntimeSchema(schema);
98
+ if (!runtime || typeof runtime.type !== "string") {
99
+ return;
100
+ }
101
+ return runtime.type;
102
+ }
103
+ function unwrapInputSchema(schema) {
104
+ let current = schema;
105
+ for (;; ) {
106
+ const type = getSchemaType(current);
107
+ const runtime = asRuntimeSchema(current);
108
+ if (!type || !runtime) {
109
+ return current;
110
+ }
111
+ if (type === "pipe" || type === "transform") {
112
+ if (runtime.in === undefined) {
113
+ return current;
114
+ }
115
+ current = runtime.in;
116
+ continue;
117
+ }
118
+ if (type === "optional" || type === "nullable" || type === "default" || type === "prefault" || type === "nonoptional" || type === "readonly" || type === "catch") {
119
+ if (typeof runtime.unwrap !== "function") {
120
+ return current;
121
+ }
122
+ current = runtime.unwrap();
123
+ continue;
124
+ }
125
+ return current;
126
+ }
127
+ }
128
+ function resolvePrimitiveInputType(schema) {
129
+ const type = getSchemaType(schema);
130
+ if (!type) {
131
+ return;
132
+ }
133
+ if (type === "string" || type === "enum") {
134
+ return "string";
135
+ }
136
+ if (type === "number") {
137
+ return "number";
138
+ }
139
+ if (type === "boolean") {
140
+ return "boolean";
141
+ }
142
+ if (type === "literal") {
143
+ const runtime = asRuntimeSchema(schema);
144
+ const first = runtime?.values?.values().next().value;
145
+ if (typeof first === "string") {
146
+ return "string";
147
+ }
148
+ if (typeof first === "number") {
149
+ return "number";
150
+ }
151
+ if (typeof first === "boolean") {
152
+ return "boolean";
153
+ }
154
+ }
155
+ return;
156
+ }
157
+ function resolveInputShape(schema, label) {
158
+ const inputSchema = unwrapInputSchema(schema);
159
+ if (getSchemaType(inputSchema) === "array") {
160
+ const runtime = asRuntimeSchema(inputSchema);
161
+ if (typeof runtime?.unwrap !== "function") {
162
+ throw new CrustError2("DEFINITION", `${label}: unable to inspect array element schema`);
163
+ }
164
+ const elementSchema = unwrapInputSchema(runtime.unwrap());
165
+ const primitive2 = resolvePrimitiveInputType(elementSchema);
166
+ if (primitive2) {
167
+ return { type: primitive2, multiple: true };
168
+ }
169
+ throw new CrustError2("DEFINITION", `${label}: array element type must be string, number, or boolean`);
170
+ }
171
+ const primitive = resolvePrimitiveInputType(inputSchema);
172
+ if (primitive) {
173
+ return { type: primitive, multiple: false };
174
+ }
175
+ throw new CrustError2("DEFINITION", `${label}: unsupported schema type for CLI parsing. Use string, number, boolean, enum/literal, or array of these.`);
176
+ }
177
+ function isOptionalInputSchema(schema) {
178
+ let current = schema;
179
+ for (;; ) {
180
+ const type = getSchemaType(current);
181
+ const runtime = asRuntimeSchema(current);
182
+ if (!type || !runtime) {
183
+ return false;
184
+ }
185
+ if (type === "optional" || type === "default" || type === "prefault" || type === "catch") {
186
+ return true;
187
+ }
188
+ if (type === "pipe" || type === "transform") {
189
+ if (runtime.in === undefined) {
190
+ return false;
191
+ }
192
+ current = runtime.in;
193
+ continue;
194
+ }
195
+ if (type === "nullable" || type === "nonoptional" || type === "readonly") {
196
+ if (typeof runtime.unwrap !== "function") {
197
+ return false;
198
+ }
199
+ current = runtime.unwrap();
200
+ continue;
201
+ }
202
+ return false;
203
+ }
204
+ }
205
+ var zodAdapter = {
206
+ resolveInputShape,
207
+ isOptionalInputSchema,
208
+ resolveDescription,
209
+ commandLabel: "defineZodCommand",
210
+ arrayHint: "z.array(...)"
211
+ };
212
+ function argsToDefinitions(args) {
213
+ return buildArgDefinitions(args, zodAdapter);
214
+ }
215
+ function flagsToDefinitions(flags) {
216
+ return buildFlagDefinitions(flags, zodAdapter);
217
+ }
218
+
219
+ // src/zod/command.ts
220
+ async function validateValue(schema, value, prefix) {
221
+ const parseResult = await safeParseAsync(schema, value);
222
+ if (parseResult.success) {
223
+ return { ok: true, value: parseResult.data };
224
+ }
225
+ const prefixed = parseResult.error.issues.map((issue) => ({
226
+ message: issue.message,
227
+ path: [...prefix, ...issue.path ?? []]
228
+ }));
229
+ return { ok: false, issues: normalizeIssues(prefixed) };
230
+ }
231
+ function defineZodCommand(config) {
232
+ const {
233
+ args: zodArgs,
234
+ flags: zodFlags,
235
+ run: userRun,
236
+ ...passthrough
237
+ } = config;
238
+ const argSpecs = zodArgs ?? [];
239
+ const generatedArgs = argsToDefinitions(argSpecs);
240
+ const generatedFlags = flagsToDefinitions(zodFlags);
241
+ const command = defineCommand({
242
+ ...passthrough,
243
+ ...generatedArgs.length > 0 && { args: generatedArgs },
244
+ ...Object.keys(generatedFlags).length > 0 && { flags: generatedFlags },
245
+ ...userRun && {
246
+ run: buildRunHandler(argSpecs, zodFlags, userRun, validateValue)
247
+ }
248
+ });
249
+ return command;
250
+ }
251
+ export {
252
+ flag,
253
+ defineZodCommand,
254
+ arg
255
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@crustjs/validate",
3
+ "version": "0.0.1",
4
+ "description": "Validation helpers for the Crust CLI framework",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "chenxin-yan",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/chenxin-yan/crust.git",
11
+ "directory": "packages/validate"
12
+ },
13
+ "homepage": "https://crustjs.com",
14
+ "bugs": {
15
+ "url": "https://github.com/chenxin-yan/crust/issues"
16
+ },
17
+ "keywords": [
18
+ "cli",
19
+ "validation",
20
+ "effect",
21
+ "zod",
22
+ "bun",
23
+ "typescript"
24
+ ],
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ },
33
+ "./effect": {
34
+ "import": "./dist/effect/index.js",
35
+ "types": "./dist/effect/index.d.ts"
36
+ },
37
+ "./zod": {
38
+ "import": "./dist/zod/index.js",
39
+ "types": "./dist/zod/index.d.ts"
40
+ }
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "bunup",
47
+ "dev": "bunup --watch",
48
+ "check:types": "tsc --noEmit",
49
+ "test": "bun test"
50
+ },
51
+ "devDependencies": {
52
+ "@crustjs/config": "0.0.0",
53
+ "@crustjs/core": "0.0.6",
54
+ "@crustjs/plugins": "0.0.6",
55
+ "bunup": "^0.16.29",
56
+ "effect": "^3.19.0",
57
+ "zod": "^4.0.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@crustjs/core": "0.0.6",
61
+ "effect": "^3.19.0",
62
+ "zod": "^4.0.0",
63
+ "typescript": "^5"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "effect": {
67
+ "optional": true
68
+ },
69
+ "zod": {
70
+ "optional": true
71
+ }
72
+ }
73
+ }