From f804d663cc143b23b3ff7796a1426bab2abe52c3 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Wed, 15 Jul 2026 12:41:43 +0200 Subject: [PATCH 1/8] refactor(schema): remove internal schema DSL Define the generated-definition meta-schema directly as JSON Schema, eliminating an internal parser and converter used only for that constant. --- AGENTS.md | 2 +- src/core/json-schema/index.ts | 187 ++++---- src/core/schema-dsl/AGENTS.md | 34 -- src/core/schema-dsl/index.ts | 109 ----- src/core/schema-dsl/parse.ts | 262 ----------- src/core/schema-dsl/runtime.ts | 609 ------------------------- src/core/schema-dsl/schema-dsl.test.ts | 425 ----------------- src/core/schema-dsl/to-json-schema.ts | 102 ----- 8 files changed, 108 insertions(+), 1622 deletions(-) delete mode 100644 src/core/schema-dsl/AGENTS.md delete mode 100644 src/core/schema-dsl/index.ts delete mode 100644 src/core/schema-dsl/parse.ts delete mode 100644 src/core/schema-dsl/runtime.ts delete mode 100644 src/core/schema-dsl/schema-dsl.test.ts delete mode 100644 src/core/schema-dsl/to-json-schema.ts diff --git a/AGENTS.md b/AGENTS.md index 2c8ab900..5ae11682 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ Schema-first, fully typed TypeScript CLI framework. Zero runtime deps. In-repo exports point at `src/*.ts`; published Node defaults point at `dist/*.mjs`, while Bun and Deno keep source exports. -Read `@DISCOVERIES.md` before planning, editing, or running task workflows. +Read @DISCOVERIES.md before planning, editing, or running task workflows. ## STRUCTURE diff --git a/src/core/json-schema/index.ts b/src/core/json-schema/index.ts index 85dc9106..97b23f4e 100644 --- a/src/core/json-schema/index.ts +++ b/src/core/json-schema/index.ts @@ -24,8 +24,6 @@ import type { PromptConfig, SelectChoice, } from '#internals/core/schema/index.ts'; -import { parseSchema } from '#internals/core/schema-dsl/runtime.ts'; -import { nodeToJsonSchema } from '#internals/core/schema-dsl/to-json-schema.ts'; import { definitionMetaSchemaDescriptions } from './meta-descriptions.generated.ts'; // --- Options @@ -788,12 +786,7 @@ function isPlainJsonObject(value: object): value is Record { return proto === Object.prototype || proto === null; } -// === Definition meta-schema — derived from schema DSL definitions - -/** Convert a DSL string to a JSON Schema object definition. */ -function def(source: string): Record { - return nodeToJsonSchema(parseSchema(source)); -} +// === Definition meta-schema interface DefinitionMetaSchemaDescriptionNode { readonly description?: string; @@ -870,11 +863,6 @@ function withDefinitionMetaSchemaDescriptions( /** * JSON Schema (draft 2020-12) that validates the output of {@link generateSchema}. * - * Each `$defs` entry is defined once as a schema DSL string — the DSL - * parser produces a runtime AST, and {@link nodeToJsonSchema} converts - * that AST to a JSON Schema fragment. No probe fixtures, no override - * maps, no manually maintained type definitions. - * * Hosted at {@link DEFINITION_SCHEMA_URL} for `$schema` resolution. Also * exported so tooling can validate definition documents without a network * round-trip. @@ -896,74 +884,113 @@ const definitionMetaSchema: Record = withDefinitionMetaSchemaDe title: '@kjanat/dreamcli definition schema', description: 'Describes the structure of a CLI built with dreamcli — commands, flags, args, types, constraints, env bindings, and prompts.', - ...def(`{ - $schema: '${DEFINITION_SCHEMA_URL}'; - name: string; - version?: string; - description?: string; - defaultCommand?: @command; - commands: @command[] - }`), + type: 'object', + additionalProperties: false, + properties: { + $schema: { const: DEFINITION_SCHEMA_URL }, + name: { type: 'string' }, + version: { type: 'string' }, + description: { type: 'string' }, + defaultCommand: { $ref: '#/$defs/command' }, + commands: { type: 'array', items: { $ref: '#/$defs/command' } }, + }, + required: ['$schema', 'name', 'commands'], $defs: { - command: def(`{ - name: string; - description?: string; - aliases?: string[]; - hidden?: true; - examples?: @example[]; - flags: Record; - args: @arg[]; - commands: @command[] - }`), - flag: def(`{ - kind: 'string' | 'number' | 'boolean' | 'enum' | 'array' | 'custom' | 'count' | 'keyValue'; - presence: 'optional' | 'required' | 'defaulted'; - defaultValue?: unknown; - aliases?: string[]; - envVar?: string; - configPath?: string; - description?: string; - enumValues?: string[]; - elementSchema?: @flag; - prompt?: @prompt; - deprecated?: string | true; - propagate?: true; - negation?: @negation; - duplicates?: 'last' | 'first' | 'error' - }`), - negation: def(`{ - alias?: string; - hidden?: true - }`), - arg: def(`{ - name: string; - kind: 'string' | 'number' | 'enum' | 'custom'; - presence: 'required' | 'optional' | 'defaulted'; - variadic?: true; - stdinMode?: true; - defaultValue?: unknown; - description?: string; - envVar?: string; - enumValues?: string[]; - deprecated?: string | true - }`), - prompt: def(`{ - kind: 'confirm' | 'input' | 'select' | 'multiselect'; - message: string; - placeholder?: string; - choices?: @choice[]; - min?: integer; - max?: integer - }`), - choice: def(`{ - value: string; - label?: string; - description?: string - }`), - example: def(`{ - command: string; - description?: string - }`), + command: { + type: 'object', + additionalProperties: false, + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + aliases: { type: 'array', items: { type: 'string' } }, + hidden: { const: true }, + examples: { type: 'array', items: { $ref: '#/$defs/example' } }, + flags: { type: 'object', additionalProperties: { $ref: '#/$defs/flag' } }, + args: { type: 'array', items: { $ref: '#/$defs/arg' } }, + commands: { type: 'array', items: { $ref: '#/$defs/command' } }, + }, + required: ['name', 'flags', 'args', 'commands'], + }, + flag: { + type: 'object', + additionalProperties: false, + properties: { + kind: { + enum: ['string', 'number', 'boolean', 'enum', 'array', 'custom', 'count', 'keyValue'], + }, + presence: { enum: ['optional', 'required', 'defaulted'] }, + defaultValue: {}, + aliases: { type: 'array', items: { type: 'string' } }, + envVar: { type: 'string' }, + configPath: { type: 'string' }, + description: { type: 'string' }, + enumValues: { type: 'array', items: { type: 'string' } }, + elementSchema: { $ref: '#/$defs/flag' }, + prompt: { $ref: '#/$defs/prompt' }, + deprecated: { oneOf: [{ type: 'string' }, { const: true }] }, + propagate: { const: true }, + negation: { $ref: '#/$defs/negation' }, + duplicates: { enum: ['last', 'first', 'error'] }, + }, + required: ['kind', 'presence'], + }, + negation: { + type: 'object', + additionalProperties: false, + properties: { + alias: { type: 'string' }, + hidden: { const: true }, + }, + }, + arg: { + type: 'object', + additionalProperties: false, + properties: { + name: { type: 'string' }, + kind: { enum: ['string', 'number', 'enum', 'custom'] }, + presence: { enum: ['required', 'optional', 'defaulted'] }, + variadic: { const: true }, + stdinMode: { const: true }, + defaultValue: {}, + description: { type: 'string' }, + envVar: { type: 'string' }, + enumValues: { type: 'array', items: { type: 'string' } }, + deprecated: { oneOf: [{ type: 'string' }, { const: true }] }, + }, + required: ['name', 'kind', 'presence'], + }, + prompt: { + type: 'object', + additionalProperties: false, + properties: { + kind: { enum: ['confirm', 'input', 'select', 'multiselect'] }, + message: { type: 'string' }, + placeholder: { type: 'string' }, + choices: { type: 'array', items: { $ref: '#/$defs/choice' } }, + min: { type: 'integer' }, + max: { type: 'integer' }, + }, + required: ['kind', 'message'], + }, + choice: { + type: 'object', + additionalProperties: false, + properties: { + value: { type: 'string' }, + label: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['value'], + }, + example: { + type: 'object', + additionalProperties: false, + properties: { + command: { type: 'string' }, + description: { type: 'string' }, + }, + required: ['command'], + }, }, }, definitionMetaSchemaDescriptions, diff --git a/src/core/schema-dsl/AGENTS.md b/src/core/schema-dsl/AGENTS.md deleted file mode 100644 index a6bebdc0..00000000 --- a/src/core/schema-dsl/AGENTS.md +++ /dev/null @@ -1,34 +0,0 @@ -# schema-dsl — String-literal schema definitions with compile-time type inference - -Enables defining commands from string-literal flag/arg specifications with full TypeScript inference. -Separate from `schema/` because it layers on top — consumers define schemas as strings, this module -parses them into typed `CommandBuilder` instances at compile time and runtime. - -## FILES - -| File | Lines | Purpose | -| -------------------- | ----: | ----------------------------------------------------------------- | -| `index.ts` | 109 | Barrel — public API + `define()` factory function | -| `parse.ts` | 241 | Compile-time string literal type parsing (template literal types) | -| `runtime.ts` | 574 | Runtime parser — string -> FlagBuilder/ArgBuilder construction | -| `to-json-schema.ts` | 99 | DSL definition -> JSON Schema conversion | -| `schema-dsl.test.ts` | 373 | Tests for both compile-time and runtime parsing | - -## ARCHITECTURE - -Two parallel paths from the same string input: - -1. **Compile-time**: `parse.ts` uses template literal types to extract flag names, types, and - optionality from string definitions -> full type inference in `.action()` handler -2. **Runtime**: `runtime.ts` parses the same strings into `FlagBuilder`/`ArgBuilder` calls -> - produces real `CommandBuilder` instance - -Both paths must agree — a string that type-checks must also produce the correct runtime behavior. - -## GOTCHAS - -- `runtime.ts` (574 lines) is the largest file — contains the full runtime string parser -- Template literal type parsing in `parse.ts` is pure type-level code (zero runtime) -- `to-json-schema.ts` bridges DSL definitions to the `json-schema/` module -- Imports from `schema/` (flag, arg, command builders) — not circular because schema-dsl depends on - schema, not vice versa diff --git a/src/core/schema-dsl/index.ts b/src/core/schema-dsl/index.ts deleted file mode 100644 index 8f945ed3..00000000 --- a/src/core/schema-dsl/index.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Schema DSL — single-source schema definitions with compile-time types - * and runtime AST. - * - * Define a schema once as a string literal. At compile time, the string is - * parsed into a TypeScript type via {@link Parse}. At runtime, the same - * string is parsed into an AST ({@link SchemaNode}) for validation, - * JSON Schema generation, and introspection. - * - * ```ts - * const user = schema('{ name: string; age: number; tags?: string[] }'); - * - * // Compile-time: inferred as { name: string; age: number; tags?: string[] } - * const data = user.parse(jsonInput); - * data.name; // string - * data.age; // number - * ``` - * - * @module dreamcli/core/schema-dsl - */ - -import type { Parse } from './parse.ts'; -import type { SchemaNode } from './runtime.ts'; -import { parseSchema, validateNode } from './runtime.ts'; - -// === Schema definition === - -/** The result of calling {@link schema}. Bundles source, AST, guard, and parse. */ -interface SchemaDefinition { - /** The original source string. */ - readonly source: T; - /** Runtime AST parsed from the source. */ - readonly ast: SchemaNode; - /** - * Type guard — returns `true` if `input` matches the schema. - * Narrows the type to `Parse` in the true branch. - */ - guard(input: unknown): input is Parse; - /** - * Parse and validate — returns the narrowed value or throws. - * @throws {TypeError} if the input does not match the schema. - */ - parse(input: unknown): Parse; -} - -/** - * Define a schema from a string literal. - * - * @param source - Schema string (e.g. `"{ name: string; age: number }"`). - * @returns A {@link SchemaDefinition} with compile-time types and runtime validation. - * - * @example - * ```ts - * const user = schema('{ name: string; age: number; admin?: boolean }'); - * - * // Type guard - * if (user.guard(input)) { - * input.name; // string - * } - * - * // Parse or throw - * const data = user.parse(input); - * data.age; // number - * ``` - */ -function schema(source: T): SchemaDefinition { - const ast = parseSchema(source); - - function guard(input: unknown): input is Parse { - return validateNode(ast, input); - } - - function parse(input: unknown): Parse { - if (!guard(input)) { - throw new TypeError(`Value does not match schema: ${source}`); - } - return input; - } - - return { source, ast, guard, parse }; -} - -// === Re-exports === - -export type { Parse } from './parse.ts'; -export type { - ArrayNode, - BooleanNode, - IntegerNode, - LiteralFalseNode, - LiteralNullNode, - LiteralStringNode, - LiteralTrueNode, - LiteralUndefinedNode, - NeverNode, - NumberNode, - ObjectNode, - PropertyNode, - RecordNode, - RefNode, - SchemaNode, - StringNode, - UnionNode, - UnknownNode, -} from './runtime.ts'; -export { parseSchema, validateNode } from './runtime.ts'; -export { nodeToJsonSchema } from './to-json-schema.ts'; -export type { SchemaDefinition }; -export { schema }; diff --git a/src/core/schema-dsl/parse.ts b/src/core/schema-dsl/parse.ts deleted file mode 100644 index b72cc2b8..00000000 --- a/src/core/schema-dsl/parse.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Compile-time schema string parser. - * - * Parses a string literal like `"{ name: string; age: number }"` into its - * corresponding TypeScript type at compile time using template literal types - * and recursive conditional inference. - * - * The compile-time parser and the runtime parser in `runtime.ts` implement - * the same grammar — the type-level version produces TypeScript types while - * the runtime version produces an AST. - * - * @module dreamcli/core/schema-dsl/parse - */ - -// === Whitespace handling === - -/** Whitespace characters recognized by the parser. */ -type WS = ' ' | '\n' | '\t' | '\r'; - -/** Strip leading whitespace. */ -type TrimLeft = T extends `${WS}${infer R}` ? TrimLeft : T; - -/** Strip trailing whitespace. */ -type TrimRight = T extends `${infer R}${WS}` ? TrimRight : T; - -/** Strip leading and trailing whitespace. */ -type Trim = TrimLeft>; - -// === Character counting for bracket balancing === - -/** - * Single-pass brace depth tracker. - * - * Walks the string character by character, pushing to the depth tuple - * on `{` and popping on `}`. Returns the final depth tuple — an empty - * tuple means balanced. Returns `false` on underflow (more `}` than `{`). - * - * This replaces the Chars + Keep + length approach, which materialized - * the entire string as a character tuple. Single-pass is O(n) recursion - * depth instead of O(3n). - */ -type TrackDepth = T extends `${infer C}${infer R}` - ? C extends '{' - ? TrackDepth - : C extends '}' - ? D extends [unknown, ...infer Rest] - ? TrackDepth - : false - : TrackDepth - : D; - -/** True when braces are balanced (depth returns to zero without underflow). */ -type Balanced = TrackDepth extends [] ? true : false; - -// === Balanced splitting === - -/** - * Naively split string T at every occurrence of delimiter D. - * Does not respect brace nesting — use {@link Rejoin} to rebalance. - */ -type RawSplit = T extends `${infer A}${D}${infer B}` - ? [A, ...RawSplit] - : [T]; - -/** - * Walk a tuple of fragments, merging adjacent entries whose cumulative - * brace count is unbalanced. Delimiter D is reinserted when merging. - * - * This is the bracket-balancing trick that makes nested-object parsing - * practical: split greedily, then glue back together where braces don't - * match. - */ -type Rejoin = T extends [ - infer A extends string, - infer B extends string, - ...infer R extends string[], -] - ? Balanced extends true - ? [A, ...Rejoin<[B, ...R], D>] - : Rejoin<[`${A}${D}${B}`, ...R], D> - : T; - -// === Filtering === - -/** Remove entries that are empty or whitespace-only after trimming. */ -type NonEmpty = T extends [infer H extends string, ...infer R extends string[]] - ? Trim extends '' - ? NonEmpty - : [H, ...NonEmpty] - : []; - -// === Top-level pipe detection === - -/** - * True when the string contains `|` outside of `{}` nesting. - * - * Splits at `|`, rebalances with {@link Rejoin}, and checks whether - * two or more fragments remain. If `|` was only inside braces, - * rebalancing collapses them back to one fragment. - */ -type HasTopLevelPipe = - Rejoin, '|'> extends [string, string, ...string[]] ? true : false; - -// === Flatten intersection into readable object type === - -/** Collapse an intersection of object types into a single flat object. */ -type Prettify = { [K in keyof T]: T[K] } & {}; - -// === Primitive mapping === - -/** Map a primitive type name to its TypeScript type. */ -type ParsePrimitive = T extends 'string' - ? string - : T extends 'number' - ? number - : T extends 'integer' - ? number - : T extends 'boolean' - ? boolean - : T extends 'true' - ? true - : T extends 'false' - ? false - : T extends 'null' - ? null - : T extends 'undefined' - ? undefined - : T extends 'unknown' - ? unknown - : T extends 'never' - ? never - : never; - -// === Union parsing === - -/** Parse each tuple entry as a value, producing a tuple of types. */ -type ParseEach = T extends [infer H extends string, ...infer R extends string[]] - ? [ParseValue, ...ParseEach] - : []; - -/** Collapse a tuple of types into a TypeScript union. */ -type TupleToUnion = T extends [infer H, ...infer R] - ? H | TupleToUnion - : never; - -/** Split at top-level `|`, parse each branch, collapse to union. */ -type ParseUnion = TupleToUnion< - ParseEach, '|'>>> ->; - -// === Value parsing === - -/** - * Parse a value type: object, union, array, literal, ref, Record, or primitive. - * - * Precedence (highest to lowest): - * 1. Object — `{...}` wrapping - * 2. Union — top-level `|` (outside braces) - * 3. Array — trailing `[]` suffix - * 4. String literal — `'...'` (maps to literal string type) - * 5. Reference — `@name` (maps to `unknown` at type level; resolved at runtime) - * 6. Record — `Record` (maps to `Record`) - * 7. Primitive — identifier lookup - * - * Union is checked before array so `string | number[]` parses as - * `string | number[]`, not `(string | number)[]`. - */ -type ParseValue> = V extends `{${infer Content}}` - ? Prettify> - : HasTopLevelPipe extends true - ? ParseUnion - : V extends `${infer Inner}[]` - ? ParseValue[] - : V extends `'${infer Literal}'` - ? Literal - : V extends `@${string}` - ? unknown - : V extends `Record<${string},${infer Val}>` - ? Record>> - : ParsePrimitive; - -// === Property parsing === - -/** - * Parse a single property string like `"name: string"` or `"age?: number"`. - * - * Matches the first `:` (leftmost), then checks whether the key ends - * with `?` to determine optionality. This avoids the trap of matching - * `?:` inside nested values. - */ -type ParseProperty = - Trim extends `${infer K}:${infer V}` - ? Trim extends `${infer Key}?` - ? { [P in Trim]?: ParseValue } - : { [P in Trim]: ParseValue } - : // biome-ignore lint/complexity/noBannedTypes: identity element for type-level intersection - {}; - -/** Recursively parse a tuple of property strings and intersect results. */ -type ParseProperties = T extends [ - infer H extends string, - ...infer R extends string[], -] - ? ParseProperty & ParseProperties - : // biome-ignore lint/complexity/noBannedTypes: identity element for type-level intersection - {}; - -// === Object parsing === - -/** - * Split content by `;` with bracket balancing, filter empties, - * parse each property, and intersect into a single object type. - */ -type ParseObject = Prettify< - ParseProperties, ';'>>> ->; - -// === Entry point === - -/** - * Parse a schema string literal into its TypeScript type. - * - * Supports primitives (`string`, `number`, `boolean`, `true`, `false`, - * `null`, `undefined`, `unknown`, `never`), arrays (`T[]`), objects - * (`{ key: T; ... }`), optional properties (`key?: T`), unions - * (`T | U`), and arbitrary nesting. - * - * @example - * ```ts - * type User = Parse<'{ name: string; age: number; tags?: string[] }'>; - * // ^? { name: string; age: number; tags?: string[] | undefined } - * - * type Status = Parse<'string | true'>; - * // ^? string | true - * ``` - */ -type Parse> = V extends `{${infer Content}}` - ? Prettify> - : ParseValue; - -export type { - Balanced, - HasTopLevelPipe, - NonEmpty, - Parse, - ParseEach, - ParseObject, - ParsePrimitive, - ParseProperties, - ParseProperty, - ParseUnion, - ParseValue, - Prettify, - RawSplit, - Rejoin, - TrackDepth, - Trim, - TrimLeft, - TrimRight, - TupleToUnion, - WS, -}; diff --git a/src/core/schema-dsl/runtime.ts b/src/core/schema-dsl/runtime.ts deleted file mode 100644 index acb8f707..00000000 --- a/src/core/schema-dsl/runtime.ts +++ /dev/null @@ -1,609 +0,0 @@ -/** - * Runtime schema string parser. - * - * Parses the same string literals as the compile-time {@link Parse} type, - * producing a runtime AST ({@link SchemaNode}) that mirrors the inferred - * TypeScript type. The AST can drive JSON Schema generation, validation, - * and introspection — all from the same source string. - * - * Grammar (matches the type-level parser in `parse.ts`): - * ``` - * value = union - * union = postfix ('|' postfix)* - * postfix = primary ('[]')* - * primary = object | record | ref | literal | IDENT - * object = '{' properties '}' - * properties = (property ';')* property? ';'? - * property = IDENT '?'? ':' value - * record = 'Record' '<' value ',' value '>' - * ref = '@' IDENT - * literal = "'" CHARS "'" - * ``` - * - * @module dreamcli/core/schema-dsl/runtime - */ - -// === AST node types (discriminated union) === - -/** Discriminated union of all schema AST nodes. */ -type SchemaNode = - | StringNode - | NumberNode - | IntegerNode - | BooleanNode - | LiteralTrueNode - | LiteralFalseNode - | LiteralNullNode - | LiteralStringNode - | LiteralUndefinedNode - | UnknownNode - | NeverNode - | ArrayNode - | ObjectNode - | UnionNode - | RefNode - | RecordNode; - -/** String primitive. */ -interface StringNode { - /** Identifies this node as a string type. */ - readonly kind: 'string'; -} - -/** Number primitive. */ -interface NumberNode { - /** Identifies this node as a number type. */ - readonly kind: 'number'; -} - -/** Integer primitive (maps to `number` in TS, `integer` in JSON Schema). */ -interface IntegerNode { - /** Identifies this node as an integer type. */ - readonly kind: 'integer'; -} - -/** Boolean primitive. */ -interface BooleanNode { - /** Identifies this node as a boolean type. */ - readonly kind: 'boolean'; -} - -/** Literal `true`. */ -interface LiteralTrueNode { - /** Identifies this node as the literal `true` value. */ - readonly kind: 'true'; -} - -/** Literal `false`. */ -interface LiteralFalseNode { - /** Identifies this node as the literal `false` value. */ - readonly kind: 'false'; -} - -/** Literal `null`. */ -interface LiteralNullNode { - /** Identifies this node as the literal `null` value. */ - readonly kind: 'null'; -} - -/** String literal value (e.g. `'confirm'`). */ -interface LiteralStringNode { - /** Identifies this node as a string literal. */ - readonly kind: 'literal'; - /** The exact string value this literal represents. */ - readonly value: string; -} - -/** Literal `undefined`. */ -interface LiteralUndefinedNode { - /** Identifies this node as the literal `undefined` value. */ - readonly kind: 'undefined'; -} - -/** The `unknown` top type. */ -interface UnknownNode { - /** Identifies this node as the `unknown` top type. */ - readonly kind: 'unknown'; -} - -/** The `never` bottom type. */ -interface NeverNode { - /** Identifies this node as the `never` bottom type. */ - readonly kind: 'never'; -} - -/** Array of element type. */ -interface ArrayNode { - /** Identifies this node as an array type. */ - readonly kind: 'array'; - /** The type of each array element. */ - readonly element: SchemaNode; -} - -/** Object with named properties. */ -interface ObjectNode { - /** Identifies this node as an object type. */ - readonly kind: 'object'; - /** Named properties keyed by field name. */ - readonly properties: Readonly>; -} - -/** Union of two or more member types. */ -interface UnionNode { - /** Identifies this node as a union type. */ - readonly kind: 'union'; - /** The constituent types of this union. */ - readonly members: readonly SchemaNode[]; -} - -/** Reference to a named definition (e.g. `@flag` → `$ref: '#/$defs/flag'`). */ -interface RefNode { - /** Identifies this node as a `$ref` reference. */ - readonly kind: 'ref'; - /** Name of the referenced definition (without the `@` prefix). */ - readonly target: string; -} - -/** Record/dictionary type (e.g. `Record`). */ -interface RecordNode { - /** Identifies this node as a record/dictionary type. */ - readonly kind: 'record'; - /** The type of each record value (keys are always strings). */ - readonly value: SchemaNode; -} - -/** A named property with optionality flag. */ -interface PropertyNode { - /** Whether the property is optional (`?` suffix in the schema DSL). */ - readonly optional: boolean; - /** The property's value type. */ - readonly schema: SchemaNode; -} - -// === Tokenizer === - -/** Token kind discriminator. */ -type TokenKind = - | 'ident' - | 'string_literal' - | 'ref' - | 'lbrace' - | 'rbrace' - | 'lbracket' - | 'rbracket' - | 'langle' - | 'rangle' - | 'comma' - | 'colon' - | 'semicolon' - | 'question' - | 'pipe'; - -/** A single token from the schema string. */ -interface Token { - readonly kind: TokenKind; - readonly value: string; -} - -/** Map single characters to their token kinds. */ -const SINGLE_CHAR_TOKENS: Readonly> = { - '{': 'lbrace', - '}': 'rbrace', - '[': 'lbracket', - ']': 'rbracket', - '<': 'langle', - '>': 'rangle', - ',': 'comma', - ':': 'colon', - ';': 'semicolon', - '?': 'question', - '|': 'pipe', -}; - -/** Whitespace characters to skip. */ -function isWhitespace(c: string): boolean { - return c === ' ' || c === '\n' || c === '\t' || c === '\r'; -} - -/** Identifier-valid characters (a-z, A-Z, 0-9, _, $). */ -function isIdentChar(c: string): boolean { - return ( - (c >= 'a' && c <= 'z') || - (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || - c === '_' || - c === '$' - ); -} - -/** Identifier start characters (a-z, A-Z, _, $). */ -function isIdentStart(c: string): boolean { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_' || c === '$'; -} - -/** - * Tokenize a schema source string. - * - * Produces a flat array of tokens: identifiers (`string`, `number`, etc.), - * punctuation (`{`, `}`, `[`, `]`, `:`, `;`, `?`, `|`), and skips - * whitespace. Throws on unrecognized characters. - */ -function tokenize(source: string): Token[] { - const tokens: Token[] = []; - let i = 0; - - while (i < source.length) { - const c = source[i] ?? ''; - - if (isWhitespace(c)) { - i++; - continue; - } - - const singleKind = SINGLE_CHAR_TOKENS[c]; - if (singleKind !== undefined) { - tokens.push({ kind: singleKind, value: c }); - i++; - continue; - } - - // String literal: 'foo' - if (c === "'") { - let lit = ''; - i++; // skip opening quote - while (i < source.length && source[i] !== "'") { - lit += source[i]; - i++; - } - if (i >= source.length) throw new SyntaxError('Unterminated string literal'); - i++; // skip closing quote - tokens.push({ kind: 'string_literal', value: lit }); - continue; - } - - // Reference: @name - if (c === '@') { - i++; // skip @ - let ref = ''; - while (i < source.length) { - const ch = source[i] ?? ''; - if (!isIdentChar(ch)) break; - ref += ch; - i++; - } - if (ref.length === 0) - throw new SyntaxError(`Expected identifier after '@' at position ${String(i)}`); - tokens.push({ kind: 'ref', value: ref }); - continue; - } - - if (isIdentStart(c)) { - let ident = ''; - while (i < source.length) { - const ch = source[i] ?? ''; - if (!isIdentChar(ch)) break; - ident += ch; - i++; - } - tokens.push({ kind: 'ident', value: ident }); - continue; - } - - throw new SyntaxError(`Unexpected character '${c}' at position ${String(i)}`); - } - - return tokens; -} - -// === Primitive lookup === - -/** Primitive type names recognized by the parser. */ -const PRIMITIVES: ReadonlySet = new Set([ - 'string', - 'number', - 'integer', - 'boolean', - 'true', - 'false', - 'null', - 'undefined', - 'unknown', - 'never', -]); - -/** Map a primitive name to its AST node. */ -function primitiveNode(name: string): SchemaNode { - switch (name) { - case 'string': - return { kind: 'string' }; - case 'number': - return { kind: 'number' }; - case 'integer': - return { kind: 'integer' }; - case 'boolean': - return { kind: 'boolean' }; - case 'true': - return { kind: 'true' }; - case 'false': - return { kind: 'false' }; - case 'null': - return { kind: 'null' }; - case 'undefined': - return { kind: 'undefined' }; - case 'unknown': - return { kind: 'unknown' }; - case 'never': - return { kind: 'never' }; - default: - throw new SyntaxError(`Unknown type '${name}'`); - } -} - -// === Recursive descent parser === - -/** - * Parse a token stream into a {@link SchemaNode} AST. - * - * Implements the grammar documented at the module level using standard - * recursive descent. Each grammar rule maps to one `parse*` method. - */ -class SchemaParser { - private pos = 0; - - constructor(private readonly tokens: readonly Token[]) {} - - /** Parse the full token stream, asserting all tokens are consumed. */ - parseRoot(): SchemaNode { - const result = this.parseValue(); - if (this.pos < this.tokens.length) { - const leftover = this.peek(); - throw new SyntaxError( - `Unexpected token '${leftover?.value ?? '?'}' at position ${String(this.pos)}`, - ); - } - return result; - } - - // --- Grammar rules --- - - /** value = union */ - private parseValue(): SchemaNode { - return this.parseUnion(); - } - - /** union = postfix ('|' postfix)* */ - private parseUnion(): SchemaNode { - const first = this.parsePostfix(); - - if (!this.check('pipe')) return first; - - const members: SchemaNode[] = [first]; - while (this.match('pipe')) { - members.push(this.parsePostfix()); - } - return { kind: 'union', members }; - } - - /** postfix = primary ('[]')* */ - private parsePostfix(): SchemaNode { - let node = this.parsePrimary(); - - while (this.check('lbracket') && this.checkAt(this.pos + 1, 'rbracket')) { - this.advance(); // [ - this.advance(); // ] - node = { kind: 'array', element: node }; - } - - return node; - } - - /** primary = object | record | ref | literal | IDENT */ - private parsePrimary(): SchemaNode { - if (this.check('lbrace')) { - return this.parseObject(); - } - - if (this.check('string_literal')) { - const token = this.advance(); - return { kind: 'literal', value: token.value }; - } - - if (this.check('ref')) { - const token = this.advance(); - return { kind: 'ref', target: token.value }; - } - - const name = this.expectIdent(); - - // Record - if (name === 'Record') { - this.expect('langle'); - const keySchema = this.parseValue(); - if (keySchema.kind !== 'string') { - throw new SyntaxError( - "Record key type must be 'string' because JSON object keys are always strings", - ); - } - this.expect('comma'); - const valueSchema = this.parseValue(); - this.expect('rangle'); - return { kind: 'record', value: valueSchema }; - } - - if (!PRIMITIVES.has(name)) { - throw new SyntaxError(`Unknown type '${name}' at position ${String(this.pos - 1)}`); - } - return primitiveNode(name); - } - - /** object = '{' properties '}' */ - private parseObject(): ObjectNode { - this.expect('lbrace'); - - const properties: Record = {}; - - while (!this.check('rbrace')) { - const name = this.expectIdent(); - if (Object.hasOwn(properties, name)) { - throw new SyntaxError(`Duplicate property '${name}' at position ${String(this.pos - 1)}`); - } - const optional = this.match('question'); - this.expect('colon'); - const schema = this.parseValue(); - - properties[name] = { optional, schema }; - - this.match('semicolon'); // optional trailing semicolon - } - - this.expect('rbrace'); - return { kind: 'object', properties }; - } - - // --- Token helpers --- - - private peek(): Token | undefined { - return this.tokens[this.pos]; - } - - private check(kind: TokenKind): boolean { - return this.peek()?.kind === kind; - } - - private checkAt(pos: number, kind: TokenKind): boolean { - return this.tokens[pos]?.kind === kind; - } - - private advance(): Token { - const token = this.tokens[this.pos]; - if (token === undefined) { - throw new SyntaxError('Unexpected end of input'); - } - this.pos++; - return token; - } - - private match(kind: TokenKind): boolean { - if (this.check(kind)) { - this.pos++; - return true; - } - return false; - } - - private expect(kind: TokenKind): Token { - if (!this.check(kind)) { - const got = this.peek(); - throw new SyntaxError( - got - ? `Expected '${kind}' but got '${got.kind}' ('${got.value}') at position ${String(this.pos)}` - : `Expected '${kind}' but reached end of input`, - ); - } - return this.advance(); - } - - private expectIdent(): string { - return this.expect('ident').value; - } -} - -/** - * Parse a schema source string into a runtime AST. - * - * @param source - Schema string (e.g. `"{ name: string; age: number }"`). - * @returns The root {@link SchemaNode}. - * @throws {@link SyntaxError} on malformed input. - */ -function parseSchema(source: string): SchemaNode { - const tokens = tokenize(source); - return new SchemaParser(tokens).parseRoot(); -} - -// === Validation === - -function isRecord(value: unknown): value is Readonly> { - return typeof value === 'object' && value !== null && !Array.isArray(value); -} - -/** - * Validate that `input` conforms to the given AST node. - * - * @param node - The schema AST to validate against. - * @param input - The value to check. - * @returns `true` if the value matches the schema. - */ -function validateNode(node: SchemaNode, input: unknown): boolean { - switch (node.kind) { - case 'string': - return typeof input === 'string'; - case 'number': - return typeof input === 'number'; - case 'integer': - return typeof input === 'number' && Number.isInteger(input); - case 'boolean': - return typeof input === 'boolean'; - case 'true': - return input === true; - case 'false': - return input === false; - case 'null': - return input === null; - case 'literal': - return input === node.value; - case 'undefined': - return input === undefined; - case 'unknown': - return true; - case 'never': - return false; - case 'ref': - return false; // fail closed until refs can be resolved against a definition map - case 'array': - return Array.isArray(input) && input.every((el) => validateNode(node.element, el)); - case 'object': { - if (!isRecord(input)) return false; - for (const key of Object.keys(input)) { - if (!Object.hasOwn(node.properties, key)) return false; - } - for (const [key, prop] of Object.entries(node.properties)) { - if (!Object.hasOwn(input, key)) { - if (!prop.optional) return false; - continue; - } - if (!validateNode(prop.schema, input[key])) return false; - } - return true; - } - case 'record': { - if (!isRecord(input)) return false; - return Object.values(input).every((v) => validateNode(node.value, v)); - } - case 'union': - return node.members.some((member) => validateNode(member, input)); - } -} - -// === Exports === - -export type { - ArrayNode, - BooleanNode, - IntegerNode, - LiteralFalseNode, - LiteralNullNode, - LiteralStringNode, - LiteralTrueNode, - LiteralUndefinedNode, - NeverNode, - NumberNode, - ObjectNode, - PropertyNode, - RecordNode, - RefNode, - SchemaNode, - StringNode, - UnionNode, - UnknownNode, -}; -export { parseSchema, validateNode }; diff --git a/src/core/schema-dsl/schema-dsl.test.ts b/src/core/schema-dsl/schema-dsl.test.ts deleted file mode 100644 index 858049e1..00000000 --- a/src/core/schema-dsl/schema-dsl.test.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { describe, expect, expectTypeOf, it } from 'vitest'; -import { nodeToJsonSchema, schema } from './index.ts'; -import type { Parse } from './parse.ts'; -import type { SchemaNode } from './runtime.ts'; -import { parseSchema, validateNode } from './runtime.ts'; - -// ── Compile-time type parser ──────────────────────────────────────── - -describe('Parse — compile-time type parser', () => { - // --- Primitives --- - - it('parses string', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses number', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses boolean', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses literal true', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses literal false', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses null', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses unknown', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - // --- Arrays --- - - it('parses string[]', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - it('parses number[][]', () => { - expectTypeOf>().toEqualTypeOf(); - }); - - // --- Objects --- - - it('parses flat object', () => { - expectTypeOf>().toEqualTypeOf<{ - name: string; - age: number; - }>(); - }); - - it('parses object with optional property', () => { - type Result = Parse<'{ name: string; bio?: string }'>; - expectTypeOf().toEqualTypeOf<{ name: string; bio?: string }>(); - }); - - it('parses nested object', () => { - type Result = Parse<'{ user: { name: string; age: number } }'>; - expectTypeOf().toEqualTypeOf<{ user: { name: string; age: number } }>(); - }); - - it('parses object with array property', () => { - type Result = Parse<'{ tags: string[]; scores: number[] }'>; - expectTypeOf().toEqualTypeOf<{ tags: string[]; scores: number[] }>(); - }); - - it('handles trailing semicolon', () => { - type Result = Parse<'{ name: string; }'>; - expectTypeOf().toEqualTypeOf<{ name: string }>(); - }); - - // --- Unions --- - - it('parses two-member union', () => { - type Result = Parse<'string | number'>; - expectTypeOf().toEqualTypeOf(); - }); - - it('parses union with literal true', () => { - type Result = Parse<'string | true'>; - expectTypeOf().toEqualTypeOf(); - }); - - it('parses union with array (array binds tighter)', () => { - type Result = Parse<'string | number[]'>; - expectTypeOf().toEqualTypeOf(); - }); - - // --- Whitespace tolerance --- - - it('handles extra whitespace', () => { - type Result = Parse<' { name : string ; age : number } '>; - expectTypeOf().toEqualTypeOf<{ name: string; age: number }>(); - }); - - // --- Complex combinations --- - - it('parses deeply nested structure', () => { - type Result = Parse<'{ user: { profile: { email: string; age: number }; tags?: string[] } }'>; - expectTypeOf().toEqualTypeOf<{ - user: { profile: { email: string; age: number }; tags?: string[] }; - }>(); - }); - - it('parses object array', () => { - type Result = Parse<'{ name: string }[]'>; - expectTypeOf().toEqualTypeOf<{ name: string }[]>(); - }); - - it('parses union inside object property', () => { - type Result = Parse<'{ deprecated?: string | true }'>; - expectTypeOf().toEqualTypeOf<{ deprecated?: string | true }>(); - }); -}); - -// ── Runtime parser ────────────────────────────────────────────────── - -describe('parseSchema — runtime parser', () => { - // --- Primitives --- - - it('parses string', () => { - expect(parseSchema('string')).toEqual({ kind: 'string' }); - }); - - it('parses number', () => { - expect(parseSchema('number')).toEqual({ kind: 'number' }); - }); - - it('parses boolean', () => { - expect(parseSchema('boolean')).toEqual({ kind: 'boolean' }); - }); - - it('parses literal true', () => { - expect(parseSchema('true')).toEqual({ kind: 'true' }); - }); - - it('parses literal false', () => { - expect(parseSchema('false')).toEqual({ kind: 'false' }); - }); - - it('parses null', () => { - expect(parseSchema('null')).toEqual({ kind: 'null' }); - }); - - it('parses unknown', () => { - expect(parseSchema('unknown')).toEqual({ kind: 'unknown' }); - }); - - // --- Arrays --- - - it('parses string[]', () => { - expect(parseSchema('string[]')).toEqual({ - kind: 'array', - element: { kind: 'string' }, - }); - }); - - it('parses number[][]', () => { - expect(parseSchema('number[][]')).toEqual({ - kind: 'array', - element: { kind: 'array', element: { kind: 'number' } }, - }); - }); - - // --- Objects --- - - it('parses flat object', () => { - const ast = parseSchema('{ name: string; age: number }'); - expect(ast).toEqual({ - kind: 'object', - properties: { - name: { optional: false, schema: { kind: 'string' } }, - age: { optional: false, schema: { kind: 'number' } }, - }, - }); - }); - - it('parses optional property', () => { - const ast = parseSchema('{ bio?: string }'); - expect(ast).toEqual({ - kind: 'object', - properties: { - bio: { optional: true, schema: { kind: 'string' } }, - }, - }); - }); - - it('parses nested object', () => { - const ast = parseSchema('{ user: { name: string } }'); - expect(ast).toEqual({ - kind: 'object', - properties: { - user: { - optional: false, - schema: { - kind: 'object', - properties: { - name: { optional: false, schema: { kind: 'string' } }, - }, - }, - }, - }, - }); - }); - - // --- Unions --- - - it('parses union', () => { - expect(parseSchema('string | number')).toEqual({ - kind: 'union', - members: [{ kind: 'string' }, { kind: 'number' }], - }); - }); - - it('parses union with literal true', () => { - expect(parseSchema('string | true')).toEqual({ - kind: 'union', - members: [{ kind: 'string' }, { kind: 'true' }], - }); - }); - - it('parses union where array binds tighter than pipe', () => { - expect(parseSchema('string | number[]')).toEqual({ - kind: 'union', - members: [{ kind: 'string' }, { kind: 'array', element: { kind: 'number' } }], - }); - }); - - // --- Complex --- - - it('parses object array', () => { - expect(parseSchema('{ name: string }[]')).toEqual({ - kind: 'array', - element: { - kind: 'object', - properties: { - name: { optional: false, schema: { kind: 'string' } }, - }, - }, - }); - }); - - it('parses union inside object property', () => { - const ast = parseSchema('{ deprecated?: string | true }'); - expect(ast).toEqual({ - kind: 'object', - properties: { - deprecated: { - optional: true, - schema: { - kind: 'union', - members: [{ kind: 'string' }, { kind: 'true' }], - }, - }, - }, - }); - }); - - // --- Errors --- - - it('throws on unknown type', () => { - expect(() => parseSchema('Widget')).toThrow(SyntaxError); - }); - - it('throws on unexpected character', () => { - expect(() => parseSchema('string!')).toThrow(SyntaxError); - }); - - it('throws on duplicate object property names', () => { - expect(() => parseSchema('{ name: string; name: number }')).toThrow(SyntaxError); - }); - - it('throws when Record key type is not string', () => { - expect(() => parseSchema('Record')).toThrow( - "Record key type must be 'string' because JSON object keys are always strings", - ); - }); - - it('parses Record', () => { - expect(parseSchema('Record')).toEqual({ - kind: 'record', - value: { kind: 'number' }, - }); - }); -}); - -// ── Validation ────────────────────────────────────────────────────── - -describe('validateNode — runtime validation', () => { - it('validates string', () => { - const node: SchemaNode = { kind: 'string' }; - expect(validateNode(node, 'hello')).toBe(true); - expect(validateNode(node, 42)).toBe(false); - }); - - it('validates number', () => { - const node: SchemaNode = { kind: 'number' }; - expect(validateNode(node, 42)).toBe(true); - expect(validateNode(node, 'hello')).toBe(false); - }); - - it('validates literal true', () => { - const node: SchemaNode = { kind: 'true' }; - expect(validateNode(node, true)).toBe(true); - expect(validateNode(node, false)).toBe(false); - expect(validateNode(node, 'true')).toBe(false); - }); - - it('validates array', () => { - const node: SchemaNode = { kind: 'array', element: { kind: 'string' } }; - expect(validateNode(node, ['a', 'b'])).toBe(true); - expect(validateNode(node, [1])).toBe(false); - expect(validateNode(node, 'not-array')).toBe(false); - }); - - it('validates object with required and optional properties', () => { - const node: SchemaNode = { - kind: 'object', - properties: { - name: { optional: false, schema: { kind: 'string' } }, - bio: { optional: true, schema: { kind: 'string' } }, - }, - }; - expect(validateNode(node, { name: 'Alice' })).toBe(true); - expect(validateNode(node, { name: 'Alice', bio: 'dev' })).toBe(true); - expect(validateNode(node, { name: 'Alice', extra: 'unexpected' })).toBe(false); - expect(validateNode(node, {})).toBe(false); - expect(validateNode(node, { name: 42 })).toBe(false); - }); - - it('does not accept prototype-inherited required properties', () => { - const node: SchemaNode = { - kind: 'object', - properties: { - name: { optional: false, schema: { kind: 'string' } }, - }, - }; - const inherited: Record = {}; - Object.setPrototypeOf(inherited, { name: 'Alice' }); - expect(validateNode(node, inherited)).toBe(false); - }); - - it('validates union', () => { - const node: SchemaNode = { - kind: 'union', - members: [{ kind: 'string' }, { kind: 'true' }], - }; - expect(validateNode(node, 'hello')).toBe(true); - expect(validateNode(node, true)).toBe(true); - expect(validateNode(node, false)).toBe(false); - expect(validateNode(node, 42)).toBe(false); - }); - - it('fails unresolved refs closed', () => { - const node: SchemaNode = { kind: 'ref', target: 'flag' }; - expect(validateNode(node, 42)).toBe(false); - expect(validateNode(node, { anything: 'goes' })).toBe(false); - }); -}); - -// ── JSON Schema conversion ────────────────────────────────────────── - -describe('nodeToJsonSchema — JSON Schema conversion', () => { - it('throws on undefined nodes', () => { - expect(() => nodeToJsonSchema({ kind: 'undefined' })).toThrow( - "Cannot convert 'undefined' type to JSON Schema; model optionality at the parent level", - ); - }); -}); - -// ── Integration — schema() ties both layers together ──────────────── - -describe('schema() — integrated compile-time + runtime', () => { - it('infers flat object type and validates at runtime', () => { - const user = schema('{ name: string; age: number }'); - - // Compile-time: the parse return type is { name: string; age: number } - const data = user.parse({ name: 'Alice', age: 30 }); - expectTypeOf(data).toEqualTypeOf<{ name: string; age: number }>(); - expect(data.name).toBe('Alice'); - expect(data.age).toBe(30); - }); - - it('infers optional properties', () => { - const s = schema('{ name: string; bio?: string }'); - const data = s.parse({ name: 'Bob' }); - expectTypeOf(data).toEqualTypeOf<{ name: string; bio?: string }>(); - expect(data.name).toBe('Bob'); - }); - - it('infers union type (string | true)', () => { - const s = schema('string | true'); - expect(s.guard('hello')).toBe(true); - expect(s.guard(true)).toBe(true); - expect(s.guard(false)).toBe(false); - }); - - it('throws on invalid input', () => { - const s = schema('{ name: string }'); - expect(() => s.parse({ name: 42 })).toThrow(TypeError); - }); - - it('guard narrows type', () => { - const s = schema('{ value: number }'); - const input: unknown = { value: 5 }; - - if (s.guard(input)) { - expectTypeOf(input).toEqualTypeOf<{ value: number }>(); - expect(input.value).toBe(5); - } - }); - - it('rejects unresolved refs at runtime', () => { - const s = schema('@flag'); - expect(s.guard(42)).toBe(false); - expect(() => s.parse(42)).toThrow(TypeError); - }); -}); diff --git a/src/core/schema-dsl/to-json-schema.ts b/src/core/schema-dsl/to-json-schema.ts deleted file mode 100644 index 87016660..00000000 --- a/src/core/schema-dsl/to-json-schema.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Convert schema DSL AST nodes to JSON Schema (draft 2020-12) fragments. - * - * This bridges the schema DSL and JSON Schema generation: define a shape - * once as a DSL string, parse it to an AST, then convert to JSON Schema - * properties — no manual override maps or probe fixtures needed. - * - * @module dreamcli/core/schema-dsl/to-json-schema - */ - -import type { SchemaNode } from './runtime.ts'; - -/** - * Convert a {@link SchemaNode} AST node to a JSON Schema fragment. - * - * Mapping: - * - `string` → `{ type: 'string' }` - * - `number` → `{ type: 'number' }` - * - `integer` → `{ type: 'integer' }` - * - `boolean` → `{ type: 'boolean' }` - * - `true` → `{ const: true }` - * - `false` → `{ const: false }` - * - `null` → `{ type: 'null' }` - * - `undefined` → throws (JSON Schema has no `undefined` type) - * - `unknown` → `{}` (accepts any value) - * - `never` → `{ not: {} }` (rejects all values) - * - `'literal'` → `{ const: 'literal' }` - * - `@ref` → `{ $ref: '#/$defs/ref' }` - * - `T[]` → `{ type: 'array', items: convert(T) }` - * - `Record` → `{ type: 'object', additionalProperties: convert(V) }` - * - `{...}` → `{ type: 'object', properties: ..., required: [...] }` - * - `A | B` → `{ enum: [...] }` if all literals, else `{ oneOf: [...] }` - * - * @param node - The AST node to convert. - * @returns A plain JSON Schema fragment. - */ -function nodeToJsonSchema(node: SchemaNode): Record { - switch (node.kind) { - case 'string': - return { type: 'string' }; - case 'number': - return { type: 'number' }; - case 'integer': - return { type: 'integer' }; - case 'boolean': - return { type: 'boolean' }; - case 'true': - return { const: true }; - case 'false': - return { const: false }; - case 'null': - return { type: 'null' }; - case 'literal': - return { const: node.value }; - case 'undefined': - throw new Error( - "Cannot convert 'undefined' type to JSON Schema; model optionality at the parent level", - ); - case 'unknown': - return {}; - case 'never': - return { not: {} }; - case 'ref': - return { $ref: `#/$defs/${node.target}` }; - case 'array': - return { type: 'array', items: nodeToJsonSchema(node.element) }; - case 'record': - return { type: 'object', additionalProperties: nodeToJsonSchema(node.value) }; - case 'object': { - const properties: Record> = {}; - const required: string[] = []; - for (const [key, prop] of Object.entries(node.properties)) { - properties[key] = nodeToJsonSchema(prop.schema); - if (!prop.optional) required.push(key); - } - const result: Record = { - type: 'object', - additionalProperties: false, - properties, - }; - if (required.length > 0) result.required = required; - return result; - } - case 'union': { - // Union of all string literals → { enum: [...] } - const literalValues: string[] = []; - let allLiteral = true; - for (const member of node.members) { - if (member.kind === 'literal') { - literalValues.push(member.value); - } else { - allLiteral = false; - } - } - if (allLiteral) return { enum: literalValues }; - // Mixed union → oneOf - return { oneOf: node.members.map(nodeToJsonSchema) }; - } - } -} - -export { nodeToJsonSchema }; From d7537f4ce244c6fee03131602719a5fc069b42cd Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Wed, 15 Jul 2026 17:17:30 +0200 Subject: [PATCH 2/8] feat(schema): Standard Schema v1 interop Accept any Standard Schema v1 validator (zod, valibot, arktype) at the `flag.custom()` / `arg.custom()` boundary. The spec is vendored as types only, so consumers bring their own validation with no new runtime dependency. Validation runs as one async pass in `resolve()` after a value settles from any source (CLI, env, config, prompt, default); sync and async validators both work and `parse()` stays synchronous. Failures surface as `CONSTRAINT_VIOLATED` errors naming the flag/arg. Also commits staged tooling: drop `@biomejs/biome` for the `biome` package, refresh `.zed` formatter/LSP settings. --- .zed/settings.json | 30 +++- biome.jsonc | 2 +- bun.lock | 22 +-- package.json | 2 +- src/core/cli/root-help.ts | 1 + src/core/completion/shells/shared.ts | 1 + src/core/json-schema/json-schema.test.ts | 1 + src/core/resolve/index.ts | 6 + .../resolve/resolve-standard-schema.test.ts | 157 ++++++++++++++++++ src/core/resolve/standard.ts | 123 ++++++++++++++ src/core/schema/arg.ts | 57 ++++++- src/core/schema/flag.ts | 57 ++++++- src/core/schema/index.ts | 12 ++ src/core/schema/standard.ts | 81 +++++++++ 14 files changed, 526 insertions(+), 26 deletions(-) create mode 100644 src/core/resolve/resolve-standard-schema.test.ts create mode 100644 src/core/resolve/standard.ts create mode 100644 src/core/schema/standard.ts diff --git a/.zed/settings.json b/.zed/settings.json index 9c754aee..bfd4202d 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -17,18 +17,38 @@ ], "languages": { "JavaScript": { + "formatter": [ + { "language_server": { "name": "biome" } }, + { "language_server": { "name": "dprint" } }, + { + "external": { + "command": "bunx", + "arguments": ["dprint", "fmt", "--stdin", "{buffer_path}"] + } + } + ], "code_actions_on_format": { "source.fixAll.biome": true, "source.organizeImports.biome": true }, - "language_servers": ["vtsls", "tsgo", "biome", "deno"] + "language_servers": ["typescript", "biome", "oxlint", "deno"] }, "TypeScript": { + "formatter": [ + { "language_server": { "name": "biome" } }, + { "language_server": { "name": "dprint" } }, + { + "external": { + "command": "bunx", + "arguments": ["dprint", "fmt", "--stdin", "{buffer_path}"] + } + } + ], "code_actions_on_format": { "source.fixAll.biome": true, "source.organizeImports.biome": true }, - "language_servers": ["vtsls", "tsgo", "biome", "deno", "vue-language-server"] + "language_servers": ["typescript", "biome", "oxlint", "deno", "vue-language-server"] } }, "lsp": { @@ -42,11 +62,7 @@ } }, "dprint": { - "binary": { - "path": "bunx", - "ignore_system_version": true, - "arguments": ["dprint", "lsp"] - } + "binary": { "path": "bunx", "arguments": ["dprint", "lsp"] } }, "json-language-server": { "settings": { diff --git a/biome.jsonc b/biome.jsonc index 58aafc92..75d9a035 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "node_modules/@biomejs/biome/configuration_schema.json", + "$schema": "node_modules/biome/configuration_schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/bun.lock b/bun.lock index 8db999b1..99f847ae 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,6 @@ }, "devDependencies": { "@arethetypeswrong/core": "^0.18.5", - "@biomejs/biome": "<=2.5.2 || >=2.5.4", "@iarna/toml": "^2.2.5", "@shikijs/transformers": "^4.3.1", "@shikijs/vitepress-twoslash": "^4.3.1", @@ -18,6 +17,7 @@ "@types/node": "catalog:", "@typescript/native": "catalog:", "@vitest/coverage-v8": "^4.1.10", + "biome": "npm:@biomejs/biome@^2.5.4", "dprint": "^0.55.1", "floating-vue": "^5.2.2", "knip": "^6.25.0", @@ -87,23 +87,21 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], - "@biomejs/biome": ["@biomejs/biome@2.5.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.2", "@biomejs/cli-darwin-x64": "2.5.2", "@biomejs/cli-linux-arm64": "2.5.2", "@biomejs/cli-linux-arm64-musl": "2.5.2", "@biomejs/cli-linux-x64": "2.5.2", "@biomejs/cli-linux-x64-musl": "2.5.2", "@biomejs/cli-win32-arm64": "2.5.2", "@biomejs/cli-win32-x64": "2.5.2" }, "bin": { "biome": "bin/biome" } }, "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww=="], - - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-U1jaluLw1qQc2Tx7/CeSoL9N5XcqIH+GWjpUAy1ouB5nVjSCMNO+NNHdY3RAs8zxNurLWAdj6pehQdCA2zyU+Q=="], "@braidai/lang": ["@braidai/lang@1.1.2", "", {}, "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA=="], @@ -739,6 +737,8 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "biome": ["@biomejs/biome@2.5.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.4", "@biomejs/cli-darwin-x64": "2.5.4", "@biomejs/cli-linux-arm64": "2.5.4", "@biomejs/cli-linux-arm64-musl": "2.5.4", "@biomejs/cli-linux-x64": "2.5.4", "@biomejs/cli-linux-x64-musl": "2.5.4", "@biomejs/cli-win32-arm64": "2.5.4", "@biomejs/cli-win32-x64": "2.5.4" }, "bin": { "biome": "bin/biome" } }, "sha512-xy5FNE5kQJKyK5MR1gJy6ztXYx4WBAbYGlK04lMEgmyPRWKybY9NFwiG9yo0XdzOU8Xvhj41u034J1ywfoWfMw=="], + "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], diff --git a/package.json b/package.json index 0f6ad087..b49e7f23 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,6 @@ }, "devDependencies": { "@arethetypeswrong/core": "^0.18.5", - "@biomejs/biome": "<=2.5.2 || >=2.5.4", "@iarna/toml": "^2.2.5", "@shikijs/transformers": "^4.3.1", "@shikijs/vitepress-twoslash": "^4.3.1", @@ -123,6 +122,7 @@ "@types/node": "catalog:", "@typescript/native": "catalog:", "@vitest/coverage-v8": "^4.1.10", + "biome": "npm:@biomejs/biome@^2.5.4", "dprint": "^0.55.1", "floating-vue": "^5.2.2", "knip": "^6.25.0", diff --git a/src/core/cli/root-help.ts b/src/core/cli/root-help.ts index 4fec2cf4..ace2d88d 100644 --- a/src/core/cli/root-help.ts +++ b/src/core/cli/root-help.ts @@ -237,6 +237,7 @@ function completionsFlagSchema(shells: readonly string[]): FlagSchema { valueHint: undefined, prompt: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, propagate: false, negation: undefined, diff --git a/src/core/completion/shells/shared.ts b/src/core/completion/shells/shared.ts index 4224a1b9..eab3bff3 100644 --- a/src/core/completion/shells/shared.ts +++ b/src/core/completion/shells/shared.ts @@ -190,6 +190,7 @@ function createSyntheticRootFlag(description: string): FlagSchema { valueHint: undefined, prompt: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, propagate: false, negation: undefined, diff --git a/src/core/json-schema/json-schema.test.ts b/src/core/json-schema/json-schema.test.ts index f9f6677d..04caf077 100644 --- a/src/core/json-schema/json-schema.test.ts +++ b/src/core/json-schema/json-schema.test.ts @@ -104,6 +104,7 @@ function argEntry( enumValues: undefined, numberConstraints: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, ...overrides, }, diff --git a/src/core/resolve/index.ts b/src/core/resolve/index.ts index e6a350e6..d628fa04 100644 --- a/src/core/resolve/index.ts +++ b/src/core/resolve/index.ts @@ -22,6 +22,7 @@ import { resolveArgs } from './args.ts'; import type { DeprecationWarning, ResolveOptions, ResolveResult } from './contracts.ts'; import { collectValidationErrors, isNonEmpty, throwAggregatedErrors } from './errors.ts'; import { resolveFlags } from './flags.ts'; +import { applyStandardValidators } from './standard.ts'; /** * Resolve parsed values against a command schema. @@ -98,6 +99,11 @@ async function resolve( errors.push(...collectValidationErrors(error)); } + const validated = await applyStandardValidators(schema, flags, args); + flags = validated.flags; + args = validated.args; + errors.push(...validated.errors); + if (isNonEmpty(errors)) { throwAggregatedErrors(errors); } diff --git a/src/core/resolve/resolve-standard-schema.test.ts b/src/core/resolve/resolve-standard-schema.test.ts new file mode 100644 index 00000000..a611b063 --- /dev/null +++ b/src/core/resolve/resolve-standard-schema.test.ts @@ -0,0 +1,157 @@ +/** + * Standard Schema v1 interop — validators attached via `flag.custom()` / + * `arg.custom()` are applied after resolution, source-agnostically, for both + * sync and async validators. + */ + +import { describe, expect, expectTypeOf, it } from 'vitest'; +import { arg } from '#internals/core/schema/arg.ts'; +import { command } from '#internals/core/schema/command.ts'; +import { flag, type InferFlag } from '#internals/core/schema/flag.ts'; +import type { StandardSchemaV1 } from '#internals/core/schema/standard.ts'; +import { runCommand } from '#internals/core/testkit/index.ts'; + +// === Test helpers + +/** Minimal hand-rolled Standard Schema validator — no external dependency. */ +function standard( + validate: StandardSchemaV1['~standard']['validate'], + vendor = 'test', +): StandardSchemaV1 { + return { '~standard': { version: 1, vendor, validate } }; +} + +/** Sync validator: coerces to an even integer or reports an issue. */ +const evenInt = standard((value) => { + const n = Number(value); + if (!Number.isInteger(n) || n % 2 !== 0) { + return { issues: [{ message: 'must be an even integer' }] }; + } + return { value: n }; +}); + +/** Async validator: accepts a string longer than two chars. */ +const asyncName = standard(async (value) => { + if (typeof value !== 'string' || value.length <= 2) { + return { issues: [{ message: 'must be longer than two characters' }] }; + } + return { value }; +}); + +// === Flags + +describe('Standard Schema v1 interop — flags', () => { + it('passes and transforms a valid CLI value', async () => { + const cmd = command('run') + .flag('count', flag.custom(evenInt)) + .action(({ flags, out }) => + out.log(`count=${String(flags.count)} type=${typeof flags.count}`), + ); + + const result = await runCommand(cmd, ['--count', '4']); + + expect(result.exitCode).toBe(0); + expect(result.stdout[0]).toBe('count=4 type=number\n'); + }); + + it('rejects an invalid CLI value with CONSTRAINT_VIOLATED', async () => { + const cmd = command('run') + .flag('count', flag.custom(evenInt)) + .action(({ out }) => out.log('unreachable')); + + const result = await runCommand(cmd, ['--count', '3']); + + expect(result.exitCode).toBe(2); + expect(result.error?.code).toBe('CONSTRAINT_VIOLATED'); + expect(result.error?.message).toContain('--count failed validation'); + expect(result.error?.message).toContain('must be an even integer'); + }); + + it('validates values from env, not just CLI (source-agnostic)', async () => { + const cmd = command('run') + .flag('count', flag.custom(evenInt).env('COUNT')) + .action(({ flags, out }) => out.log(`count=${String(flags.count)}`)); + + const ok = await runCommand(cmd, [], { env: { COUNT: '8' } }); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('count=8\n'); + + const bad = await runCommand(cmd, [], { env: { COUNT: '7' } }); + expect(bad.exitCode).toBe(2); + expect(bad.error?.code).toBe('CONSTRAINT_VIOLATED'); + }); + + it('skips validation for an absent optional flag', async () => { + const cmd = command('run') + .flag('count', flag.custom(evenInt)) + .action(({ flags, out }) => out.log(`count=${String(flags.count)}`)); + + const result = await runCommand(cmd, []); + + expect(result.exitCode).toBe(0); + expect(result.stdout[0]).toBe('count=undefined\n'); + }); + + it('awaits an async validator', async () => { + const cmd = command('run') + .flag('name', flag.custom(asyncName)) + .action(({ flags, out }) => out.log(`name=${String(flags.name)}`)); + + const ok = await runCommand(cmd, ['--name', 'booga']); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('name=booga\n'); + + const bad = await runCommand(cmd, ['--name', 'no']); + expect(bad.exitCode).toBe(2); + expect(bad.error?.message).toContain('must be longer than two characters'); + }); +}); + +// === Args + +describe('Standard Schema v1 interop — args', () => { + it('validates and transforms a positional arg', async () => { + const cmd = command('run') + .arg('n', arg.custom(evenInt)) + .action(({ args, out }) => out.log(`n=${String(args.n)} type=${typeof args.n}`)); + + const ok = await runCommand(cmd, ['6']); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('n=6 type=number\n'); + + const bad = await runCommand(cmd, ['5']); + expect(bad.exitCode).toBe(2); + expect(bad.error?.code).toBe('CONSTRAINT_VIOLATED'); + expect(bad.error?.message).toContain(' failed validation'); + }); + + it('awaits an async validator for a positional arg', async () => { + const cmd = command('run') + .arg('name', arg.custom(asyncName)) + .action(({ args, out }) => out.log(`name=${String(args.name)}`)); + + const ok = await runCommand(cmd, ['booga']); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('name=booga\n'); + + const bad = await runCommand(cmd, ['no']); + expect(bad.exitCode).toBe(2); + expect(bad.error?.code).toBe('CONSTRAINT_VIOLATED'); + expect(bad.error?.message).toContain('must be longer than two characters'); + }); +}); + +// === Type inference + +describe('Standard Schema v1 interop — types', () => { + it('infers the validator output type on flags', () => { + const typed = standard((value) => ({ value: Number(value) })); + const built = flag.custom(typed); + expectTypeOf>().toEqualTypeOf(); + }); + + it('still infers a parse function return type on flags', () => { + const built = flag.custom((raw) => new URL(String(raw))); + expectTypeOf>().toEqualTypeOf(); + }); +}); diff --git a/src/core/resolve/standard.ts b/src/core/resolve/standard.ts new file mode 100644 index 00000000..fd037c14 --- /dev/null +++ b/src/core/resolve/standard.ts @@ -0,0 +1,123 @@ +/** + * Standard Schema v1 validation pass. + * + * Runs after CLI/env/config/prompt/default resolution so a validator attached + * via `flag.custom()` / `arg.custom()` sees the final value regardless of its + * source. `~standard.validate` may be sync or async; both are awaited here. + * Issues become `CONSTRAINT_VIOLATED` validation errors; a successful result + * replaces the value with the validator's (possibly transformed) output. + * + * @module dreamcli/core/resolve/standard + */ + +import { ValidationError } from '#internals/core/errors/index.ts'; +import type { CommandSchema } from '#internals/core/schema/index.ts'; +import type { + StandardSchemaV1, + StandardSchemaV1Issue, + StandardSchemaV1PathSegment, +} from '#internals/core/schema/standard.ts'; + +/** Resolved values after the Standard Schema pass, plus any issues found. */ +interface StandardValidationResult { + readonly flags: Readonly>; + readonly args: Readonly>; + readonly errors: readonly ValidationError[]; +} + +/** Render an issue path (e.g. `['a', 0]` → `a.0`), or `undefined` when empty. */ +function formatIssuePath( + path: ReadonlyArray | undefined, +): string | undefined { + if (path === undefined || path.length === 0) { + return undefined; + } + return path + .map((segment) => { + const key = typeof segment === 'object' ? segment.key : segment; + return typeof key === 'symbol' ? key.toString() : String(key); + }) + .join('.'); +} + +/** Build a `CONSTRAINT_VIOLATED` message from a validator's issues. */ +function issuesMessage(label: string, issues: ReadonlyArray): string { + const rendered = issues.map((issue) => { + const path = formatIssuePath(issue.path); + return path === undefined ? issue.message : `${path}: ${issue.message}`; + }); + return `${label} failed validation: ${rendered.join('; ')}`; +} + +/** Await a validator against a single value, mapping failure to a typed error. */ +async function validateValue( + label: string, + value: unknown, + validator: StandardSchemaV1, +): Promise< + | { readonly ok: true; readonly value: unknown } + | { readonly ok: false; readonly error: ValidationError } +> { + const result = await validator['~standard'].validate(value); + if (result.issues !== undefined) { + return { + ok: false, + error: new ValidationError(issuesMessage(label, result.issues), { + code: 'CONSTRAINT_VIOLATED', + details: { value, issues: result.issues.map((issue) => issue.message) }, + suggest: `Provide a value for ${label} that satisfies its validator`, + }), + }; + } + return { ok: true, value: result.value }; +} + +/** + * Validate resolved flag and arg values against any attached Standard Schema + * validators. Values without a validator, or resolved to `undefined`, pass + * through untouched. + * + * @param schema - The command schema carrying per-flag/arg validators. + * @param flags - Resolved flag values, keyed by flag name. + * @param args - Resolved arg values, keyed by arg name. + * @returns The (possibly transformed) values and any constraint violations. + * @internal + */ +async function applyStandardValidators( + schema: CommandSchema, + flags: Readonly>, + args: Readonly>, +): Promise { + const errors: ValidationError[] = []; + const nextFlags: Record = { ...flags }; + for (const [name, flagSchema] of Object.entries(schema.flags)) { + const validator = flagSchema.standard; + if (validator === undefined || flags[name] === undefined) { + continue; + } + const result = await validateValue(`--${name}`, flags[name], validator); + if (result.ok) { + nextFlags[name] = result.value; + } else { + errors.push(result.error); + } + } + + const nextArgs: Record = { ...args }; + for (const entry of schema.args) { + const validator = entry.schema.standard; + if (validator === undefined || args[entry.name] === undefined) { + continue; + } + const result = await validateValue(`<${entry.name}>`, args[entry.name], validator); + if (result.ok) { + nextArgs[entry.name] = result.value; + } else { + errors.push(result.error); + } + } + + return { flags: nextFlags, args: nextArgs, errors }; +} + +export { applyStandardValidators }; diff --git a/src/core/schema/arg.ts b/src/core/schema/arg.ts index e296b9c0..c1af719c 100644 --- a/src/core/schema/arg.ts +++ b/src/core/schema/arg.ts @@ -9,6 +9,7 @@ */ import { assertNumberConstraints, type NumberConstraints } from './number-constraints.ts'; +import type { InferStandardOutput, StandardSchemaV1 } from './standard.ts'; // --- Type-level configuration (phantom state tracked through the chain) @@ -99,6 +100,13 @@ type ArgKind = (typeof ARG_KINDS)[number]; /** Custom parse function for `arg.custom()`. */ type ArgParseFn = (raw: string) => T; +/** + * The value type produced by `arg.custom()` for a given argument: the return + * type of a parse function, or the output type of a Standard Schema validator. + */ +type CustomArgValue = + A extends ArgParseFn ? T : A extends StandardSchemaV1 ? InferStandardOutput : never; + /** * The runtime descriptor stored inside every {@linkcode ArgBuilder}. Consumers (parser, * help generator) read this to understand the arg's shape without touching @@ -140,6 +148,15 @@ interface ArgSchema { readonly numberConstraints: NumberConstraints | undefined; /** Custom parse function (only when `kind === 'custom'`). */ readonly parseFn: ArgParseFn | undefined; + /** + * Standard Schema v1 validator applied to the resolved value. + * + * When set, the value from any source (CLI, env, stdin, default) is + * validated after resolution via `~standard.validate`. Sync and async + * validators are both awaited; issues surface as a `CONSTRAINT_VIOLATED` + * {@link ValidationError}. Only meaningful when `kind === 'custom'`. + */ + readonly standard: StandardSchemaV1 | undefined; /** * Deprecation marker. * @@ -189,6 +206,7 @@ function createArgSchema(kind: ArgKind, overrides?: Partial): ArgSche enumValues: undefined, numberConstraints: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, ...overrides, }; @@ -745,6 +763,34 @@ interface ArgFactory { readonly variadic: false; readonly argKind: 'custom'; }>; + + /** + * Custom positional argument validated by a Standard Schema v1 validator + * (zod, valibot, arktype, …). The resolved value from any source is + * validated after resolution; the arg's value type is the validator's + * output type. + * + * Sync and async validators are both supported. Validation issues surface + * as a `CONSTRAINT_VIOLATED` error naming the argument. + * + * @example + * ```ts + * import { z } from 'zod'; + * arg.custom(z.string().uuid()) + * // inferred type: string + * ``` + * + * @param schema - A Standard Schema v1 validator. + * @returns A required custom {@link ArgBuilder} typed to the validator's output. + */ + custom( + schema: S, + ): ArgBuilder<{ + readonly valueType: InferStandardOutput; + readonly presence: 'required'; + readonly variadic: false; + readonly argKind: 'custom'; + }>; } /** @@ -813,13 +859,18 @@ const arg: ArgFactory = { return new ArgBuilder(createArgSchema('enum', { enumValues: values })); }, - custom(parseFn: ArgParseFn): ArgBuilder<{ - readonly valueType: T; + custom | StandardSchemaV1>( + parseFnOrSchema: A, + ): ArgBuilder<{ + readonly valueType: CustomArgValue; readonly presence: 'required'; readonly variadic: false; readonly argKind: 'custom'; }> { - return new ArgBuilder(createArgSchema('custom', { parseFn: parseFn as ArgParseFn })); + if (typeof parseFnOrSchema === 'function') { + return new ArgBuilder(createArgSchema('custom', { parseFn: parseFnOrSchema })); + } + return new ArgBuilder(createArgSchema('custom', { standard: parseFnOrSchema })); }, }; diff --git a/src/core/schema/flag.ts b/src/core/schema/flag.ts index 616cd75e..1071c0ae 100644 --- a/src/core/schema/flag.ts +++ b/src/core/schema/flag.ts @@ -17,6 +17,7 @@ import type { PromptConfig, SelectPromptConfig, } from './prompt.ts'; +import type { InferStandardOutput, StandardSchemaV1 } from './standard.ts'; import { assertStringConstraints, type StringConstraints } from './string-constraints.ts'; import { type DateFlagOptions, @@ -180,6 +181,13 @@ type FlagKind = (typeof FLAG_KINDS)[number]; */ type FlagParseFn = (raw: unknown) => T; +/** + * The value type produced by `flag.custom()` for a given argument: the return + * type of a parse function, or the output type of a Standard Schema validator. + */ +type CustomFlagValue = + A extends FlagParseFn ? T : A extends StandardSchemaV1 ? InferStandardOutput : never; + /** Options accepted by `flag.path()`. */ interface PathFlagOptions { /** @@ -324,6 +332,15 @@ interface FlagSchema { readonly prompt: PromptConfig | undefined; /** Custom parse function (only when `kind === 'custom'`). */ readonly parseFn: FlagParseFn | undefined; + /** + * Standard Schema v1 validator applied to the resolved value. + * + * When set, the value from any source (CLI, env, config, prompt, default) + * is validated after resolution via `~standard.validate`. Sync and async + * validators are both awaited; issues surface as a `CONSTRAINT_VIOLATED` + * {@link ValidationError}. Only meaningful when `kind === 'custom'`. + */ + readonly standard: StandardSchemaV1 | undefined; /** * Deprecation marker. * @@ -484,6 +501,7 @@ function createSchema(kind: FlagKind, overrides?: FlagSchemaOverrides): FlagSche valueHint: undefined, prompt: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, propagate: false, negation: undefined, @@ -1183,6 +1201,34 @@ interface FlagFactory { readonly elementEligible: true; }>; + /** + * Custom flag validated by a Standard Schema v1 validator (zod, valibot, + * arktype, …). The resolved value from any source is validated after + * resolution; the flag's value type is the validator's output type. + * + * Sync and async validators are both supported. Validation issues surface + * as a `CONSTRAINT_VIOLATED` error naming the flag. + * + * @example + * ```ts + * import { z } from 'zod'; + * flag.custom(z.string().url()) + * // inferred type: string | undefined + * ``` + * + * @param schema - A Standard Schema v1 validator. + * @returns A {@link FlagBuilder} whose value type is the validator's output. + */ + custom( + schema: S, + ): FlagBuilder<{ + readonly valueType: InferStandardOutput; + readonly presence: 'optional'; + readonly optionalFallback: 'undefined'; + readonly flagKind: 'custom'; + readonly elementEligible: true; + }>; + /** * URL-valued flag. Parses into a `URL`; invalid URLs are rejected with an * `INVALID_VALUE` error naming the flag. @@ -1419,14 +1465,19 @@ const flag: FlagFactory = { return new FlagBuilder(createSchema('array', { elementSchema: element.schema })); }, - custom(parseFn: FlagParseFn): FlagBuilder<{ - readonly valueType: T; + custom | StandardSchemaV1>( + parseFnOrSchema: A, + ): FlagBuilder<{ + readonly valueType: CustomFlagValue; readonly presence: 'optional'; readonly optionalFallback: 'undefined'; readonly flagKind: 'custom'; readonly elementEligible: true; }> { - return new FlagBuilder(createSchema('custom', { parseFn: parseFn as FlagParseFn })); + if (typeof parseFnOrSchema === 'function') { + return new FlagBuilder(createSchema('custom', { parseFn: parseFnOrSchema })); + } + return new FlagBuilder(createSchema('custom', { standard: parseFnOrSchema })); }, url(options?: UrlFlagOptions): FlagBuilder<{ diff --git a/src/core/schema/index.ts b/src/core/schema/index.ts index 900c1b81..1edc8465 100644 --- a/src/core/schema/index.ts +++ b/src/core/schema/index.ts @@ -101,6 +101,18 @@ export { validateNumberConstraints, } from './number-constraints.ts'; export type { RunOptions, RunResult } from './run.ts'; +export type { + InferStandardInput, + InferStandardOutput, + StandardSchemaV1, + StandardSchemaV1FailureResult, + StandardSchemaV1Issue, + StandardSchemaV1PathSegment, + StandardSchemaV1Props, + StandardSchemaV1Result, + StandardSchemaV1SuccessResult, + StandardSchemaV1Types, +} from './standard.ts'; export type { StringConstraints, StringConstraintViolation } from './string-constraints.ts'; export { describeStringConstraintViolation, diff --git a/src/core/schema/standard.ts b/src/core/schema/standard.ts new file mode 100644 index 00000000..0a750c0f --- /dev/null +++ b/src/core/schema/standard.ts @@ -0,0 +1,81 @@ +/** + * The Standard Schema v1 interface, vendored as types only. + * + * Mirrors the specification at https://standardschema.dev so validators from + * zod, valibot, arktype, and any other conforming library can be passed to + * `flag.custom()` / `arg.custom()` without adding a runtime dependency. Every + * member is a type or interface, so this module erases to nothing at runtime. + * + * @module + */ + +/** A schema that conforms to the Standard Schema v1 specification. */ +export interface StandardSchemaV1 { + /** The Standard Schema properties, namespaced under a well-known key. */ + readonly '~standard': StandardSchemaV1Props; +} + +/** The properties exposed under a validator's `~standard` key. */ +export interface StandardSchemaV1Props { + /** The version number of the specification. */ + readonly version: 1; + /** The vendor name of the schema library. */ + readonly vendor: string; + /** Validate an unknown value, returning its output or the issues found. */ + readonly validate: ( + value: unknown, + ) => StandardSchemaV1Result | Promise>; + /** Inferred input and output types, present only at the type level. */ + readonly types?: StandardSchemaV1Types | undefined; +} + +/** The result of validating a value: either its output or a list of issues. */ +export type StandardSchemaV1Result = + | StandardSchemaV1SuccessResult + | StandardSchemaV1FailureResult; + +/** A successful validation carrying the parsed output value. */ +export interface StandardSchemaV1SuccessResult { + /** The validated (and possibly transformed) value. */ + readonly value: Output; + /** Absent on success. */ + readonly issues?: undefined; +} + +/** A failed validation carrying a non-empty list of issues. */ +export interface StandardSchemaV1FailureResult { + /** The issues that caused validation to fail. */ + readonly issues: ReadonlyArray; +} + +/** A single validation issue. */ +export interface StandardSchemaV1Issue { + /** The human-readable error message. */ + readonly message: string; + /** The path to the offending value, when the validator reports one. */ + readonly path?: ReadonlyArray | undefined; +} + +/** One segment of an issue path. */ +export interface StandardSchemaV1PathSegment { + /** The key of this path segment. */ + readonly key: PropertyKey; +} + +/** The type-level input and output carried by a validator. */ +export interface StandardSchemaV1Types { + /** The input type accepted by the validator. */ + readonly input: Input; + /** The output type produced by the validator. */ + readonly output: Output; +} + +/** Infer the input type of a Standard Schema validator. */ +export type InferStandardInput = NonNullable< + Schema['~standard']['types'] +>['input']; + +/** Infer the output type of a Standard Schema validator. */ +export type InferStandardOutput = NonNullable< + Schema['~standard']['types'] +>['output']; From d9352ded220f02d14017f71e085c4b9e9073f4a3 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 16 Jul 2026 13:33:16 +0200 Subject: [PATCH 3/8] chore(examples): make gh workspace standalone Let the GH canary link the repository package during install, use explicit workspace-local tool versions, and exercise whole-`import.meta` manifest anchoring. Refresh the lockfile to capture the resulting toolchain state. --- bun.lock | 108 ++++++++++++++++++++------------------- examples/gh/package.json | 15 ++++-- examples/gh/src/main.ts | 2 +- 3 files changed, 67 insertions(+), 58 deletions(-) diff --git a/bun.lock b/bun.lock index 99f847ae..fea8d13b 100644 --- a/bun.lock +++ b/bun.lock @@ -42,8 +42,8 @@ "@kjanat/dreamcli": "link:@kjanat/dreamcli", }, "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", + "@types/bun": "latest", + "typescript": "latest", }, }, "examples/pwsh-demo": { @@ -113,15 +113,15 @@ "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260708.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-HXFCvhS1wpg3uXO0CLUwmwC41i2loM5FSK69EUchOBpmYBAXxT1oHLm6EOA5lqhTk5Mu9kjRiQYxa1GwKPwfJg=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260710.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-OqJl2eWF5+y9jarMm3YqqCTUe7Hd4ihogX5jyRU8iaAgOVyDr/Bk6aXpPCVUi1/MHzO93a18R/TmSTtzmB0sQw=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260708.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JVlJaKDoRTVKSroHIlf8g3UCPjKj4iDbMZE2CNYht5qQ+2rL0FAUiVlV82G3BqKnnw9kHYnnsMzC08b9zVtdzA=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260710.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MYBqWgUblO+VlGvO73zYsH3hB9tdRj+yLyt5IHDFWryipb2l1efmNiWtAOkIhSRfypqLYGFrfpaDm2Hg00XVKw=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260708.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3daE60YdD7YX0Jtuzc9DE/r/qMkmx8ZvHTkF8Mzmp3F5tbzlV0DAzmu5PFUPF2WuvtKbAhZKbvC2cHmWpQYxnA=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260710.1", "", { "os": "linux", "cpu": "x64" }, "sha512-lVWUgqI8qrkqvaCBGElu1kdaUFdAvaS2RD8K4qkCFP9hI3f5TCXumEs5qWSeZkvKum0+X/uJZ5hBFWsYI5SmoQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260708.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-VLdNYOx5Hj+9C6isy0ACWZsbMtSxex2DIJWEe7cZxUdlphZ58ZT8zxNXK8yunFiowd34hn3VwGMopdvdj8lvmA=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260710.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-kDwDPItBjAI4JL0df9Fma2N+Qggbm77IB/DnroAkEGQ79fpR80sYMyuB/ZQKyjEk9f48Ocq7HCCLq59qVSyNqA=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260710.1", "", { "os": "win32", "cpu": "x64" }, "sha512-GcLHy1oN1dfK6g1Z7UDV9f5xMGyTfPwcjWQ0sfWKH31IsoEVCRapnj3IC0PoIrDbnoo6irGPP0CwVs3WzdTajw=="], "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], @@ -131,35 +131,35 @@ "@docsearch/sidepanel-js": ["@docsearch/sidepanel-js@4.6.3", "", {}, "sha512-grGSmvXzG0if+mrzdIKykvpIAuEQ9u0sEJ2eLRRCaQfJvsWqh2C2/aY04bIzWvDh7myi5rvl8D+tUNsVrjYQ3A=="], - "@dprint/android-arm64": ["@dprint/android-arm64@0.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-H1UQIcBJEo1dA8AVrkPnw5AYMMGHNe4w0lq9rEDSY3Lds0VUKACu2MK3JLNARpjljh3WKeO109J26Wpq8PTuGQ=="], + "@dprint/android-arm64": ["@dprint/android-arm64@0.55.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cywqM0E2h8lCA/b5BMFcuascaGuekm2lnyKb5hRH9juKbsqKeR8A7qf2p3nVWRQ8cM7djOnQwhy8ecQ3qd6j9A=="], - "@dprint/android-x64": ["@dprint/android-x64@0.55.1", "", { "os": "android", "cpu": "x64" }, "sha512-8dPiGpEo4S4cQhnLaoHtNm4HiqR9DIPr/81z3gWb5qTmJp8bKj+3DO4mAmmldMRBicmpXCss0xudniNTxnWOMg=="], + "@dprint/android-x64": ["@dprint/android-x64@0.55.2", "", { "os": "android", "cpu": "x64" }, "sha512-BvcqquD94dSYQP2FzBuojU/8vITpwz80uAkub2xCCx5g7zYdUZ98tK+N3GKfRS5AUJ1LpRPJgbO09rRLEo8leA=="], - "@dprint/darwin-arm64": ["@dprint/darwin-arm64@0.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c65u8f63R/etCKP0CZ/RkbMCRC99wgZDt/vhwjY2QdO0k23dMKXKEV6+ruUEwV2toh+QIRT02dRL7BtxjA0V7w=="], + "@dprint/darwin-arm64": ["@dprint/darwin-arm64@0.55.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HjzasuPaC0EBHxEpS3Px/OPcxqZYln37xuAyYE0GFAhDuaotZEUpM8VE05Tzo4E32y7LKgLPT8CZVtEG5Ck7mQ=="], - "@dprint/darwin-x64": ["@dprint/darwin-x64@0.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-AsET9+4rMk7ZFEMq3HYl/hYNBMBrR/juiSzclEU0oUCf3koxFbVKL8Srad8Vhyv8jEnOaMn78TCnahmHhp1SCQ=="], + "@dprint/darwin-x64": ["@dprint/darwin-x64@0.55.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-7XSF9XERvimg3XakXNlJRpJM9an5HKibWWShwCJr7Dz1Xp7P3pomjx9AtYV5EeD49GXu9FKdFfDiTC4BJtAVjA=="], - "@dprint/linux-arm64-glibc": ["@dprint/linux-arm64-glibc@0.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-vt7w+aL2MjtD3RRfnUTDuK/b7xg1/feBe3WoV0S2rGNmPEJI+K7wwBXkOl/I77V9w3PP2zvN/aRY5dMOZrkbSg=="], + "@dprint/linux-arm64-glibc": ["@dprint/linux-arm64-glibc@0.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2h5oe4tHGXJOTLU5BxVJLGPP1qQQxxECdjUaRkLxGn2rZIEf/7HwoJ+TjwMUaIVg4QxAFxfiMJJ3MRZjumJpsg=="], - "@dprint/linux-arm64-musl": ["@dprint/linux-arm64-musl@0.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-7shUIOvJcTn4IYEPlb/Up4KLt5kbDuCTdc3DN68i88XT+nRBFQatD7Qok+6Ysra34KJMxF88QVHiimjH1ABy4A=="], + "@dprint/linux-arm64-musl": ["@dprint/linux-arm64-musl@0.55.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z1X9mNLHWoSI7CHz888H2w27IV2UQ5lL/YbTwgguJM9ApLgvkMer5qr4pLlXF6vEY3+Rm0445eo3uojbjE7PTw=="], - "@dprint/linux-loong64-glibc": ["@dprint/linux-loong64-glibc@0.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-G2mXiOGpkeC3LP8huSnImxU7hMvRY6rHIW+yBuw1vDDm5r6+H9CVhcXr3TMOk3poZ7IlWwQnDmP0/RuGQUEpYw=="], + "@dprint/linux-loong64-glibc": ["@dprint/linux-loong64-glibc@0.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-qxk4Cg/SjO14DALoG1MFZL+n6pEvCyd7VDHg1cQIpp82mZ+wtRaWkJqSaItWj12x8jihA32wAoQ/8OHz+5FFsQ=="], - "@dprint/linux-loong64-musl": ["@dprint/linux-loong64-musl@0.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uO2r4QPYawDfQUyATr0opqfMkG2hVpz3yScRKs5ve9PWzqx2f5kJaaLuLVoXwSbWqDkozMGPcldAVSpOwGJ24Q=="], + "@dprint/linux-loong64-musl": ["@dprint/linux-loong64-musl@0.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-Mr+kRdZnxhUHBmhhADkYOo1g81PYvbh7eBo+lpMStgIvL/5Z93mGAcp/1NFcpeN02pilaI1N59awV08ApJ19cA=="], - "@dprint/linux-ppc64-glibc": ["@dprint/linux-ppc64-glibc@0.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NVR+BN4kTQd/vJ91YOGSkBotAM2f1hFD+HD5zq5Oc1djxjl1U2Zvc667UWKamxaXdv/ssZxkLuj0lbvbx9E3/A=="], + "@dprint/linux-ppc64-glibc": ["@dprint/linux-ppc64-glibc@0.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C8XEL3f3yg+MWZD4qlot+ibJ0GDPvCT5jq9ErwPdKteyIPpe6IPSqiEApYSQ194Q7audL8rgS0WdJsW4HNVhmw=="], - "@dprint/linux-ppc64-musl": ["@dprint/linux-ppc64-musl@0.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-34Db83XGoKbwmvIY52sokywIMCmmIZM1vzw4ILlOkKugG51TltBOzLCssT9Rkfxz/TjR9dgwtBHEQJZjftuQ6Q=="], + "@dprint/linux-ppc64-musl": ["@dprint/linux-ppc64-musl@0.55.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tSGe4bTmL5/EhNH38baCKWBGxOXVB0M5Loxnpls7wsQjuLpzWnzDCw4wiO3C7PrKLbAe4sJUWhbgdDXa3TJ2eQ=="], - "@dprint/linux-riscv64-glibc": ["@dprint/linux-riscv64-glibc@0.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MVeYH48GhR3QkfhMhhYbXl9x+iO2sK1nNpr2ByJ4IKogHIlvlFff0eeukqAZZAod+rhAzJnLMBGgIDbIUHWK1g=="], + "@dprint/linux-riscv64-glibc": ["@dprint/linux-riscv64-glibc@0.55.2", "", { "os": "linux", "cpu": "none" }, "sha512-gSaoq9e3vGTfYm5jcEd86YysFsp8OJF/CGZgBzNNu3NSjLEs8VVgTObDxQI+U7ex2mA17lbpWyr3diejaaK+bg=="], - "@dprint/linux-x64-glibc": ["@dprint/linux-x64-glibc@0.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ILHgXOIIXeZDjt51egaC4kmu744UuNfaauUM1El+dG9QGan4Vv4f0RRDDbQMKN95zTpacECuNKEhuUtS8euNWA=="], + "@dprint/linux-x64-glibc": ["@dprint/linux-x64-glibc@0.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-GBLWjuqTT5OGbqh2gQENQihhfRn/AjdDu2SVwjmbZcOFR80HMppIfFrIIolBB3FLyrZk+M8rMk8EAo0tQ3PAXw=="], - "@dprint/linux-x64-musl": ["@dprint/linux-x64-musl@0.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-SwWxWd313VkeH63fR4IlQCDfJNJ+mSw50BidP0PQInCNbUHYOIeWTcyzAaVa8GQdLvmhiNcJ0pmTDOL0F98QIQ=="], + "@dprint/linux-x64-musl": ["@dprint/linux-x64-musl@0.55.2", "", { "os": "linux", "cpu": "x64" }, "sha512-GZEUpmtxCOiauRtqOqT/erAjy2ws8dOxx7oQJFf9g5bypisuapwymvr7fUVVb8nqccFqjx7E5kN4IxKDlzntHA=="], - "@dprint/win32-arm64": ["@dprint/win32-arm64@0.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-9jiZWZ2AkaaXIkaOpOoiP6v0XxuBFp1Hyi3kUMUAY4NVCmvlSEDQhr+UZgnDhozYygDbAV13iNMLERDeoUa0nA=="], + "@dprint/win32-arm64": ["@dprint/win32-arm64@0.55.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-dxDdxU4a6wQ4K0omdAxW7o0mUdOI03z0/Ah3N6O9Ja7wAMQI5sbfzq7DOJ739UVXtHSB4zBTtYuk1MrBOIyVQQ=="], - "@dprint/win32-x64": ["@dprint/win32-x64@0.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-+RmeJL8zzlEWTcrvyFN51rz0S98tV43Socs7xeVGE2S/EH5dG2fG2/fBe6RB3kuyZSfGqByApYeicIdshYIAgw=="], + "@dprint/win32-x64": ["@dprint/win32-x64@0.55.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UTiDSShHbrkx+z719/MffgwGYD8PaypdVmG4vVJVjmF5Hoin6EfugHhSoBf8+G6U9rF+uDIIl0idZq+6Bifzwg=="], "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], @@ -677,49 +677,49 @@ "@vueuse/shared": ["@vueuse/shared@14.3.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg=="], - "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.6.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LDJtpOKtcv9f3V0eDUwFmmy47t2VC+DAuN+gq80R1IA+fa0d408i6sHsVtt6n+g5rf8f86ySoPSAe94lHt6Ixw=="], + "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.6.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pbDcFygFmbvo0jGFq5U0m5Sa9U8aVttVJWbBHZDZ68w/X48HdDWS1V4XvBacs8XkmWbTr/ef5fMGG7HsngqTmg=="], - "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.6.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-fBwpBOh33W7N87F94SVNExwm2KUV3ROhk51okr3Oy2ost1/JJuBWINVjcgwd2WPKZEFzUXgCzj/03UR/G+WIrQ=="], + "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.6.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-qZMrnA4i8OfqL3NMGoOvLdh1vby8cCGsmNo+tJQIIezXO557m3fdQLenz/57GqtBnnGWzWqd/aDp+faNxWlLwQ=="], - "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.6.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UpMkskQV3a5oPnJV+GFFupIqnLwSBD4ZsZAVUZuNVkrqct433FHKqTwuG+P5JhHZbmhf+++3Ie/V2sgduyXrAQ=="], + "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.6.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2+SFpfem2GBH6BlCTAq6R44bZwuieduwRWHkCQnSgbK8tdEjMwB0Ix0IchryVBn6hdiWCZSTkSE3UILliRXsRQ=="], - "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.6.1", "", { "os": "linux", "cpu": "arm" }, "sha512-HICjDelfEDeD6TD8OEz/Dvt8KHxJiETR+paI/Fr7eVTQbjMfRrXJz8O1qV1qGH5SHZUGl2SAw2Rp+MLtXOjCrQ=="], + "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.6.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fbxg3cBPdJ++36DXtdzcoKw2xzFov91Wxvmn1khX9MXQbDqJQLJmITZhtokcZsj4uGJe32sUmxAZnKbUtZLjmA=="], - "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.6.1", "", { "os": "linux", "cpu": "arm" }, "sha512-pUswnwa+WOmtH2ZGOWL05kFLMNY7/TnEAryfIv1yVFzKQnmSy9TKYi3oIOxGZL3w+cdUKCZ6Q+jaD0oI10ztzA=="], + "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.6.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Jk4P7kocGEisSvUFIm1VuHO3hC01LvS3sYAAmVVu1/ve5TuZ0iXyl9kIGtd1ZrgUvchgvZWNOaB+/Kq/RO63FA=="], - "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.6.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zro0FOu9clLCmqCnUKzEWHAu30tss0iEfhs+KDXm9Dpm1FkIHAKu43tF6FQU2hsTA7a8xd93NGddzc2EOJFKUg=="], + "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-i1xE8Bx1YZLheWtBZHD0Mq3nAIDrhgiH7o8VB4GiCbHufKb4XKj4CqSDMWSC0RYPmn//E+UEd1NsE2NbOux1tQ=="], - "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.6.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-NZaT+mp9toqWuFEA4MYW5HMRxgIa8DCqnTTnM5SrrojZgm4QoMI/mJfifVet1ZHgl/Dly5m6H6GPpq43uXVj8g=="], + "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZeLkC6xZrlDoIJTadHfqTABTmsj2f6wCCtYBYx/RPGgdmQcGLA0NALRl7m0tnK2CT45eNRuOYzsfEZyF0XWM/A=="], - "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.6.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M5macseSCBPvJ4yfKNyQpMc7nBQBtj39MNfMt0r+8UkTnR5qJE00JJx06puHgPxT5hnGxMAuAWZ+3a9H2ngqAw=="], + "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-HNYt7zjIChPcnjZRG42CZq3Zn5mqaRo4UcFLr4mIbmGqdhX5hDDE8O8US8YgYyOUosfbBTBFdsbvFwVAx1TOkQ=="], - "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.6.1", "", { "os": "linux", "cpu": "x64" }, "sha512-liAyBZI5AbazZGeeNfWj0jCD/TE2L84hgVYh4KkjJA/N9bNzFQCDf4BvWP76nEO89r2tIGEUjbXdM4mM26riHg=="], + "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-/1ttT31dAQc7hGtXWSEYEgzGtakAyO2C+/GqAzIuKXlGLpNPZgXdR8LZ0iDHDalbEr3AuHIPRV43sWLUrHWdsA=="], - "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.6.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qItzfH3x6MYChPeGfvh22rHD92WLgXQRSuwvspRVSnLvpnubEfZd+9REPRQVT2l9fIuETDCEkDNRqkDROQTkgA=="], + "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.6.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-oAArRDU1lkKg+xFEtiQ7C+/wghpqrkBrFWM05W73S+3sZz8JIHH79Q+Qh5gWls3i8vccitUgCnvln5V7xKn3XQ=="], - "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.6.1", "", { "os": "win32", "cpu": "x64" }, "sha512-DFFkKROZ9ZAHmFMUFRtRTkosZds1KH8BOx5t3UpNULIjT3iuUmEx9V5pWR0xOi66sANY38Ap77nz1kOZraQxEg=="], + "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.6.3", "", { "os": "win32", "cpu": "x64" }, "sha512-bhFDcDsvmp5KuBfWTt4carNo1E4LXH7++S8qqZgDtXVqJxs0xMm7gbuqPihA8kBbphM+NzAUm5qmq6ocfqM6kg=="], - "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.6.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jORysyRZg5zGDgVw15LGMsjZDh7jwjpUIJRBHgFt0ir15O5pEazfvuF2dnwvrJiTF0IT1EgHAVbTAYJWwQLCjg=="], + "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.6.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Xate6yyZgvi7da/gdnZy+Vu5jlFB0LRlb5m4MY6Y98KSQeJPZIhoVXMK2Vsl48XOmPlDIS8lge414HUVUEo+hg=="], - "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.6.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-dTeYFkkFlbP/WCB2DtezXas3NApOPtFlXSdssB7wGtY9wpNp4HAkVo1KBwI5mcHK0e2joyUcqTSf44mFE+q7vg=="], + "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.6.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-WKMZ5UU2HBdGZDEpQoLq+21f1FlS+BjroH1FaVE6zCSeqKmZ7xRP5jIRGtQ4vCYj/k2KHgyABZ16lgK9mTe2Sg=="], - "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.6.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GExDp3rebo28mt3EAjvKQs0ZC3gkznAErV0I9TUNDa9muuhvD35kft61Mpsc6+NWeE+BG+kUyKbm6iO5B6ZMMA=="], + "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.6.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-OWX8V2k4bmtl/DNwU3yz3PZa2QXiSqWN3Xk7pNB5IPt7FcJBDlnpkOcX6h3tjMS7CyKE0lMvPUwwcmWSdbjkyA=="], - "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.6.1", "", { "os": "linux", "cpu": "arm" }, "sha512-a9MjABj4J0VE3Z2oROGhmeddZZrhwwrnl4ZWZOuHUhD/smDtDiNtr0LpCbKB7rEYaQ29snopOPdZ/0T3YgLglQ=="], + "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.6.3", "", { "os": "linux", "cpu": "arm" }, "sha512-/4LzmPXPaCWqIpY1j3+XVb8rbXIratqCte3A4sGEjua6aVhvVxEVAqeKlBsoGkORWLeqbpcxhgxKwOGM17eexA=="], - "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.6.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ctuvXJgDRKKlmJfHxT4RsTvcAcHEFNJHTCsGbtt4rluQpVDc+ezk9JvQ534ehoIfZ9T0eIHSBgqYAZ4xCatNmQ=="], + "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.6.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Df4jk0M/eNKKQfYzXBBKhKkmJBpB+XoX2LkMxmlK3GN+fxUdeb8EM78wX+1+eLVl5dZNo6f7gOd6oDV0gChevw=="], - "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.6.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-vRtyoTtT0Ltowuh9LWOl/ZNU9h79J89ilOz5SEGspcw0jfhoUt19i07VNitll4jjfg5p2EN00q+MqX0pAobrFw=="], + "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sRCtDktUgIbbV78SYX3wdGVVm1Hz/nSUS24JgXB4MzUeGNwBNB+eQAWMtBxCGriyJNXK3zwfj+SSgvTmUdPf/A=="], - "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.6.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-vCsc3GOe1ylmRyfo/WLjIhjiCmaTtJbWNF4ZtgjNegDjpsRsFuCcP9duXB41QcfnJK38repKVFqFh0LR3l48FA=="], + "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.6.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-3J/jV3ROSqlhLyB/6i5EUHxjkom5i59iPvrtiAsnAjHzMsZJJPEke9LSaOsB0rf4MJFH9AmjvsK8gDahTjZy+A=="], - "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.6.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gw3d81RdUHSYwjDW2IG6gEtm4VDoPP4ZOqpuC6Nc8+UBfos+4gTWOgzmuxIOVhkSV2fJCcUDpSJIlPzEU0FLZw=="], + "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-a5mPn/OMSq2Aa2i7eJXcc37Jtw0b89gDO2mDpXN769b06IirEiqOzLNNJd6R7R8DxoadHzY0nLFhYNChE+jyAg=="], - "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.6.1", "", { "os": "linux", "cpu": "x64" }, "sha512-nzU+Doq9UgZvYYvald36lZJ2Neeyheije6WE/YpoFt/oJiNmnjArRgr2CMtb/7gWBl80YSMwcHK4Ju0E+7wfWg=="], + "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.6.3", "", { "os": "linux", "cpu": "x64" }, "sha512-kQSjfa6zdvotuXEGNKQ9vZZxE+lcEEbTUMSuvY/5+0crlwBCjEqrf9/W9ViFDoQAEt8WJiu/mtVNYkKAOkjLmA=="], - "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.6.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-r3tXFVDliWCPe7TL6DVxUkT4rkqnXyeFVSEDf+V9My6Gztq99/gIe3POQqFbshTRuSrpEYMGMbGxeFh+m+stxA=="], + "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.6.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZawdN3R0YKr48BeXCpbax+WDWbgEG6nWyDosVeZasrT5TjgfF4XMP5SfuyMNvRJ4gbTitrcYHZkENSXZ9ncqcg=="], - "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.6.1", "", { "os": "win32", "cpu": "x64" }, "sha512-FqYMOqeCS2XTdn5yvaKlOhtSQ84mVO3aTXp6LGfMd9Zq8RsV4H8qLWv+sxJsgPCXfuBV64u8/f+CTr3uIwNLWA=="], + "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.6.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NcqZwBXQhKyfC0Eb+yBxXQcPjZoUstD1Dbr3X4UlOD1QiYfnvAYRKCZJ6nQ6KQ5p30dGaKkQeBL4t3qQtDQNWA=="], "@yuku-toolchain/types": ["@yuku-toolchain/types@0.5.43", "", {}, "sha512-kSpvPntnXw5+lYjO71ffBEnQ5ycQ74KGIYknh0TS4xeyCuBkOqxyJumxZkMhLBBUCLjDAbx2+Icnr3Zh4ftjpQ=="], @@ -863,7 +863,7 @@ "dompurify": ["dompurify@3.4.12", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg=="], - "dprint": ["dprint@0.55.1", "", { "optionalDependencies": { "@dprint/android-arm64": "0.55.1", "@dprint/android-x64": "0.55.1", "@dprint/darwin-arm64": "0.55.1", "@dprint/darwin-x64": "0.55.1", "@dprint/linux-arm64-glibc": "0.55.1", "@dprint/linux-arm64-musl": "0.55.1", "@dprint/linux-loong64-glibc": "0.55.1", "@dprint/linux-loong64-musl": "0.55.1", "@dprint/linux-ppc64-glibc": "0.55.1", "@dprint/linux-ppc64-musl": "0.55.1", "@dprint/linux-riscv64-glibc": "0.55.1", "@dprint/linux-x64-glibc": "0.55.1", "@dprint/linux-x64-musl": "0.55.1", "@dprint/win32-arm64": "0.55.1", "@dprint/win32-x64": "0.55.1" }, "bin": { "dprint": "bin.cjs" } }, "sha512-tgUCT9gAM7veMvLX5NmRoYVLnKGKrIYRXpNpJDJj5U7/YIIJef5rLJhPZaJXwB7ad2Vo0Kv//nQM1xkrn5j8kQ=="], + "dprint": ["dprint@0.55.2", "", { "optionalDependencies": { "@dprint/android-arm64": "0.55.2", "@dprint/android-x64": "0.55.2", "@dprint/darwin-arm64": "0.55.2", "@dprint/darwin-x64": "0.55.2", "@dprint/linux-arm64-glibc": "0.55.2", "@dprint/linux-arm64-musl": "0.55.2", "@dprint/linux-loong64-glibc": "0.55.2", "@dprint/linux-loong64-musl": "0.55.2", "@dprint/linux-ppc64-glibc": "0.55.2", "@dprint/linux-ppc64-musl": "0.55.2", "@dprint/linux-riscv64-glibc": "0.55.2", "@dprint/linux-x64-glibc": "0.55.2", "@dprint/linux-x64-musl": "0.55.2", "@dprint/win32-arm64": "0.55.2", "@dprint/win32-x64": "0.55.2" }, "bin": { "dprint": "bin.cjs" } }, "sha512-1d4D4SB9KiD2qFnBWbl3aoYjLvPVpZoth28yxTT00xJv2yXugdz0Xoyv7sGG0w0hzE6hQZMgd6B333GnySDpGQ=="], "dts-resolver": ["dts-resolver@3.0.0", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="], @@ -941,7 +941,7 @@ "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - "knip": ["knip@6.26.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "oxc-parser": "^0.137.0", "oxc-resolver": "11.21.3", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.1", "yaml": "^2.9.0", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-e9eELEEpBpGTd4H4HB7818/DYj9dMzMyUqAddfYwUN/EbSkgIjOuWEF96W/xHsmV0SDrsdXjIM+oZ2xpPzPsBA=="], + "knip": ["knip@6.27.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "oxc-parser": "^0.137.0", "oxc-resolver": "11.21.3", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^4.0.1", "yaml": "^2.9.0", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-CngYEYrD0n20N06FXA8n3u/0Wnnugoa+B9k14OP+iKIgkCHuzvIdsP3nfwjhByoc1WfogpxfiriMboAXFETDUw=="], "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], @@ -1065,7 +1065,7 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "miniflare": ["miniflare@4.20260708.1", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260708.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-c94O9zRDISdqO18EHt6l0iF/fWgWt8p18PJvRsA/L/NJZ9Cfke3s/F5Blg1XXF7WDutVRzWVWy8Vy4LaT5ifsA=="], + "miniflare": ["miniflare@4.20260710.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260710.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-x1LLRkU6o1p7hiKrB0TRnL0MJn6xFOT+/vrlEQINz5cRDKLP8ru4hBqWTIvXAetzr1acKAnmAaG84pQ4W/K14g=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -1189,7 +1189,7 @@ "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], - "tsdown": ["tsdown@0.22.7", "", { "dependencies": { "ansis": "^4.3.1", "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.1", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.3", "picomatch": "^4.0.5", "rolldown": "~1.1.5", "rolldown-plugin-dts": "^0.27.8", "semver": "^7.8.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.7", "@tsdown/exe": "0.22.7", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-4egbOzc9dxVv/QS+gDV75FIxDIjQQeOnXBlUuikyjmn0ozuc6FW11djJjEEo3vqkuJRygpnKHurnj+Iwftk4VA=="], + "tsdown": ["tsdown@0.22.8", "", { "dependencies": { "ansis": "^4.3.1", "cac": "^7.0.0", "defu": "^6.1.7", "empathic": "^2.0.1", "hookable": "^6.1.1", "import-without-cache": "^0.4.0", "obug": "^2.1.3", "picomatch": "^4.0.5", "rolldown": "~1.1.5", "rolldown-plugin-dts": "^0.27.9", "semver": "^7.8.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "tree-kill": "^1.2.2", "unconfig-core": "^7.5.0" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@tsdown/css": "0.22.8", "@tsdown/exe": "0.22.8", "@vitejs/devtools": "*", "publint": "^0.3.8", "tsx": "*", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": "^0.5.0", "unrun": "*" }, "optionalPeers": ["@arethetypeswrong/core", "@tsdown/css", "@tsdown/exe", "@vitejs/devtools", "publint", "tsx", "typescript", "unplugin-unused", "unrun"], "bin": { "tsdown": "./dist/run.mjs" } }, "sha512-6FOLlr1iLcE3LheqQt13hVUWtTduJNwF2akPskPe8Tf1hr+N5UULHzrNZYTMNwL6lr2UyQ8iefVBB6tdqp1PCQ=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -1205,7 +1205,7 @@ "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], - "unbash": ["unbash@4.0.2", "", {}, "sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg=="], + "unbash": ["unbash@4.0.3", "", {}, "sha512-3cudTErfToSc4Ggv8XGXVNVli/xHKUtUZvaY5UVwhOcUPbQGz7PeaEnT/SAVgNziZtX67KEN9swMUYkLghxA1w=="], "unconfig-core": ["unconfig-core@7.5.0", "", { "dependencies": { "@quansync/fs": "^1.0.0", "quansync": "^1.0.0" } }, "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w=="], @@ -1247,9 +1247,9 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "workerd": ["workerd@1.20260708.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260708.1", "@cloudflare/workerd-darwin-arm64": "1.20260708.1", "@cloudflare/workerd-linux-64": "1.20260708.1", "@cloudflare/workerd-linux-arm64": "1.20260708.1", "@cloudflare/workerd-windows-64": "1.20260708.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-WAK+Kt/VVCSldH2qSr8lx46XCJ4Q+bdlHNaFqUtOHthBEIB8C1N8HVW+VOLrxDoTCk0NGNv0zajnBeQK4JOB9w=="], + "workerd": ["workerd@1.20260710.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260710.1", "@cloudflare/workerd-darwin-arm64": "1.20260710.1", "@cloudflare/workerd-linux-64": "1.20260710.1", "@cloudflare/workerd-linux-arm64": "1.20260710.1", "@cloudflare/workerd-windows-64": "1.20260710.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-U2sBPPrb9U97sBKnnMN6Kv8p65903P35nwMkPE9vSH/bRuRqkZ3a1EjUw3jV28RhiyXpkLF77Evzw8XimFxyTw=="], - "wrangler": ["wrangler@4.110.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260708.1", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260708.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260708.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-xZeXKYi7hxQRF5anL+v77RkufJNpF9f3Eqeyqq2QBsETpLZgh0Agj0jJ6JPtkbgn6ukZdh8OK5egsGPWIditgg=="], + "wrangler": ["wrangler@4.111.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260710.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260710.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260710.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-bffpI9EyrnpKkF/1S+RaIv8oRD93GtbsA7TlfWwOsGJGB7VO3jVbdGzpC9TU7Bqom3z7jUxcte4Z9MPhaQ4HoQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], @@ -1261,9 +1261,9 @@ "yuku-ast": ["yuku-ast@0.1.7", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" } }, "sha512-2RiMEWv500TixY5rJy6OZd4fSy9WYZKWh6gGbIJ7y7vAGcuCugWOWwOLGaQcRZrXcPUfqtLtvpaJ3SdXtWlhKA=="], - "yuku-codegen": ["yuku-codegen@0.6.1", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-codegen/binding-darwin-arm64": "0.6.1", "@yuku-codegen/binding-darwin-x64": "0.6.1", "@yuku-codegen/binding-freebsd-x64": "0.6.1", "@yuku-codegen/binding-linux-arm-gnu": "0.6.1", "@yuku-codegen/binding-linux-arm-musl": "0.6.1", "@yuku-codegen/binding-linux-arm64-gnu": "0.6.1", "@yuku-codegen/binding-linux-arm64-musl": "0.6.1", "@yuku-codegen/binding-linux-x64-gnu": "0.6.1", "@yuku-codegen/binding-linux-x64-musl": "0.6.1", "@yuku-codegen/binding-win32-arm64": "0.6.1", "@yuku-codegen/binding-win32-x64": "0.6.1" } }, "sha512-6RJqqON2xYhMEp/sZv5oOSI3uOpWwRwzAi2fc/rMcRFjcqedAC5Fyp4AD9Vn2b8SB7hf9ESqVW+YwbDs/KvyKA=="], + "yuku-codegen": ["yuku-codegen@0.6.3", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-codegen/binding-darwin-arm64": "0.6.3", "@yuku-codegen/binding-darwin-x64": "0.6.3", "@yuku-codegen/binding-freebsd-x64": "0.6.3", "@yuku-codegen/binding-linux-arm-gnu": "0.6.3", "@yuku-codegen/binding-linux-arm-musl": "0.6.3", "@yuku-codegen/binding-linux-arm64-gnu": "0.6.3", "@yuku-codegen/binding-linux-arm64-musl": "0.6.3", "@yuku-codegen/binding-linux-x64-gnu": "0.6.3", "@yuku-codegen/binding-linux-x64-musl": "0.6.3", "@yuku-codegen/binding-win32-arm64": "0.6.3", "@yuku-codegen/binding-win32-x64": "0.6.3" } }, "sha512-3c9H521tf1RRDu4cNUySfH01sKlALve4HKu2sITk33gLl5HhsvI6ngSuarpWxMPAiJEgqJc/HTvojWQRnYm9/g=="], - "yuku-parser": ["yuku-parser@0.6.1", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.6.1", "@yuku-parser/binding-darwin-x64": "0.6.1", "@yuku-parser/binding-freebsd-x64": "0.6.1", "@yuku-parser/binding-linux-arm-gnu": "0.6.1", "@yuku-parser/binding-linux-arm-musl": "0.6.1", "@yuku-parser/binding-linux-arm64-gnu": "0.6.1", "@yuku-parser/binding-linux-arm64-musl": "0.6.1", "@yuku-parser/binding-linux-x64-gnu": "0.6.1", "@yuku-parser/binding-linux-x64-musl": "0.6.1", "@yuku-parser/binding-win32-arm64": "0.6.1", "@yuku-parser/binding-win32-x64": "0.6.1" } }, "sha512-dPE3/+H2VBw9LhjoIVeW/axKidYGd+XzNtrwGGseZ0325cQFl0Dpwyh0R74XWe/WqQn4M8CR5YApsv2KF2zN1A=="], + "yuku-parser": ["yuku-parser@0.6.3", "", { "dependencies": { "@yuku-toolchain/types": "0.5.43" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.6.3", "@yuku-parser/binding-darwin-x64": "0.6.3", "@yuku-parser/binding-freebsd-x64": "0.6.3", "@yuku-parser/binding-linux-arm-gnu": "0.6.3", "@yuku-parser/binding-linux-arm-musl": "0.6.3", "@yuku-parser/binding-linux-arm64-gnu": "0.6.3", "@yuku-parser/binding-linux-arm64-musl": "0.6.3", "@yuku-parser/binding-linux-x64-gnu": "0.6.3", "@yuku-parser/binding-linux-x64-musl": "0.6.3", "@yuku-parser/binding-win32-arm64": "0.6.3", "@yuku-parser/binding-win32-x64": "0.6.3" } }, "sha512-iI6uABvvup9mvv8Mcpz7Tp//gehQlvcSnX4A4/0bf9i6X3RVQDuVUZel8jdpljwlF7WrbKsvD19y55Mc6+sKZw=="], "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -1303,6 +1303,8 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + "gh/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], "rolldown-plugin-dts/get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], diff --git a/examples/gh/package.json b/examples/gh/package.json index ee57a7d8..0527d8ba 100644 --- a/examples/gh/package.json +++ b/examples/gh/package.json @@ -15,6 +15,7 @@ "scripts": { "bd": "bun run build", "build": "bun build src/main.ts --install --outdir=dist --format=esm --target=node --external @kjanat/dreamcli", + "preinstall": "cd ../.. && bun link", "test": "bun test", "typecheck": "tsc --noEmit" }, @@ -22,10 +23,9 @@ "@kjanat/dreamcli": "link:@kjanat/dreamcli" }, "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/bun": "latest", + "typescript": "latest" }, - "packageManager": "bun@1.3.12", "engines": { "bun": "^1.3" }, @@ -34,6 +34,13 @@ "name": "bun", "version": "^1.3", "onFail": "error" - } + }, + "packageManager": [ + { + "name": "bun", + "onFail": "error", + "version": "^1.3" + } + ] } } diff --git a/examples/gh/src/main.ts b/examples/gh/src/main.ts index bcd01bdb..5737b2ef 100755 --- a/examples/gh/src/main.ts +++ b/examples/gh/src/main.ts @@ -33,7 +33,7 @@ import { pr } from './commands/pr.ts'; * Command registration order determines the order shown in `--help`. */ export const gh = cli('gh') - .manifest({ from: import.meta.url }) + .manifest({ from: import.meta, files: ['package.json'] }) .command(auth) .command(pr) .command(issue) From 1a03e42842201f1e158b0075a247605c44e8abf7 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 16 Jul 2026 13:34:42 +0200 Subject: [PATCH 4/8] fix(schema): harden Standard Schema interop Match the official v1 namespace and options shape, and detect callable validators before parse functions so conforming libraries work as advertised. Validate array and variadic elements individually, expose public types, and cover the behavior in tests and docs. --- CHANGELOG.md | 12 ++ docs/guide/arguments.md | 5 + docs/guide/flags.md | 18 ++ .../resolve/resolve-standard-schema.test.ts | 28 +++ src/core/resolve/standard.ts | 45 ++++- src/core/schema/arg.test.ts | 17 ++ src/core/schema/arg.ts | 64 +++---- src/core/schema/flag.test.ts | 28 +++ src/core/schema/flag.ts | 64 +++---- src/core/schema/index.ts | 1 + src/core/schema/standard.test.ts | 43 +++++ src/core/schema/standard.ts | 178 ++++++++++++------ src/index.ts | 11 ++ 13 files changed, 388 insertions(+), 126 deletions(-) create mode 100644 src/core/schema/standard.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b59ed893..eab6cea7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- **Standard Schema v1 validation** — `flag.custom(schema)` and + `arg.custom(schema)` accept any conforming validator (including callable, + sync, and async schemas) with inferred output types and no runtime + dependency. Validation runs after source resolution, so argv, env, config, + prompt, stdin, and default values behave consistently; array flags and + variadic args validate each element. - **`isMainModule(import.meta)`**: Compatibility helper for consumers whose ambient `ImportMeta` interface omits `main`. Projects with normal Node, Bun, or Deno runtime typings can keep using the conventional @@ -25,6 +31,12 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ambient `ImportMeta` interface to pass both TypeScript/Deno checks and JSR publish validation. +### Removed + +- **Internal schema DSL** — the private template-literal parser, validator, and + JSON Schema converter were replaced by direct definition-schema objects, + removing roughly 1,500 lines without changing the generated schema. + ## [3.0.0-rc.11] - 2026-07-15 ### Fixed diff --git a/docs/guide/arguments.md b/docs/guide/arguments.md index bd246fdd..4280903c 100644 --- a/docs/guide/arguments.md +++ b/docs/guide/arguments.md @@ -57,6 +57,11 @@ argTypes.custom; // ^? ``` +`arg.custom()` also accepts any [Standard Schema v1](https://standardschema.dev/schema) +validator. Its output type is inferred, sync and async validators are supported, and validation +runs after CLI, env, stdin, or default resolution. A variadic custom argument validates each +resolved element separately. + ## Declaration ```ts twoslash diff --git a/docs/guide/flags.md b/docs/guide/flags.md index bf22f2a4..8fa33ed4 100644 --- a/docs/guide/flags.md +++ b/docs/guide/flags.md @@ -631,6 +631,24 @@ flag.custom((value) => { The parse function receives the raw string value and returns the parsed type. Thrown errors become validation errors with the flag name in context. +### Standard Schema Validators + +`flag.custom()` also accepts any [Standard Schema v1](https://standardschema.dev/schema) +validator, including Zod, Valibot, and ArkType schemas: + +```ts +import { flag } from '@kjanat/dreamcli'; +import { z } from 'zod'; + +const port = flag.custom(z.coerce.number().int().min(1).max(65_535)); +// inferred type: number | undefined +``` + +Standard Schema validation runs after source resolution, so the same sync or async validator +handles CLI, env, config, prompt, and default values. Validation issues become +`CONSTRAINT_VIOLATED` errors. When used as a `flag.array()` element, the validator runs once per +resolved element. + ## Propagation Flags marked with `.propagate()` are inherited by all subcommands: diff --git a/src/core/resolve/resolve-standard-schema.test.ts b/src/core/resolve/resolve-standard-schema.test.ts index a611b063..43bcb33d 100644 --- a/src/core/resolve/resolve-standard-schema.test.ts +++ b/src/core/resolve/resolve-standard-schema.test.ts @@ -105,6 +105,20 @@ describe('Standard Schema v1 interop — flags', () => { expect(bad.exitCode).toBe(2); expect(bad.error?.message).toContain('must be longer than two characters'); }); + + it('validates and transforms Standard Schema array elements', async () => { + const cmd = command('run') + .flag('count', flag.array(flag.custom(evenInt))) + .action(({ flags, out }) => out.log(flags.count.join(','))); + + const ok = await runCommand(cmd, ['--count', '2', '--count', '4']); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('2,4\n'); + + const bad = await runCommand(cmd, ['--count', '2', '--count', '3']); + expect(bad.exitCode).toBe(2); + expect(bad.error?.message).toContain('--count[1] failed validation'); + }); }); // === Args @@ -139,6 +153,20 @@ describe('Standard Schema v1 interop — args', () => { expect(bad.error?.code).toBe('CONSTRAINT_VIOLATED'); expect(bad.error?.message).toContain('must be longer than two characters'); }); + + it('validates and transforms every variadic positional value', async () => { + const cmd = command('run') + .arg('n', arg.custom(evenInt).variadic()) + .action(({ args, out }) => out.log(args.n.join(','))); + + const ok = await runCommand(cmd, ['2', '4']); + expect(ok.exitCode).toBe(0); + expect(ok.stdout[0]).toBe('2,4\n'); + + const bad = await runCommand(cmd, ['2', '3']); + expect(bad.exitCode).toBe(2); + expect(bad.error?.message).toContain('[1] failed validation'); + }); }); // === Type inference diff --git a/src/core/resolve/standard.ts b/src/core/resolve/standard.ts index fd037c14..79456e77 100644 --- a/src/core/resolve/standard.ts +++ b/src/core/resolve/standard.ts @@ -91,11 +91,32 @@ async function applyStandardValidators( const errors: ValidationError[] = []; const nextFlags: Record = { ...flags }; for (const [name, flagSchema] of Object.entries(schema.flags)) { + const value = flags[name]; + if (value === undefined) { + continue; + } + const elementValidator = + flagSchema.kind === 'array' ? flagSchema.elementSchema?.standard : undefined; + if (elementValidator !== undefined && Array.isArray(value)) { + const nextValue: unknown[] = []; + for (const [index, element] of value.entries()) { + const result = await validateValue(`--${name}[${index}]`, element, elementValidator); + if (result.ok) { + nextValue.push(result.value); + } else { + nextValue.push(element); + errors.push(result.error); + } + } + nextFlags[name] = nextValue; + continue; + } + const validator = flagSchema.standard; - if (validator === undefined || flags[name] === undefined) { + if (validator === undefined) { continue; } - const result = await validateValue(`--${name}`, flags[name], validator); + const result = await validateValue(`--${name}`, value, validator); if (result.ok) { nextFlags[name] = result.value; } else { @@ -106,10 +127,26 @@ async function applyStandardValidators( const nextArgs: Record = { ...args }; for (const entry of schema.args) { const validator = entry.schema.standard; - if (validator === undefined || args[entry.name] === undefined) { + const value = args[entry.name]; + if (validator === undefined || value === undefined) { + continue; + } + if (entry.schema.variadic && Array.isArray(value)) { + const nextValue: unknown[] = []; + for (const [index, element] of value.entries()) { + const result = await validateValue(`<${entry.name}>[${index}]`, element, validator); + if (result.ok) { + nextValue.push(result.value); + } else { + nextValue.push(element); + errors.push(result.error); + } + } + nextArgs[entry.name] = nextValue; continue; } - const result = await validateValue(`<${entry.name}>`, args[entry.name], validator); + + const result = await validateValue(`<${entry.name}>`, value, validator); if (result.ok) { nextArgs[entry.name] = result.value; } else { diff --git a/src/core/schema/arg.test.ts b/src/core/schema/arg.test.ts index d50f2806..2762d7a7 100644 --- a/src/core/schema/arg.test.ts +++ b/src/core/schema/arg.test.ts @@ -1,6 +1,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import type { ArgSchema, InferArg, InferArgs } from './arg.ts'; import { ArgBuilder, arg } from './arg.ts'; +import type { StandardSchemaV1 } from './standard.ts'; /** Test-only wrapper for custom parse validation. */ interface ParsedPath { @@ -10,6 +11,14 @@ function parsePath(raw: string): ParsedPath { return { segments: raw.split('/') }; } +const standardNumber: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'test', + validate: (value) => ({ value: Number(value) }), + }, +}; + // --- Factory functions — runtime schema describe('arg.string() — creates and modifies string args', () => { @@ -153,6 +162,14 @@ describe('arg.custom() — creates and modifies custom args', () => { const a = arg.custom(parseFn); expect(a.schema.parseFn).toBe(parseFn); }); + + it('stores a Standard Schema validator and infers its output type', () => { + const a = arg.custom(standardNumber); + expect(a.schema.kind).toBe('custom'); + expect(a.schema.standard).toBe(standardNumber); + expect(a.schema.parseFn).toBeUndefined(); + expectTypeOf>().toEqualTypeOf(); + }); }); // --- Modifiers — runtime schema mutations diff --git a/src/core/schema/arg.ts b/src/core/schema/arg.ts index c1af719c..21b1dd9f 100644 --- a/src/core/schema/arg.ts +++ b/src/core/schema/arg.ts @@ -9,7 +9,7 @@ */ import { assertNumberConstraints, type NumberConstraints } from './number-constraints.ts'; -import type { InferStandardOutput, StandardSchemaV1 } from './standard.ts'; +import { type InferStandardOutput, isStandardSchemaV1, type StandardSchemaV1 } from './standard.ts'; // --- Type-level configuration (phantom state tracked through the chain) @@ -732,6 +732,34 @@ interface ArgFactory { readonly argKind: 'enum'; }>; + /** + * Custom positional argument validated by a Standard Schema v1 validator + * (zod, valibot, arktype, …). The resolved value from any source is + * validated after resolution; the arg's value type is the validator's + * output type. + * + * Sync and async validators are both supported. Validation issues surface + * as a `CONSTRAINT_VIOLATED` error naming the argument. + * + * @example + * ```ts + * import { z } from 'zod'; + * arg.custom(z.string().uuid()) + * // inferred type: string + * ``` + * + * @param schema - A Standard Schema v1 validator. + * @returns A required custom {@link ArgBuilder} typed to the validator's output. + */ + custom( + schema: S, + ): ArgBuilder<{ + readonly valueType: InferStandardOutput; + readonly presence: 'required'; + readonly variadic: false; + readonly argKind: 'custom'; + }>; + /** * Custom-parsed positional argument. Required by default. * @@ -763,34 +791,6 @@ interface ArgFactory { readonly variadic: false; readonly argKind: 'custom'; }>; - - /** - * Custom positional argument validated by a Standard Schema v1 validator - * (zod, valibot, arktype, …). The resolved value from any source is - * validated after resolution; the arg's value type is the validator's - * output type. - * - * Sync and async validators are both supported. Validation issues surface - * as a `CONSTRAINT_VIOLATED` error naming the argument. - * - * @example - * ```ts - * import { z } from 'zod'; - * arg.custom(z.string().uuid()) - * // inferred type: string - * ``` - * - * @param schema - A Standard Schema v1 validator. - * @returns A required custom {@link ArgBuilder} typed to the validator's output. - */ - custom( - schema: S, - ): ArgBuilder<{ - readonly valueType: InferStandardOutput; - readonly presence: 'required'; - readonly variadic: false; - readonly argKind: 'custom'; - }>; } /** @@ -867,10 +867,10 @@ const arg: ArgFactory = { readonly variadic: false; readonly argKind: 'custom'; }> { - if (typeof parseFnOrSchema === 'function') { - return new ArgBuilder(createArgSchema('custom', { parseFn: parseFnOrSchema })); + if (isStandardSchemaV1(parseFnOrSchema)) { + return new ArgBuilder(createArgSchema('custom', { standard: parseFnOrSchema })); } - return new ArgBuilder(createArgSchema('custom', { standard: parseFnOrSchema })); + return new ArgBuilder(createArgSchema('custom', { parseFn: parseFnOrSchema })); }, }; diff --git a/src/core/schema/flag.test.ts b/src/core/schema/flag.test.ts index c6c24e33..2a89b3a8 100644 --- a/src/core/schema/flag.test.ts +++ b/src/core/schema/flag.test.ts @@ -1,11 +1,20 @@ import { describe, expect, expectTypeOf, it } from 'vitest'; import type { FlagAlias, FlagSchema, InferFlag, InferFlags } from './flag.ts'; import { FlagBuilder, flag } from './flag.ts'; +import type { StandardSchemaV1 } from './standard.ts'; function alias(name: string, hidden = false): FlagAlias { return { name, hidden }; } +const standardNumber: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'test', + validate: (value) => ({ value: Number(value) }), + }, +}; + // --- Factory functions — runtime schema describe('flag.string()', () => { @@ -189,6 +198,25 @@ describe('flag.custom()', () => { }); expectTypeOf>().toEqualTypeOf<{ host: string; port: number } | undefined>(); }); + + it('stores a Standard Schema validator and infers its output type', () => { + const f = flag.custom(standardNumber); + expect(f.schema.kind).toBe('custom'); + expect(f.schema.standard).toBe(standardNumber); + expect(f.schema.parseFn).toBeUndefined(); + expectTypeOf>().toEqualTypeOf(); + }); + + it('recognizes callable Standard Schema validators before parse functions', () => { + const callable = Object.assign((value: unknown) => String(value), standardNumber) as (( + value: unknown, + ) => string) & + StandardSchemaV1; + const f = flag.custom(callable); + expect(f.schema.standard).toBe(callable); + expect(f.schema.parseFn).toBeUndefined(); + expectTypeOf>().toEqualTypeOf(); + }); }); // --- Modifiers — runtime schema mutations diff --git a/src/core/schema/flag.ts b/src/core/schema/flag.ts index 1071c0ae..b1e269e2 100644 --- a/src/core/schema/flag.ts +++ b/src/core/schema/flag.ts @@ -17,7 +17,7 @@ import type { PromptConfig, SelectPromptConfig, } from './prompt.ts'; -import type { InferStandardOutput, StandardSchemaV1 } from './standard.ts'; +import { type InferStandardOutput, isStandardSchemaV1, type StandardSchemaV1 } from './standard.ts'; import { assertStringConstraints, type StringConstraints } from './string-constraints.ts'; import { type DateFlagOptions, @@ -1164,6 +1164,34 @@ interface FlagFactory { readonly elementEligible: false; }>; + /** + * Custom flag validated by a Standard Schema v1 validator (zod, valibot, + * arktype, …). The resolved value from any source is validated after + * resolution; the flag's value type is the validator's output type. + * + * Sync and async validators are both supported. Validation issues surface + * as a `CONSTRAINT_VIOLATED` error naming the flag. + * + * @example + * ```ts + * import { z } from 'zod'; + * flag.custom(z.string().url()) + * // inferred type: string | undefined + * ``` + * + * @param schema - A Standard Schema v1 validator. + * @returns A {@link FlagBuilder} whose value type is the validator's output. + */ + custom( + schema: S, + ): FlagBuilder<{ + readonly valueType: InferStandardOutput; + readonly presence: 'optional'; + readonly optionalFallback: 'undefined'; + readonly flagKind: 'custom'; + readonly elementEligible: true; + }>; + /** * Custom-parsed flag. The parse function receives the raw value and must * return a value of type `T`. The return type is inferred from `parseFn`. @@ -1201,34 +1229,6 @@ interface FlagFactory { readonly elementEligible: true; }>; - /** - * Custom flag validated by a Standard Schema v1 validator (zod, valibot, - * arktype, …). The resolved value from any source is validated after - * resolution; the flag's value type is the validator's output type. - * - * Sync and async validators are both supported. Validation issues surface - * as a `CONSTRAINT_VIOLATED` error naming the flag. - * - * @example - * ```ts - * import { z } from 'zod'; - * flag.custom(z.string().url()) - * // inferred type: string | undefined - * ``` - * - * @param schema - A Standard Schema v1 validator. - * @returns A {@link FlagBuilder} whose value type is the validator's output. - */ - custom( - schema: S, - ): FlagBuilder<{ - readonly valueType: InferStandardOutput; - readonly presence: 'optional'; - readonly optionalFallback: 'undefined'; - readonly flagKind: 'custom'; - readonly elementEligible: true; - }>; - /** * URL-valued flag. Parses into a `URL`; invalid URLs are rejected with an * `INVALID_VALUE` error naming the flag. @@ -1474,10 +1474,10 @@ const flag: FlagFactory = { readonly flagKind: 'custom'; readonly elementEligible: true; }> { - if (typeof parseFnOrSchema === 'function') { - return new FlagBuilder(createSchema('custom', { parseFn: parseFnOrSchema })); + if (isStandardSchemaV1(parseFnOrSchema)) { + return new FlagBuilder(createSchema('custom', { standard: parseFnOrSchema })); } - return new FlagBuilder(createSchema('custom', { standard: parseFnOrSchema })); + return new FlagBuilder(createSchema('custom', { parseFn: parseFnOrSchema })); }, url(options?: UrlFlagOptions): FlagBuilder<{ diff --git a/src/core/schema/index.ts b/src/core/schema/index.ts index 1edc8465..07ec1f8f 100644 --- a/src/core/schema/index.ts +++ b/src/core/schema/index.ts @@ -107,6 +107,7 @@ export type { StandardSchemaV1, StandardSchemaV1FailureResult, StandardSchemaV1Issue, + StandardSchemaV1Options, StandardSchemaV1PathSegment, StandardSchemaV1Props, StandardSchemaV1Result, diff --git a/src/core/schema/standard.test.ts b/src/core/schema/standard.test.ts new file mode 100644 index 00000000..58ad5971 --- /dev/null +++ b/src/core/schema/standard.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; +import type { StandardSchemaV1 as PublicStandardSchemaV1 } from '../../index.ts'; +import { isStandardSchemaV1, type StandardSchemaV1 } from './standard.ts'; + +describe('Standard Schema v1 types and detection', () => { + it('accepts the official optional validation options', () => { + const schema: StandardSchemaV1 = { + '~standard': { + version: 1, + vendor: 'test', + validate: (value, options) => ({ + value: `${String(value)}:${String(options?.libraryOptions?.suffix ?? '')}`, + }), + }, + }; + + expect(schema['~standard'].validate('value', { libraryOptions: { suffix: 'ok' } })).toEqual({ + value: 'value:ok', + }); + expectTypeOf().toEqualTypeOf(); + }); + + it('recognizes object and callable validators', () => { + const properties: StandardSchemaV1['~standard'] = { + version: 1, + vendor: 'test', + validate: (value) => ({ value }), + }; + const objectSchema: StandardSchemaV1 = { '~standard': properties }; + const callableSchema = Object.assign(() => undefined, { '~standard': properties }); + + expect(isStandardSchemaV1(objectSchema)).toBe(true); + expect(isStandardSchemaV1(callableSchema)).toBe(true); + }); + + it('rejects parse functions and malformed schema-like values', () => { + expect(isStandardSchemaV1((value: unknown) => value)).toBe(false); + expect( + isStandardSchemaV1({ '~standard': { version: 2, validate: () => ({ value: 1 }) } }), + ).toBe(false); + expect(isStandardSchemaV1(null)).toBe(false); + }); +}); diff --git a/src/core/schema/standard.ts b/src/core/schema/standard.ts index 0a750c0f..0ee94620 100644 --- a/src/core/schema/standard.ts +++ b/src/core/schema/standard.ts @@ -3,8 +3,7 @@ * * Mirrors the specification at https://standardschema.dev so validators from * zod, valibot, arktype, and any other conforming library can be passed to - * `flag.custom()` / `arg.custom()` without adding a runtime dependency. Every - * member is a type or interface, so this module erases to nothing at runtime. + * `flag.custom()` / `arg.custom()` without adding a runtime dependency. * * @module */ @@ -12,70 +11,133 @@ /** A schema that conforms to the Standard Schema v1 specification. */ export interface StandardSchemaV1 { /** The Standard Schema properties, namespaced under a well-known key. */ - readonly '~standard': StandardSchemaV1Props; + readonly '~standard': StandardSchemaV1.Props; } -/** The properties exposed under a validator's `~standard` key. */ -export interface StandardSchemaV1Props { - /** The version number of the specification. */ - readonly version: 1; - /** The vendor name of the schema library. */ - readonly vendor: string; - /** Validate an unknown value, returning its output or the issues found. */ - readonly validate: ( - value: unknown, - ) => StandardSchemaV1Result | Promise>; - /** Inferred input and output types, present only at the type level. */ - readonly types?: StandardSchemaV1Types | undefined; -} +/** Types copied from the Standard Schema v1 specification. */ +export declare namespace StandardSchemaV1 { + /** The properties exposed under a validator's `~standard` key. */ + export interface Props { + /** The version number of the specification. */ + readonly version: 1; + /** The vendor name of the schema library. */ + readonly vendor: string; + /** Validate an unknown value, returning its output or the issues found. */ + readonly validate: ( + value: unknown, + options?: Options | undefined, + ) => Result | Promise>; + /** Inferred input and output types, present only at the type level. */ + readonly types?: Types | undefined; + } -/** The result of validating a value: either its output or a list of issues. */ -export type StandardSchemaV1Result = - | StandardSchemaV1SuccessResult - | StandardSchemaV1FailureResult; + /** Optional parameters passed to a validator. */ + export interface Options { + /** Explicit support for vendor-specific validation parameters. */ + readonly libraryOptions?: Record | undefined; + } -/** A successful validation carrying the parsed output value. */ -export interface StandardSchemaV1SuccessResult { - /** The validated (and possibly transformed) value. */ - readonly value: Output; - /** Absent on success. */ - readonly issues?: undefined; -} + /** The result of validating a value: either its output or a list of issues. */ + export type Result = SuccessResult | FailureResult; -/** A failed validation carrying a non-empty list of issues. */ -export interface StandardSchemaV1FailureResult { - /** The issues that caused validation to fail. */ - readonly issues: ReadonlyArray; -} + /** A successful validation carrying the parsed output value. */ + export interface SuccessResult { + /** The validated (and possibly transformed) value. */ + readonly value: Output; + /** Absent on success. */ + readonly issues?: undefined; + } -/** A single validation issue. */ -export interface StandardSchemaV1Issue { - /** The human-readable error message. */ - readonly message: string; - /** The path to the offending value, when the validator reports one. */ - readonly path?: ReadonlyArray | undefined; -} + /** A failed validation carrying validation issues. */ + export interface FailureResult { + /** The issues that caused validation to fail. */ + readonly issues: ReadonlyArray; + } -/** One segment of an issue path. */ -export interface StandardSchemaV1PathSegment { - /** The key of this path segment. */ - readonly key: PropertyKey; -} + /** A single validation issue. */ + export interface Issue { + /** The human-readable error message. */ + readonly message: string; + /** The path to the offending value, when the validator reports one. */ + readonly path?: ReadonlyArray | undefined; + } + + /** One segment of an issue path. */ + export interface PathSegment { + /** The key of this path segment. */ + readonly key: PropertyKey; + } + + /** The type-level input and output carried by a validator. */ + export interface Types { + /** The input type accepted by the validator. */ + readonly input: Input; + /** The output type produced by the validator. */ + readonly output: Output; + } -/** The type-level input and output carried by a validator. */ -export interface StandardSchemaV1Types { - /** The input type accepted by the validator. */ - readonly input: Input; - /** The output type produced by the validator. */ - readonly output: Output; + /** Infer the input type of a Standard Schema validator. */ + export type InferInput = NonNullable< + Schema['~standard']['types'] + >['input']; + + /** Infer the output type of a Standard Schema validator. */ + export type InferOutput = NonNullable< + Schema['~standard']['types'] + >['output']; } -/** Infer the input type of a Standard Schema validator. */ -export type InferStandardInput = NonNullable< - Schema['~standard']['types'] ->['input']; +/** Backward-compatible flat alias for {@link StandardSchemaV1.Props}. */ +export type StandardSchemaV1Props = StandardSchemaV1.Props< + Input, + Output +>; +/** Flat alias for {@link StandardSchemaV1.Options}. */ +export type StandardSchemaV1Options = StandardSchemaV1.Options; +/** Flat alias for {@link StandardSchemaV1.Result}. */ +export type StandardSchemaV1Result = StandardSchemaV1.Result; +/** Flat alias for {@link StandardSchemaV1.SuccessResult}. */ +export type StandardSchemaV1SuccessResult = StandardSchemaV1.SuccessResult; +/** Flat alias for {@link StandardSchemaV1.FailureResult}. */ +export type StandardSchemaV1FailureResult = StandardSchemaV1.FailureResult; +/** Flat alias for {@link StandardSchemaV1.Issue}. */ +export type StandardSchemaV1Issue = StandardSchemaV1.Issue; +/** Flat alias for {@link StandardSchemaV1.PathSegment}. */ +export type StandardSchemaV1PathSegment = StandardSchemaV1.PathSegment; +/** Backward-compatible flat alias for {@link StandardSchemaV1.Types}. */ +export type StandardSchemaV1Types = StandardSchemaV1.Types< + Input, + Output +>; +/** Flat alias for {@link StandardSchemaV1.InferInput}. */ +export type InferStandardInput = + StandardSchemaV1.InferInput; +/** Flat alias for {@link StandardSchemaV1.InferOutput}. */ +export type InferStandardOutput = + StandardSchemaV1.InferOutput; + +/** + * Detect a Standard Schema validator without assuming validators are plain + * objects. Some conforming libraries expose callable schema values. + * + * @internal + */ +function isStandardSchemaV1(value: unknown): value is StandardSchemaV1 { + if (value === null || (typeof value !== 'object' && typeof value !== 'function')) { + return false; + } + if (!('~standard' in value)) { + return false; + } + const properties = value['~standard']; + return ( + properties !== null && + typeof properties === 'object' && + 'version' in properties && + properties.version === 1 && + 'validate' in properties && + typeof properties.validate === 'function' + ); +} -/** Infer the output type of a Standard Schema validator. */ -export type InferStandardOutput = NonNullable< - Schema['~standard']['types'] ->['output']; +export { isStandardSchemaV1 }; diff --git a/src/index.ts b/src/index.ts index da5cae28..6c96b5af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -144,6 +144,8 @@ export type { InferArgs, InferFlag, InferFlags, + InferStandardInput, + InferStandardOutput, InputPromptConfig, InteractiveParams, InteractiveResolver, @@ -170,6 +172,15 @@ export type { SelectPromptConfig, SpinnerHandle, SpinnerOptions, + StandardSchemaV1, + StandardSchemaV1FailureResult, + StandardSchemaV1Issue, + StandardSchemaV1Options, + StandardSchemaV1PathSegment, + StandardSchemaV1Props, + StandardSchemaV1Result, + StandardSchemaV1SuccessResult, + StandardSchemaV1Types, StringConstraints, StringConstraintViolation, TableColumn, From dd62edd4538a0d8c5830f38a8cdc104185c61b6f Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 16 Jul 2026 13:40:46 +0200 Subject: [PATCH 5/8] add CODEOWNERS --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..10129939 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @kjanat From 02f153a3312531c53e222712818e1e818393c3ef Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 16 Jul 2026 18:01:00 +0200 Subject: [PATCH 6/8] fix(ci): make gh canary install self-contained Resolve the repository package by path instead of requiring a globally registered Bun link, so clean CI runners can install the workspace without prior machine state. --- bun.lock | 6 ++++-- examples/gh/package.json | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index fea8d13b..dde080b0 100644 --- a/bun.lock +++ b/bun.lock @@ -39,7 +39,7 @@ "version": "0.0.0-example", "bin": "src/main.ts", "dependencies": { - "@kjanat/dreamcli": "link:@kjanat/dreamcli", + "@kjanat/dreamcli": "file:../..", }, "devDependencies": { "@types/bun": "latest", @@ -291,7 +291,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@kjanat/dreamcli": ["@kjanat/dreamcli@link:@kjanat/dreamcli", {}], + "@kjanat/dreamcli": ["@kjanat/dreamcli@root:", {}], "@loaderkit/resolve": ["@loaderkit/resolve@1.0.6", "", { "dependencies": { "@braidai/lang": "^1.0.0" } }, "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg=="], @@ -1305,6 +1305,8 @@ "gh/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "pwsh-demo/@kjanat/dreamcli": ["@kjanat/dreamcli@link:@kjanat/dreamcli", {}], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], "rolldown-plugin-dts/get-tsconfig": ["get-tsconfig@5.0.0-beta.5", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ=="], diff --git a/examples/gh/package.json b/examples/gh/package.json index 0527d8ba..1214f439 100644 --- a/examples/gh/package.json +++ b/examples/gh/package.json @@ -15,12 +15,11 @@ "scripts": { "bd": "bun run build", "build": "bun build src/main.ts --install --outdir=dist --format=esm --target=node --external @kjanat/dreamcli", - "preinstall": "cd ../.. && bun link", "test": "bun test", "typecheck": "tsc --noEmit" }, "dependencies": { - "@kjanat/dreamcli": "link:@kjanat/dreamcli" + "@kjanat/dreamcli": "file:../.." }, "devDependencies": { "@types/bun": "latest", From 9b5e9a8efaa2b15cb96149c5134c846024381d15 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Fri, 17 Jul 2026 15:19:52 +0200 Subject: [PATCH 7/8] fix(schema): serialize constrained flag metadata Keep generated definitions and their meta-schema aligned for numeric, string, array, path, and value-hint fields. Add compile-time and runtime exhaustiveness checks while excluding runtime validators. --- src/core/json-schema/index.ts | 63 ++++++++- src/core/json-schema/json-schema.test.ts | 173 +++++++++++++++++++++++ 2 files changed, 235 insertions(+), 1 deletion(-) diff --git a/src/core/json-schema/index.ts b/src/core/json-schema/index.ts index 97b23f4e..b89611a2 100644 --- a/src/core/json-schema/index.ts +++ b/src/core/json-schema/index.ts @@ -67,6 +67,9 @@ interface ResolvedOptions { readonly includePrompts: boolean; } +/** Flag fields that survive definition serialization. */ +type SerializedFlagField = Exclude; + /** * Apply defaults to optional {@link JsonSchemaOptions}. * @@ -246,9 +249,35 @@ function serializeFlag(schema: FlagSchema, opts: ResolvedOptions): Record = withDefinitionMetaSchemaDe configPath: { type: 'string' }, description: { type: 'string' }, enumValues: { type: 'array', items: { type: 'string' } }, + numberConstraints: { + type: 'object', + additionalProperties: false, + properties: { + min: { type: 'number' }, + max: { type: 'number' }, + int: { type: 'boolean' }, + finite: { type: 'boolean' }, + }, + }, + stringConstraints: { + type: 'object', + additionalProperties: false, + properties: { + nonEmpty: { type: 'boolean' }, + minLength: { type: 'integer', minimum: 0 }, + maxLength: { type: 'integer', minimum: 0 }, + pattern: { type: 'string' }, + }, + }, elementSchema: { $ref: '#/$defs/flag' }, + separator: { type: 'string', minLength: 1 }, + unique: { type: 'boolean' }, + pathChecks: { + type: 'object', + additionalProperties: false, + properties: { + mustExist: { type: 'boolean' }, + type: { enum: ['file', 'directory'] }, + }, + required: ['mustExist'], + }, + valueHint: { type: 'string' }, prompt: { $ref: '#/$defs/prompt' }, deprecated: { oneOf: [{ type: 'string' }, { const: true }] }, propagate: { const: true }, negation: { $ref: '#/$defs/negation' }, duplicates: { enum: ['last', 'first', 'error'] }, - }, + } satisfies Record>, required: ['kind', 'presence'], }, negation: { diff --git a/src/core/json-schema/json-schema.test.ts b/src/core/json-schema/json-schema.test.ts index 04caf077..801a86e8 100644 --- a/src/core/json-schema/json-schema.test.ts +++ b/src/core/json-schema/json-schema.test.ts @@ -21,6 +21,17 @@ function flagDef(overrides: FlagSchemaOverrides = {}): FlagSchema { return createSchema(overrides.kind ?? 'string', overrides); } +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function expectRecord(value: unknown): Record { + if (!isRecord(value)) { + throw new TypeError('expected a record'); + } + return value; +} + /** Minimal CommandSchema with all required fields. */ function commandDef(overrides: Partial = {}): CommandSchema { return { @@ -259,6 +270,68 @@ describe('generateSchema — definition metadata', () => { expect(result).toHaveProperty(['commands', 0, 'flags', 'c', 'kind'], 'custom'); }); + it('serializes constrained and hinted flag fields', () => { + const cmd = commandDef({ + name: 'test', + flags: { + retries: flagDef({ + kind: 'number', + numberConstraints: { min: 1, max: 5, int: true, finite: false }, + }), + tag: flagDef({ + kind: 'string', + stringConstraints: { + nonEmpty: true, + minLength: 2, + maxLength: 12, + pattern: /^v\d+$/i, + }, + }), + files: flagDef({ + kind: 'array', + elementSchema: flagDef({ kind: 'string' }), + separator: ',', + unique: true, + }), + path: flagDef({ + kind: 'string', + pathChecks: { mustExist: true, type: 'file' }, + valueHint: 'path', + }), + }, + }); + const result = generateSchema(minimalCLI({ commands: [erased(cmd)] })); + + expect(result).toHaveProperty(['commands', 0, 'flags', 'retries'], { + kind: 'number', + presence: 'optional', + numberConstraints: { min: 1, max: 5, int: true, finite: false }, + }); + expect(result).toHaveProperty(['commands', 0, 'flags', 'tag'], { + kind: 'string', + presence: 'optional', + stringConstraints: { + nonEmpty: true, + minLength: 2, + maxLength: 12, + pattern: '^v\\d+$', + }, + }); + expect(result).toHaveProperty(['commands', 0, 'flags', 'files'], { + kind: 'array', + presence: 'optional', + elementSchema: { kind: 'string', presence: 'optional' }, + separator: ',', + unique: true, + }); + expect(result).toHaveProperty(['commands', 0, 'flags', 'path'], { + kind: 'string', + presence: 'optional', + pathChecks: { mustExist: true, type: 'file' }, + valueHint: 'path', + }); + }); + it('includes flag defaultValue when defaulted and serializable', () => { const cmd = commandDef({ name: 'test', @@ -706,6 +779,106 @@ describe('generateSchema — definition metadata', () => { ['kind', 'presence'], ); }); + + it('describes constrained flag fixtures in the meta-schema', () => { + expect(definitionMetaSchema).toMatchObject({ + $defs: { + flag: { + properties: { + numberConstraints: { + type: 'object', + additionalProperties: false, + properties: { + min: { type: 'number' }, + max: { type: 'number' }, + int: { type: 'boolean' }, + finite: { type: 'boolean' }, + }, + }, + stringConstraints: { + type: 'object', + additionalProperties: false, + properties: { + nonEmpty: { type: 'boolean' }, + minLength: { type: 'integer', minimum: 0 }, + maxLength: { type: 'integer', minimum: 0 }, + pattern: { type: 'string' }, + }, + }, + separator: { type: 'string', minLength: 1 }, + unique: { type: 'boolean' }, + pathChecks: { + type: 'object', + additionalProperties: false, + properties: { + mustExist: { type: 'boolean' }, + type: { enum: ['file', 'directory'] }, + }, + required: ['mustExist'], + }, + valueHint: { type: 'string' }, + }, + }, + }, + }); + }); + + it('keeps serialized FlagSchema fields exhaustive against the meta-schema', () => { + const allFieldsFlag = { + kind: 'array', + presence: 'defaulted', + defaultValue: ['v1'], + aliases: [{ name: 'a', hidden: false }], + envVar: 'VERSIONS', + configPath: 'release.versions', + description: 'Release versions', + enumValues: ['v1'], + numberConstraints: { min: 1, max: 5, int: true, finite: false }, + stringConstraints: { + nonEmpty: true, + minLength: 2, + maxLength: 12, + pattern: /^v\d+$/, + }, + elementSchema: flagDef({ kind: 'string' }), + separator: ',', + unique: true, + pathChecks: { mustExist: true, type: 'file' }, + valueHint: 'version', + prompt: { kind: 'input', message: 'Version?', placeholder: 'v1' }, + parseFn: (value: unknown) => value, + standard: { + '~standard': { + version: 1, + vendor: 'test', + validate: (value: unknown) => ({ value }), + }, + }, + deprecated: 'use --release', + propagate: true, + negation: { alias: 'no-versions', hidden: true }, + duplicates: 'error', + } satisfies FlagSchema; + const result = generateSchema( + minimalCLI({ + commands: [erased(commandDef({ flags: { versions: allFieldsFlag } }))], + }), + ); + const commands = expectRecord(result).commands; + if (!Array.isArray(commands)) { + throw new TypeError('expected commands'); + } + const command = expectRecord(commands[0]); + const flags = expectRecord(command.flags); + const serializedFlag = expectRecord(flags.versions); + const defs = expectRecord(definitionMetaSchema.$defs); + const flagMetaSchema = expectRecord(defs.flag); + const flagMetaProperties = expectRecord(flagMetaSchema.properties); + + expect(Object.keys(serializedFlag).sort()).toEqual(Object.keys(flagMetaProperties).sort()); + expect(serializedFlag).not.toHaveProperty('parseFn'); + expect(serializedFlag).not.toHaveProperty('standard'); + }); }); // === generateInputSchema — JSON Schema validation From 638de6b0209ab2ca7ca937bedd679d094b9d7cd8 Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Fri, 17 Jul 2026 15:50:41 +0200 Subject: [PATCH 8/8] fix(schema): preserve serialized regexp flags Represent pattern metadata as `{ source, flags }` so generated definitions retain RegExp semantics. Require both fields in the meta-schema and cover flagged and unflagged expressions. --- src/core/json-schema/index.ts | 17 +++++++++++++++-- src/core/json-schema/json-schema.test.ts | 16 ++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/json-schema/index.ts b/src/core/json-schema/index.ts index b89611a2..cceea8ca 100644 --- a/src/core/json-schema/index.ts +++ b/src/core/json-schema/index.ts @@ -256,7 +256,12 @@ function serializeFlag(schema: FlagSchema, opts: ResolvedOptions): Record = withDefinitionMetaSchemaDe nonEmpty: { type: 'boolean' }, minLength: { type: 'integer', minimum: 0 }, maxLength: { type: 'integer', minimum: 0 }, - pattern: { type: 'string' }, + pattern: { + type: 'object', + additionalProperties: false, + properties: { + source: { type: 'string' }, + flags: { type: 'string' }, + }, + required: ['source', 'flags'], + }, }, }, elementSchema: { $ref: '#/$defs/flag' }, diff --git a/src/core/json-schema/json-schema.test.ts b/src/core/json-schema/json-schema.test.ts index 801a86e8..c14db0a3 100644 --- a/src/core/json-schema/json-schema.test.ts +++ b/src/core/json-schema/json-schema.test.ts @@ -314,7 +314,7 @@ describe('generateSchema — definition metadata', () => { nonEmpty: true, minLength: 2, maxLength: 12, - pattern: '^v\\d+$', + pattern: { source: '^v\\d+$', flags: 'i' }, }, }); expect(result).toHaveProperty(['commands', 0, 'flags', 'files'], { @@ -802,7 +802,15 @@ describe('generateSchema — definition metadata', () => { nonEmpty: { type: 'boolean' }, minLength: { type: 'integer', minimum: 0 }, maxLength: { type: 'integer', minimum: 0 }, - pattern: { type: 'string' }, + pattern: { + type: 'object', + additionalProperties: false, + properties: { + source: { type: 'string' }, + flags: { type: 'string' }, + }, + required: ['source', 'flags'], + }, }, }, separator: { type: 'string', minLength: 1 }, @@ -876,6 +884,10 @@ describe('generateSchema — definition metadata', () => { const flagMetaProperties = expectRecord(flagMetaSchema.properties); expect(Object.keys(serializedFlag).sort()).toEqual(Object.keys(flagMetaProperties).sort()); + expect(serializedFlag).toHaveProperty(['stringConstraints', 'pattern'], { + source: '^v\\d+$', + flags: '', + }); expect(serializedFlag).not.toHaveProperty('parseFn'); expect(serializedFlag).not.toHaveProperty('standard'); });