@warlock.js/core 5.7.0 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/esm/cli/cli-commands.manager.mjs +2 -2
  3. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  4. package/esm/cli/commands/doctor/checks/index.mjs +2 -0
  5. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  6. package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs +55 -0
  7. package/esm/cli/commands/doctor/checks/jwt-secret.check.mjs.map +1 -0
  8. package/esm/cli/commands/generate/generators/controller.generator.mjs +4 -4
  9. package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
  10. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  11. package/esm/cli/commands/generate/generators/model.generator.mjs +4 -4
  12. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  13. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  14. package/esm/cli/commands/generate/generators/repository.generator.mjs +4 -4
  15. package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generators/resource.generator.mjs +4 -4
  17. package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/service.generator.mjs +4 -4
  19. package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
  20. package/esm/cli/commands/generate/templates/stubs.mjs +6 -3
  21. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  22. package/esm/dev-server/files-orchestrator.mjs +1 -0
  23. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  24. package/esm/dev-server/parse-imports.mjs +1 -1
  25. package/esm/dev-server/parse-imports.mjs.map +1 -1
  26. package/esm/generations/features/shadcn.feature.mjs +1 -0
  27. package/esm/generations/features/shadcn.feature.mjs.map +1 -1
  28. package/esm/generations/stubs.mjs +6 -6
  29. package/esm/generations/stubs.mjs.map +1 -1
  30. package/esm/http/boot-port-preflight.d.mts +25 -1
  31. package/esm/http/boot-port-preflight.d.mts.map +1 -1
  32. package/esm/http/boot-port-preflight.mjs +51 -2
  33. package/esm/http/boot-port-preflight.mjs.map +1 -1
  34. package/esm/http/index.d.mts +1 -1
  35. package/esm/http/index.mjs +1 -1
  36. package/esm/index.d.mts +2 -2
  37. package/esm/index.mjs +2 -2
  38. package/esm/utils/load-environment.mjs +4 -1
  39. package/esm/utils/load-environment.mjs.map +1 -1
  40. package/esm/utils/recorded-environment-overrides.mjs +41 -0
  41. package/esm/utils/recorded-environment-overrides.mjs.map +1 -0
  42. package/llms-full.txt +3 -0
  43. package/package.json +12 -12
  44. package/skills/warlock-doctor/SKILL.md +3 -0
