diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..10129939 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @kjanat 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/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/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/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..dde080b0 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", @@ -39,11 +39,11 @@ "version": "0.0.0-example", "bin": "src/main.ts", "dependencies": { - "@kjanat/dreamcli": "link:@kjanat/dreamcli", + "@kjanat/dreamcli": "file:../..", }, "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:", + "@types/bun": "latest", + "typescript": "latest", }, }, "examples/pwsh-demo": { @@ -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.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4o3NFRobXHynkgcFVrlZsoDAFtF2ldlEGN8sORSws5ZQqyY4PXnPUIylu4ksfyHuwkfvDREuWh3JK+niRwGq3w=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-D32P5HkU2Y6PySuC/WsVDTOgsDwVFmujzhhhOQjajtATpVWFDXuVd3oRbsWNSEA+aaFzyzZm22szsyydBYlSyQ=="], - "@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": ["@biomejs/cli-linux-arm64@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-pSEfW7B8kTsXUjUxC1xVVK+y85Ht3C5XxZ9gclmC7/3Ku9Vqz8jmI7k0p/BNIjQ6t4sFERI2sFeH73ybiZl6YQ=="], - "@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-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Rpm5/AT1m+DlJmUoYvS4/vXc+0tXJPJ2NQz25TGPyHVF5JrWy75PE0GH6kVxsKtQDuCH4OgzquZq0R4kj/wCVg=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-FNxojWJkL7EajAuzBgoLe0T2G0y112M4lBrDIFl/DomFTx8yqenYOIdsRLNXvOvBBofE8hJi85LjzLmBDpY7/Q=="], - "@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-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-aby/PohmmgbShcHqFsZVzG8H6D98+P+A6xRWRrQcLW1pCjabcov5UUlke4UqNQBYTkDQav+jB4zyyDDeKB2GaA=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-emoXexPZIPAZkz2RKmA95WJUqK3I5MJNYtwEbL5ESciRzhmFMMyekDhNG8hpeOaK+ZGRDxAU4wvGuA5IHQ0h0w=="], - "@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=="], @@ -115,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=="], @@ -133,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=="], @@ -293,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=="], @@ -679,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=="], @@ -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=="], @@ -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,10 @@ "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=="], + + "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/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/examples/gh/package.json b/examples/gh/package.json index ee57a7d8..1214f439 100644 --- a/examples/gh/package.json +++ b/examples/gh/package.json @@ -19,13 +19,12 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@kjanat/dreamcli": "link:@kjanat/dreamcli" + "@kjanat/dreamcli": "file:../.." }, "devDependencies": { - "@types/bun": "catalog:", - "typescript": "catalog:" + "@types/bun": "latest", + "typescript": "latest" }, - "packageManager": "bun@1.3.12", "engines": { "bun": "^1.3" }, @@ -34,6 +33,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) 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/index.ts b/src/core/json-schema/index.ts index 85dc9106..cceea8ca 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 @@ -69,6 +67,9 @@ interface ResolvedOptions { readonly includePrompts: boolean; } +/** Flag fields that survive definition serialization. */ +type SerializedFlagField = Exclude; + /** * Apply defaults to optional {@link JsonSchemaOptions}. * @@ -248,9 +249,40 @@ function serializeFlag(schema: FlagSchema, opts: ResolvedOptions): 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 +897,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 +918,153 @@ 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' } }, + 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: 'object', + additionalProperties: false, + properties: { + source: { type: 'string' }, + flags: { type: 'string' }, + }, + required: ['source', 'flags'], + }, + }, + }, + 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: { + 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/json-schema/json-schema.test.ts b/src/core/json-schema/json-schema.test.ts index f9f6677d..c14db0a3 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 { @@ -104,6 +115,7 @@ function argEntry( enumValues: undefined, numberConstraints: undefined, parseFn: undefined, + standard: undefined, deprecated: undefined, ...overrides, }, @@ -258,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: { source: '^v\\d+$', flags: 'i' }, + }, + }); + 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', @@ -705,6 +779,118 @@ 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: 'object', + additionalProperties: false, + properties: { + source: { type: 'string' }, + flags: { type: 'string' }, + }, + required: ['source', 'flags'], + }, + }, + }, + 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).toHaveProperty(['stringConstraints', 'pattern'], { + source: '^v\\d+$', + flags: '', + }); + expect(serializedFlag).not.toHaveProperty('parseFn'); + expect(serializedFlag).not.toHaveProperty('standard'); + }); }); // === generateInputSchema — JSON Schema validation 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..43bcb33d --- /dev/null +++ b/src/core/resolve/resolve-standard-schema.test.ts @@ -0,0 +1,185 @@ +/** + * 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'); + }); + + 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 + +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'); + }); + + 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 + +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..79456e77 --- /dev/null +++ b/src/core/resolve/standard.ts @@ -0,0 +1,160 @@ +/** + * 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 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) { + continue; + } + const result = await validateValue(`--${name}`, value, 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; + 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}>`, value, 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-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 }; 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 e296b9c0..21b1dd9f 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, isStandardSchemaV1, type 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, }; @@ -714,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. * @@ -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 (isStandardSchemaV1(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 616cd75e..b1e269e2 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, isStandardSchemaV1, type 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, @@ -1146,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`. @@ -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 (isStandardSchemaV1(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 900c1b81..07ec1f8f 100644 --- a/src/core/schema/index.ts +++ b/src/core/schema/index.ts @@ -101,6 +101,19 @@ export { validateNumberConstraints, } from './number-constraints.ts'; export type { RunOptions, RunResult } from './run.ts'; +export type { + InferStandardInput, + InferStandardOutput, + StandardSchemaV1, + StandardSchemaV1FailureResult, + StandardSchemaV1Issue, + StandardSchemaV1Options, + 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.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 new file mode 100644 index 00000000..0ee94620 --- /dev/null +++ b/src/core/schema/standard.ts @@ -0,0 +1,143 @@ +/** + * 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. + * + * @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': StandardSchemaV1.Props; +} + +/** 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; + } + + /** Optional parameters passed to a validator. */ + export interface Options { + /** Explicit support for vendor-specific validation parameters. */ + readonly libraryOptions?: Record | undefined; + } + + /** The result of validating a value: either its output or a list of issues. */ + export type Result = SuccessResult | FailureResult; + + /** 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 failed validation carrying validation issues. */ + export interface FailureResult { + /** The issues that caused validation to fail. */ + readonly issues: ReadonlyArray; + } + + /** 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; + } + + /** 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']; +} + +/** 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' + ); +} + +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,