@@ -1 +1 @@
1
- {"version":3,"file":"shadcn.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/shadcn.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\n\n/**\n * `warlock add shadcn` installs the PREREQUISITES for shadcn/ui. It does not\n * wrap `shadcn add`, and it never will.\n *\n * shadcn/ui is a copy-in generator with its own CLI and its own registry, not a\n * dependency: components are written into your source tree and become yours the\n * moment they land. Wrapping their CLI would make their moving target our bug\n * reports, for a command that adds nothing but a rename.\n *\n * What is worth owning is the part their CLI gets wrong here. Measured against\n * this framework's layout on 2026-08-25, `shadcn add button card` exited 0 and\n * produced components that rendered COMPLETELY UNSTYLED. It writes components\n * that import `cn` and reference `bg-primary` / `ring-ring`, but only\n * `shadcn init` creates `lib/utils` and the theme tokens those names resolve\n * against. The class was in the generated CSS and the element carried it, and\n * the rule still evaluated to an empty `var()` — a silent failure with a zero\n * exit code, which is the worst kind to hand a user.\n *\n * So this feature ships the five things that make `npx shadcn add <component>`\n * work the first time:\n *\n * 1. `components.json` written for OUR layout (their defaults assume `src/app`\n * or a bare `components/`, neither of which is where pages live here).\n * 2. `src/web/lib/utils.ts` exporting `cn`.\n * 3. The design tokens appended to `src/web/app.css`.\n * 4. A `web/*` entry in tsconfig `paths`, so the generated imports typecheck.\n * 5. The packages `init` would have installed — see `printNextStep` below.\n * Skipping `init` is correct; skipping its dependency list was a bug, and\n * it cost a generated button that imports `cva` and cannot compile.\n *\n * After that, the user talks to shadcn directly, and their docs are true.\n */\n\n/**\n * The aliases shadcn's CLI rewrites every generated import against.\n *\n * These are the whole reason this feature exists. shadcn's defaults do not fit\n * `src/web`, and no user would guess this mapping: the alias keys are shadcn's\n * vocabulary, the values are tsconfig `paths` prefixes (hence `web/...`, not\n * `src/web/...`), and `tailwind.css` is a real path from the project root\n * (hence `src/web/app.css`, WITH the `src`). Getting one of them wrong produces\n * components in the wrong folder importing `cn` from somewhere that does not\n * exist.\n *\n * `tailwind.config` is deliberately empty: v4 is CSS-first and there is no\n * config file for it to point at. `rsc: false` because these are SSR React\n * pages rendered by the Warlock HTTP server, not React Server Components.\n */\nconst componentsJsonStub = `{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"tailwind\": {\n \"config\": \"\",\n \"css\": \"src/web/app.css\",\n \"baseColor\": \"neutral\",\n \"cssVariables\": true,\n \"prefix\": \"\"\n },\n \"aliases\": {\n \"components\": \"web/components\",\n \"utils\": \"web/lib/utils\",\n \"ui\": \"web/components/ui\",\n \"lib\": \"web/lib\",\n \"hooks\": \"web/hooks\"\n },\n \"iconLibrary\": \"lucide\"\n}\n`;\n\n/**\n * `cn` — the one import every single shadcn component makes.\n *\n * `clsx` resolves the conditional/array/object class syntax, and `tailwind-merge`\n * then de-duplicates conflicting Tailwind utilities so a caller's `className`\n * actually beats the component's own default rather than depending on which one\n * happens to come later in the generated stylesheet. Both halves are required:\n * clsx alone leaves `px-2 px-4` in the attribute and the loser wins at random.\n */\nconst cnUtilStub = `import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names, with later Tailwind utilities winning over earlier ones.\n *\n * Every shadcn/ui component imports this. Keep the export name and signature.\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`;\n\n/**\n * Marker for \"the tokens are already in this stylesheet\".\n *\n * A comment rather than a token name, because a user is free to retune every\n * value below and we must still recognise our own block on a re-run.\n */\nconst SHADCN_TOKENS_MARKER = \"shadcn/ui design tokens\";\n\n/**\n * The shadcn token set, as a plain `@theme` block with LITERAL values.\n *\n * Not `@theme inline` over `:root` custom properties, which is what shadcn's own\n * `init` writes. That form was verified against this stylesheet pipeline on\n * 2026-08-25 and it produces utilities that resolve to nothing: the class is in\n * the generated CSS, the class is on the element, and the rule still evaluates\n * to an empty `var()`. Literal values in `@theme` are what make `bg-primary`\n * emit a colour instead of a dangling reference.\n *\n * Dark mode still works, and works the way shadcn expects: `@theme` emits these\n * as real custom properties, so the `.dark` block re-declares the same\n * `--color-*` names and the cascade does the rest. `@custom-variant dark` is\n * what points Tailwind's `dark:` prefix at that class instead of the OS setting.\n *\n * Values are shadcn's `neutral` base. They are the starting point, not the\n * answer — this is the block to edit when the project gets a real palette.\n */\nconst shadcnTokensStub = `\n/* ${SHADCN_TOKENS_MARKER} — edit freely, this is your palette now.\n\n These are LITERAL values in a plain @theme block, on purpose. shadcn's own\n \\`init\\` writes \\`@theme inline\\` over :root custom properties; that form emits\n utilities which resolve to an empty var() here, so every component renders\n unstyled while the class sits right there on the element. Do not convert it.\n*/\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: oklch(1 0 0);\n --color-foreground: oklch(0.145 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.145 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.145 0 0);\n --color-primary: oklch(0.205 0 0);\n --color-primary-foreground: oklch(0.985 0 0);\n --color-secondary: oklch(0.97 0 0);\n --color-secondary-foreground: oklch(0.205 0 0);\n --color-muted: oklch(0.97 0 0);\n --color-muted-foreground: oklch(0.556 0 0);\n --color-accent: oklch(0.97 0 0);\n --color-accent-foreground: oklch(0.205 0 0);\n --color-destructive: oklch(0.577 0.245 27.325);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(0.922 0 0);\n --color-input: oklch(0.922 0 0);\n --color-ring: oklch(0.708 0 0);\n\n --color-chart-1: oklch(0.646 0.222 41.116);\n --color-chart-2: oklch(0.6 0.118 184.704);\n --color-chart-3: oklch(0.398 0.07 227.392);\n --color-chart-4: oklch(0.828 0.189 84.429);\n --color-chart-5: oklch(0.769 0.188 70.08);\n\n --color-sidebar: oklch(0.985 0 0);\n --color-sidebar-foreground: oklch(0.145 0 0);\n --color-sidebar-primary: oklch(0.205 0 0);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.97 0 0);\n --color-sidebar-accent-foreground: oklch(0.205 0 0);\n --color-sidebar-border: oklch(0.922 0 0);\n --color-sidebar-ring: oklch(0.708 0 0);\n\n /* shadcn components reach for rounded-lg/md/sm and expect them to track one\n radius. Changing --radius-lg here also retunes Tailwind's own rounded-lg,\n which is the intended trade: one radius scale per project, not two. */\n --radius-sm: 0.375rem;\n --radius-md: 0.5rem;\n --radius-lg: 0.625rem;\n --radius-xl: 1rem;\n}\n\n.dark {\n --color-background: oklch(0.145 0 0);\n --color-foreground: oklch(0.985 0 0);\n --color-card: oklch(0.205 0 0);\n --color-card-foreground: oklch(0.985 0 0);\n --color-popover: oklch(0.205 0 0);\n --color-popover-foreground: oklch(0.985 0 0);\n --color-primary: oklch(0.922 0 0);\n --color-primary-foreground: oklch(0.205 0 0);\n --color-secondary: oklch(0.269 0 0);\n --color-secondary-foreground: oklch(0.985 0 0);\n --color-muted: oklch(0.269 0 0);\n --color-muted-foreground: oklch(0.708 0 0);\n --color-accent: oklch(0.269 0 0);\n --color-accent-foreground: oklch(0.985 0 0);\n --color-destructive: oklch(0.704 0.191 22.216);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(1 0 0 / 10%);\n --color-input: oklch(1 0 0 / 15%);\n --color-ring: oklch(0.556 0 0);\n\n --color-chart-1: oklch(0.488 0.243 264.376);\n --color-chart-2: oklch(0.696 0.17 162.48);\n --color-chart-3: oklch(0.769 0.188 70.08);\n --color-chart-4: oklch(0.627 0.265 303.9);\n --color-chart-5: oklch(0.645 0.246 16.439);\n\n --color-sidebar: oklch(0.205 0 0);\n --color-sidebar-foreground: oklch(0.985 0 0);\n --color-sidebar-primary: oklch(0.488 0.243 264.376);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.269 0 0);\n --color-sidebar-accent-foreground: oklch(0.985 0 0);\n --color-sidebar-border: oklch(1 0 0 / 10%);\n --color-sidebar-ring: oklch(0.556 0 0);\n}\n`;\n\n/**\n * Write `components.json` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file, so its presence means\n * `add shadcn` has been here and a human may since have retuned the aliases,\n * the style, or the base colour. It is never rewritten: shadcn's CLI reads this\n * file on every `add`, so overwriting it would silently relocate a project's\n * component folder out from under the components already in it.\n */\nasync function writeComponentsJson(): Promise<void> {\n const componentsJsonPath = rootPath(\"components.json\");\n\n if (await fileExistsAsync(componentsJsonPath)) {\n console.log(`${colors.yellowBright(\"components.json\")} already exists, skipping...`);\n\n return;\n }\n\n await putFileAsync(componentsJsonPath, componentsJsonStub);\n console.log(`${colors.green(\"✓\")} Created components.json`);\n}\n\n/**\n * Write `src/web/lib/utils.ts`.\n *\n * Guarded on its own rather than on the sentinel above, because `lib/utils` is\n * a name a project may well already own — and if it does, whatever is in there\n * is user code with other callers. We print instead of merging.\n */\nasync function writeCnUtil(): Promise<void> {\n const utilsFile = srcPath(\"web/lib/utils.ts\");\n\n if (await fileExistsAsync(utilsFile)) {\n const current = await getFileAsync(utilsFile).catch(() => \"\");\n\n if (/export\\s+(function|const)\\s+cn\\b/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/lib/utils.ts\")} already exports cn, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/lib/utils.ts\")} exists but does not export ` +\n `${colors.yellowBright(\"cn\")} — add it yourself:\\n` +\n \" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }\\n\" +\n \" Every shadcn component imports it, and none of them will compile until it is there.\",\n );\n }\n\n return;\n }\n\n await ensureDirectoryAsync(srcPath(\"web/lib\"));\n await putFileAsync(utilsFile, cnUtilStub);\n console.log(`${colors.green(\"✓\")} Created src/web/lib/utils.ts`);\n}\n\n/**\n * Append the token block to `src/web/app.css`.\n *\n * The stylesheet is guaranteed to exist by the time this runs: `requires:\n * [\"tailwind\"]` makes the add command resolve `tailwind` first and run its\n * `onExecuting` ahead of this one, and that is what creates the file. The check\n * below is for a stylesheet a human has since moved or deleted — worth a\n * printed instruction, not a failure.\n *\n * Appended, never rewritten. Everything already in that file is either\n * Tailwind's own `@import` or the project's design system.\n */\nasync function appendThemeTokens(): Promise<void> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (!(await fileExistsAsync(cssFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/app.css\")} not found — ` +\n \"append the shadcn token block to your Tailwind stylesheet yourself.\\n\" +\n \" Without the tokens, shadcn components render unstyled: the classes are emitted and applied, \" +\n \"but `bg-primary` and friends resolve to an empty var().\",\n );\n\n return;\n }\n\n const current = await getFileAsync(cssFile);\n\n if (current.includes(SHADCN_TOKENS_MARKER) || current.includes(\"--color-primary-foreground\")) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already has the tokens, skipping...`);\n\n return;\n }\n\n await putFileAsync(cssFile, `${current.trimEnd()}\\n${shadcnTokensStub}`);\n console.log(`${colors.green(\"✓\")} Appended the shadcn tokens to src/web/app.css`);\n}\n\n/** The tsconfig `paths` entry the generated imports resolve through. */\nconst WEB_PATH_ALIAS = '\"web/*\": [\"./src/web/*\"]';\n\n/**\n * Add `web/*` to tsconfig `compilerOptions.paths`.\n *\n * The template declares only `app/*`, so every import shadcn generates against\n * the aliases above (`web/lib/utils`, `web/components/ui/button`) would fail to\n * typecheck the moment it lands. This is the first `add` feature to patch\n * `paths` rather than `include`.\n *\n * String surgery, and NOT the parse-and-write that the `include` patches use.\n * The project template's `tsconfig.json` carries `//` comments — it is JSONC,\n * and `JSON.parse` throws on it — so a parse-first patch would fail on exactly\n * the projects this feature is for. Editing the text also preserves those\n * comments, which are load-bearing documentation in that file.\n */\nasync function addWebPathAlias(): Promise<void> {\n const tsconfigPath = rootPath(\"tsconfig.json\");\n\n const printManualInstruction = (reason: string) => {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"tsconfig.json\")} ${reason} — ` +\n \"add this to `compilerOptions.paths` yourself:\\n\" +\n ` ${WEB_PATH_ALIAS}\\n` +\n \" Without it, every import shadcn generates against the `web/*` aliases fails to typecheck.\",\n );\n };\n\n if (!(await fileExistsAsync(tsconfigPath))) {\n printManualInstruction(\"not found\");\n\n return;\n }\n\n const current = await getFileAsync(tsconfigPath);\n\n // Matches the alias whichever quote style and spacing the file uses, so a\n // re-run against a hand-edited tsconfig does not stack a second entry.\n if (/[\"']web\\/\\*[\"']\\s*:/.test(current)) {\n console.log(`${colors.yellowBright(\"tsconfig.json\")} already maps web/*, skipping...`);\n\n return;\n }\n\n let next: string;\n\n if (/\"paths\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(/\"paths\"\\s*:\\s*\\{/, `$&\\n ${WEB_PATH_ALIAS},`);\n } else if (/\"compilerOptions\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(\n /\"compilerOptions\"\\s*:\\s*\\{/,\n `$&\\n \"paths\": {\\n ${WEB_PATH_ALIAS}\\n },`,\n );\n } else {\n printManualInstruction(\"has no recognisable compilerOptions block\");\n\n return;\n }\n\n await putFileAsync(tsconfigPath, next);\n console.log(`${colors.green(\"✓\")} Added ${WEB_PATH_ALIAS} to tsconfig.json paths`);\n}\n\n/**\n * Tell the user the next command is theirs to run.\n *\n * This is the seam. Everything above is prerequisite; from here the shadcn docs\n * apply verbatim, which is the entire point of not wrapping their CLI.\n *\n * WHY THIS FEATURE DECLARES cva AND lucide-react, and where the list came from.\n *\n * shadcn's registry splits dependencies across two levels. Each component item\n * (`.../new-york-v4/button.json`) declares only what that file pulls beyond the\n * baseline — for button, `radix-ui` and nothing else. Everything the baseline\n * assumes lives on the STYLE INDEX (`.../new-york-v4/index.json`), which is\n * fetched by `init` and only by `init`:\n *\n * dependencies: class-variance-authority, lucide-react, radix-ui\n * devDependencies: tw-animate-css, shadcn\n *\n * We skip `init` on purpose — it would rewrite components.json and replace the\n * literal tokens with the `@theme inline` block that resolves to nothing here.\n * Skipping it is right; inheriting nothing from it was the bug. `shadcn add\n * button` exits 0 and writes `import { cva } from \"class-variance-authority\"`\n * against a package no one installed. Zero exit code, TS2307, blank page.\n *\n * `radix-ui` stays off our list: it is the one style-index dependency that is\n * ALSO declared per-component, so `add` really does install it on demand, and\n * declaring it here would pull the whole primitive set into projects using two\n * components. `shadcn` itself stays off too — it is the CLI, and the user is\n * invoking it via `npx`.\n *\n * `tw-animate-css` was checked and deliberately EXCLUDED. It is not imported by\n * any component; it is a plain stylesheet whose only entry point is the\n * `@import \"tw-animate-css\"` line that `init` writes into the CSS — and we do\n * not write that line, so the package would install and never load. Nothing\n * fails to compile or render without it. What you lose is the enter/exit\n * animation on overlay components (dialog, dropdown, tooltip, sheet): their\n * `animate-in` / `fade-in-0` classes are simply never generated, so the overlay\n * appears instantly instead of fading. That is opt-in, and the note below is how\n * a user opts in — the `@import` has to go at the TOP of app.css, next to\n * Tailwind's own, which is why this feature cannot append it to the token block.\n */\nfunction printNextStep(): void {\n console.log(\n `\\n${colors.green(\"✓\")} shadcn/ui prerequisites are in place. Add components with shadcn's own CLI:\\n` +\n ` ${colors.yellowBright(\"npx shadcn@latest add button card\")}\\n` +\n \" Skip `shadcn init` — this feature did its job, and running it would rewrite components.json\\n\" +\n \" and replace the theme tokens with an `@theme inline` block that resolves to nothing here.\\n\" +\n ` ${colors.yellowBright(\"class-variance-authority\")} and ${colors.yellowBright(\"lucide-react\")} are already installed: shadcn declares\\n` +\n \" them on the style index that only `init` reads, so `add` would never install them for you.\\n\" +\n ` ${colors.yellowBright(\"radix-ui\")} (the unified package, not @radix-ui/react-*) IS declared per component,\\n` +\n \" so shadcn's CLI installs that one itself as each component needs it.\\n\" +\n ` For overlay animations, add ${colors.yellowBright(\"tw-animate-css\")} and put ${colors.yellowBright('@import \"tw-animate-css\";')}\\n` +\n \" at the TOP of src/web/app.css, under the Tailwind import. Without it dialogs and dropdowns\\n\" +\n \" still work, they just appear instantly instead of animating.\",\n );\n}\n\n/**\n * Lay the ground shadcn's CLI expects to find, and nothing more.\n *\n * Four files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute shadcn, clsx, or Tailwind.\n */\nasync function completeShadcnInstallation(_options: CommandActionData) {\n await writeComponentsJson();\n await writeCnUtil();\n await appendThemeTokens();\n await addWebPathAlias();\n printNextStep();\n}\n\nexport const shadcnFeature: FeatureDefinition = {\n description:\n \"Sets up the prerequisites for shadcn/ui so `npx shadcn add <component>` works first time: components.json aliased to src/web, src/web/lib/utils.ts (cn), the design tokens in src/web/app.css, and a web/* tsconfig path. It does NOT wrap shadcn's CLI — components stay theirs to generate and yours to own.\",\n // `tailwind` owns src/web/app.css, which the token block is appended to.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so the stylesheet exists before this feature writes into it.\n requires: [\"tailwind\"],\n // Everything `shadcn init` would have installed, minus the parts their CLI\n // genuinely does install per-component. See the note above `printNextStep`\n // for how this list was derived and what is deliberately NOT in it.\n dependencies: {\n // Both are runtime dependencies of `cn`, which every generated component\n // calls on every render — not build-time tooling.\n clsx: \"^2.1.1\",\n // v3 is the Tailwind v4 line; tailwind-merge v2 knows the v3 utility set and\n // silently fails to de-duplicate against v4 class names.\n \"tailwind-merge\": \"^3.3.1\",\n // `cva` is imported on line 2 of the generated button — and of every other\n // component with a `variant` prop. The registry declares it ONCE, on the\n // style index that only `init` applies, so `shadcn add button` resolves the\n // component's own deps, exits 0, and leaves TS2307 on a file it just wrote.\n // Still 0.x upstream, so this caret pins to 0.7.x; expect 0.7.1 to land.\n \"class-variance-authority\": \"^0.7.1\",\n // Same trap, one layer further in. `components.json` declares\n // `iconLibrary: \"lucide\"`, and dialog/select/checkbox/dropdown-menu all\n // import `lucide-react` in their source while declaring only `radix-ui` —\n // verified against the new-york-v4 registry items. So the CLI installs it\n // for nobody, and the first icon-bearing component fails to resolve.\n // Peer range covers React 19, which is what `web` brings.\n \"lucide-react\": \"^1.34.0\",\n },\n onExecuting: completeShadcnInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC3B,MAAM,aAAa;;;;;;;;;;;;;;;;;;AAmBnB,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;AAoB7B,MAAM,mBAAmB;KACpB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqG1B,eAAe,sBAAqC;CAClD,MAAM,qBAAqB,SAAS,iBAAiB;CAErD,IAAI,MAAM,gBAAgB,kBAAkB,GAAG;EAC7C,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF;CACF;CAEA,MAAM,aAAa,oBAAoB,kBAAkB;CACzD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;AAC5D;;;;;;;;AASA,eAAe,cAA6B;CAC1C,MAAM,YAAY,QAAQ,kBAAkB;CAE5C,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,MAAM,UAAU,MAAM,aAAa,SAAS,CAAC,CAAC,YAAY,EAAE;EAE5D,IAAI,mCAAmC,KAAK,OAAO,GACjD,QAAQ,IAAI,GAAG,OAAO,aAAa,sBAAsB,EAAE,iCAAiC;OAE5F,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,sBAAsB,EAAE,8BACtE,OAAO,aAAa,IAAI,EAAE;sFAGjC;EAGF;CACF;CAEA,MAAM,qBAAqB,QAAQ,SAAS,CAAC;CAC7C,MAAM,aAAa,WAAW,UAAU;CACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8BAA8B;AACjE;;;;;;;;;;;;;AAcA,eAAe,oBAAmC;CAChD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,CAAE,MAAM,gBAAgB,OAAO,GAAI;EACrC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,iBAAiB,EAAE;wJAIxE;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,OAAO;CAE1C,IAAI,QAAQ,SAAS,oBAAoB,KAAK,QAAQ,SAAS,4BAA4B,GAAG;EAC5F,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,qCAAqC;EAE3F;CACF;CAEA,MAAM,aAAa,SAAS,GAAG,QAAQ,QAAQ,EAAE,IAAI,kBAAkB;CACvE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,+CAA+C;AAClF;;AAGA,MAAM,iBAAiB;;;;;;;;;;;;;;;AAgBvB,eAAe,kBAAiC;CAC9C,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,0BAA0B,WAAmB;EACjD,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,GAAG,OAAO;IAEvE,eAAe,gGAExB;CACF;CAEA,IAAI,CAAE,MAAM,gBAAgB,YAAY,GAAI;EAC1C,uBAAuB,WAAW;EAElC;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,YAAY;CAI/C,IAAI,sBAAsB,KAAK,OAAO,GAAG;EACvC,QAAQ,IAAI,GAAG,OAAO,aAAa,eAAe,EAAE,iCAAiC;EAErF;CACF;CAEA,IAAI;CAEJ,IAAI,mBAAmB,KAAK,OAAO,GACjC,OAAO,QAAQ,QAAQ,oBAAoB,aAAa,eAAe,EAAE;MACpE,IAAI,6BAA6B,KAAK,OAAO,GAClD,OAAO,QAAQ,QACb,8BACA,6BAA6B,eAAe,SAC9C;MACK;EACL,uBAAuB,2CAA2C;EAElE;CACF;CAEA,MAAM,aAAa,cAAc,IAAI;CACrC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,eAAe,wBAAwB;AACnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,gBAAsB;CAC7B,QAAQ,IACN,KAAK,OAAO,MAAM,GAAG,EAAE,kFAChB,OAAO,aAAa,mCAAmC,EAAE;;IAGzD,OAAO,aAAa,0BAA0B,EAAE,OAAO,OAAO,aAAa,cAAc,EAAE;IAE3F,OAAO,aAAa,UAAU,EAAE;gCAEJ,OAAO,aAAa,gBAAgB,EAAE,WAAW,OAAO,aAAa,6BAA2B,EAAE;+DAGvI;AACF;;;;;;;;;AAUA,eAAe,2BAA2B,UAA6B;CACrE,MAAM,oBAAoB;CAC1B,MAAM,YAAY;CAClB,MAAM,kBAAkB;CACxB,MAAM,gBAAgB;CACtB,cAAc;AAChB;AAEA,MAAa,gBAAmC;CAC9C,aACE;CAIF,UAAU,CAAC,UAAU;CAIrB,cAAc;EAGZ,MAAM;EAGN,kBAAkB;EAMlB,4BAA4B;EAO5B,gBAAgB;CAClB;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"shadcn.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/shadcn.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport type { FeatureDefinition } from \"./types\";\n\n/**\n * `warlock add shadcn` installs the PREREQUISITES for shadcn/ui. It does not\n * wrap `shadcn add`, and it never will.\n *\n * shadcn/ui is a copy-in generator with its own CLI and its own registry, not a\n * dependency: components are written into your source tree and become yours the\n * moment they land. Wrapping their CLI would make their moving target our bug\n * reports, for a command that adds nothing but a rename.\n *\n * What is worth owning is the part their CLI gets wrong here. Measured against\n * this framework's layout on 2026-08-25, `shadcn add button card` exited 0 and\n * produced components that rendered COMPLETELY UNSTYLED. It writes components\n * that import `cn` and reference `bg-primary` / `ring-ring`, but only\n * `shadcn init` creates `lib/utils` and the theme tokens those names resolve\n * against. The class was in the generated CSS and the element carried it, and\n * the rule still evaluated to an empty `var()` — a silent failure with a zero\n * exit code, which is the worst kind to hand a user.\n *\n * So this feature ships the five things that make `npx shadcn add <component>`\n * work the first time:\n *\n * 1. `components.json` written for OUR layout (their defaults assume `src/app`\n * or a bare `components/`, neither of which is where pages live here).\n * 2. `src/web/lib/utils.ts` exporting `cn`.\n * 3. The design tokens appended to `src/web/app.css`.\n * 4. A `web/*` entry in tsconfig `paths`, so the generated imports typecheck.\n * 5. The packages `init` would have installed — see `printNextStep` below.\n * Skipping `init` is correct; skipping its dependency list was a bug, and\n * it cost a generated button that imports `cva` and cannot compile.\n *\n * After that, the user talks to shadcn directly, and their docs are true.\n */\n\n/**\n * The aliases shadcn's CLI rewrites every generated import against.\n *\n * These are the whole reason this feature exists. shadcn's defaults do not fit\n * `src/web`, and no user would guess this mapping: the alias keys are shadcn's\n * vocabulary, the values are tsconfig `paths` prefixes (hence `web/...`, not\n * `src/web/...`), and `tailwind.css` is a real path from the project root\n * (hence `src/web/app.css`, WITH the `src`). Getting one of them wrong produces\n * components in the wrong folder importing `cn` from somewhere that does not\n * exist.\n *\n * `tailwind.config` is deliberately empty: v4 is CSS-first and there is no\n * config file for it to point at. `rsc: false` because these are SSR React\n * pages rendered by the Warlock HTTP server, not React Server Components.\n */\nconst componentsJsonStub = `{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": false,\n \"tsx\": true,\n \"tailwind\": {\n \"config\": \"\",\n \"css\": \"src/web/app.css\",\n \"baseColor\": \"neutral\",\n \"cssVariables\": true,\n \"prefix\": \"\"\n },\n \"aliases\": {\n \"components\": \"web/components\",\n \"utils\": \"web/lib/utils\",\n \"ui\": \"web/components/ui\",\n \"lib\": \"web/lib\",\n \"hooks\": \"web/hooks\"\n },\n \"iconLibrary\": \"lucide\"\n}\n`;\n\n/**\n * `cn` — the one import every single shadcn component makes.\n *\n * `clsx` resolves the conditional/array/object class syntax, and `tailwind-merge`\n * then de-duplicates conflicting Tailwind utilities so a caller's `className`\n * actually beats the component's own default rather than depending on which one\n * happens to come later in the generated stylesheet. Both halves are required:\n * clsx alone leaves `px-2 px-4` in the attribute and the loser wins at random.\n */\nconst cnUtilStub = `import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Merge class names, with later Tailwind utilities winning over earlier ones.\n *\n * Every shadcn/ui component imports this. Keep the export name and signature.\n */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n`;\n\n/**\n * Marker for \"the tokens are already in this stylesheet\".\n *\n * A comment rather than a token name, because a user is free to retune every\n * value below and we must still recognise our own block on a re-run.\n */\nconst SHADCN_TOKENS_MARKER = \"shadcn/ui design tokens\";\n\n/**\n * The shadcn token set, as a plain `@theme` block with LITERAL values.\n *\n * Not `@theme inline` over `:root` custom properties, which is what shadcn's own\n * `init` writes. That form was verified against this stylesheet pipeline on\n * 2026-08-25 and it produces utilities that resolve to nothing: the class is in\n * the generated CSS, the class is on the element, and the rule still evaluates\n * to an empty `var()`. Literal values in `@theme` are what make `bg-primary`\n * emit a colour instead of a dangling reference.\n *\n * Dark mode still works, and works the way shadcn expects: `@theme` emits these\n * as real custom properties, so the `.dark` block re-declares the same\n * `--color-*` names and the cascade does the rest. `@custom-variant dark` is\n * what points Tailwind's `dark:` prefix at that class instead of the OS setting.\n *\n * Values are shadcn's `neutral` base. They are the starting point, not the\n * answer — this is the block to edit when the project gets a real palette.\n */\nconst shadcnTokensStub = `\n/* ${SHADCN_TOKENS_MARKER} — edit freely, this is your palette now.\n\n These are LITERAL values in a plain @theme block, on purpose. shadcn's own\n \\`init\\` writes \\`@theme inline\\` over :root custom properties; that form emits\n utilities which resolve to an empty var() here, so every component renders\n unstyled while the class sits right there on the element. Do not convert it.\n*/\n@custom-variant dark (&:is(.dark *));\n\n@theme {\n --color-background: oklch(1 0 0);\n --color-foreground: oklch(0.145 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.145 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.145 0 0);\n --color-primary: oklch(0.205 0 0);\n --color-primary-foreground: oklch(0.985 0 0);\n --color-secondary: oklch(0.97 0 0);\n --color-secondary-foreground: oklch(0.205 0 0);\n --color-muted: oklch(0.97 0 0);\n --color-muted-foreground: oklch(0.556 0 0);\n --color-accent: oklch(0.97 0 0);\n --color-accent-foreground: oklch(0.205 0 0);\n --color-destructive: oklch(0.577 0.245 27.325);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(0.922 0 0);\n --color-input: oklch(0.922 0 0);\n --color-ring: oklch(0.708 0 0);\n\n --color-chart-1: oklch(0.646 0.222 41.116);\n --color-chart-2: oklch(0.6 0.118 184.704);\n --color-chart-3: oklch(0.398 0.07 227.392);\n --color-chart-4: oklch(0.828 0.189 84.429);\n --color-chart-5: oklch(0.769 0.188 70.08);\n\n --color-sidebar: oklch(0.985 0 0);\n --color-sidebar-foreground: oklch(0.145 0 0);\n --color-sidebar-primary: oklch(0.205 0 0);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.97 0 0);\n --color-sidebar-accent-foreground: oklch(0.205 0 0);\n --color-sidebar-border: oklch(0.922 0 0);\n --color-sidebar-ring: oklch(0.708 0 0);\n\n /* shadcn components reach for rounded-lg/md/sm and expect them to track one\n radius. Changing --radius-lg here also retunes Tailwind's own rounded-lg,\n which is the intended trade: one radius scale per project, not two. */\n --radius-sm: 0.375rem;\n --radius-md: 0.5rem;\n --radius-lg: 0.625rem;\n --radius-xl: 1rem;\n}\n\n.dark {\n --color-background: oklch(0.145 0 0);\n --color-foreground: oklch(0.985 0 0);\n --color-card: oklch(0.205 0 0);\n --color-card-foreground: oklch(0.985 0 0);\n --color-popover: oklch(0.205 0 0);\n --color-popover-foreground: oklch(0.985 0 0);\n --color-primary: oklch(0.922 0 0);\n --color-primary-foreground: oklch(0.205 0 0);\n --color-secondary: oklch(0.269 0 0);\n --color-secondary-foreground: oklch(0.985 0 0);\n --color-muted: oklch(0.269 0 0);\n --color-muted-foreground: oklch(0.708 0 0);\n --color-accent: oklch(0.269 0 0);\n --color-accent-foreground: oklch(0.985 0 0);\n --color-destructive: oklch(0.704 0.191 22.216);\n --color-destructive-foreground: oklch(0.985 0 0);\n --color-border: oklch(1 0 0 / 10%);\n --color-input: oklch(1 0 0 / 15%);\n --color-ring: oklch(0.556 0 0);\n\n --color-chart-1: oklch(0.488 0.243 264.376);\n --color-chart-2: oklch(0.696 0.17 162.48);\n --color-chart-3: oklch(0.769 0.188 70.08);\n --color-chart-4: oklch(0.627 0.265 303.9);\n --color-chart-5: oklch(0.645 0.246 16.439);\n\n --color-sidebar: oklch(0.205 0 0);\n --color-sidebar-foreground: oklch(0.985 0 0);\n --color-sidebar-primary: oklch(0.488 0.243 264.376);\n --color-sidebar-primary-foreground: oklch(0.985 0 0);\n --color-sidebar-accent: oklch(0.269 0 0);\n --color-sidebar-accent-foreground: oklch(0.985 0 0);\n --color-sidebar-border: oklch(1 0 0 / 10%);\n --color-sidebar-ring: oklch(0.556 0 0);\n}\n`;\n\n/**\n * Write `components.json` — the sentinel for \"this feature already ran\".\n *\n * Nothing in the project template creates this file, so its presence means\n * `add shadcn` has been here and a human may since have retuned the aliases,\n * the style, or the base colour. It is never rewritten: shadcn's CLI reads this\n * file on every `add`, so overwriting it would silently relocate a project's\n * component folder out from under the components already in it.\n */\nasync function writeComponentsJson(): Promise<void> {\n const componentsJsonPath = rootPath(\"components.json\");\n\n if (await fileExistsAsync(componentsJsonPath)) {\n console.log(`${colors.yellowBright(\"components.json\")} already exists, skipping...`);\n\n return;\n }\n\n await putFileAsync(componentsJsonPath, componentsJsonStub);\n console.log(`${colors.green(\"✓\")} Created components.json`);\n}\n\n/**\n * Write `src/web/lib/utils.ts`.\n *\n * Guarded on its own rather than on the sentinel above, because `lib/utils` is\n * a name a project may well already own — and if it does, whatever is in there\n * is user code with other callers. We print instead of merging.\n */\nasync function writeCnUtil(): Promise<void> {\n const utilsFile = srcPath(\"web/lib/utils.ts\");\n\n if (await fileExistsAsync(utilsFile)) {\n const current = await getFileAsync(utilsFile).catch(() => \"\");\n\n if (/export\\s+(function|const)\\s+cn\\b/.test(current)) {\n console.log(`${colors.yellowBright(\"src/web/lib/utils.ts\")} already exports cn, skipping...`);\n } else {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/lib/utils.ts\")} exists but does not export ` +\n `${colors.yellowBright(\"cn\")} — add it yourself:\\n` +\n \" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }\\n\" +\n \" Every shadcn component imports it, and none of them will compile until it is there.\",\n );\n }\n\n return;\n }\n\n await ensureDirectoryAsync(srcPath(\"web/lib\"));\n await putFileAsync(utilsFile, cnUtilStub);\n console.log(`${colors.green(\"✓\")} Created src/web/lib/utils.ts`);\n}\n\n/**\n * Append the token block to `src/web/app.css`.\n *\n * The stylesheet is guaranteed to exist by the time this runs: `requires:\n * [\"tailwind\"]` makes the add command resolve `tailwind` first and run its\n * `onExecuting` ahead of this one, and that is what creates the file. The check\n * below is for a stylesheet a human has since moved or deleted — worth a\n * printed instruction, not a failure.\n *\n * Appended, never rewritten. Everything already in that file is either\n * Tailwind's own `@import` or the project's design system.\n */\nasync function appendThemeTokens(): Promise<void> {\n const cssFile = srcPath(\"web/app.css\");\n\n if (!(await fileExistsAsync(cssFile))) {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"src/web/app.css\")} not found — ` +\n \"append the shadcn token block to your Tailwind stylesheet yourself.\\n\" +\n \" Without the tokens, shadcn components render unstyled: the classes are emitted and applied, \" +\n \"but `bg-primary` and friends resolve to an empty var().\",\n );\n\n return;\n }\n\n const current = await getFileAsync(cssFile);\n\n if (current.includes(SHADCN_TOKENS_MARKER) || current.includes(\"--color-primary-foreground\")) {\n console.log(`${colors.yellowBright(\"src/web/app.css\")} already has the tokens, skipping...`);\n\n return;\n }\n\n await putFileAsync(cssFile, `${current.trimEnd()}\\n${shadcnTokensStub}`);\n console.log(`${colors.green(\"✓\")} Appended the shadcn tokens to src/web/app.css`);\n}\n\n/** The tsconfig `paths` entry the generated imports resolve through. */\nconst WEB_PATH_ALIAS = '\"web/*\": [\"./src/web/*\"]';\n\n/**\n * Add `web/*` to tsconfig `compilerOptions.paths`.\n *\n * The template declares only `app/*`, so every import shadcn generates against\n * the aliases above (`web/lib/utils`, `web/components/ui/button`) would fail to\n * typecheck the moment it lands. This is the first `add` feature to patch\n * `paths` rather than `include`.\n *\n * String surgery, and NOT the parse-and-write that the `include` patches use.\n * The project template's `tsconfig.json` carries `//` comments — it is JSONC,\n * and `JSON.parse` throws on it — so a parse-first patch would fail on exactly\n * the projects this feature is for. Editing the text also preserves those\n * comments, which are load-bearing documentation in that file.\n */\nasync function addWebPathAlias(): Promise<void> {\n const tsconfigPath = rootPath(\"tsconfig.json\");\n\n const printManualInstruction = (reason: string) => {\n console.log(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(\"tsconfig.json\")} ${reason} — ` +\n \"add this to `compilerOptions.paths` yourself:\\n\" +\n ` ${WEB_PATH_ALIAS}\\n` +\n \" Without it, every import shadcn generates against the `web/*` aliases fails to typecheck.\",\n );\n };\n\n if (!(await fileExistsAsync(tsconfigPath))) {\n printManualInstruction(\"not found\");\n\n return;\n }\n\n const current = await getFileAsync(tsconfigPath);\n\n // Matches the alias whichever quote style and spacing the file uses, so a\n // re-run against a hand-edited tsconfig does not stack a second entry.\n if (/[\"']web\\/\\*[\"']\\s*:/.test(current)) {\n console.log(`${colors.yellowBright(\"tsconfig.json\")} already maps web/*, skipping...`);\n\n return;\n }\n\n let next: string;\n\n if (/\"paths\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(/\"paths\"\\s*:\\s*\\{/, `$&\\n ${WEB_PATH_ALIAS},`);\n } else if (/\"compilerOptions\"\\s*:\\s*\\{/.test(current)) {\n next = current.replace(\n /\"compilerOptions\"\\s*:\\s*\\{/,\n `$&\\n \"paths\": {\\n ${WEB_PATH_ALIAS}\\n },`,\n );\n } else {\n printManualInstruction(\"has no recognisable compilerOptions block\");\n\n return;\n }\n\n await putFileAsync(tsconfigPath, next);\n console.log(`${colors.green(\"✓\")} Added ${WEB_PATH_ALIAS} to tsconfig.json paths`);\n}\n\n/**\n * Tell the user the next command is theirs to run.\n *\n * This is the seam. Everything above is prerequisite; from here the shadcn docs\n * apply verbatim, which is the entire point of not wrapping their CLI.\n *\n * WHY THIS FEATURE DECLARES cva AND lucide-react, and where the list came from.\n *\n * shadcn's registry splits dependencies across two levels. Each component item\n * (`.../new-york-v4/button.json`) declares only what that file pulls beyond the\n * baseline — for button, `radix-ui` and nothing else. Everything the baseline\n * assumes lives on the STYLE INDEX (`.../new-york-v4/index.json`), which is\n * fetched by `init` and only by `init`:\n *\n * dependencies: class-variance-authority, lucide-react, radix-ui\n * devDependencies: tw-animate-css, shadcn\n *\n * We skip `init` on purpose — it would rewrite components.json and replace the\n * literal tokens with the `@theme inline` block that resolves to nothing here.\n * Skipping it is right; inheriting nothing from it was the bug. `shadcn add\n * button` exits 0 and writes `import { cva } from \"class-variance-authority\"`\n * against a package no one installed. Zero exit code, TS2307, blank page.\n *\n * `radix-ui` stays off our list: it is the one style-index dependency that is\n * ALSO declared per-component, so `add` really does install it on demand, and\n * declaring it here would pull the whole primitive set into projects using two\n * components. `shadcn` itself stays off too — it is the CLI, and the user is\n * invoking it via `npx`.\n *\n * `tw-animate-css` was checked and deliberately EXCLUDED. It is not imported by\n * any component; it is a plain stylesheet whose only entry point is the\n * `@import \"tw-animate-css\"` line that `init` writes into the CSS — and we do\n * not write that line, so the package would install and never load. Nothing\n * fails to compile or render without it. What you lose is the enter/exit\n * animation on overlay components (dialog, dropdown, tooltip, sheet): their\n * `animate-in` / `fade-in-0` classes are simply never generated, so the overlay\n * appears instantly instead of fading. That is opt-in, and the note below is how\n * a user opts in — the `@import` has to go at the TOP of app.css, next to\n * Tailwind's own, which is why this feature cannot append it to the token block.\n */\nfunction printNextStep(): void {\n console.log(\n `\\n${colors.green(\"✓\")} shadcn/ui prerequisites are in place. Add components with shadcn's own CLI:\\n` +\n ` ${colors.yellowBright(\"npx shadcn@latest add button card\")}\\n` +\n \" Skip `shadcn init` — this feature did its job, and running it would rewrite components.json\\n\" +\n \" and replace the theme tokens with an `@theme inline` block that resolves to nothing here.\\n\" +\n ` Since shadcn's September 2026 change, ${colors.yellowBright(\"shadcn add\")} installs the ${colors.yellowBright(\"cn\")} package and its generated\\n` +\n ` components import ${colors.yellowBright(\"cn\")} from ${colors.yellowBright('\"cn\"')}, not from the src/web/lib/utils.ts written here.\\n` +\n \" That file stays as a working local `cn` for your own imports; new shadcn components no longer route through it.\\n\" +\n ` ${colors.yellowBright(\"class-variance-authority\")} and ${colors.yellowBright(\"lucide-react\")} are already installed: shadcn declares\\n` +\n \" them on the style index that only `init` reads, so `add` would never install them for you.\\n\" +\n ` ${colors.yellowBright(\"radix-ui\")} (the unified package, not @radix-ui/react-*) IS declared per component,\\n` +\n \" so shadcn's CLI installs that one itself as each component needs it.\\n\" +\n ` For overlay animations, add ${colors.yellowBright(\"tw-animate-css\")} and put ${colors.yellowBright('@import \"tw-animate-css\";')}\\n` +\n \" at the TOP of src/web/app.css, under the Tailwind import. Without it dialogs and dropdowns\\n\" +\n \" still work, they just appear instantly instead of animating.\",\n );\n}\n\n/**\n * Lay the ground shadcn's CLI expects to find, and nothing more.\n *\n * Four files, none of which needs `node_modules` to be populated: on the\n * `create-warlock` path this runs under `--no-install`, so the dependencies\n * declared below are only recorded in `package.json` and nothing here may\n * import, resolve, or execute shadcn, clsx, or Tailwind.\n */\nasync function completeShadcnInstallation(_options: CommandActionData) {\n await writeComponentsJson();\n await writeCnUtil();\n await appendThemeTokens();\n await addWebPathAlias();\n printNextStep();\n}\n\nexport const shadcnFeature: FeatureDefinition = {\n description:\n \"Sets up the prerequisites for shadcn/ui so `npx shadcn add <component>` works first time: components.json aliased to src/web, src/web/lib/utils.ts (cn), the design tokens in src/web/app.css, and a web/* tsconfig path. It does NOT wrap shadcn's CLI — components stay theirs to generate and yours to own.\",\n // `tailwind` owns src/web/app.css, which the token block is appended to.\n // Requiring it also fixes the order: the add command resolves requirements\n // depth-first, so the stylesheet exists before this feature writes into it.\n requires: [\"tailwind\"],\n // Everything `shadcn init` would have installed, minus the parts their CLI\n // genuinely does install per-component. See the note above `printNextStep`\n // for how this list was derived and what is deliberately NOT in it.\n dependencies: {\n // Both are runtime dependencies of `cn`, which every generated component\n // calls on every render — not build-time tooling.\n clsx: \"^2.1.1\",\n // v3 is the Tailwind v4 line; tailwind-merge v2 knows the v3 utility set and\n // silently fails to de-duplicate against v4 class names.\n \"tailwind-merge\": \"^3.3.1\",\n // `cva` is imported on line 2 of the generated button — and of every other\n // component with a `variant` prop. The registry declares it ONCE, on the\n // style index that only `init` applies, so `shadcn add button` resolves the\n // component's own deps, exits 0, and leaves TS2307 on a file it just wrote.\n // Still 0.x upstream, so this caret pins to 0.7.x; expect 0.7.1 to land.\n \"class-variance-authority\": \"^0.7.1\",\n // Same trap, one layer further in. `components.json` declares\n // `iconLibrary: \"lucide\"`, and dialog/select/checkbox/dropdown-menu all\n // import `lucide-react` in their source while declaring only `radix-ui` —\n // verified against the new-york-v4 registry items. So the CLI installs it\n // for nobody, and the first icon-bearing component fails to resolve.\n // Peer range covers React 19, which is what `web` brings.\n \"lucide-react\": \"^1.34.0\",\n },\n onExecuting: completeShadcnInstallation,\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgC3B,MAAM,aAAa;;;;;;;;;;;;;;;;;;AAmBnB,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;AAoB7B,MAAM,mBAAmB;KACpB,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqG1B,eAAe,sBAAqC;CAClD,MAAM,qBAAqB,SAAS,iBAAiB;CAErD,IAAI,MAAM,gBAAgB,kBAAkB,GAAG;EAC7C,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,6BAA6B;EAEnF;CACF;CAEA,MAAM,aAAa,oBAAoB,kBAAkB;CACzD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yBAAyB;AAC5D;;;;;;;;AASA,eAAe,cAA6B;CAC1C,MAAM,YAAY,QAAQ,kBAAkB;CAE5C,IAAI,MAAM,gBAAgB,SAAS,GAAG;EACpC,MAAM,UAAU,MAAM,aAAa,SAAS,CAAC,CAAC,YAAY,EAAE;EAE5D,IAAI,mCAAmC,KAAK,OAAO,GACjD,QAAQ,IAAI,GAAG,OAAO,aAAa,sBAAsB,EAAE,iCAAiC;OAE5F,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,sBAAsB,EAAE,8BACtE,OAAO,aAAa,IAAI,EAAE;sFAGjC;EAGF;CACF;CAEA,MAAM,qBAAqB,QAAQ,SAAS,CAAC;CAC7C,MAAM,aAAa,WAAW,UAAU;CACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8BAA8B;AACjE;;;;;;;;;;;;;AAcA,eAAe,oBAAmC;CAChD,MAAM,UAAU,QAAQ,aAAa;CAErC,IAAI,CAAE,MAAM,gBAAgB,OAAO,GAAI;EACrC,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,iBAAiB,EAAE;wJAIxE;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,OAAO;CAE1C,IAAI,QAAQ,SAAS,oBAAoB,KAAK,QAAQ,SAAS,4BAA4B,GAAG;EAC5F,QAAQ,IAAI,GAAG,OAAO,aAAa,iBAAiB,EAAE,qCAAqC;EAE3F;CACF;CAEA,MAAM,aAAa,SAAS,GAAG,QAAQ,QAAQ,EAAE,IAAI,kBAAkB;CACvE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,+CAA+C;AAClF;;AAGA,MAAM,iBAAiB;;;;;;;;;;;;;;;AAgBvB,eAAe,kBAAiC;CAC9C,MAAM,eAAe,SAAS,eAAe;CAE7C,MAAM,0BAA0B,WAAmB;EACjD,QAAQ,IACN,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,GAAG,OAAO;IAEvE,eAAe,gGAExB;CACF;CAEA,IAAI,CAAE,MAAM,gBAAgB,YAAY,GAAI;EAC1C,uBAAuB,WAAW;EAElC;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,YAAY;CAI/C,IAAI,sBAAsB,KAAK,OAAO,GAAG;EACvC,QAAQ,IAAI,GAAG,OAAO,aAAa,eAAe,EAAE,iCAAiC;EAErF;CACF;CAEA,IAAI;CAEJ,IAAI,mBAAmB,KAAK,OAAO,GACjC,OAAO,QAAQ,QAAQ,oBAAoB,aAAa,eAAe,EAAE;MACpE,IAAI,6BAA6B,KAAK,OAAO,GAClD,OAAO,QAAQ,QACb,8BACA,6BAA6B,eAAe,SAC9C;MACK;EACL,uBAAuB,2CAA2C;EAElE;CACF;CAEA,MAAM,aAAa,cAAc,IAAI;CACrC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,SAAS,eAAe,wBAAwB;AACnF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAAS,gBAAsB;CAC7B,QAAQ,IACN,KAAK,OAAO,MAAM,GAAG,EAAE,kFAChB,OAAO,aAAa,mCAAmC,EAAE;;0CAGnB,OAAO,aAAa,YAAY,EAAE,gBAAgB,OAAO,aAAa,IAAI,EAAE,kDAChG,OAAO,aAAa,IAAI,EAAE,QAAQ,OAAO,aAAa,QAAM,EAAE;IAEhF,OAAO,aAAa,0BAA0B,EAAE,OAAO,OAAO,aAAa,cAAc,EAAE;IAE3F,OAAO,aAAa,UAAU,EAAE;gCAEJ,OAAO,aAAa,gBAAgB,EAAE,WAAW,OAAO,aAAa,6BAA2B,EAAE;+DAGvI;AACF;;;;;;;;;AAUA,eAAe,2BAA2B,UAA6B;CACrE,MAAM,oBAAoB;CAC1B,MAAM,YAAY;CAClB,MAAM,kBAAkB;CACxB,MAAM,gBAAgB;CACtB,cAAc;AAChB;AAEA,MAAa,gBAAmC;CAC9C,aACE;CAIF,UAAU,CAAC,UAAU;CAIrB,cAAc;EAGZ,MAAM;EAGN,kBAAkB;EAMlB,4BAA4B;EAO5B,gBAAgB;CAClB;CACA,aAAa;AACf"}
@@ -648,9 +648,9 @@ const webContactControllerStub = `import { type Request, type RequestHandler } f
648
648
  import { type Infer, v } from "@warlock.js/seal";
649
649
 
650
650
  export const contactSchema = v.object({
651
- name: v.string().min(2).required(),
652
- email: v.email().required(),
653
- message: v.string().min(10).required(),
651
+ name: v.string().min(2),
652
+ email: v.email(),
653
+ message: v.string().min(10),
654
654
  });
655
655
 
656
656
  export type ContactSchema = Infer.Output<typeof contactSchema>;
@@ -743,9 +743,9 @@ export const route = { path: "/", name: "index" } as const;
743
743
  export const metadata = { title: "Home" };
744
744
 
745
745
  const contactSchema = v.object({
746
- name: v.string().min(2).required(),
747
- email: v.email().required(),
748
- message: v.string().min(10).required(),
746
+ name: v.string().min(2),
747
+ email: v.email(),
748
+ message: v.string().min(10),
749
749
  });
750
750
 
751
751
  function TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {
@@ -1 +1 @@
1
- {"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\r\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\r\n\r\n/**\r\n * Authorization configuration — read by @warlock.js/access on boot.\r\n *\r\n * The resolver is the one required piece: it tells the engine how to read a\r\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\r\n * from the user_roles table and maps them through the roles catalog table (so\r\n * roles + their permissions are managed at runtime, in the DB).\r\n *\r\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\r\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\r\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\r\n *\r\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\r\n * tenant from the request; checks then scope to it automatically.\r\n */\r\nconst access: AccessConfigurations = {\r\n resolver: new DatabaseAccessResolver(),\r\n\r\n // Cache resolved permission sets (default \"10m\").\r\n // cache: { ttl: \"10m\" },\r\n};\r\n\r\nexport default access;\r\n`;\r\n\r\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\r\n\r\n// >>> warlock:ai-packages (auto-managed) >>>\r\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\r\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\r\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\r\n// side-effect import below; keep them so the augmentation + runtime registration\r\n// load before the ai connector applies this config.\r\n// <<< warlock:ai-packages <<<\r\n\r\n/**\r\n * AI configuration — applied on boot by the ai connector, which calls\r\n * ai.config(...) with the object below. Cross-cutting defaults live here\r\n * (shared cache / snapshot stores, observability); per-call options always win.\r\n *\r\n * Wire a default model from a provider you installed, e.g.:\r\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\r\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\r\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\r\n */\r\nconst ai: Partial<AIConfig> = {\r\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\r\n // defaultStore: cache.driver(\"redis\", { client }),\r\n\r\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\r\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\r\n};\r\n\r\nexport default ai;\r\n`;\r\n\r\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the roles catalog — mirrors the migration columns\r\n * (snake_case). Each row is a role name plus the permission strings it grants;\r\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\r\n * assigned role names through this table to their effective permissions.\r\n */\r\nexport const roleSchema = v.object({\r\n name: v.string(),\r\n permissions: v.array(v.string()).default([]),\r\n});\r\n\r\nexport type RoleSchema = Infer<typeof roleSchema>;\r\n\r\n/**\r\n * The roles catalog — role name → the permissions it grants. Managed at runtime\r\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\r\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\r\n */\r\n@RegisterModel()\r\nexport class Role extends Model<RoleSchema> {\r\n public static table = \"roles\";\r\n\r\n public static schema = roleSchema;\r\n\r\n /** The permission strings this role grants. */\r\n public get permissions(): string[] {\r\n return this.get<string[]>(\"permissions\", []);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\r\n`;\r\n\r\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\r\nimport { Role } from \"../role.model\";\r\n\r\n/**\r\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\r\n * text array of the permission strings the role grants.\r\n */\r\nexport default Migration.create(Role, {\r\n name: text().notNullable().unique(),\r\n permissions: arrayText().nullable(),\r\n});\r\n`;\r\n\r\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for a role assignment — mirrors the migration columns\r\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\r\n */\r\nexport const userRoleSchema = v.object({\r\n user_id: v.string(),\r\n user_type: v.string(),\r\n role: v.string(),\r\n tenant: v.string().optional(),\r\n});\r\n\r\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\r\n\r\n/**\r\n * The role-assignment table — which roles a user holds, optionally per tenant.\r\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\r\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\r\n * never need to call \\`access.flush(user, tenant)\\` themselves.\r\n */\r\n@RegisterModel()\r\nexport class UserRole extends Model<UserRoleSchema> {\r\n public static table = \"user_roles\";\r\n\r\n public static schema = userRoleSchema;\r\n\r\n /**\r\n * Role names assigned to the user in the given tenant.\r\n *\r\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\r\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\r\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\r\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\r\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\r\n */\r\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\r\n const rows = await this.query()\r\n .where({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n tenant: tenant ?? null,\r\n })\r\n .get();\r\n\r\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\r\n // existence check) can't distort the resolved set.\r\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\r\n }\r\n\r\n /**\r\n * Assign a role to the user. No-op if the assignment already exists.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\r\n const existing = await this.first({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n if (existing) return;\r\n\r\n await this.create({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n\r\n /**\r\n * Remove a role assignment from the user.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\r\n await this.delete({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\r\n`;\r\n\r\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\r\nimport { UserRole } from \"../user-role.model\";\r\n\r\n/**\r\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\r\n * user ids are integers. The composite index powers the per-user (per-tenant)\r\n * lookup the resolver runs on every check.\r\n */\r\nexport default Migration.create(\r\n UserRole,\r\n {\r\n user_id: uuid().notNullable().index(),\r\n user_type: text().notNullable(),\r\n role: text().notNullable().index(),\r\n tenant: text().nullable().index(),\r\n },\r\n {\r\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\r\n },\r\n);\r\n`;\r\n\r\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Role } from \"app/access/models/role\";\r\nimport { UserRole } from \"app/access/models/user-role\";\r\n\r\n/**\r\n * The app's access adapter — connects @warlock.js/access to the ejected role\r\n * tables. Roles come from the user_roles assignment table; permissions are\r\n * expanded by mapping those role names through the roles catalog table. Both\r\n * are managed at runtime (in the DB), so admins can add roles + edit their\r\n * permissions without a deploy.\r\n *\r\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\r\n * resolver only fetches — keep it dumb, never cache inside it.\r\n */\r\nexport class DatabaseAccessResolver implements AccessResolver {\r\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\r\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\r\n return UserRole.rolesFor(user, tenant);\r\n }\r\n\r\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\r\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\r\n const names = await this.resolveRoles(user, tenant);\r\n\r\n if (names.length === 0) return [];\r\n\r\n const roles = await Role.query().whereIn(\"name\", names).get();\r\n\r\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\r\n return [...new Set(roles.flatMap((role) => role.permissions))];\r\n }\r\n\r\n /**\r\n * Optional. Resolve the ambient tenant when a check doesn't pass one\r\n * explicitly — derive it from the authenticated user (safer than reading\r\n * client request input, which a caller could spoof). Uncomment + adapt for a\r\n * multi-tenant app (single-tenant apps leave this off and return undefined).\r\n */\r\n // public resolveTenant(user: Auth): string | undefined {\r\n // return user.get(\"organization_id\");\r\n // }\r\n}\r\n`;\r\n\r\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\r\n\r\n/**\r\n * Socket.IO configuration — read by the framework's socket connector\r\n * on boot. When the HTTP server is running the socket server attaches\r\n * to it; otherwise it listens on its own configured port.\r\n *\r\n * Remove this file to disable the socket server entirely.\r\n */\r\nexport default {\r\n options: {\r\n cors: {\r\n origin: \"*\",\r\n },\r\n },\r\n} as SocketOptions;\r\n`;\r\n\r\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\r\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\r\n\r\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\r\n driver: \"rabbitmq\",\r\n name: \"default\",\r\n isDefault: true,\r\n\r\n // ============================================================================\r\n // Connection Settings\r\n // ============================================================================\r\n\r\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\r\n port: env(\"RABBITMQ_PORT\", 5672),\r\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\r\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\r\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\r\n\r\n // Or use connection URI (takes precedence over host/port)\r\n // uri: env(\"RABBITMQ_URL\"),\r\n\r\n // ============================================================================\r\n // Connection Options\r\n // ============================================================================\r\n\r\n /** Heartbeat interval in seconds */\r\n heartbeat: 60,\r\n\r\n /** Connection timeout in milliseconds */\r\n connectionTimeout: 10000,\r\n\r\n /** Enable automatic reconnection on disconnect */\r\n reconnect: true,\r\n\r\n /** Delay between reconnection attempts in milliseconds */\r\n reconnectDelay: 5_000,\r\n\r\n // ============================================================================\r\n // Consumer Options\r\n // ============================================================================\r\n\r\n /** Default prefetch count (number of unacknowledged messages per consumer) */\r\n prefetch: 10,\r\n\r\n // ============================================================================\r\n // Client Options (Native amqplib options)\r\n // ============================================================================\r\n // These options are passed directly to amqplib.connect()\r\n // for low-level configuration like frame size, TLS, socket options, etc.\r\n // ============================================================================\r\n clientOptions: {\r\n // Frame max size in bytes (0 = no limit)\r\n // frameMax: 0,\r\n\r\n // Channel max (0 = unlimited)\r\n // channelMax: 0,\r\n\r\n // Socket options\r\n socket: {\r\n // Enable TCP keep-alive\r\n keepAlive: true,\r\n\r\n // Disable Nagle's algorithm for lower latency\r\n noDelay: true,\r\n\r\n // Socket timeout (in addition to heartbeat)\r\n // timeout: 30000,\r\n },\r\n\r\n // TLS/SSL options (uncomment for secure connections)\r\n // socket: {\r\n // ca: fs.readFileSync('/path/to/ca.pem'),\r\n // cert: fs.readFileSync('/path/to/cert.pem'),\r\n // key: fs.readFileSync('/path/to/key.pem'),\r\n // rejectUnauthorized: true,\r\n // },\r\n },\r\n};\r\n\r\nexport default heraldConfigurations;\r\n`;\r\n\r\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"app/notifications/notification.model\";\r\n\r\n/**\r\n * Notifications configuration. Auto-loaded from src/config on boot — the\r\n * framework's notifications connector reads this default export and hands it to\r\n * setNotificationConfig, so this file stays declarative (no side-effect call).\r\n *\r\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\r\n * and defineNotification are type-checked against the registry.\r\n *\r\n * Channels enabled here:\r\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\r\n * The \"from\" address defaults to config/mail.ts; override per\r\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\r\n * - database in-app store backed by the Notification model. The \"inApp\"\r\n * facade exposes the recipient-scoped read API: listUnread,\r\n * countUnread, markAsRead, dismiss, ...\r\n *\r\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\r\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\r\n * queue line below.\r\n */\r\nconst config: NotificationConfig = {\r\n channels: {\r\n mail: mailChannel(),\r\n database: inApp.configure({ model: Notification }),\r\n },\r\n\r\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\r\n // queue: heraldQueue(),\r\n};\r\n\r\nexport default config;\r\n`;\r\n\r\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\r\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\r\nimport { v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the notifications table — mirrors the migration\r\n * columns (snake_case). Cascade validates + casts every write against it:\r\n * nullable columns use .nullish() (may be absent or null), and payload is\r\n * free-form JSON. Keep this in sync with the migration + columnMap when you\r\n * add or rename columns.\r\n */\r\nconst notificationSchema = v.object({\r\n user_id: v.string(),\r\n type: v.string(),\r\n title: v.string(),\r\n body: v.string().nullish(),\r\n payload: v.record(v.any()).nullish(),\r\n read_at: v.date().nullish(),\r\n idempotency_key: v.string().nullish(),\r\n});\r\n\r\n/**\r\n * In-app notification model.\r\n *\r\n * Extends the package's DatabaseNotification base, which provides the stable\r\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\r\n * from the columnMap below. The read/write API lives on the inApp facade\r\n * (configured in config/notifications.ts); you rarely touch this class directly.\r\n */\r\n@RegisterModel()\r\nexport class Notification extends DatabaseNotification {\r\n public static table = \"notifications\";\r\n public static schema = notificationSchema;\r\n\r\n /**\r\n * Maps the in-app store's roles to your columns. This default is\r\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\r\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\r\n * track a boolean read flag. The migration + accessors all follow this map.\r\n */\r\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\r\n}\r\n`;\r\n\r\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\r\nimport { notificationColumns } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"../notification.model\";\r\n\r\n/**\r\n * Notifications table.\r\n *\r\n * Columns come from notificationColumns(Notification) — the recipient / tenant\r\n * / read-state names follow the model's columnMap; type / title / body /\r\n * payload / idempotency_key are fixed. Spread it to add your own columns\r\n * (remember to mirror them in the model schema):\r\n *\r\n * import { uuid } from \"@warlock.js/cascade\";\r\n *\r\n * export default Migration.create(Notification, {\r\n * ...notificationColumns(Notification),\r\n * // category_id: uuid().index().nullable(),\r\n * });\r\n */\r\nexport default Migration.create(Notification, notificationColumns(Notification));\r\n`;\r\n\r\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\r\nimport { inApp, type Id } from \"@warlock.js/notifications\";\r\n\r\n/**\r\n * The authenticated user's notification HTTP surface — thin wrappers over the\r\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\r\n * rows). Notifications are produced by domain events, never over HTTP, so there\r\n * is no create. Trim or split these as your app grows.\r\n */\r\n\r\n/**\r\n * Read \\`id\\` off \\`request.user\\` without assuming this app's \\`RequestUser\\`\r\n * augmentation declares it — \\`RequestUser\\` is empty by default (see\r\n * \\`@warlock.js/core\\`'s \\`RequestUser\\` docs), so a narrow runtime read survives\r\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\r\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\r\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\r\n * \\`request.user\\` itself — also skips a needless \\`Notifiable\\` cast.\r\n */\r\nfunction recipientId(user: unknown): Id {\r\n if (user && typeof user === \"object\" && \"id\" in user) {\r\n const id = (user as { id?: unknown }).id;\r\n\r\n if (typeof id === \"string\" || typeof id === \"number\") return id;\r\n }\r\n\r\n throw new Error(\"Authenticated request is missing a usable user id\");\r\n}\r\n\r\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\r\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\r\n const { data, pagination } = await inApp.list(recipientId(request.user), request.all());\r\n\r\n return response.success({ notifications: data, pagination });\r\n};\r\n\r\nlistNotificationsController.description = \"List notifications\";\r\n\r\n/** GET /notifications/unread-count — drives the bell badge. */\r\nexport const unreadNotificationsCountController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.countUnread(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nunreadNotificationsCountController.description = \"Unread notifications count\";\r\n\r\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\r\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\r\n const id = request.input(\"id\");\r\n const userId = recipientId(request.user);\r\n\r\n await inApp.markAsRead(userId, id);\r\n const notification = await inApp.find(userId, id);\r\n\r\n return response.success({ notification });\r\n};\r\n\r\nmarkNotificationReadController.description = \"Mark notification read\";\r\n\r\n/** PATCH /notifications/read-all — mark every unread one read. */\r\nexport const markAllNotificationsReadController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.markAsRead(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\r\n\r\n/** DELETE /notifications — dismiss all for the user. */\r\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user));\r\n\r\n return response.noContent();\r\n};\r\n\r\nclearNotificationsController.description = \"Clear notifications\";\r\n\r\n/** DELETE /notifications/:id — dismiss one. */\r\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user), request.input(\"id\"));\r\n\r\n return response.noContent();\r\n};\r\n\r\ndeleteNotificationController.description = \"Delete notification\";\r\n`;\r\n\r\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\r\nimport { router } from \"@warlock.js/core\";\r\nimport {\r\n clearNotificationsController,\r\n deleteNotificationController,\r\n listNotificationsController,\r\n markAllNotificationsReadController,\r\n markNotificationReadController,\r\n unreadNotificationsCountController,\r\n} from \"./controllers/notifications.controller\";\r\n\r\n/**\r\n * Notification routes — the authenticated user's read + dismiss surface.\r\n *\r\n * Notifications are produced by domain events (never created over HTTP), so\r\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\r\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\r\n * don't need; if your app reads notifications over sockets/GraphQL instead,\r\n * delete this file + the controllers entirely.\r\n */\r\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\r\n router.get(\"/\", listNotificationsController);\r\n router.get(\"/unread-count\", unreadNotificationsCountController);\r\n router.patch(\"/read-all\", markAllNotificationsReadController);\r\n router.patch(\"/:id/read\", markNotificationReadController);\r\n router.delete(\"/\", clearNotificationsController);\r\n router.delete(\"/:id\", deleteNotificationController);\r\n});\r\n`;\r\n\r\n/**\r\n * `src/web/root.tsx` — the application root for the SSR page layer.\r\n *\r\n * Deliberately minimal. The framework ships a default root, so this exists to\r\n * give you a place to start rather than because anything requires it. The\r\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\r\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\r\n */\r\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\r\nimport { Head, Scripts } from \"@warlock.js/web\";\r\n\r\n/**\r\n * The application root.\r\n *\r\n * NOT async, and it receives no request/response: it renders on the server and\r\n * again in the browser during hydration, where neither exists.\r\n */\r\nexport default function App({ children }: AppProps) {\r\n return (\r\n <html lang=\"en\">\r\n <head>\r\n {/*\r\n Placement only. The framework injects the page's \\`metadata\\`, the\r\n stylesheet and preload tags for this route, and the canonical links\r\n into <head> by default — <Head /> just says WHERE they land.\r\n\r\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\r\n that emits one too produces two.\r\n */}\r\n <Head />\r\n <link rel=\"icon\" href=\"data:,\" />\r\n </head>\r\n <body>\r\n {/*\r\n REQUIRED — this is the hydration mount point, not a styling wrapper.\r\n\r\n The browser runtime looks up \\`#root\\` and hydrates that element only.\r\n Remove this div, or rename the id, and the page still renders from the\r\n server but never becomes interactive: the runtime throws in the console\r\n and nothing on screen changes.\r\n\r\n Wrap it in your own markup freely, and put anything that must live\r\n outside the hydrated tree (a static footer, a portal target) outside\r\n it — just keep an element with \\`id=\"root\"\\` around {children}.\r\n */}\r\n <div id=\"root\">{children}</div>\r\n {/*\r\n The hydration payload and module tags. Written explicitly because\r\n placement occasionally matters — a CSP nonce, or ordering against\r\n your own scripts.\r\n */}\r\n <Scripts />\r\n </body>\r\n </html>\r\n );\r\n}\r\n`;\r\n\r\n/**\r\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\r\n * for the Web starter's contact form. It intentionally has no persistence\r\n * dependency: replace the acknowledgement with a mail/job/database action.\r\n */\r\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nexport const contactSchema = v.object({\r\n name: v.string().min(2).required(),\r\n email: v.email().required(),\r\n message: v.string().min(10).required(),\r\n});\r\n\r\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\r\n\r\n/** POST /api/contact — validates the starter contact form. */\r\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\r\n request,\r\n response,\r\n}) => {\r\n const contact = request.validated();\r\n\r\n // Replace this with delivery/persistence for your app. Keeping the accepted\r\n // payload visible makes the endpoint useful while remaining side-effect free.\r\n return response.success({\r\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\r\n });\r\n};\r\n\r\ncontactController.validation = { schema: contactSchema };\r\n`;\r\n\r\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\r\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\r\nimport { contactController } from \"./controllers/contact.controller\";\r\n\r\nrouter.post(\"/api/contact\", contactController);\r\n`;\r\n\r\n/**\r\n * `src/web/index.register.ts` — universal static setup for the starter page.\r\n *\r\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\r\n * still sees it in both realms without making React Fast Refresh treat every\r\n * JSX edit as an incompatible function-export replacement.\r\n */\r\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\r\n\r\nexport function register() {\r\n extend(\"en\", {\r\n starter: {\r\n title: \"Your Warlock app is running.\",\r\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\r\n language: \"العربية\",\r\n contact: \"Send a message\",\r\n name: \"Name\",\r\n email: \"Email\",\r\n message: \"Message\",\r\n submit: \"Send message\",\r\n sent: \"Thanks — your message has been received.\",\r\n },\r\n });\r\n extend(\"ar\", {\r\n starter: {\r\n title: \"تطبيق Warlock يعمل الآن.\",\r\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\r\n language: \"English\",\r\n contact: \"أرسل رسالة\",\r\n name: \"الاسم\",\r\n email: \"البريد الإلكتروني\",\r\n message: \"الرسالة\",\r\n submit: \"إرسال الرسالة\",\r\n sent: \"شكرًا — تم استلام رسالتك.\",\r\n },\r\n });\r\n}\r\n`;\r\n\r\n/**\r\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\r\n * the moment this finishes.\r\n */\r\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\r\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\r\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\r\nimport { transX } from \"@mongez/react-localization\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { Link, type PageProps } from \"@warlock.js/web\";\r\nimport { useState } from \"react\";\r\n\r\nexport { register } from \"./index.register\";\r\n\r\n/**\r\n * A page route is an ordinary Warlock route whose handler renders React\r\n * instead of returning JSON.\r\n *\r\n * The URL and stable hydration name are the ones this file DECLARES below.\r\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\r\n * where the file lives. A page file with\r\n * no \\`route\\` export is REFUSED by both the dev server and the build.\r\n */\r\nexport const route = { path: \"/\", name: \"index\" } as const;\r\n\r\nexport const metadata = { title: \"Home\" };\r\n\r\nconst contactSchema = v.object({\r\n name: v.string().min(2).required(),\r\n email: v.email().required(),\r\n message: v.string().min(10).required(),\r\n});\r\n\r\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\r\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor={controlProps.name}>{label}</label>\r\n <input {...getInputProps()} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n\r\n/**\r\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\r\n * \\`data\\`, typed:\r\n *\r\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\r\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\r\n */\r\nexport default function HomePage(_props: PageProps) {\r\n // Live state. If the button below does nothing, the page rendered on the\r\n // server but never hydrated — the runtime never mounted at \\`#root\\`. This is\r\n // deliberately here so that failure is impossible to miss.\r\n const [count, setCount] = useState(0);\r\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\r\n const [submitted, setSubmitted] = useState(false);\r\n const [submitError, setSubmitError] = useState<string | null>(null);\r\n\r\n const toggleLocale = () => {\r\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\r\n setCurrentLocaleCode(nextLocale);\r\n setLocale(nextLocale);\r\n };\r\n\r\n return (\r\n <>\r\n {/*\r\n Self-contained, dependency-free styling: plain CSS, system fonts, and\r\n CSS custom properties, scoped to this page. No CSS framework, no utility\r\n classes, no external stylesheet — this page looks the same whether or\r\n not \\`warlock add tailwind\\` has ever been run.\r\n */}\r\n <style>{\\`\r\n .wk-home {\r\n --wk-fg: #0f172a;\r\n --wk-muted: #64748b;\r\n --wk-accent: #4f46e5;\r\n --wk-border: #e2e8f0;\r\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n color: var(--wk-fg);\r\n max-width: 42rem;\r\n margin: 4rem auto;\r\n padding: 0 1.5rem;\r\n line-height: 1.6;\r\n }\r\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\r\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\r\n .wk-home code {\r\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\r\n background: #f1f5f9;\r\n padding: 0.1rem 0.35rem;\r\n border-radius: 0.25rem;\r\n }\r\n .wk-check {\r\n border: 1px solid var(--wk-border);\r\n border-radius: 0.75rem;\r\n padding: 1.25rem 1.5rem;\r\n margin: 2rem 0;\r\n }\r\n .wk-check strong { display: block; font-size: 1.5rem; }\r\n .wk-check button {\r\n font: inherit;\r\n cursor: pointer;\r\n background: var(--wk-accent);\r\n color: #fff;\r\n border: 0;\r\n border-radius: 0.5rem;\r\n padding: 0.5rem 1rem;\r\n margin-top: 0.75rem;\r\n }\r\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\r\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\r\n .wk-links a:hover { text-decoration: underline; }\r\n .wk-language { margin-left: auto; }\r\n .wk-contact { margin-top: 2rem; }\r\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\r\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\r\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\r\n .wk-success { color: #047857; }\r\n \\`}</style>\r\n\r\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\r\n <nav className=\"wk-links\" aria-label=\"Starter links\">\r\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\r\n Docs\r\n </a>\r\n <Link href=\"/\" aria-current=\"page\">\r\n Home\r\n </Link>\r\n <button\r\n className=\"wk-language\"\r\n type=\"button\"\r\n aria-pressed={locale === \"ar\"}\r\n onClick={toggleLocale}\r\n >\r\n {transX(\"starter.language\")}\r\n </button>\r\n </nav>\r\n\r\n <h1>{transX(\"starter.title\")}</h1>\r\n <p>{transX(\"starter.introduction\")}</p>\r\n\r\n <section className=\"wk-check\">\r\n <label>If this number goes up when you click, React is hydrated:</label>\r\n <strong>{count}</strong>\r\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\r\n Count up\r\n </button>\r\n </section>\r\n\r\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\r\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\r\n <Form<typeof contactSchema>\r\n id=\"contact-form\"\r\n schema={contactSchema}\r\n onSubmit={async ({ form, values }) => {\r\n setSubmitted(false);\r\n setSubmitError(null);\r\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\r\n\r\n if (result.error) {\r\n if (result.error.isValidationError) {\r\n const body = result.error.body as {\r\n errors?: Array<{ input: string; error: string }>;\r\n message?: string;\r\n };\r\n form.setErrors(\r\n Object.fromEntries(\r\n (body.errors ?? []).map(({ input, error }) => [input, error]),\r\n ),\r\n );\r\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\r\n } else {\r\n setSubmitError(\"Your message could not be sent. Please try again.\");\r\n }\r\n return;\r\n }\r\n\r\n setSubmitted(true);\r\n form.reset();\r\n }}\r\n >\r\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\r\n <TextInput\r\n name=\"email\"\r\n label={transX(\"starter.email\")}\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n />\r\n <ContactMessage />\r\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\r\n {submitError && (\r\n <p className=\"wk-submit-error\" role=\"alert\">\r\n {submitError}\r\n </p>\r\n )}\r\n {submitted && (\r\n <p className=\"wk-success\" role=\"status\">\r\n {transX(\"starter.sent\")}\r\n </p>\r\n )}\r\n </Form>\r\n </section>\r\n </main>\r\n </>\r\n );\r\n}\r\n\r\nfunction ContactMessage() {\r\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\r\n <textarea {...getInputProps()} rows={5} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n`;\r\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
1
+ {"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\r\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\r\n\r\n/**\r\n * Authorization configuration — read by @warlock.js/access on boot.\r\n *\r\n * The resolver is the one required piece: it tells the engine how to read a\r\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\r\n * from the user_roles table and maps them through the roles catalog table (so\r\n * roles + their permissions are managed at runtime, in the DB).\r\n *\r\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\r\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\r\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\r\n *\r\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\r\n * tenant from the request; checks then scope to it automatically.\r\n */\r\nconst access: AccessConfigurations = {\r\n resolver: new DatabaseAccessResolver(),\r\n\r\n // Cache resolved permission sets (default \"10m\").\r\n // cache: { ttl: \"10m\" },\r\n};\r\n\r\nexport default access;\r\n`;\r\n\r\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\r\n\r\n// >>> warlock:ai-packages (auto-managed) >>>\r\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\r\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\r\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\r\n// side-effect import below; keep them so the augmentation + runtime registration\r\n// load before the ai connector applies this config.\r\n// <<< warlock:ai-packages <<<\r\n\r\n/**\r\n * AI configuration — applied on boot by the ai connector, which calls\r\n * ai.config(...) with the object below. Cross-cutting defaults live here\r\n * (shared cache / snapshot stores, observability); per-call options always win.\r\n *\r\n * Wire a default model from a provider you installed, e.g.:\r\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\r\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\r\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\r\n */\r\nconst ai: Partial<AIConfig> = {\r\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\r\n // defaultStore: cache.driver(\"redis\", { client }),\r\n\r\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\r\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\r\n};\r\n\r\nexport default ai;\r\n`;\r\n\r\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the roles catalog — mirrors the migration columns\r\n * (snake_case). Each row is a role name plus the permission strings it grants;\r\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\r\n * assigned role names through this table to their effective permissions.\r\n */\r\nexport const roleSchema = v.object({\r\n name: v.string(),\r\n permissions: v.array(v.string()).default([]),\r\n});\r\n\r\nexport type RoleSchema = Infer<typeof roleSchema>;\r\n\r\n/**\r\n * The roles catalog — role name → the permissions it grants. Managed at runtime\r\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\r\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\r\n */\r\n@RegisterModel()\r\nexport class Role extends Model<RoleSchema> {\r\n public static table = \"roles\";\r\n\r\n public static schema = roleSchema;\r\n\r\n /** The permission strings this role grants. */\r\n public get permissions(): string[] {\r\n return this.get<string[]>(\"permissions\", []);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\r\n`;\r\n\r\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\r\nimport { Role } from \"../role.model\";\r\n\r\n/**\r\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\r\n * text array of the permission strings the role grants.\r\n */\r\nexport default Migration.create(Role, {\r\n name: text().notNullable().unique(),\r\n permissions: arrayText().nullable(),\r\n});\r\n`;\r\n\r\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for a role assignment — mirrors the migration columns\r\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\r\n */\r\nexport const userRoleSchema = v.object({\r\n user_id: v.string(),\r\n user_type: v.string(),\r\n role: v.string(),\r\n tenant: v.string().optional(),\r\n});\r\n\r\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\r\n\r\n/**\r\n * The role-assignment table — which roles a user holds, optionally per tenant.\r\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\r\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\r\n * never need to call \\`access.flush(user, tenant)\\` themselves.\r\n */\r\n@RegisterModel()\r\nexport class UserRole extends Model<UserRoleSchema> {\r\n public static table = \"user_roles\";\r\n\r\n public static schema = userRoleSchema;\r\n\r\n /**\r\n * Role names assigned to the user in the given tenant.\r\n *\r\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\r\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\r\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\r\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\r\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\r\n */\r\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\r\n const rows = await this.query()\r\n .where({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n tenant: tenant ?? null,\r\n })\r\n .get();\r\n\r\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\r\n // existence check) can't distort the resolved set.\r\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\r\n }\r\n\r\n /**\r\n * Assign a role to the user. No-op if the assignment already exists.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\r\n const existing = await this.first({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n if (existing) return;\r\n\r\n await this.create({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n\r\n /**\r\n * Remove a role assignment from the user.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\r\n await this.delete({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\r\n`;\r\n\r\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\r\nimport { UserRole } from \"../user-role.model\";\r\n\r\n/**\r\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\r\n * user ids are integers. The composite index powers the per-user (per-tenant)\r\n * lookup the resolver runs on every check.\r\n */\r\nexport default Migration.create(\r\n UserRole,\r\n {\r\n user_id: uuid().notNullable().index(),\r\n user_type: text().notNullable(),\r\n role: text().notNullable().index(),\r\n tenant: text().nullable().index(),\r\n },\r\n {\r\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\r\n },\r\n);\r\n`;\r\n\r\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Role } from \"app/access/models/role\";\r\nimport { UserRole } from \"app/access/models/user-role\";\r\n\r\n/**\r\n * The app's access adapter — connects @warlock.js/access to the ejected role\r\n * tables. Roles come from the user_roles assignment table; permissions are\r\n * expanded by mapping those role names through the roles catalog table. Both\r\n * are managed at runtime (in the DB), so admins can add roles + edit their\r\n * permissions without a deploy.\r\n *\r\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\r\n * resolver only fetches — keep it dumb, never cache inside it.\r\n */\r\nexport class DatabaseAccessResolver implements AccessResolver {\r\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\r\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\r\n return UserRole.rolesFor(user, tenant);\r\n }\r\n\r\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\r\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\r\n const names = await this.resolveRoles(user, tenant);\r\n\r\n if (names.length === 0) return [];\r\n\r\n const roles = await Role.query().whereIn(\"name\", names).get();\r\n\r\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\r\n return [...new Set(roles.flatMap((role) => role.permissions))];\r\n }\r\n\r\n /**\r\n * Optional. Resolve the ambient tenant when a check doesn't pass one\r\n * explicitly — derive it from the authenticated user (safer than reading\r\n * client request input, which a caller could spoof). Uncomment + adapt for a\r\n * multi-tenant app (single-tenant apps leave this off and return undefined).\r\n */\r\n // public resolveTenant(user: Auth): string | undefined {\r\n // return user.get(\"organization_id\");\r\n // }\r\n}\r\n`;\r\n\r\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\r\n\r\n/**\r\n * Socket.IO configuration — read by the framework's socket connector\r\n * on boot. When the HTTP server is running the socket server attaches\r\n * to it; otherwise it listens on its own configured port.\r\n *\r\n * Remove this file to disable the socket server entirely.\r\n */\r\nexport default {\r\n options: {\r\n cors: {\r\n origin: \"*\",\r\n },\r\n },\r\n} as SocketOptions;\r\n`;\r\n\r\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\r\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\r\n\r\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\r\n driver: \"rabbitmq\",\r\n name: \"default\",\r\n isDefault: true,\r\n\r\n // ============================================================================\r\n // Connection Settings\r\n // ============================================================================\r\n\r\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\r\n port: env(\"RABBITMQ_PORT\", 5672),\r\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\r\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\r\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\r\n\r\n // Or use connection URI (takes precedence over host/port)\r\n // uri: env(\"RABBITMQ_URL\"),\r\n\r\n // ============================================================================\r\n // Connection Options\r\n // ============================================================================\r\n\r\n /** Heartbeat interval in seconds */\r\n heartbeat: 60,\r\n\r\n /** Connection timeout in milliseconds */\r\n connectionTimeout: 10000,\r\n\r\n /** Enable automatic reconnection on disconnect */\r\n reconnect: true,\r\n\r\n /** Delay between reconnection attempts in milliseconds */\r\n reconnectDelay: 5_000,\r\n\r\n // ============================================================================\r\n // Consumer Options\r\n // ============================================================================\r\n\r\n /** Default prefetch count (number of unacknowledged messages per consumer) */\r\n prefetch: 10,\r\n\r\n // ============================================================================\r\n // Client Options (Native amqplib options)\r\n // ============================================================================\r\n // These options are passed directly to amqplib.connect()\r\n // for low-level configuration like frame size, TLS, socket options, etc.\r\n // ============================================================================\r\n clientOptions: {\r\n // Frame max size in bytes (0 = no limit)\r\n // frameMax: 0,\r\n\r\n // Channel max (0 = unlimited)\r\n // channelMax: 0,\r\n\r\n // Socket options\r\n socket: {\r\n // Enable TCP keep-alive\r\n keepAlive: true,\r\n\r\n // Disable Nagle's algorithm for lower latency\r\n noDelay: true,\r\n\r\n // Socket timeout (in addition to heartbeat)\r\n // timeout: 30000,\r\n },\r\n\r\n // TLS/SSL options (uncomment for secure connections)\r\n // socket: {\r\n // ca: fs.readFileSync('/path/to/ca.pem'),\r\n // cert: fs.readFileSync('/path/to/cert.pem'),\r\n // key: fs.readFileSync('/path/to/key.pem'),\r\n // rejectUnauthorized: true,\r\n // },\r\n },\r\n};\r\n\r\nexport default heraldConfigurations;\r\n`;\r\n\r\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"app/notifications/notification.model\";\r\n\r\n/**\r\n * Notifications configuration. Auto-loaded from src/config on boot — the\r\n * framework's notifications connector reads this default export and hands it to\r\n * setNotificationConfig, so this file stays declarative (no side-effect call).\r\n *\r\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\r\n * and defineNotification are type-checked against the registry.\r\n *\r\n * Channels enabled here:\r\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\r\n * The \"from\" address defaults to config/mail.ts; override per\r\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\r\n * - database in-app store backed by the Notification model. The \"inApp\"\r\n * facade exposes the recipient-scoped read API: listUnread,\r\n * countUnread, markAsRead, dismiss, ...\r\n *\r\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\r\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\r\n * queue line below.\r\n */\r\nconst config: NotificationConfig = {\r\n channels: {\r\n mail: mailChannel(),\r\n database: inApp.configure({ model: Notification }),\r\n },\r\n\r\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\r\n // queue: heraldQueue(),\r\n};\r\n\r\nexport default config;\r\n`;\r\n\r\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\r\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\r\nimport { v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the notifications table — mirrors the migration\r\n * columns (snake_case). Cascade validates + casts every write against it:\r\n * nullable columns use .nullish() (may be absent or null), and payload is\r\n * free-form JSON. Keep this in sync with the migration + columnMap when you\r\n * add or rename columns.\r\n */\r\nconst notificationSchema = v.object({\r\n user_id: v.string(),\r\n type: v.string(),\r\n title: v.string(),\r\n body: v.string().nullish(),\r\n payload: v.record(v.any()).nullish(),\r\n read_at: v.date().nullish(),\r\n idempotency_key: v.string().nullish(),\r\n});\r\n\r\n/**\r\n * In-app notification model.\r\n *\r\n * Extends the package's DatabaseNotification base, which provides the stable\r\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\r\n * from the columnMap below. The read/write API lives on the inApp facade\r\n * (configured in config/notifications.ts); you rarely touch this class directly.\r\n */\r\n@RegisterModel()\r\nexport class Notification extends DatabaseNotification {\r\n public static table = \"notifications\";\r\n public static schema = notificationSchema;\r\n\r\n /**\r\n * Maps the in-app store's roles to your columns. This default is\r\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\r\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\r\n * track a boolean read flag. The migration + accessors all follow this map.\r\n */\r\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\r\n}\r\n`;\r\n\r\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\r\nimport { notificationColumns } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"../notification.model\";\r\n\r\n/**\r\n * Notifications table.\r\n *\r\n * Columns come from notificationColumns(Notification) — the recipient / tenant\r\n * / read-state names follow the model's columnMap; type / title / body /\r\n * payload / idempotency_key are fixed. Spread it to add your own columns\r\n * (remember to mirror them in the model schema):\r\n *\r\n * import { uuid } from \"@warlock.js/cascade\";\r\n *\r\n * export default Migration.create(Notification, {\r\n * ...notificationColumns(Notification),\r\n * // category_id: uuid().index().nullable(),\r\n * });\r\n */\r\nexport default Migration.create(Notification, notificationColumns(Notification));\r\n`;\r\n\r\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\r\nimport { inApp, type Id } from \"@warlock.js/notifications\";\r\n\r\n/**\r\n * The authenticated user's notification HTTP surface — thin wrappers over the\r\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\r\n * rows). Notifications are produced by domain events, never over HTTP, so there\r\n * is no create. Trim or split these as your app grows.\r\n */\r\n\r\n/**\r\n * Read \\`id\\` off \\`request.user\\` without assuming this app's \\`RequestUser\\`\r\n * augmentation declares it — \\`RequestUser\\` is empty by default (see\r\n * \\`@warlock.js/core\\`'s \\`RequestUser\\` docs), so a narrow runtime read survives\r\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\r\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\r\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\r\n * \\`request.user\\` itself — also skips a needless \\`Notifiable\\` cast.\r\n */\r\nfunction recipientId(user: unknown): Id {\r\n if (user && typeof user === \"object\" && \"id\" in user) {\r\n const id = (user as { id?: unknown }).id;\r\n\r\n if (typeof id === \"string\" || typeof id === \"number\") return id;\r\n }\r\n\r\n throw new Error(\"Authenticated request is missing a usable user id\");\r\n}\r\n\r\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\r\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\r\n const { data, pagination } = await inApp.list(recipientId(request.user), request.all());\r\n\r\n return response.success({ notifications: data, pagination });\r\n};\r\n\r\nlistNotificationsController.description = \"List notifications\";\r\n\r\n/** GET /notifications/unread-count — drives the bell badge. */\r\nexport const unreadNotificationsCountController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.countUnread(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nunreadNotificationsCountController.description = \"Unread notifications count\";\r\n\r\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\r\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\r\n const id = request.input(\"id\");\r\n const userId = recipientId(request.user);\r\n\r\n await inApp.markAsRead(userId, id);\r\n const notification = await inApp.find(userId, id);\r\n\r\n return response.success({ notification });\r\n};\r\n\r\nmarkNotificationReadController.description = \"Mark notification read\";\r\n\r\n/** PATCH /notifications/read-all — mark every unread one read. */\r\nexport const markAllNotificationsReadController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.markAsRead(recipientId(request.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\r\n\r\n/** DELETE /notifications — dismiss all for the user. */\r\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user));\r\n\r\n return response.noContent();\r\n};\r\n\r\nclearNotificationsController.description = \"Clear notifications\";\r\n\r\n/** DELETE /notifications/:id — dismiss one. */\r\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.user), request.input(\"id\"));\r\n\r\n return response.noContent();\r\n};\r\n\r\ndeleteNotificationController.description = \"Delete notification\";\r\n`;\r\n\r\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\r\nimport { router } from \"@warlock.js/core\";\r\nimport {\r\n clearNotificationsController,\r\n deleteNotificationController,\r\n listNotificationsController,\r\n markAllNotificationsReadController,\r\n markNotificationReadController,\r\n unreadNotificationsCountController,\r\n} from \"./controllers/notifications.controller\";\r\n\r\n/**\r\n * Notification routes — the authenticated user's read + dismiss surface.\r\n *\r\n * Notifications are produced by domain events (never created over HTTP), so\r\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\r\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\r\n * don't need; if your app reads notifications over sockets/GraphQL instead,\r\n * delete this file + the controllers entirely.\r\n */\r\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\r\n router.get(\"/\", listNotificationsController);\r\n router.get(\"/unread-count\", unreadNotificationsCountController);\r\n router.patch(\"/read-all\", markAllNotificationsReadController);\r\n router.patch(\"/:id/read\", markNotificationReadController);\r\n router.delete(\"/\", clearNotificationsController);\r\n router.delete(\"/:id\", deleteNotificationController);\r\n});\r\n`;\r\n\r\n/**\r\n * `src/web/root.tsx` — the application root for the SSR page layer.\r\n *\r\n * Deliberately minimal. The framework ships a default root, so this exists to\r\n * give you a place to start rather than because anything requires it. The\r\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\r\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\r\n */\r\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\r\nimport { Head, Scripts } from \"@warlock.js/web\";\r\n\r\n/**\r\n * The application root.\r\n *\r\n * NOT async, and it receives no request/response: it renders on the server and\r\n * again in the browser during hydration, where neither exists.\r\n */\r\nexport default function App({ children }: AppProps) {\r\n return (\r\n <html lang=\"en\">\r\n <head>\r\n {/*\r\n Placement only. The framework injects the page's \\`metadata\\`, the\r\n stylesheet and preload tags for this route, and the canonical links\r\n into <head> by default — <Head /> just says WHERE they land.\r\n\r\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\r\n that emits one too produces two.\r\n */}\r\n <Head />\r\n <link rel=\"icon\" href=\"data:,\" />\r\n </head>\r\n <body>\r\n {/*\r\n REQUIRED — this is the hydration mount point, not a styling wrapper.\r\n\r\n The browser runtime looks up \\`#root\\` and hydrates that element only.\r\n Remove this div, or rename the id, and the page still renders from the\r\n server but never becomes interactive: the runtime throws in the console\r\n and nothing on screen changes.\r\n\r\n Wrap it in your own markup freely, and put anything that must live\r\n outside the hydrated tree (a static footer, a portal target) outside\r\n it — just keep an element with \\`id=\"root\"\\` around {children}.\r\n */}\r\n <div id=\"root\">{children}</div>\r\n {/*\r\n The hydration payload and module tags. Written explicitly because\r\n placement occasionally matters — a CSP nonce, or ordering against\r\n your own scripts.\r\n */}\r\n <Scripts />\r\n </body>\r\n </html>\r\n );\r\n}\r\n`;\r\n\r\n/**\r\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\r\n * for the Web starter's contact form. It intentionally has no persistence\r\n * dependency: replace the acknowledgement with a mail/job/database action.\r\n */\r\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nexport const contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\r\n\r\n/** POST /api/contact — validates the starter contact form. */\r\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\r\n request,\r\n response,\r\n}) => {\r\n const contact = request.validated();\r\n\r\n // Replace this with delivery/persistence for your app. Keeping the accepted\r\n // payload visible makes the endpoint useful while remaining side-effect free.\r\n return response.success({\r\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\r\n });\r\n};\r\n\r\ncontactController.validation = { schema: contactSchema };\r\n`;\r\n\r\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\r\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\r\nimport { contactController } from \"./controllers/contact.controller\";\r\n\r\nrouter.post(\"/api/contact\", contactController);\r\n`;\r\n\r\n/**\r\n * `src/web/index.register.ts` — universal static setup for the starter page.\r\n *\r\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\r\n * still sees it in both realms without making React Fast Refresh treat every\r\n * JSX edit as an incompatible function-export replacement.\r\n */\r\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\r\n\r\nexport function register() {\r\n extend(\"en\", {\r\n starter: {\r\n title: \"Your Warlock app is running.\",\r\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\r\n language: \"العربية\",\r\n contact: \"Send a message\",\r\n name: \"Name\",\r\n email: \"Email\",\r\n message: \"Message\",\r\n submit: \"Send message\",\r\n sent: \"Thanks — your message has been received.\",\r\n },\r\n });\r\n extend(\"ar\", {\r\n starter: {\r\n title: \"تطبيق Warlock يعمل الآن.\",\r\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\r\n language: \"English\",\r\n contact: \"أرسل رسالة\",\r\n name: \"الاسم\",\r\n email: \"البريد الإلكتروني\",\r\n message: \"الرسالة\",\r\n submit: \"إرسال الرسالة\",\r\n sent: \"شكرًا — تم استلام رسالتك.\",\r\n },\r\n });\r\n}\r\n`;\r\n\r\n/**\r\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\r\n * the moment this finishes.\r\n */\r\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\r\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\r\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\r\nimport { transX } from \"@mongez/react-localization\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { Link, type PageProps } from \"@warlock.js/web\";\r\nimport { useState } from \"react\";\r\n\r\nexport { register } from \"./index.register\";\r\n\r\n/**\r\n * A page route is an ordinary Warlock route whose handler renders React\r\n * instead of returning JSON.\r\n *\r\n * The URL and stable hydration name are the ones this file DECLARES below.\r\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\r\n * where the file lives. A page file with\r\n * no \\`route\\` export is REFUSED by both the dev server and the build.\r\n */\r\nexport const route = { path: \"/\", name: \"index\" } as const;\r\n\r\nexport const metadata = { title: \"Home\" };\r\n\r\nconst contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\r\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor={controlProps.name}>{label}</label>\r\n <input {...getInputProps()} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n\r\n/**\r\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\r\n * \\`data\\`, typed:\r\n *\r\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\r\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\r\n */\r\nexport default function HomePage(_props: PageProps) {\r\n // Live state. If the button below does nothing, the page rendered on the\r\n // server but never hydrated — the runtime never mounted at \\`#root\\`. This is\r\n // deliberately here so that failure is impossible to miss.\r\n const [count, setCount] = useState(0);\r\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\r\n const [submitted, setSubmitted] = useState(false);\r\n const [submitError, setSubmitError] = useState<string | null>(null);\r\n\r\n const toggleLocale = () => {\r\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\r\n setCurrentLocaleCode(nextLocale);\r\n setLocale(nextLocale);\r\n };\r\n\r\n return (\r\n <>\r\n {/*\r\n Self-contained, dependency-free styling: plain CSS, system fonts, and\r\n CSS custom properties, scoped to this page. No CSS framework, no utility\r\n classes, no external stylesheet — this page looks the same whether or\r\n not \\`warlock add tailwind\\` has ever been run.\r\n */}\r\n <style>{\\`\r\n .wk-home {\r\n --wk-fg: #0f172a;\r\n --wk-muted: #64748b;\r\n --wk-accent: #4f46e5;\r\n --wk-border: #e2e8f0;\r\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n color: var(--wk-fg);\r\n max-width: 42rem;\r\n margin: 4rem auto;\r\n padding: 0 1.5rem;\r\n line-height: 1.6;\r\n }\r\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\r\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\r\n .wk-home code {\r\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\r\n background: #f1f5f9;\r\n padding: 0.1rem 0.35rem;\r\n border-radius: 0.25rem;\r\n }\r\n .wk-check {\r\n border: 1px solid var(--wk-border);\r\n border-radius: 0.75rem;\r\n padding: 1.25rem 1.5rem;\r\n margin: 2rem 0;\r\n }\r\n .wk-check strong { display: block; font-size: 1.5rem; }\r\n .wk-check button {\r\n font: inherit;\r\n cursor: pointer;\r\n background: var(--wk-accent);\r\n color: #fff;\r\n border: 0;\r\n border-radius: 0.5rem;\r\n padding: 0.5rem 1rem;\r\n margin-top: 0.75rem;\r\n }\r\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\r\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\r\n .wk-links a:hover { text-decoration: underline; }\r\n .wk-language { margin-left: auto; }\r\n .wk-contact { margin-top: 2rem; }\r\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\r\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\r\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\r\n .wk-success { color: #047857; }\r\n \\`}</style>\r\n\r\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\r\n <nav className=\"wk-links\" aria-label=\"Starter links\">\r\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\r\n Docs\r\n </a>\r\n <Link href=\"/\" aria-current=\"page\">\r\n Home\r\n </Link>\r\n <button\r\n className=\"wk-language\"\r\n type=\"button\"\r\n aria-pressed={locale === \"ar\"}\r\n onClick={toggleLocale}\r\n >\r\n {transX(\"starter.language\")}\r\n </button>\r\n </nav>\r\n\r\n <h1>{transX(\"starter.title\")}</h1>\r\n <p>{transX(\"starter.introduction\")}</p>\r\n\r\n <section className=\"wk-check\">\r\n <label>If this number goes up when you click, React is hydrated:</label>\r\n <strong>{count}</strong>\r\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\r\n Count up\r\n </button>\r\n </section>\r\n\r\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\r\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\r\n <Form<typeof contactSchema>\r\n id=\"contact-form\"\r\n schema={contactSchema}\r\n onSubmit={async ({ form, values }) => {\r\n setSubmitted(false);\r\n setSubmitError(null);\r\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\r\n\r\n if (result.error) {\r\n if (result.error.isValidationError) {\r\n const body = result.error.body as {\r\n errors?: Array<{ input: string; error: string }>;\r\n message?: string;\r\n };\r\n form.setErrors(\r\n Object.fromEntries(\r\n (body.errors ?? []).map(({ input, error }) => [input, error]),\r\n ),\r\n );\r\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\r\n } else {\r\n setSubmitError(\"Your message could not be sent. Please try again.\");\r\n }\r\n return;\r\n }\r\n\r\n setSubmitted(true);\r\n form.reset();\r\n }}\r\n >\r\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\r\n <TextInput\r\n name=\"email\"\r\n label={transX(\"starter.email\")}\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n />\r\n <ContactMessage />\r\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\r\n {submitError && (\r\n <p className=\"wk-submit-error\" role=\"alert\">\r\n {submitError}\r\n </p>\r\n )}\r\n {submitted && (\r\n <p className=\"wk-success\" role=\"status\">\r\n {transX(\"starter.sent\")}\r\n </p>\r\n )}\r\n </Form>\r\n </section>\r\n </main>\r\n </>\r\n );\r\n}\r\n\r\nfunction ContactMessage() {\r\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\r\n <textarea {...getInputProps()} rows={5} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n`;\r\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
@@ -1,4 +1,18 @@
1
1
  //#region ../core/src/http/boot-port-preflight.d.ts
2
+ /**
3
+ * Whether the HTTP-port preflight should run for a command's `connectors`
4
+ * preload.
5
+ *
6
+ * The bind-and-release probe is only meaningful when this boot will actually
7
+ * start the http connector: `true` (a full boot — http starts in its late
8
+ * phase) or an explicit list that names `"http"`. A scoped list WITHOUT http —
9
+ * e.g. `warlock seed`'s `["database", "cache", "logger"]`, or `migrate` — never
10
+ * binds the port, so probing it is pointless and actively harmful: the probe
11
+ * collides with a dev server already listening on that port (and inherits a
12
+ * stray `HTTP_PORT`), so a read-only data command fails on a port it was never
13
+ * going to use. Finding f9ace89e.
14
+ */
15
+ declare function shouldPreflightHttpPort(connectors: readonly string[] | true): boolean;
2
16
  /**
3
17
  * Probe the port declared in `src/config/http.ts`, if there is one.
4
18
  *
@@ -41,6 +55,16 @@ declare function assertConfiguredHttpPortIsFree(): Promise<void>;
41
55
  * would otherwise have booted. `HttpConnector` still runs the real check.
42
56
  */
43
57
  declare function preflightConfiguredHttpPort(): Promise<void>;
58
+ /**
59
+ * The remedy lines, chosen by where `http.port` actually came from.
60
+ *
61
+ * The provenance is the detector's own finding from env-load, carried here via
62
+ * {@link getRecordedEnvironmentOverride} rather than recomputed — so this asks
63
+ * "did an ambient HTTP_PORT beat .env, and is that the value now colliding?"
64
+ * and, only when the answer is yes, replaces the config-edit advice with the
65
+ * one that will actually free the port.
66
+ */
67
+ declare function remedyLines(collidingPort: number): string[];
44
68
  //#endregion
45
- export { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort };
69
+ export { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort, remedyLines, shouldPreflightHttpPort };
46
70
  //# sourceMappingURL=boot-port-preflight.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"boot-port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/boot-port-preflight.ts"],"mappings":";;AAiCA;;;;AAA+D;AA4D/D;;;;AAA4D;;;;iBA5DtC,8BAAA,IAAkC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4DzC,2BAAA,IAA+B,OAAO"}
1
+ {"version":3,"file":"boot-port-preflight.d.mts","names":[],"sources":["../../../../../../../core/src/http/boot-port-preflight.ts"],"mappings":";;AAyCA;;;;AAA4E;AAkB5E;;;;AAA+D;AA4D/D;;iBA9EgB,uBAAA,CAAwB,UAAoC;;AA8EhB;AAwD5D;;;;AAAiD;;;;;;;;iBApH3B,8BAAA,IAAkC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4DzC,2BAAA,IAA+B,OAAO;;;;;;;;;;iBAwD5C,WAAA,CAAY,aAAqB"}
@@ -1,9 +1,17 @@
1
+ import { getRecordedEnvironmentOverride } from "../utils/recorded-environment-overrides.mjs";
1
2
  import { PortInUseError, assertPortIsAvailable } from "./port-preflight.mjs";
2
3
  import { resolveBindPort } from "./resolve-bind-port.mjs";
3
4
  import config from "@mongez/config";
4
5
 
5
6
  //#region ../core/src/http/boot-port-preflight.ts
6
7
  /**
8
+ * The env var whose value `src/config/http.ts` reads for `http.port`
9
+ * (`port: env("HTTP_PORT", …)`). When an ambient value for it beats `.env`,
10
+ * `http.port` resolves to the ambient value and editing the config literal
11
+ * changes nothing — the whole point of the provenance branch below.
12
+ */
13
+ const HTTP_PORT_ENV_KEY = "HTTP_PORT";
14
+ /**
7
15
  * Mirrors `dev-server/supervisor.ts`'s `BOOT_PRECONDITION_EXIT_CODE`. Kept as
8
16
  * its own literal here (rather than imported) so this file — reached by both
9
17
  * the dev-preload path and the generated, standalone production entry — never
@@ -17,6 +25,22 @@ const BOOT_PRECONDITION_EXIT_CODE = 78;
17
25
  */
18
26
  const DEFAULT_BIND_HOST = "localhost";
19
27
  /**
28
+ * Whether the HTTP-port preflight should run for a command's `connectors`
29
+ * preload.
30
+ *
31
+ * The bind-and-release probe is only meaningful when this boot will actually
32
+ * start the http connector: `true` (a full boot — http starts in its late
33
+ * phase) or an explicit list that names `"http"`. A scoped list WITHOUT http —
34
+ * e.g. `warlock seed`'s `["database", "cache", "logger"]`, or `migrate` — never
35
+ * binds the port, so probing it is pointless and actively harmful: the probe
36
+ * collides with a dev server already listening on that port (and inherits a
37
+ * stray `HTTP_PORT`), so a read-only data command fails on a port it was never
38
+ * going to use. Finding f9ace89e.
39
+ */
40
+ function shouldPreflightHttpPort(connectors) {
41
+ return connectors === true || connectors.includes("http");
42
+ }
43
+ /**
20
44
  * Probe the port declared in `src/config/http.ts`, if there is one.
21
45
  *
22
46
  * Reads config rather than taking arguments because the only honest answer to
@@ -82,6 +106,13 @@ async function preflightConfiguredHttpPort() {
82
106
  * grep for, the port and host by name, and the command that names the process
83
107
  * holding it — the supervisor cannot discover the owning PID for them, but it
84
108
  * can hand them the one line that will.
109
+ *
110
+ * The remedy is chosen by PROVENANCE, never appended as a menu. When the
111
+ * colliding port came from an ambient `HTTP_PORT` that beat `.env` (the exact
112
+ * two-rebuild trap of finding 8782b840), "edit src/config/http.ts and rebuild"
113
+ * is actively wrong — the env var wins, so the rebuild changes nothing. In that
114
+ * case name the variable and give the remedy that works (unset it); only when
115
+ * the port genuinely came from config do we point at the config file.
85
116
  */
86
117
  function reportPortInUse(error) {
87
118
  const ownerCommand = process.platform === "win32" ? `netstat -ano | findstr :${error.port}` : `lsof -i :${error.port}`;
@@ -90,12 +121,30 @@ function reportPortInUse(error) {
90
121
  ` ✖ EADDRINUSE: port ${error.port} is already in use on ${error.host}`,
91
122
  ` the application cannot start because something else is already listening there.`,
92
123
  ` find the owning process: ${ownerCommand}`,
93
- ` then stop it, or change http.port in src/config/http.ts and rebuild.`,
124
+ ...remedyLines(error.port),
94
125
  ""
95
126
  ];
96
127
  for (const line of lines) console.error(line);
97
128
  }
129
+ /**
130
+ * The remedy lines, chosen by where `http.port` actually came from.
131
+ *
132
+ * The provenance is the detector's own finding from env-load, carried here via
133
+ * {@link getRecordedEnvironmentOverride} rather than recomputed — so this asks
134
+ * "did an ambient HTTP_PORT beat .env, and is that the value now colliding?"
135
+ * and, only when the answer is yes, replaces the config-edit advice with the
136
+ * one that will actually free the port.
137
+ */
138
+ function remedyLines(collidingPort) {
139
+ const httpPortOverride = getRecordedEnvironmentOverride(HTTP_PORT_ENV_KEY);
140
+ if (httpPortOverride !== void 0 && Number(String(httpPortOverride.effectiveValue).trim()) === collidingPort) return [
141
+ ` this port came from the ${HTTP_PORT_ENV_KEY} environment variable (=${httpPortOverride.effectiveValue}), which overrode .env's ${HTTP_PORT_ENV_KEY}=${httpPortOverride.fileValue}.`,
142
+ ` editing src/config/http.ts and rebuilding will NOT change it — the environment variable wins.`,
143
+ ` then stop the process above, or unset ${HTTP_PORT_ENV_KEY} and run again.`
144
+ ];
145
+ return [` then stop it, or change http.port in src/config/http.ts and rebuild.`];
146
+ }
98
147
 
99
148
  //#endregion
100
- export { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort };
149
+ export { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort, remedyLines, shouldPreflightHttpPort };
101
150
  //# sourceMappingURL=boot-port-preflight.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"boot-port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/boot-port-preflight.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { assertPortIsAvailable, PortInUseError } from \"./port-preflight\";\nimport { resolveBindPort } from \"./resolve-bind-port\";\n\n/**\n * Mirrors `dev-server/supervisor.ts`'s `BOOT_PRECONDITION_EXIT_CODE`. Kept as\n * its own literal here (rather than imported) so this file — reached by both\n * the dev-preload path and the generated, standalone production entry — never\n * pulls dev-server's supervisor module (child_process, TTY handling, ...)\n * into a production bundle that has no supervisor at all. EX_CONFIG.\n */\nconst BOOT_PRECONDITION_EXIT_CODE = 78;\n\n/**\n * Host used when `http.host` is unset — the same default `HttpConnector`\n * binds with, so the probe tests the address the server will actually take.\n */\nconst DEFAULT_BIND_HOST = \"localhost\";\n\n/**\n * Probe the port declared in `src/config/http.ts`, if there is one.\n *\n * Reads config rather than taking arguments because the only honest answer to\n * \"which port will this boot bind\" lives in the loaded config, and the callers\n * that need this check (the generated production entry) run before anything\n * that could hand it over.\n *\n * A no-op for an app with no `http` config, or an `http.port` that is not a\n * usable port number — those apps never bind, so there is nothing to preflight\n * and nothing to fail on.\n *\n * @throws {PortInUseError} when the configured port is already taken.\n */\nexport async function assertConfiguredHttpPortIsFree(): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n return;\n }\n\n // This runs BEFORE the connectors in the generated production entry, and\n // `preflightConfiguredHttpPort()` below only exits on `PortInUseError` —\n // any other throw is swallowed there. So a bad port must not throw HERE:\n // it stays a no-op, exactly like the previous hand-rolled coercion, and\n // `HttpConnector` (which now also calls `resolveBindPort`) is the one that\n // reports it, a moment later, with a channel a developer will actually see.\n let port: number;\n\n try {\n port = resolveBindPort(httpConfig.port);\n } catch {\n return;\n }\n\n // Port 0 asks the OS to pick a free one, so there is nothing to preflight:\n // probing 0 binds SOME unrelated ephemeral port and \"passes\" without proving\n // anything about the port this boot will end up on. The hand-rolled coercion\n // this replaced skipped 0 by rejecting `port <= 0`; `resolveBindPort` accepts\n // 0 as a legitimate TCP value, so the skip has to be stated here instead of\n // falling out of a range check.\n if (port === 0) {\n return;\n }\n\n await assertPortIsAvailable(port, httpConfig.host || DEFAULT_BIND_HOST);\n}\n\n/**\n * Fail the boot immediately, and by name, when the http port is already taken.\n *\n * Called from the generated production entry BEFORE the early-phase connectors,\n * because the port is the cheapest thing in the boot to check and the one most\n * likely to be wrong. `HttpConnector` is a LATE-phase connector, so its own\n * preflight is not reached until the database has connected, the cache has\n * connected and every module of app code has been imported — 7-13 seconds of\n * work, all of it discarded, before anyone learns the port was busy. This check\n * costs a bind-and-release on a socket that is never served.\n *\n * It does NOT replace `HttpConnector`'s preflight: a port can be taken in the\n * seconds between the two, and only the connector's check sits immediately\n * before the real `listen()`. This one exists to make the common case fast and\n * legible.\n *\n * Reports through `console.error` and NOT through `log.fatal`: the logger has\n * no channels yet at this point in the boot (the logger connector is part of\n * the early phase this runs ahead of), so a logged message here would reach\n * nobody. stderr always reaches the terminal, and `warlock start` forwards the\n * child's stderr verbatim.\n *\n * A probe that fails for any OTHER reason is deliberately swallowed: this is a\n * fast-fail convenience, and it must never be the thing that stops an app which\n * would otherwise have booted. `HttpConnector` still runs the real check.\n */\nexport async function preflightConfiguredHttpPort(): Promise<void> {\n try {\n await assertConfiguredHttpPortIsFree();\n } catch (error) {\n if (!(error instanceof PortInUseError)) {\n return;\n }\n\n reportPortInUse(error);\n\n process.exit(BOOT_PRECONDITION_EXIT_CODE);\n }\n}\n\n/**\n * Print the collision the way the developer needs to read it: the errno they\n * grep for, the port and host by name, and the command that names the process\n * holding it — the supervisor cannot discover the owning PID for them, but it\n * can hand them the one line that will.\n */\nfunction reportPortInUse(error: PortInUseError): void {\n const ownerCommand =\n process.platform === \"win32\"\n ? `netstat -ano | findstr :${error.port}`\n : `lsof -i :${error.port}`;\n\n const lines = [\n \"\",\n ` ✖ EADDRINUSE: port ${error.port} is already in use on ${error.host}`,\n ` the application cannot start because something else is already listening there.`,\n ` find the owning process: ${ownerCommand}`,\n ` then stop it, or change http.port in src/config/http.ts and rebuild.`,\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,8BAA8B;;;;;AAMpC,MAAM,oBAAoB;;;;;;;;;;;;;;;AAgB1B,eAAsB,iCAAgD;CACpE,MAAM,aAAa,OAAO,IAAI,MAAM;CAEpC,IAAI,CAAC,YACH;CASF,IAAI;CAEJ,IAAI;EACF,OAAO,gBAAgB,WAAW,IAAI;CACxC,QAAQ;EACN;CACF;CAQA,IAAI,SAAS,GACX;CAGF,MAAM,sBAAsB,MAAM,WAAW,QAAQ,iBAAiB;AACxE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,8BAA6C;CACjE,IAAI;EACF,MAAM,+BAA+B;CACvC,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,iBACrB;EAGF,gBAAgB,KAAK;EAErB,QAAQ,KAAK,2BAA2B;CAC1C;AACF;;;;;;;AAQA,SAAS,gBAAgB,OAA6B;CACpD,MAAM,eACJ,QAAQ,aAAa,UACjB,2BAA2B,MAAM,SACjC,YAAY,MAAM;CAExB,MAAM,QAAQ;EACZ;EACA,wBAAwB,MAAM,KAAK,wBAAwB,MAAM;EACjE;EACA,8BAA8B;EAC9B;EACA;CACF;CAEA,KAAK,MAAM,QAAQ,OACjB,QAAQ,MAAM,IAAI;AAEtB"}
1
+ {"version":3,"file":"boot-port-preflight.mjs","names":[],"sources":["../../../../../../../core/src/http/boot-port-preflight.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { getRecordedEnvironmentOverride } from \"../utils/recorded-environment-overrides\";\nimport { assertPortIsAvailable, PortInUseError } from \"./port-preflight\";\nimport { resolveBindPort } from \"./resolve-bind-port\";\n\n/**\n * The env var whose value `src/config/http.ts` reads for `http.port`\n * (`port: env(\"HTTP_PORT\", …)`). When an ambient value for it beats `.env`,\n * `http.port` resolves to the ambient value and editing the config literal\n * changes nothing — the whole point of the provenance branch below.\n */\nconst HTTP_PORT_ENV_KEY = \"HTTP_PORT\";\n\n/**\n * Mirrors `dev-server/supervisor.ts`'s `BOOT_PRECONDITION_EXIT_CODE`. Kept as\n * its own literal here (rather than imported) so this file — reached by both\n * the dev-preload path and the generated, standalone production entry — never\n * pulls dev-server's supervisor module (child_process, TTY handling, ...)\n * into a production bundle that has no supervisor at all. EX_CONFIG.\n */\nconst BOOT_PRECONDITION_EXIT_CODE = 78;\n\n/**\n * Host used when `http.host` is unset — the same default `HttpConnector`\n * binds with, so the probe tests the address the server will actually take.\n */\nconst DEFAULT_BIND_HOST = \"localhost\";\n\n/**\n * Whether the HTTP-port preflight should run for a command's `connectors`\n * preload.\n *\n * The bind-and-release probe is only meaningful when this boot will actually\n * start the http connector: `true` (a full boot — http starts in its late\n * phase) or an explicit list that names `\"http\"`. A scoped list WITHOUT http —\n * e.g. `warlock seed`'s `[\"database\", \"cache\", \"logger\"]`, or `migrate` — never\n * binds the port, so probing it is pointless and actively harmful: the probe\n * collides with a dev server already listening on that port (and inherits a\n * stray `HTTP_PORT`), so a read-only data command fails on a port it was never\n * going to use. Finding f9ace89e.\n */\nexport function shouldPreflightHttpPort(connectors: readonly string[] | true): boolean {\n return connectors === true || connectors.includes(\"http\");\n}\n\n/**\n * Probe the port declared in `src/config/http.ts`, if there is one.\n *\n * Reads config rather than taking arguments because the only honest answer to\n * \"which port will this boot bind\" lives in the loaded config, and the callers\n * that need this check (the generated production entry) run before anything\n * that could hand it over.\n *\n * A no-op for an app with no `http` config, or an `http.port` that is not a\n * usable port number — those apps never bind, so there is nothing to preflight\n * and nothing to fail on.\n *\n * @throws {PortInUseError} when the configured port is already taken.\n */\nexport async function assertConfiguredHttpPortIsFree(): Promise<void> {\n const httpConfig = config.get(\"http\");\n\n if (!httpConfig) {\n return;\n }\n\n // This runs BEFORE the connectors in the generated production entry, and\n // `preflightConfiguredHttpPort()` below only exits on `PortInUseError` —\n // any other throw is swallowed there. So a bad port must not throw HERE:\n // it stays a no-op, exactly like the previous hand-rolled coercion, and\n // `HttpConnector` (which now also calls `resolveBindPort`) is the one that\n // reports it, a moment later, with a channel a developer will actually see.\n let port: number;\n\n try {\n port = resolveBindPort(httpConfig.port);\n } catch {\n return;\n }\n\n // Port 0 asks the OS to pick a free one, so there is nothing to preflight:\n // probing 0 binds SOME unrelated ephemeral port and \"passes\" without proving\n // anything about the port this boot will end up on. The hand-rolled coercion\n // this replaced skipped 0 by rejecting `port <= 0`; `resolveBindPort` accepts\n // 0 as a legitimate TCP value, so the skip has to be stated here instead of\n // falling out of a range check.\n if (port === 0) {\n return;\n }\n\n await assertPortIsAvailable(port, httpConfig.host || DEFAULT_BIND_HOST);\n}\n\n/**\n * Fail the boot immediately, and by name, when the http port is already taken.\n *\n * Called from the generated production entry BEFORE the early-phase connectors,\n * because the port is the cheapest thing in the boot to check and the one most\n * likely to be wrong. `HttpConnector` is a LATE-phase connector, so its own\n * preflight is not reached until the database has connected, the cache has\n * connected and every module of app code has been imported — 7-13 seconds of\n * work, all of it discarded, before anyone learns the port was busy. This check\n * costs a bind-and-release on a socket that is never served.\n *\n * It does NOT replace `HttpConnector`'s preflight: a port can be taken in the\n * seconds between the two, and only the connector's check sits immediately\n * before the real `listen()`. This one exists to make the common case fast and\n * legible.\n *\n * Reports through `console.error` and NOT through `log.fatal`: the logger has\n * no channels yet at this point in the boot (the logger connector is part of\n * the early phase this runs ahead of), so a logged message here would reach\n * nobody. stderr always reaches the terminal, and `warlock start` forwards the\n * child's stderr verbatim.\n *\n * A probe that fails for any OTHER reason is deliberately swallowed: this is a\n * fast-fail convenience, and it must never be the thing that stops an app which\n * would otherwise have booted. `HttpConnector` still runs the real check.\n */\nexport async function preflightConfiguredHttpPort(): Promise<void> {\n try {\n await assertConfiguredHttpPortIsFree();\n } catch (error) {\n if (!(error instanceof PortInUseError)) {\n return;\n }\n\n reportPortInUse(error);\n\n process.exit(BOOT_PRECONDITION_EXIT_CODE);\n }\n}\n\n/**\n * Print the collision the way the developer needs to read it: the errno they\n * grep for, the port and host by name, and the command that names the process\n * holding it — the supervisor cannot discover the owning PID for them, but it\n * can hand them the one line that will.\n *\n * The remedy is chosen by PROVENANCE, never appended as a menu. When the\n * colliding port came from an ambient `HTTP_PORT` that beat `.env` (the exact\n * two-rebuild trap of finding 8782b840), \"edit src/config/http.ts and rebuild\"\n * is actively wrong — the env var wins, so the rebuild changes nothing. In that\n * case name the variable and give the remedy that works (unset it); only when\n * the port genuinely came from config do we point at the config file.\n */\nfunction reportPortInUse(error: PortInUseError): void {\n const ownerCommand =\n process.platform === \"win32\"\n ? `netstat -ano | findstr :${error.port}`\n : `lsof -i :${error.port}`;\n\n const lines = [\n \"\",\n ` ✖ EADDRINUSE: port ${error.port} is already in use on ${error.host}`,\n ` the application cannot start because something else is already listening there.`,\n ` find the owning process: ${ownerCommand}`,\n ...remedyLines(error.port),\n \"\",\n ];\n\n for (const line of lines) {\n console.error(line);\n }\n}\n\n/**\n * The remedy lines, chosen by where `http.port` actually came from.\n *\n * The provenance is the detector's own finding from env-load, carried here via\n * {@link getRecordedEnvironmentOverride} rather than recomputed — so this asks\n * \"did an ambient HTTP_PORT beat .env, and is that the value now colliding?\"\n * and, only when the answer is yes, replaces the config-edit advice with the\n * one that will actually free the port.\n */\nexport function remedyLines(collidingPort: number): string[] {\n const httpPortOverride = getRecordedEnvironmentOverride(HTTP_PORT_ENV_KEY);\n\n // Only claim the environment is the source when the ambient value is the one\n // that is actually colliding — an HTTP_PORT override of an UNRELATED port\n // must not misdirect the reader away from their real (config) port.\n const cameFromEnvironment =\n httpPortOverride !== undefined &&\n Number(String(httpPortOverride.effectiveValue).trim()) === collidingPort;\n\n if (cameFromEnvironment) {\n return [\n ` this port came from the ${HTTP_PORT_ENV_KEY} environment variable (=${httpPortOverride!.effectiveValue}), which overrode .env's ${HTTP_PORT_ENV_KEY}=${httpPortOverride!.fileValue}.`,\n ` editing src/config/http.ts and rebuilding will NOT change it — the environment variable wins.`,\n ` then stop the process above, or unset ${HTTP_PORT_ENV_KEY} and run again.`,\n ];\n }\n\n return [` then stop it, or change http.port in src/config/http.ts and rebuild.`];\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,oBAAoB;;;;;;;;AAS1B,MAAM,8BAA8B;;;;;AAMpC,MAAM,oBAAoB;;;;;;;;;;;;;;AAe1B,SAAgB,wBAAwB,YAA+C;CACrF,OAAO,eAAe,QAAQ,WAAW,SAAS,MAAM;AAC1D;;;;;;;;;;;;;;;AAgBA,eAAsB,iCAAgD;CACpE,MAAM,aAAa,OAAO,IAAI,MAAM;CAEpC,IAAI,CAAC,YACH;CASF,IAAI;CAEJ,IAAI;EACF,OAAO,gBAAgB,WAAW,IAAI;CACxC,QAAQ;EACN;CACF;CAQA,IAAI,SAAS,GACX;CAGF,MAAM,sBAAsB,MAAM,WAAW,QAAQ,iBAAiB;AACxE;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,eAAsB,8BAA6C;CACjE,IAAI;EACF,MAAM,+BAA+B;CACvC,SAAS,OAAO;EACd,IAAI,EAAE,iBAAiB,iBACrB;EAGF,gBAAgB,KAAK;EAErB,QAAQ,KAAK,2BAA2B;CAC1C;AACF;;;;;;;;;;;;;;AAeA,SAAS,gBAAgB,OAA6B;CACpD,MAAM,eACJ,QAAQ,aAAa,UACjB,2BAA2B,MAAM,SACjC,YAAY,MAAM;CAExB,MAAM,QAAQ;EACZ;EACA,wBAAwB,MAAM,KAAK,wBAAwB,MAAM;EACjE;EACA,8BAA8B;EAC9B,GAAG,YAAY,MAAM,IAAI;EACzB;CACF;CAEA,KAAK,MAAM,QAAQ,OACjB,QAAQ,MAAM,IAAI;AAEtB;;;;;;;;;;AAWA,SAAgB,YAAY,eAAiC;CAC3D,MAAM,mBAAmB,+BAA+B,iBAAiB;CASzE,IAHE,qBAAqB,UACrB,OAAO,OAAO,iBAAiB,cAAc,CAAC,CAAC,KAAK,CAAC,MAAM,eAG3D,OAAO;EACL,6BAA6B,kBAAkB,0BAA0B,iBAAkB,eAAe,2BAA2B,kBAAkB,GAAG,iBAAkB,UAAU;EACtL;EACA,2CAA2C,kBAAkB;CAC/D;CAGF,OAAO,CAAC,wEAAwE;AAClF"}
@@ -25,7 +25,7 @@ import { buildIdempotencyCacheKey, hashBody, isValidIdempotencyKey } from "./mid
25
25
  import { parseSize } from "./middleware/utils/parse-size.mjs";
26
26
  import { ClosableServer, FastifyInstance, closeServerWithTimeout, getHttpServer, startHttpServer } from "./server.mjs";
27
27
  import { registerHttpPlugins } from "./plugins.mjs";
28
- import { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort } from "./boot-port-preflight.mjs";
28
+ import { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort, remedyLines, shouldPreflightHttpPort } from "./boot-port-preflight.mjs";
29
29
  import { PortInUseError, assertPortIsAvailable, isPortAvailable } from "./port-preflight.mjs";
30
30
  import { HttpReadyReport, getHttpReadyReport, resetHttpReadyReport, setHttpReadyReport } from "./ready-report.mjs";
31
31
  import { RequestContextStore, requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
@@ -20,7 +20,7 @@ import { parseSize } from "./middleware/utils/parse-size.mjs";
20
20
  import { middleware } from "./middleware/middleware-list.mjs";
21
21
  import "./middleware/index.mjs";
22
22
  import { PortInUseError, assertPortIsAvailable, isPortAvailable } from "./port-preflight.mjs";
23
- import { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort } from "./boot-port-preflight.mjs";
23
+ import { assertConfiguredHttpPortIsFree, preflightConfiguredHttpPort, remedyLines, shouldPreflightHttpPort } from "./boot-port-preflight.mjs";
24
24
  import { getHttpReadyReport, resetHttpReadyReport, setHttpReadyReport } from "./ready-report.mjs";
25
25
  import { requestMemo } from "./context/request-memo.mjs";
26
26