@warlock.js/core 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an *Upgrading* section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 5.5.0 - 2026-09-07
10
+
11
+ ### Fixed
12
+
13
+ - `warlock add web` silently overwrote an existing `src/app/contact` module. It guarded `src/web/root.tsx` against clobbering a human's work but wrote the contact route and controller unconditionally, destroying them without a word. Each file is now guarded on its own existence, and a skip reports the consequence — that the contact form's `POST /api/contact` endpoint is missing and the form will 404 until you wire it.
14
+ - Documentation shipped in `skills/` told users to run `pnpm`-specific commands — including `pnpm warlock routes --json`, which cannot work under npm at all, since `pnpm <binary>` has no npm equivalent. Commands are now package-manager neutral.
15
+
9
16
  ## 5.4.0 - 2026-09-07
10
17
 
11
18
  ### Fixed
@@ -0,0 +1,45 @@
1
+ import { colors } from "@mongez/copper";
2
+
3
+ //#region ../core/src/generations/features/shared/resolve-contact-scaffold.ts
4
+ const CONTROLLER_PATH = "src/app/contact/controllers/contact.controller.ts";
5
+ const ROUTES_PATH = "src/app/contact/routes.ts";
6
+ /**
7
+ * Decide which of the two contact-module files are safe to (re)write, and
8
+ * report loudly about the ones that are not.
9
+ *
10
+ * Pure by design: no filesystem access here, so a test can exercise every
11
+ * combination directly. {@link completeWebInstallation} in `web.feature.ts`
12
+ * is the thin I/O wrapper — it stats both files, calls this, writes back
13
+ * whatever this says to write, and prints whatever this says to print.
14
+ *
15
+ * `src/web/root.tsx` is the "a human has been here" sentinel for the web
16
+ * layer as a whole, but `src/app/contact/**` is an ordinary application
17
+ * module the web feature happens to also scaffold — nothing stops a project
18
+ * from having its own `contact` module (with its own controller and routes)
19
+ * independently of ever having run `warlock add web`. Trusting root.tsx's
20
+ * absence as license to write these two files meant `warlock add web` could
21
+ * silently overwrite a human's existing `contact` module. Exactly like
22
+ * {@link relocateConflictingHomeRoute} already insists for the home route:
23
+ * "`warlock add web` runs against a project a human has been living in, and
24
+ * silently unlinking their code is not a thing an `add` command gets to do."
25
+ * So each file gets its own independent existence guard, and an existing
26
+ * file is skipped — never overwritten, never a reason to abort the rest of
27
+ * the install.
28
+ */
29
+ function resolveContactScaffold(state) {
30
+ const writeController = !state.controllerExists;
31
+ const writeRoutes = !state.routesExists;
32
+ const messages = [];
33
+ if (state.controllerExists) messages.push(`${colors.yellowBright("!")} ${colors.yellowBright(CONTROLLER_PATH)} already exists, skipping — the contact form's endpoint is missing this controller's logic unless it already handles ${colors.yellowBright("POST /api/contact")}, so the form will 404 until you wire it yourself.`);
34
+ if (state.routesExists) messages.push(`${colors.yellowBright("!")} ${colors.yellowBright(ROUTES_PATH)} already exists, skipping — the contact form's endpoint (${colors.yellowBright("POST /api/contact")}) was not registered here, so the form will 404 until you wire it yourself.`);
35
+ if (writeController && writeRoutes) messages.push(`${colors.green("✓")} Created POST /api/contact starter route`);
36
+ return {
37
+ writeController,
38
+ writeRoutes,
39
+ messages
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { resolveContactScaffold };
45
+ //# sourceMappingURL=resolve-contact-scaffold.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-contact-scaffold.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/resolve-contact-scaffold.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\n\n/** Whether each contact-module file `completeWebInstallation` writes already exists on disk. */\nexport type ContactScaffoldState = {\n controllerExists: boolean;\n routesExists: boolean;\n};\n\n/** Which contact-module files may be written, and what to tell the user. */\nexport type ContactScaffoldPlan = {\n writeController: boolean;\n writeRoutes: boolean;\n messages: string[];\n};\n\nconst CONTROLLER_PATH = \"src/app/contact/controllers/contact.controller.ts\";\nconst ROUTES_PATH = \"src/app/contact/routes.ts\";\n\n/**\n * Decide which of the two contact-module files are safe to (re)write, and\n * report loudly about the ones that are not.\n *\n * Pure by design: no filesystem access here, so a test can exercise every\n * combination directly. {@link completeWebInstallation} in `web.feature.ts`\n * is the thin I/O wrapper — it stats both files, calls this, writes back\n * whatever this says to write, and prints whatever this says to print.\n *\n * `src/web/root.tsx` is the \"a human has been here\" sentinel for the web\n * layer as a whole, but `src/app/contact/**` is an ordinary application\n * module the web feature happens to also scaffold — nothing stops a project\n * from having its own `contact` module (with its own controller and routes)\n * independently of ever having run `warlock add web`. Trusting root.tsx's\n * absence as license to write these two files meant `warlock add web` could\n * silently overwrite a human's existing `contact` module. Exactly like\n * {@link relocateConflictingHomeRoute} already insists for the home route:\n * \"`warlock add web` runs against a project a human has been living in, and\n * silently unlinking their code is not a thing an `add` command gets to do.\"\n * So each file gets its own independent existence guard, and an existing\n * file is skipped — never overwritten, never a reason to abort the rest of\n * the install.\n */\nexport function resolveContactScaffold(state: ContactScaffoldState): ContactScaffoldPlan {\n const writeController = !state.controllerExists;\n const writeRoutes = !state.routesExists;\n const messages: string[] = [];\n\n if (state.controllerExists) {\n messages.push(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(CONTROLLER_PATH)} already exists, skipping — ` +\n \"the contact form's endpoint is missing this controller's logic unless it already handles \" +\n `${colors.yellowBright(\"POST /api/contact\")}, so the form will 404 until you wire it yourself.`,\n );\n }\n\n if (state.routesExists) {\n messages.push(\n `${colors.yellowBright(\"!\")} ${colors.yellowBright(ROUTES_PATH)} already exists, skipping — ` +\n `the contact form's endpoint (${colors.yellowBright(\"POST /api/contact\")}) was not registered here, ` +\n \"so the form will 404 until you wire it yourself.\",\n );\n }\n\n if (writeController && writeRoutes) {\n messages.push(`${colors.green(\"✓\")} Created POST /api/contact starter route`);\n }\n\n return { writeController, writeRoutes, messages };\n}\n"],"mappings":";;;AAeA,MAAM,kBAAkB;AACxB,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;AAyBpB,SAAgB,uBAAuB,OAAkD;CACvF,MAAM,kBAAkB,CAAC,MAAM;CAC/B,MAAM,cAAc,CAAC,MAAM;CAC3B,MAAM,WAAqB,CAAC;CAE5B,IAAI,MAAM,kBACR,SAAS,KACP,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,eAAe,EAAE,uHAE/D,OAAO,aAAa,mBAAmB,EAAE,mDAChD;CAGF,IAAI,MAAM,cACR,SAAS,KACP,GAAG,OAAO,aAAa,GAAG,EAAE,GAAG,OAAO,aAAa,WAAW,EAAE,2DAC9B,OAAO,aAAa,mBAAmB,EAAE,4EAE7E;CAGF,IAAI,mBAAmB,aACrB,SAAS,KAAK,GAAG,OAAO,MAAM,GAAG,EAAE,yCAAyC;CAG9E,OAAO;EAAE;EAAiB;EAAa;CAAS;AAClD"}
@@ -3,6 +3,7 @@ import "../../utils/index.mjs";
3
3
  import { webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeRegisterStub, webRootStub } from "../stubs.mjs";
4
4
  import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
5
5
  import { relocateConflictingHomeRoute } from "./shared/relocate-conflicting-home-route.mjs";
6
+ import { resolveContactScaffold } from "./shared/resolve-contact-scaffold.mjs";
6
7
  import { colors } from "@mongez/copper";
7
8
  import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
8
9
 
@@ -70,11 +71,20 @@ async function completeWebInstallation(_options) {
70
71
  } else {
71
72
  await putFileAsync(srcPath("web/index.page.tsx"), webHomePageStub);
72
73
  await putFileAsync(srcPath("web/index.register.ts"), webHomeRegisterStub);
73
- await ensureDirectoryAsync(srcPath("app/contact/controllers"));
74
- await putFileAsync(srcPath("app/contact/controllers/contact.controller.ts"), webContactControllerStub);
75
- await putFileAsync(srcPath("app/contact/routes.ts"), webContactRoutesStub);
74
+ const contactPlan = resolveContactScaffold({
75
+ controllerExists: await fileExistsAsync(srcPath("app/contact/controllers/contact.controller.ts")),
76
+ routesExists: await fileExistsAsync(srcPath("app/contact/routes.ts"))
77
+ });
78
+ if (contactPlan.writeController) {
79
+ await ensureDirectoryAsync(srcPath("app/contact/controllers"));
80
+ await putFileAsync(srcPath("app/contact/controllers/contact.controller.ts"), webContactControllerStub);
81
+ }
82
+ if (contactPlan.writeRoutes) {
83
+ await ensureDirectoryAsync(srcPath("app/contact"));
84
+ await putFileAsync(srcPath("app/contact/routes.ts"), webContactRoutesStub);
85
+ }
76
86
  console.log(`${colors.green("✓")} Created src/web/index.page.tsx`);
77
- console.log(`${colors.green("✓")} Created POST /api/contact starter route`);
87
+ for (const message of contactPlan.messages) console.log(message);
78
88
  }
79
89
  }
80
90
  await registerWebConnector();
@@ -1 +1 @@
1
- {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb = collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n console.log(`${colors.green(\"✓\")} Created POST /api/contact starter route`);\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,eAAe,uBAAsC;CACnD,MAAM,aAAa,SAAS,mBAAmB;CAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,+JAG9C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,UAAU;CAE7C,IAAI,QAAQ,SAAS,cAAc,GAAG;EACpC,QAAQ,IAAI,GAAG,OAAO,aAAa,cAAc,EAAE,iCAAiC;EAEpF;CACF;CAEA,MAAM,aAAa;CACnB,IAAI,OAAO,QAAQ,SAAS,UAAU,IAAI,UAAU,GAAG,WAAW,IAAI;CAItE,IAAI,mBAAmB,KAAK,IAAI,GAC9B,OAAO,KAAK,QAAQ,oBAAoB,8BAA8B;MACjE,IAAI,KAAK,SAAS,gBAAgB,GACvC,OAAO,KAAK,QAAQ,kBAAkB,iDAAiD;MAClF;EACL,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,0FAE9C;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,IAAI;CACnC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8CAA8C;AACjF;;;;;;;AAQA,eAAe,wBAAwB,UAA6B;CAClE,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,MAAM,gBAAgB,QAAQ,GAChC,QAAQ,IAAI,GAAG,OAAO,aAAa,SAAS,EAAE,iCAAiC;MAC1E;EACL,MAAM,qBAAqB,QAAQ,KAAK,CAAC;EACzC,MAAM,aAAa,UAAU,WAAW;EACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,0BAA0B;EAE3D,MAAM,YAAY,MAAM,6BAA6B;EAErD,IAAI,UAAU,YAAY,aACxB,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,sBAAsB,OAAO,aAAa,WAAS,EAAE,YACrE,OAAO,aAAa,cAAY,EAAE,MAAM,OAAO,aAAa,OAAO,UAAU,cAAc,EAAE,sFAEpG;EAMF,IAAI,UAAU,YAAY,cAAc,UAAU,YAAY,UAAU;GACtE,MAAM,OAAO,UAAU,YAAY,WAAW,OAAO,UAAU,GAAG,IAAI,OAAO,aAAa,GAAG;GAE7F,QAAQ,IACN,GAAG,KAAK,0CAA0C,UAAU,OAAO,8BACrC,OAAO,aAAa,oBAAkB,EAAE;YAEvD,OAAO,aAAa,WAAS,EAAE,mKAGhD;GAUA,QAAQ,WAAW;EACrB,OAAO;GACL,MAAM,aAAa,QAAQ,oBAAoB,GAAG,eAAe;GACjE,MAAM,aAAa,QAAQ,uBAAuB,GAAG,mBAAmB;GACxE,MAAM,qBAAqB,QAAQ,yBAAyB,CAAC;GAC7D,MAAM,aACJ,QAAQ,+CAA+C,GACvD,wBACF;GACA,MAAM,aAAa,QAAQ,uBAAuB,GAAG,oBAAoB;GACzE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,gCAAgC;GACjE,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,yCAAyC;EAC5E;CACF;CAEA,MAAM,qBAAqB;AAC7B;AAEA,MAAa,aAAgC;CAC3C,aACE;CACF,cAAc;EACZ,mBAAmB;EACnB,gBAAgB;EAChB,sBAAsB;EACtB,8BAA8B;EAC9B,OAAO;EACP,aAAa;CACf;CACA,iBAAiB;EACf,gBAAgB;EAChB,oBAAoB;EAGpB,MAAM;EACN,wBAAwB;CAC1B;CACA,aAAa;AACf"}
1
+ {"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\r\nimport type { CommandActionData } from \"../../commands/types\";\r\nimport { rootPath, srcPath } from \"../../utils\";\r\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\r\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\r\nimport {\r\n webContactControllerStub,\r\n webContactRoutesStub,\r\n webHomePageStub,\r\n webHomeRegisterStub,\r\n webRootStub,\r\n} from \"../stubs\";\r\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\r\n\r\n/**\r\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\r\n *\r\n * It belongs to the config array or to app code, never both. Both halves are\r\n * registered before app code loads — the CLI preloader in dev, the generated\r\n * entry in production — so also calling `connectorsManager.register(...)` in\r\n * `src/app/main.ts` boots the connector twice and installs every page route\r\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\r\n * taken`, because pages and API routes share one route-name namespace.\r\n *\r\n * The config array is the half to prefer: `warlock build` reads the same array\r\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\r\n * cannot drift.\r\n *\r\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\r\n * app-owned file that may carry any formatting, and a parse-and-print would\r\n * reformat the parts we did not come to change.\r\n */\r\nasync function registerWebConnector(): Promise<void> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\r\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\r\n ` export default defineConfig({ connectors: [webConnector()] });`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(configPath);\r\n\r\n if (current.includes(\"webConnector\")) {\r\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\r\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\r\n\r\n // An existing `connectors: [` gains one entry; otherwise the key is added to\r\n // the object `defineConfig` receives.\r\n if (/connectors:\\s*\\[/.test(next)) {\r\n next = next.replace(/connectors:\\s*\\[/, \"connectors: [webConnector(),\");\r\n } else if (next.includes(\"defineConfig({\")) {\r\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\r\n } else {\r\n console.log(\r\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\r\n \"add `connectors: [webConnector()]` yourself.\",\r\n );\r\n\r\n return;\r\n }\r\n\r\n await putFileAsync(configPath, next);\r\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\r\n}\r\n\r\n/**\r\n * Scaffold the smallest page layer that renders, and register the connector.\r\n *\r\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\r\n * ships a default root, so its presence means a human has been here.\r\n */\r\nasync function completeWebInstallation(_options: CommandActionData) {\r\n const rootFile = srcPath(\"web/root.tsx\");\r\n\r\n if (await fileExistsAsync(rootFile)) {\r\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\r\n } else {\r\n await ensureDirectoryAsync(srcPath(\"web\"));\r\n await putFileAsync(rootFile, webRootStub);\r\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\r\n\r\n const collision = await relocateConflictingHomeRoute();\r\n\r\n if (collision.outcome === \"relocated\") {\r\n console.log(\r\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\r\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\r\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\r\n );\r\n }\r\n\r\n // The page is written ONLY when `/` is provably free. Writing it while\r\n // another handler holds `/` produces a homepage that 500s on first request,\r\n // which is precisely the outcome a scaffolder must never hand back.\r\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\r\n const verb = collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\r\n\r\n console.log(\r\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\r\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\r\n \"path is a 500 at request time, not a startup error.\\n\" +\r\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\r\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\r\n \"than `/` works too.\",\r\n );\r\n\r\n // Non-zero on BOTH branches. The page layer this command exists to\r\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\r\n // worked\" signal that put `/` in this state to begin with — a conflict we\r\n // declined to guess at is still an incomplete install, not a success.\r\n //\r\n // `exitCode` rather than `exit(1)`: the connector below still has to be\r\n // registered, and any other feature in the same `warlock add` invocation\r\n // still has to install, or the project is left half-wired on top of this.\r\n process.exitCode = 1;\r\n } else {\r\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\r\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\r\n\r\n // Unlike root.tsx above, the contact controller and its routes file are\r\n // ordinary application files a project can already have — independently\r\n // of ever having run `warlock add web`. Each is guarded on its OWN\r\n // existence, not on the (already-consumed) root.tsx sentinel, so an\r\n // existing `contact` module is skipped rather than clobbered. See\r\n // resolveContactScaffold's doc comment for why.\r\n const contactPlan = resolveContactScaffold({\r\n controllerExists: await fileExistsAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n ),\r\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\r\n });\r\n\r\n if (contactPlan.writeController) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\r\n webContactControllerStub,\r\n );\r\n }\r\n\r\n if (contactPlan.writeRoutes) {\r\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\r\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\r\n }\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\r\n\r\n for (const message of contactPlan.messages) {\r\n console.log(message);\r\n }\r\n }\r\n }\r\n\r\n await registerWebConnector();\r\n}\r\n\r\nexport const webFeature: FeatureDefinition = {\r\n description:\r\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\r\n dependencies: {\r\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\r\n \"@mongez/http\": \"^3.5.0\",\r\n \"@mongez/react-form\": \"^4.0.0\",\r\n \"@mongez/react-localization\": \"^3.4.7\",\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n // Loaded through `await import()` by the dev server only, so both are\r\n // optional peers of `web` rather than hard dependencies.\r\n vite: \"^7.3.5\",\r\n \"@vitejs/plugin-react\": \"^5.2.0\",\r\n },\r\n onExecuting: completeWebInstallation,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,eAAe,uBAAsC;CACnD,MAAM,aAAa,SAAS,mBAAmB;CAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,+JAG9C;EAEA;CACF;CAEA,MAAM,UAAU,MAAM,aAAa,UAAU;CAE7C,IAAI,QAAQ,SAAS,cAAc,GAAG;EACpC,QAAQ,IAAI,GAAG,OAAO,aAAa,cAAc,EAAE,iCAAiC;EAEpF;CACF;CAEA,MAAM,aAAa;CACnB,IAAI,OAAO,QAAQ,SAAS,UAAU,IAAI,UAAU,GAAG,WAAW,IAAI;CAItE,IAAI,mBAAmB,KAAK,IAAI,GAC9B,OAAO,KAAK,QAAQ,oBAAoB,8BAA8B;MACjE,IAAI,KAAK,SAAS,gBAAgB,GACvC,OAAO,KAAK,QAAQ,kBAAkB,iDAAiD;MAClF;EACL,QAAQ,IACN,GAAG,OAAO,aAAa,mBAAmB,EAAE,0FAE9C;EAEA;CACF;CAEA,MAAM,aAAa,YAAY,IAAI;CACnC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,8CAA8C;AACjF;;;;;;;AAQA,eAAe,wBAAwB,UAA6B;CAClE,MAAM,WAAW,QAAQ,cAAc;CAEvC,IAAI,MAAM,gBAAgB,QAAQ,GAChC,QAAQ,IAAI,GAAG,OAAO,aAAa,SAAS,EAAE,iCAAiC;MAC1E;EACL,MAAM,qBAAqB,QAAQ,KAAK,CAAC;EACzC,MAAM,aAAa,UAAU,WAAW;EACxC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,0BAA0B;EAE3D,MAAM,YAAY,MAAM,6BAA6B;EAErD,IAAI,UAAU,YAAY,aACxB,QAAQ,IACN,GAAG,OAAO,MAAM,GAAG,EAAE,sBAAsB,OAAO,aAAa,WAAS,EAAE,YACrE,OAAO,aAAa,cAAY,EAAE,MAAM,OAAO,aAAa,OAAO,UAAU,cAAc,EAAE,sFAEpG;EAMF,IAAI,UAAU,YAAY,cAAc,UAAU,YAAY,UAAU;GACtE,MAAM,OAAO,UAAU,YAAY,WAAW,OAAO,UAAU,GAAG,IAAI,OAAO,aAAa,GAAG;GAE7F,QAAQ,IACN,GAAG,KAAK,0CAA0C,UAAU,OAAO,8BACrC,OAAO,aAAa,oBAAkB,EAAE;YAEvD,OAAO,aAAa,WAAS,EAAE,mKAGhD;GAUA,QAAQ,WAAW;EACrB,OAAO;GACL,MAAM,aAAa,QAAQ,oBAAoB,GAAG,eAAe;GACjE,MAAM,aAAa,QAAQ,uBAAuB,GAAG,mBAAmB;GAQxE,MAAM,cAAc,uBAAuB;IACzC,kBAAkB,MAAM,gBACtB,QAAQ,+CAA+C,CACzD;IACA,cAAc,MAAM,gBAAgB,QAAQ,uBAAuB,CAAC;GACtE,CAAC;GAED,IAAI,YAAY,iBAAiB;IAC/B,MAAM,qBAAqB,QAAQ,yBAAyB,CAAC;IAC7D,MAAM,aACJ,QAAQ,+CAA+C,GACvD,wBACF;GACF;GAEA,IAAI,YAAY,aAAa;IAC3B,MAAM,qBAAqB,QAAQ,aAAa,CAAC;IACjD,MAAM,aAAa,QAAQ,uBAAuB,GAAG,oBAAoB;GAC3E;GAEA,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,gCAAgC;GAEjE,KAAK,MAAM,WAAW,YAAY,UAChC,QAAQ,IAAI,OAAO;EAEvB;CACF;CAEA,MAAM,qBAAqB;AAC7B;AAEA,MAAa,aAAgC;CAC3C,aACE;CACF,cAAc;EACZ,mBAAmB;EACnB,gBAAgB;EAChB,sBAAsB;EACtB,8BAA8B;EAC9B,OAAO;EACP,aAAa;CACf;CACA,iBAAiB;EACf,gBAAgB;EAChB,oBAAoB;EAGpB,MAAM;EACN,wBAAwB;CAC1B;CACA,aAAa;AACf"}
package/llms-full.txt CHANGED
@@ -1235,7 +1235,7 @@ That's the full contract. The `RequestHandler` annotation carries both parameter
1235
1235
  - File: `src/app/<module>/controllers/<action>.controller.ts`.
1236
1236
  - Export name matches the action in camelCase + `Controller` suffix: `listProductsController`, `createProductController`, `getProductController`.
1237
1237
 
1238
- Scaffold with: `pnpm warlock generate.controller <module>/<action>` (add `--with-validation` to get the schema generated alongside).
1238
+ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-validation` to get the schema generated alongside).
1239
1239
 
1240
1240
  ## Reading input
1241
1241
 
@@ -1437,7 +1437,7 @@ No `if (!product)` branch in the controller. The error class carries the HTTP se
1437
1437
 
1438
1438
  ---
1439
1439
  name: create-module
1440
- description: 'Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `pnpm warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.'
1440
+ description: 'Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.'
1441
1441
  ---
1442
1442
 
1443
1443
  # Warlock — create a module
@@ -1447,8 +1447,8 @@ A module is a self-contained feature folder under `src/app/<name>/`. The CLI sca
1447
1447
  ## The shape
1448
1448
 
1449
1449
  ```bash
1450
- pnpm warlock generate.module products # full CRUD bootstrap (default — controllers, model, services, repository, resource, schemas, routes, seed)
1451
- pnpm warlock generate.module products --minimal # bare bones (routes.ts + main.ts + utils/locales.ts + empty subfolders)
1450
+ npx warlock generate.module products # full CRUD bootstrap (default — controllers, model, services, repository, resource, schemas, routes, seed)
1451
+ npx warlock generate.module products --minimal # bare bones (routes.ts + main.ts + utils/locales.ts + empty subfolders)
1452
1452
  ```
1453
1453
 
1454
1454
  Full CRUD is the default — opt down to a bare skeleton with `--minimal` (`-m`) when you want to build the module piece by piece. `--force` (`-f`) overwrites existing files. The plural form is auto-derived: `generate.module product` and `generate.module products` produce the same `src/app/products/` folder.
@@ -1554,9 +1554,9 @@ Inside the same module, plain relative imports (`./`, `../`).
1554
1554
  ### Full CRUD bootstrap
1555
1555
 
1556
1556
  ```bash
1557
- pnpm warlock generate.module products
1557
+ npx warlock generate.module products
1558
1558
  # edit schemas + model fields, then
1559
- pnpm warlock migrate
1559
+ npx warlock migrate
1560
1560
  ```
1561
1561
 
1562
1562
  The CRUD scaffold's `routes.ts` already chains the five controllers behind `guarded(...)`:
@@ -1584,11 +1584,11 @@ guarded(() => {
1584
1584
  ### Skeleton module, add pieces piecemeal
1585
1585
 
1586
1586
  ```bash
1587
- pnpm warlock generate.module orders --minimal
1588
- pnpm warlock generate.model orders/order --with-resource
1589
- pnpm warlock generate.repository orders/order
1590
- pnpm warlock generate.controller orders/place-order --with-validation
1591
- pnpm warlock generate.controller orders/list-orders
1587
+ npx warlock generate.module orders --minimal
1588
+ npx warlock generate.model orders/order --with-resource
1589
+ npx warlock generate.repository orders/order
1590
+ npx warlock generate.controller orders/place-order --with-validation
1591
+ npx warlock generate.controller orders/list-orders
1592
1592
  ```
1593
1593
 
1594
1594
  Then wire URLs by editing `src/app/orders/routes.ts` and the schema rules in `src/app/orders/schema/`.
@@ -1609,7 +1609,7 @@ await warmupProductCache();
1609
1609
  - **There's no standalone `generate.validation` command.** Validation is no longer scaffolded on its own — each controller carries its own schema (imported from `schema/` and bound via `controller.validation`). Generate the controller with `--with-validation` to get the paired schema file, or hand-write the `schema/*.schema.ts`.
1610
1610
  - **No `requests/` folder.** Controllers import the schema's exported type + value directly from `schema/*.schema.ts`; there is no `*.request.ts` alias.
1611
1611
  - **Subfolder is `seeds/` (plural), not `seed/`.** The seed file is `<module>.seed.ts`.
1612
- - **`generate.module` does not run the migration.** It only creates the migration file. Run `pnpm warlock migrate` separately to apply it.
1612
+ - **`generate.module` does not run the migration.** It only creates the migration file. Run `npx warlock migrate` separately to apply it.
1613
1613
  - **`models/<entity>/` is its own folder, not a flat file.** The generator puts `product.model.ts` inside `models/product/` so migrations can sit beside the model in `models/product/migrations/`.
1614
1614
  - **Don't import `routes.ts`, `main.ts`, or anything in `events/`.** They're auto-loaded; double-loading errors out at boot.
1615
1615
  - **`utils/locales.ts` is mandatory for translation keys.** Skip it and `t("products.notFound")` silently falls back to the key itself.
@@ -2292,7 +2292,7 @@ The "same error for missing user vs wrong password" pattern is deliberate — it
2292
2292
  `hashPassword` / `verifyPassword` need `bcryptjs`. Install it directly:
2293
2293
 
2294
2294
  ```bash
2295
- pnpm add bcryptjs
2295
+ npm install bcryptjs
2296
2296
  ```
2297
2297
 
2298
2298
  If you skip the install, the first call throws with the framework's install hint:
@@ -2301,14 +2301,14 @@ If you skip the install, the first call throws with the framework's install hint
2301
2301
  Password encryption requires the bcryptjs package.
2302
2302
  Install it with:
2303
2303
 
2304
- pnpm add bcryptjs
2304
+ npm install bcryptjs
2305
2305
 
2306
2306
  Or with your preferred package manager:
2307
2307
 
2308
- pnpm add bcryptjs
2308
+ npm install bcryptjs
2309
2309
  ```
2310
2310
 
2311
- There is no `warlock add` feature for password hashing — `bcryptjs` is a plain dependency, so install it directly with `pnpm add bcryptjs`.
2311
+ There is no `warlock add` feature for password hashing — `bcryptjs` is a plain dependency, so install it directly with `npm install bcryptjs`.
2312
2312
 
2313
2313
  ## Gotchas
2314
2314
 
@@ -3288,13 +3288,13 @@ Three commands move the app through its lifecycle: `dev` while you're editing, `
3288
3288
 
3289
3289
  ```bash
3290
3290
  # Local development
3291
- pnpm warlock dev
3291
+ npx warlock dev
3292
3292
 
3293
3293
  # Production build
3294
- pnpm warlock build
3294
+ npx warlock build
3295
3295
 
3296
3296
  # Run the built bundle
3297
- pnpm warlock start
3297
+ npx warlock start
3298
3298
  ```
3299
3299
 
3300
3300
  `dev` and `start` are **persistent** (long-running, no auto-exit). `build` is one-shot — it exits when the bundle is written.
@@ -3511,9 +3511,9 @@ The three cases that reach it are: never built; a build that failed before promo
3511
3511
  ### Behavior
3512
3512
 
3513
3513
  ```bash
3514
- pnpm warlock start # → spawns node --enable-source-maps dist/app.js
3515
- pnpm warlock start --inspect # → spawns node --enable-source-maps --inspect dist/app.js
3516
- pnpm warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
3514
+ npx warlock start # → spawns node --enable-source-maps dist/app.js
3515
+ npx warlock start --inspect # → spawns node --enable-source-maps --inspect dist/app.js
3516
+ npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
3517
3517
  ```
3518
3518
 
3519
3519
  Everything you pass after `start` is forwarded to the spawned Node process. Use this to attach a debugger (`--inspect`), tune memory (`--max-old-space-size`), or pass any other Node flag without editing the command.
@@ -3565,7 +3565,7 @@ The started banner prints **only** when the running application reports a comple
3565
3565
 
3566
3566
  ```bash
3567
3567
  # a CI gate can be this blunt, and it is now correct
3568
- pnpm warlock start | grep -q "production server started"
3568
+ npx warlock start | grep -q "production server started"
3569
3569
  ```
3570
3570
 
3571
3571
  ### Reading a failed start (5.2)
@@ -3648,17 +3648,17 @@ If you need conditional behavior, branch on `Application.environment` (the ortho
3648
3648
  }
3649
3649
  ```
3650
3650
 
3651
- Now `pnpm dev` / `pnpm build` / `pnpm start`. Standard Node hosting providers (Render, Fly, Railway, Heroku) recognize this layout.
3651
+ Now `npm run dev` / `npm run build` / `npm run start`. Standard Node hosting providers (Render, Fly, Railway, Heroku) recognize this layout.
3652
3652
 
3653
3653
  ### Production Dockerfile
3654
3654
 
3655
3655
  ```dockerfile
3656
3656
  FROM node:20-alpine AS build
3657
3657
  WORKDIR /app
3658
- COPY package.json yarn.lock ./
3659
- RUN pnpm install --frozen-lockfile
3658
+ COPY package.json package-lock.json ./
3659
+ RUN npm ci
3660
3660
  COPY . .
3661
- RUN pnpm warlock build
3661
+ RUN npx warlock build
3662
3662
 
3663
3663
  FROM node:20-alpine
3664
3664
  WORKDIR /app
@@ -3667,7 +3667,7 @@ COPY --from=build /app/node_modules ./node_modules
3667
3667
  COPY --from=build /app/package.json ./
3668
3668
  COPY --from=build /app/warlock.config.ts ./
3669
3669
  ENV NODE_ENV=production
3670
- CMD ["yarn", "warlock", "start"]
3670
+ CMD ["npx", "warlock", "start"]
3671
3671
  ```
3672
3672
 
3673
3673
  Two-stage build trims `devDependencies` out of the runtime image. Keep `warlock.config.ts` in the runtime stage — `start` reads it to resolve the bundle path.
@@ -3702,7 +3702,7 @@ That is deliberate: `build` and `start` do **not** force `production`. Forcing i
3702
3702
  ### Skip type-gen on machines without write access
3703
3703
 
3704
3704
  ```bash
3705
- pnpm warlock dev --skip-typings
3705
+ npx warlock dev --skip-typings
3706
3706
  ```
3707
3707
 
3708
3708
  Or persist it:
@@ -3720,20 +3720,20 @@ Useful in a containerized dev environment where `.warlock/typings.d.ts` is read-
3720
3720
  ### Memory-tune the production process
3721
3721
 
3722
3722
  ```bash
3723
- pnpm warlock start --max-old-space-size=4096
3723
+ npx warlock start --max-old-space-size=4096
3724
3724
  ```
3725
3725
 
3726
3726
  Or via `NODE_OPTIONS` in the deployment env if you don't want to change the start invocation:
3727
3727
 
3728
3728
  ```bash
3729
- NODE_OPTIONS=--max-old-space-size=4096 pnpm warlock start
3729
+ NODE_OPTIONS=--max-old-space-size=4096 npx warlock start
3730
3730
  ```
3731
3731
 
3732
3732
  ## Gotchas
3733
3733
 
3734
3734
  - **`warlock dev` is persistent — `Ctrl+C` to stop.** The framework's `persistent: true` flag keeps the process alive after `action` returns. Same for `start`.
3735
3735
  - **`--fresh` only deletes the manifest, not the transpile cache.** If you're chasing a stale-compile bug, `rm -rf .warlock/` clears everything. The manifest restoring is what `--fresh` solves.
3736
- - **`warlock build` does NOT run migrations.** Production bundles ship the migration files but don't apply them. Run `pnpm warlock migrate` against the production DB separately.
3736
+ - **`warlock build` does NOT run migrations.** Production bundles ship the migration files but don't apply them. Run `npx warlock migrate` against the production DB separately.
3737
3737
  - **`warlock start` requires a build it can vouch for.** Since 5.2 it refuses any `outdir` without the `.warlock-build.json` success marker — a hand-assembled `dist/`, or one left behind by a build that failed, is rejected by that reason instead of being spawned and crashing halfway through boot. Run `warlock build` first.
3738
3738
  - **Do not add `.warlock-build.json` to `.gitignore`-driven artifact pruning.** Stripping it from a `dist/` you ship makes `warlock start` refuse the artifact on the target host. Copy `outdir` whole.
3739
3739
  - **`outdir` is the directory, `outFile` is the filename within it.** A common mistake is putting the full path in one and leaving the other default — you end up with `<full-path>/app.js` or `dist/<full-path>`. They concatenate.
@@ -3840,7 +3840,7 @@ const config: MailConfigurations = {
3840
3840
  };
3841
3841
  ```
3842
3842
 
3843
- Requires `@aws-sdk/client-sesv2` installed (`pnpm add @aws-sdk/client-sesv2`).
3843
+ Requires `@aws-sdk/client-sesv2` installed (`npm install @aws-sdk/client-sesv2`).
3844
3844
 
3845
3845
  ## Mail modes
3846
3846
 
@@ -4116,7 +4116,7 @@ await Mail.to(user.email)
4116
4116
 
4117
4117
  - **`.send()` validates** — `to`, `subject`, and at least one of `text`/`html`/`component` are required. Missing any throws synchronously.
4118
4118
  - **`@react-email/render` is optional.** Without it you get the basic fallback (inline styles, no MSO conditionals). Install it for production-quality HTML.
4119
- - **`nodemailer` is loaded lazily** at import time. If you see `nodemailer is not installed` errors, run `warlock add mail` (or `pnpm add nodemailer`).
4119
+ - **`nodemailer` is loaded lazily** at import time. If you see `nodemailer is not installed` errors, run `warlock add mail` (or `npm install nodemailer`).
4120
4120
  - **`secure: true` requires port 465.** For port 587 use `secure: false` and `tls: true` (STARTTLS).
4121
4121
  - **Test mode is process-global.** Set it in `beforeAll`/`beforeEach`; reset with `setMailMode("production")` (or rely on test runner isolation).
4122
4122
  - **Per-mail handlers don't replace global ones** — both fire. Avoid double-counting metrics.
@@ -6894,7 +6894,7 @@ What it does at save time:
6894
6894
  | Existing row, password unchanged | Pass through (no re-hashing — stored hash preserved). |
6895
6895
  | Empty / undefined value | Pass through untouched. |
6896
6896
 
6897
- Calls `authService.hashPassword(String(value))` under the hood — same bcryptjs path as the standalone `hashPassword()` helper. See [`hash-password/SKILL.md`](../hash-password/SKILL.md) for full bcrypt setup (salt rounds, `pnpm add bcryptjs`).
6897
+ Calls `authService.hashPassword(String(value))` under the hood — same bcryptjs path as the standalone `hashPassword()` helper. See [`hash-password/SKILL.md`](../hash-password/SKILL.md) for full bcrypt setup (salt rounds, `npm install bcryptjs`).
6898
6898
 
6899
6899
  ### Why declarative wins
6900
6900
 
@@ -7048,7 +7048,7 @@ Rule of thumb: transformers are for **pure, deterministic** transforms of the ro
7048
7048
 
7049
7049
  ## See also
7050
7050
 
7051
- - [`hash-password/SKILL.md`](../hash-password/SKILL.md) — the bcrypt setup that `useHashedPassword` calls under the hood; salt rounds, `pnpm add bcryptjs`.
7051
+ - [`hash-password/SKILL.md`](../hash-password/SKILL.md) — the bcrypt setup that `useHashedPassword` calls under the hood; salt rounds, `npm install bcryptjs`.
7052
7052
  - [`use-repository/SKILL.md`](../use-repository/SKILL.md) — where `create` / `save` calls happen that trigger the transformers.
7053
7053
  - [`define-resource/SKILL.md`](../define-resource/SKILL.md) — filtering transformed fields (`password`) out of API responses.
7054
7054
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — schema files live in `src/app/<module>/models/<entity>/<entity>.model.ts`.
@@ -7111,7 +7111,7 @@ Five lines do the heavy lifting:
7111
7111
  4. **`defaultOptions`** — applied to every call (`orderBy`, default `limit`, etc.).
7112
7112
  5. **`new FaqsRepository()`** singleton — import this everywhere; never instantiate again.
7113
7113
 
7114
- The class is intentionally private — only the singleton escapes the module. Scaffold with `pnpm warlock generate.repository <module>/<entity>`.
7114
+ The class is intentionally private — only the singleton escapes the module. Scaffold with `npx warlock generate.repository <module>/<entity>`.
7115
7115
 
7116
7116
  ## The `filterBy` rules
7117
7117
 
@@ -7710,7 +7710,7 @@ Two pieces, always:
7710
7710
 
7711
7711
  No separate `*.request.ts` alias file. `RequestHandler<Request<TSchema>>` types `request.validated()` directly off the schema's inferred type.
7712
7712
 
7713
- Scaffold with `pnpm warlock generate.controller <module>/<action> --with-validation`. If the scaffolder emits a `requests/<action>.request.ts` file, delete it — the inline pattern is the convention.
7713
+ Scaffold with `npx warlock generate.controller <module>/<action> --with-validation`. If the scaffolder emits a `requests/<action>.request.ts` file, delete it — the inline pattern is the convention.
7714
7714
 
7715
7715
  ## The `v.*` factory surface
7716
7716
 
@@ -8097,7 +8097,7 @@ Cascade uses `@RegisterModel()` for the model registry and `@BelongsTo` / `@HasM
8097
8097
 
8098
8098
  ---
8099
8099
  name: warlock-doctor
8100
- description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `pnpm warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
8100
+ description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `npx warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
8101
8101
  ---
8102
8102
 
8103
8103
  # Warlock — `warlock doctor`
@@ -8105,7 +8105,7 @@ description: 'Run `warlock doctor` — a read-only diagnostics command that chec
8105
8105
  `warlock doctor` is a read-only preflight. It boots the app far enough to introspect it — loads every config file and bootstrap code so routes and connectors register — but **starts no connectors**, so it never opens a database, cache, or socket connection. It then runs a set of checks and prints a grouped pass / warn / fail report.
8106
8106
 
8107
8107
  ```bash
8108
- pnpm warlock doctor
8108
+ npx warlock doctor
8109
8109
  ```
8110
8110
 
8111
8111
  ```
@@ -8208,7 +8208,7 @@ if (report.hasFailures) process.exit(report.exitCode);
8208
8208
  ### CI / pre-release gate
8209
8209
 
8210
8210
  ```bash
8211
- pnpm warlock doctor || exit 1 # non-zero exit fails the job
8211
+ npx warlock doctor || exit 1 # non-zero exit fails the job
8212
8212
  ```
8213
8213
 
8214
8214
  A red `release-hygiene` line catches the classic "bumped `package.json` but forgot the CHANGELOG heading" mistake before a publish.
@@ -8236,7 +8236,7 @@ A `⚠ routes: 0 routes registered` line is the tell that a route module threw o
8236
8236
 
8237
8237
  ---
8238
8238
  name: warlock-routes
8239
- description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`''s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `pnpm warlock routes` / `pnpm warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
8239
+ description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`''s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `npx warlock routes` / `npx warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
8240
8240
  ---
8241
8241
 
8242
8242
  # Warlock — `warlock routes`
@@ -8244,7 +8244,7 @@ description: 'Run `warlock routes` — a read-only command that lists the regist
8244
8244
  `warlock routes` lists every registered HTTP route as a table. It's the read-only sibling of [`warlock doctor`](../warlock-doctor/SKILL.md): it boots the app far enough to register route modules — but **starts no connectors**, so it never opens a database, cache, or socket connection.
8245
8245
 
8246
8246
  ```bash
8247
- pnpm warlock routes
8247
+ npx warlock routes
8248
8248
  ```
8249
8249
 
8250
8250
  ```
@@ -8275,10 +8275,10 @@ The `METHOD` column is verb-colored (GET green, POST blue, PUT/PATCH yellow, DEL
8275
8275
  Optional, case-insensitive, AND-combined:
8276
8276
 
8277
8277
  ```bash
8278
- pnpm warlock routes --method GET # -m exact HTTP method
8279
- pnpm warlock routes --path /users # -p path substring
8280
- pnpm warlock routes --name users # -n route-name substring
8281
- pnpm warlock routes -m POST -p /users
8278
+ npx warlock routes --method GET # -m exact HTTP method
8279
+ npx warlock routes --path /users # -p path substring
8280
+ npx warlock routes --name users # -n route-name substring
8281
+ npx warlock routes -m POST -p /users
8282
8282
  ```
8283
8283
 
8284
8284
  ## JSON output
@@ -8286,7 +8286,7 @@ pnpm warlock routes -m POST -p /users
8286
8286
  `--json` (`-j`) emits the normalized rows instead of the table — for `jq`, a CI diff, or a generated API map. Filters apply before serialization.
8287
8287
 
8288
8288
  ```bash
8289
- pnpm warlock routes --json
8289
+ npx warlock routes --json
8290
8290
  ```
8291
8291
 
8292
8292
  ```json
@@ -8300,7 +8300,7 @@ pnpm warlock routes --json
8300
8300
  ### Audit which routes are guarded
8301
8301
 
8302
8302
  ```bash
8303
- pnpm warlock routes --json | jq '[.[] | select(.middleware == 0)]'
8303
+ npx warlock routes --json | jq '[.[] | select(.middleware == 0)]'
8304
8304
  ```
8305
8305
 
8306
8306
  Surfaces public routes (no middleware) — a quick check that auth-protected paths actually carry a guard.
@@ -8308,7 +8308,7 @@ Surfaces public routes (no middleware) — a quick check that auth-protected pat
8308
8308
  ### Confirm a route registered
8309
8309
 
8310
8310
  ```bash
8311
- pnpm warlock routes --name users.create
8311
+ npx warlock routes --name users.create
8312
8312
  ```
8313
8313
 
8314
8314
  An empty result means the route isn't registered — re-run `warlock dev` and read the boot error (the route-module loader is fail-loud, so a throwing route file aborts boot rather than being silently dropped).
@@ -8318,7 +8318,7 @@ An empty result means the route isn't registered — re-run `warlock dev` and re
8318
8318
  Compares the **live dev-server page routes** (`router.list().filter(r => r.isPage)`) against a **snapshot written by the last successful `warlock build`** (`page-routes.manifest.json` in `resolveBuildConfig().outdir`, e.g. `dist/page-routes.manifest.json`). Boots the same diagnostic way as `warlock routes` — route modules registered, no connectors started — then diffs.
8319
8319
 
8320
8320
  ```bash
8321
- pnpm warlock routes:diff
8321
+ npx warlock routes:diff
8322
8322
  ```
8323
8323
 
8324
8324
  ```
@@ -8696,7 +8696,7 @@ export default command({
8696
8696
  });
8697
8697
  ```
8698
8698
 
8699
- Run it: `pnpm warlock users.promote --email=hasan@example.com` (or `pnpm warlock up -e hasan@example.com`).
8699
+ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock up -e hasan@example.com`).
8700
8700
 
8701
8701
  ## `CLICommandOptions` — the factory input
8702
8702
 
@@ -9229,12 +9229,12 @@ export default seeder({
9229
9229
  Run them:
9230
9230
 
9231
9231
  ```bash
9232
- pnpm warlock seed # discover + run all
9233
- pnpm warlock seed --list # show registry, don't run
9234
- pnpm warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
9235
- pnpm warlock seed --fresh # truncate every table first, then run all
9236
- pnpm warlock seed --drop # undo every tracked record, reset the log
9237
- pnpm warlock seed --drop=default-roles # undo just one seeder's records
9232
+ npx warlock seed # discover + run all
9233
+ npx warlock seed --list # show registry, don't run
9234
+ npx warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
9235
+ npx warlock seed --fresh # truncate every table first, then run all
9236
+ npx warlock seed --drop # undo every tracked record, reset the log
9237
+ npx warlock seed --drop=default-roles # undo just one seeder's records
9238
9238
  ```
9239
9239
 
9240
9240
  `--fresh` truncates **every** table in the DB (`datasource.driver.truncateTable(table, { cascade: true })`), including the `seeds` tracking table. After `--fresh`, `once: true` seeds will run again.
@@ -9372,8 +9372,8 @@ Every record you `track()` is written to a `seed_records` table (created on firs
9372
9372
  `warlock seed --drop` reads those refs and undoes the seed:
9373
9373
 
9374
9374
  ```bash
9375
- pnpm warlock seed --drop # undo every tracked record across all seeders
9376
- pnpm warlock seed --drop=default-roles # undo just one seeder's records
9375
+ npx warlock seed --drop # undo every tracked record across all seeders
9376
+ npx warlock seed --drop=default-roles # undo just one seeder's records
9377
9377
  ```
9378
9378
 
9379
9379
  What it does, inside a single transaction:
package/llms.txt CHANGED
@@ -12,7 +12,7 @@
12
12
  - [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
13
13
  - [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, and the `config()` getter for runtime reads. Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
14
14
  - [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
15
- - [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `pnpm warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
15
+ - [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
16
16
  - [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
17
17
  - [encrypt-data](@warlock.js/core/encrypt-data/SKILL.md): Reversible AES-256-GCM `encrypt` / `decrypt` for secrets you need to read back; one-way HMAC-SHA256 `hmacHash` for deterministic fingerprints (lookup/dedup of encrypted columns). Keys come from `src/config/encryption.ts`. Triggers: `encrypt`, `decrypt`, `hmacHash`, `EncryptionConfigurations`, `APP_ENCRYPTION_KEY`, `APP_HMAC_KEY`; "store an API key reversibly", "fingerprint an encrypted column for lookup", "AES-256-GCM secret", "HMAC-SHA256 dedup key"; typical import `import { encrypt, decrypt, hmacHash } from "@warlock.js/core"`. Skip: password hashing — `@warlock.js/core/hash-password/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs Node `crypto` direct, `crypto-js`, `libsodium-wrappers`.
18
18
  - [hash-password](@warlock.js/core/hash-password/SKILL.md): One-way bcrypt password hashing — `hashPassword` / `verifyPassword`, plus the declarative `useHashedPassword()` schema transformer that auto-hashes a model's password field on save. Salt rounds come from `src/config/encryption.ts`. Triggers: `hashPassword`, `verifyPassword`, `useHashedPassword`, `password.salt`, `bcryptjs`; "hash a user password", "verify login credentials", "auto-hash on save", "rotate a password"; typical import `import { hashPassword, verifyPassword } from "@warlock.js/core"`. Skip: reversible secrets — `@warlock.js/core/encrypt-data/SKILL.md`; the other transformers — `@warlock.js/core/use-model-transformers/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `bcrypt` native, `argon2`, `scrypt`.
@@ -39,8 +39,8 @@
39
39
  - [use-request-locals](@warlock.js/core/use-request-locals/SKILL.md): Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.
40
40
  - [validate-input](@warlock.js/core/validate-input/SKILL.md): Author seal schemas, attach them to controllers via `controller.validation = { schema }`, infer types via `Infer<typeof schema>`, and layer DB-aware (`unique`/`exists`) and file validators on top. Triggers: `v.object`, `v.string`, `v.email`, `Infer`, `controller.validation`, `.unique`, `.exists`, `uniqueExceptCurrentId`, `request.validated`; "validate a request body", "attach a schema to a controller", "DB-aware unique rule", "infer schema types"; typical import `import { v, type Infer } from "@warlock.js/seal"`. Skip: schema authoring foundations — `@warlock.js/seal/seal-basics/SKILL.md`; controller wiring — `@warlock.js/core/create-controller/SKILL.md`; file rules deep-dive — `@warlock.js/core/upload-file/SKILL.md`; competing libs `zod`, `joi`, `yup`, `class-validator`.
41
41
  - [warlock-conventions](@warlock.js/core/warlock-conventions/SKILL.md): Framework-wide invariants for projects built on @warlock.js/core — module layout, canonical imports, layered flow, file naming, and the non-negotiable rules every other warlock skill assumes. Triggers: `src/app/<module>`, `routes.ts`, `main.ts`, `Request<TSchema>`, `RequestHandler`, `GuardedRequestHandler`, `app/<module>/...`; "where do files go in this project", "canonical Warlock imports", "module layout rules", "controller-service-repository layering"; typical import `import { router, type RequestHandler } from "@warlock.js/core"`. Skip: scaffold a new module — `@warlock.js/core/create-module/SKILL.md`; route shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing patterns: `express` ad-hoc layouts, `@nestjs/common` decorator-driven structure.
42
- - [warlock-doctor](@warlock.js/core/warlock-doctor/SKILL.md): Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `pnpm warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.
43
- - [warlock-routes](@warlock.js/core/warlock-routes/SKILL.md): Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`'s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `pnpm warlock routes` / `pnpm warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.
42
+ - [warlock-doctor](@warlock.js/core/warlock-doctor/SKILL.md): Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `npx warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.
43
+ - [warlock-routes](@warlock.js/core/warlock-routes/SKILL.md): Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`'s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `npx warlock routes` / `npx warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.
44
44
  - [wire-socket](@warlock.js/core/wire-socket/SKILL.md): Configure Socket.IO via `src/config/socket.ts`, reach the live server through `getSocketServer()` (or `app.socket` post-bootstrap), register `connection` handlers once the late-phase socket connector has booted, emit from controllers/services, use rooms and namespaces. Triggers: `app.socket`, `getSocketServer`, `SocketOptions`, `socket.io` `Server`, `socket.join`, `socket.to`, `io.of`, `io.use`; "add realtime chat", "emit socket events from a service", "use rooms and namespaces", "per-socket JWT auth". Skip: connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; app context accessors — `@warlock.js/core/use-app-context/SKILL.md`; competing libs `ws`, `socket.io` direct without Warlock connector, `uWebSockets.js`.
45
45
  - [write-cli-command](@warlock.js/core/write-cli-command/SKILL.md): Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.
46
46
  - [write-middleware](@warlock.js/core/write-middleware/SKILL.md): Author HTTP middleware for @warlock.js/core — the `({ request, response })` signature, short-circuit by returning a response, enrich the request with extra fields, register per-route, per-group, or app-wide. Triggers: `Middleware`, `MiddlewareResponse`, `router.group`, `guarded`, `request.detectIp`, `authMiddleware`; "write a custom middleware", "short-circuit a request", "enrich the request with extra fields", "per-route vs per-group middleware"; typical import `import type { Middleware } from "@warlock.js/core"`. Skip: built-in middleware catalog — `@warlock.js/core/use-middleware/SKILL.md`; route attachment — `@warlock.js/core/register-route/SKILL.md`; response helpers — `@warlock.js/core/send-response/SKILL.md`; competing patterns: `express` `(req, res, next)` middleware, Fastify `preHandler` hooks.
package/package.json CHANGED
@@ -25,13 +25,13 @@
25
25
  "@mongez/slug": "^1.0.7",
26
26
  "@mongez/supportive-is": "^2.1.4",
27
27
  "@mongez/time-wizard": "^1.0.6",
28
- "@warlock.js/auth": "5.4.0",
29
- "@warlock.js/cache": "5.4.0",
30
- "@warlock.js/cascade": "5.4.0",
31
- "@warlock.js/context": "5.4.0",
32
- "@warlock.js/logger": "5.4.0",
33
- "@warlock.js/seal": "5.4.0",
34
- "@warlock.js/fs": "5.4.0",
28
+ "@warlock.js/auth": "5.5.0",
29
+ "@warlock.js/cache": "5.5.0",
30
+ "@warlock.js/cascade": "5.5.0",
31
+ "@warlock.js/context": "5.5.0",
32
+ "@warlock.js/logger": "5.5.0",
33
+ "@warlock.js/seal": "5.5.0",
34
+ "@warlock.js/fs": "5.5.0",
35
35
  "chokidar": "^5.0.0",
36
36
  "dayjs": "^1.11.19",
37
37
  "es-module-lexer": "^2.0.0",
@@ -57,10 +57,10 @@
57
57
  "react": "^19.2.3",
58
58
  "react-dom": "^19.2.3",
59
59
  "@react-email/render": "^2.0.5",
60
- "@warlock.js/herald": "5.4.0",
61
- "@warlock.js/ai": "5.4.0",
62
- "@warlock.js/access": "5.4.0",
63
- "@warlock.js/notifications": "5.4.0"
60
+ "@warlock.js/herald": "5.5.0",
61
+ "@warlock.js/ai": "5.5.0",
62
+ "@warlock.js/access": "5.5.0",
63
+ "@warlock.js/notifications": "5.5.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "sharp": {
@@ -123,7 +123,7 @@
123
123
  ],
124
124
  "author": "hassanzohdy",
125
125
  "license": "MIT",
126
- "version": "5.4.0",
126
+ "version": "5.5.0",
127
127
  "type": "module",
128
128
  "main": "./esm/index.mjs",
129
129
  "module": "./esm/index.mjs",
@@ -25,7 +25,7 @@ That's the full contract. The `RequestHandler` annotation carries both parameter
25
25
  - File: `src/app/<module>/controllers/<action>.controller.ts`.
26
26
  - Export name matches the action in camelCase + `Controller` suffix: `listProductsController`, `createProductController`, `getProductController`.
27
27
 
28
- Scaffold with: `pnpm warlock generate.controller <module>/<action>` (add `--with-validation` to get the schema generated alongside).
28
+ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-validation` to get the schema generated alongside).
29
29
 
30
30
  ## Reading input
31
31
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: create-module
3
- description: 'Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `pnpm warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.'
3
+ description: 'Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.'
4
4
  ---
5
5
 
6
6
  # Warlock — create a module
@@ -10,8 +10,8 @@ A module is a self-contained feature folder under `src/app/<name>/`. The CLI sca
10
10
  ## The shape
11
11
 
12
12
  ```bash
13
- pnpm warlock generate.module products # full CRUD bootstrap (default — controllers, model, services, repository, resource, schemas, routes, seed)
14
- pnpm warlock generate.module products --minimal # bare bones (routes.ts + main.ts + utils/locales.ts + empty subfolders)
13
+ npx warlock generate.module products # full CRUD bootstrap (default — controllers, model, services, repository, resource, schemas, routes, seed)
14
+ npx warlock generate.module products --minimal # bare bones (routes.ts + main.ts + utils/locales.ts + empty subfolders)
15
15
  ```
16
16
 
17
17
  Full CRUD is the default — opt down to a bare skeleton with `--minimal` (`-m`) when you want to build the module piece by piece. `--force` (`-f`) overwrites existing files. The plural form is auto-derived: `generate.module product` and `generate.module products` produce the same `src/app/products/` folder.
@@ -117,9 +117,9 @@ Inside the same module, plain relative imports (`./`, `../`).
117
117
  ### Full CRUD bootstrap
118
118
 
119
119
  ```bash
120
- pnpm warlock generate.module products
120
+ npx warlock generate.module products
121
121
  # edit schemas + model fields, then
122
- pnpm warlock migrate
122
+ npx warlock migrate
123
123
  ```
124
124
 
125
125
  The CRUD scaffold's `routes.ts` already chains the five controllers behind `guarded(...)`:
@@ -147,11 +147,11 @@ guarded(() => {
147
147
  ### Skeleton module, add pieces piecemeal
148
148
 
149
149
  ```bash
150
- pnpm warlock generate.module orders --minimal
151
- pnpm warlock generate.model orders/order --with-resource
152
- pnpm warlock generate.repository orders/order
153
- pnpm warlock generate.controller orders/place-order --with-validation
154
- pnpm warlock generate.controller orders/list-orders
150
+ npx warlock generate.module orders --minimal
151
+ npx warlock generate.model orders/order --with-resource
152
+ npx warlock generate.repository orders/order
153
+ npx warlock generate.controller orders/place-order --with-validation
154
+ npx warlock generate.controller orders/list-orders
155
155
  ```
156
156
 
157
157
  Then wire URLs by editing `src/app/orders/routes.ts` and the schema rules in `src/app/orders/schema/`.
@@ -172,7 +172,7 @@ await warmupProductCache();
172
172
  - **There's no standalone `generate.validation` command.** Validation is no longer scaffolded on its own — each controller carries its own schema (imported from `schema/` and bound via `controller.validation`). Generate the controller with `--with-validation` to get the paired schema file, or hand-write the `schema/*.schema.ts`.
173
173
  - **No `requests/` folder.** Controllers import the schema's exported type + value directly from `schema/*.schema.ts`; there is no `*.request.ts` alias.
174
174
  - **Subfolder is `seeds/` (plural), not `seed/`.** The seed file is `<module>.seed.ts`.
175
- - **`generate.module` does not run the migration.** It only creates the migration file. Run `pnpm warlock migrate` separately to apply it.
175
+ - **`generate.module` does not run the migration.** It only creates the migration file. Run `npx warlock migrate` separately to apply it.
176
176
  - **`models/<entity>/` is its own folder, not a flat file.** The generator puts `product.model.ts` inside `models/product/` so migrations can sit beside the model in `models/product/migrations/`.
177
177
  - **Don't import `routes.ts`, `main.ts`, or anything in `events/`.** They're auto-loaded; double-loading errors out at boot.
178
178
  - **`utils/locales.ts` is mandatory for translation keys.** Skip it and `t("products.notFound")` silently falls back to the key itself.
@@ -148,7 +148,7 @@ The "same error for missing user vs wrong password" pattern is deliberate — it
148
148
  `hashPassword` / `verifyPassword` need `bcryptjs`. Install it directly:
149
149
 
150
150
  ```bash
151
- pnpm add bcryptjs
151
+ npm install bcryptjs
152
152
  ```
153
153
 
154
154
  If you skip the install, the first call throws with the framework's install hint:
@@ -157,14 +157,14 @@ If you skip the install, the first call throws with the framework's install hint
157
157
  Password encryption requires the bcryptjs package.
158
158
  Install it with:
159
159
 
160
- pnpm add bcryptjs
160
+ npm install bcryptjs
161
161
 
162
162
  Or with your preferred package manager:
163
163
 
164
- pnpm add bcryptjs
164
+ npm install bcryptjs
165
165
  ```
166
166
 
167
- There is no `warlock add` feature for password hashing — `bcryptjs` is a plain dependency, so install it directly with `pnpm add bcryptjs`.
167
+ There is no `warlock add` feature for password hashing — `bcryptjs` is a plain dependency, so install it directly with `npm install bcryptjs`.
168
168
 
169
169
  ## Gotchas
170
170
 
@@ -11,13 +11,13 @@ Three commands move the app through its lifecycle: `dev` while you're editing, `
11
11
 
12
12
  ```bash
13
13
  # Local development
14
- pnpm warlock dev
14
+ npx warlock dev
15
15
 
16
16
  # Production build
17
- pnpm warlock build
17
+ npx warlock build
18
18
 
19
19
  # Run the built bundle
20
- pnpm warlock start
20
+ npx warlock start
21
21
  ```
22
22
 
23
23
  `dev` and `start` are **persistent** (long-running, no auto-exit). `build` is one-shot — it exits when the bundle is written.
@@ -234,9 +234,9 @@ The three cases that reach it are: never built; a build that failed before promo
234
234
  ### Behavior
235
235
 
236
236
  ```bash
237
- pnpm warlock start # → spawns node --enable-source-maps dist/app.js
238
- pnpm warlock start --inspect # → spawns node --enable-source-maps --inspect dist/app.js
239
- pnpm warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
237
+ npx warlock start # → spawns node --enable-source-maps dist/app.js
238
+ npx warlock start --inspect # → spawns node --enable-source-maps --inspect dist/app.js
239
+ npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
240
240
  ```
241
241
 
242
242
  Everything you pass after `start` is forwarded to the spawned Node process. Use this to attach a debugger (`--inspect`), tune memory (`--max-old-space-size`), or pass any other Node flag without editing the command.
@@ -288,7 +288,7 @@ The started banner prints **only** when the running application reports a comple
288
288
 
289
289
  ```bash
290
290
  # a CI gate can be this blunt, and it is now correct
291
- pnpm warlock start | grep -q "production server started"
291
+ npx warlock start | grep -q "production server started"
292
292
  ```
293
293
 
294
294
  ### Reading a failed start (5.2)
@@ -371,17 +371,17 @@ If you need conditional behavior, branch on `Application.environment` (the ortho
371
371
  }
372
372
  ```
373
373
 
374
- Now `pnpm dev` / `pnpm build` / `pnpm start`. Standard Node hosting providers (Render, Fly, Railway, Heroku) recognize this layout.
374
+ Now `npm run dev` / `npm run build` / `npm run start`. Standard Node hosting providers (Render, Fly, Railway, Heroku) recognize this layout.
375
375
 
376
376
  ### Production Dockerfile
377
377
 
378
378
  ```dockerfile
379
379
  FROM node:20-alpine AS build
380
380
  WORKDIR /app
381
- COPY package.json yarn.lock ./
382
- RUN pnpm install --frozen-lockfile
381
+ COPY package.json package-lock.json ./
382
+ RUN npm ci
383
383
  COPY . .
384
- RUN pnpm warlock build
384
+ RUN npx warlock build
385
385
 
386
386
  FROM node:20-alpine
387
387
  WORKDIR /app
@@ -390,7 +390,7 @@ COPY --from=build /app/node_modules ./node_modules
390
390
  COPY --from=build /app/package.json ./
391
391
  COPY --from=build /app/warlock.config.ts ./
392
392
  ENV NODE_ENV=production
393
- CMD ["yarn", "warlock", "start"]
393
+ CMD ["npx", "warlock", "start"]
394
394
  ```
395
395
 
396
396
  Two-stage build trims `devDependencies` out of the runtime image. Keep `warlock.config.ts` in the runtime stage — `start` reads it to resolve the bundle path.
@@ -425,7 +425,7 @@ That is deliberate: `build` and `start` do **not** force `production`. Forcing i
425
425
  ### Skip type-gen on machines without write access
426
426
 
427
427
  ```bash
428
- pnpm warlock dev --skip-typings
428
+ npx warlock dev --skip-typings
429
429
  ```
430
430
 
431
431
  Or persist it:
@@ -443,20 +443,20 @@ Useful in a containerized dev environment where `.warlock/typings.d.ts` is read-
443
443
  ### Memory-tune the production process
444
444
 
445
445
  ```bash
446
- pnpm warlock start --max-old-space-size=4096
446
+ npx warlock start --max-old-space-size=4096
447
447
  ```
448
448
 
449
449
  Or via `NODE_OPTIONS` in the deployment env if you don't want to change the start invocation:
450
450
 
451
451
  ```bash
452
- NODE_OPTIONS=--max-old-space-size=4096 pnpm warlock start
452
+ NODE_OPTIONS=--max-old-space-size=4096 npx warlock start
453
453
  ```
454
454
 
455
455
  ## Gotchas
456
456
 
457
457
  - **`warlock dev` is persistent — `Ctrl+C` to stop.** The framework's `persistent: true` flag keeps the process alive after `action` returns. Same for `start`.
458
458
  - **`--fresh` only deletes the manifest, not the transpile cache.** If you're chasing a stale-compile bug, `rm -rf .warlock/` clears everything. The manifest restoring is what `--fresh` solves.
459
- - **`warlock build` does NOT run migrations.** Production bundles ship the migration files but don't apply them. Run `pnpm warlock migrate` against the production DB separately.
459
+ - **`warlock build` does NOT run migrations.** Production bundles ship the migration files but don't apply them. Run `npx warlock migrate` against the production DB separately.
460
460
  - **`warlock start` requires a build it can vouch for.** Since 5.2 it refuses any `outdir` without the `.warlock-build.json` success marker — a hand-assembled `dist/`, or one left behind by a build that failed, is rejected by that reason instead of being spawned and crashing halfway through boot. Run `warlock build` first.
461
461
  - **Do not add `.warlock-build.json` to `.gitignore`-driven artifact pruning.** Stripping it from a `dist/` you ship makes `warlock start` refuse the artifact on the target host. Copy `outdir` whole.
462
462
  - **`outdir` is the directory, `outFile` is the filename within it.** A common mistake is putting the full path in one and leaving the other default — you end up with `<full-path>/app.js` or `dist/<full-path>`. They concatenate.
@@ -85,7 +85,7 @@ const config: MailConfigurations = {
85
85
  };
86
86
  ```
87
87
 
88
- Requires `@aws-sdk/client-sesv2` installed (`pnpm add @aws-sdk/client-sesv2`).
88
+ Requires `@aws-sdk/client-sesv2` installed (`npm install @aws-sdk/client-sesv2`).
89
89
 
90
90
  ## Mail modes
91
91
 
@@ -361,7 +361,7 @@ await Mail.to(user.email)
361
361
 
362
362
  - **`.send()` validates** — `to`, `subject`, and at least one of `text`/`html`/`component` are required. Missing any throws synchronously.
363
363
  - **`@react-email/render` is optional.** Without it you get the basic fallback (inline styles, no MSO conditionals). Install it for production-quality HTML.
364
- - **`nodemailer` is loaded lazily** at import time. If you see `nodemailer is not installed` errors, run `warlock add mail` (or `pnpm add nodemailer`).
364
+ - **`nodemailer` is loaded lazily** at import time. If you see `nodemailer is not installed` errors, run `warlock add mail` (or `npm install nodemailer`).
365
365
  - **`secure: true` requires port 465.** For port 587 use `secure: false` and `tls: true` (STARTTLS).
366
366
  - **Test mode is process-global.** Set it in `beforeAll`/`beforeEach`; reset with `setMailMode("production")` (or rely on test runner isolation).
367
367
  - **Per-mail handlers don't replace global ones** — both fire. Avoid double-counting metrics.
@@ -60,7 +60,7 @@ What it does at save time:
60
60
  | Existing row, password unchanged | Pass through (no re-hashing — stored hash preserved). |
61
61
  | Empty / undefined value | Pass through untouched. |
62
62
 
63
- Calls `authService.hashPassword(String(value))` under the hood — same bcryptjs path as the standalone `hashPassword()` helper. See [`hash-password/SKILL.md`](../hash-password/SKILL.md) for full bcrypt setup (salt rounds, `pnpm add bcryptjs`).
63
+ Calls `authService.hashPassword(String(value))` under the hood — same bcryptjs path as the standalone `hashPassword()` helper. See [`hash-password/SKILL.md`](../hash-password/SKILL.md) for full bcrypt setup (salt rounds, `npm install bcryptjs`).
64
64
 
65
65
  ### Why declarative wins
66
66
 
@@ -214,7 +214,7 @@ Rule of thumb: transformers are for **pure, deterministic** transforms of the ro
214
214
 
215
215
  ## See also
216
216
 
217
- - [`hash-password/SKILL.md`](../hash-password/SKILL.md) — the bcrypt setup that `useHashedPassword` calls under the hood; salt rounds, `pnpm add bcryptjs`.
217
+ - [`hash-password/SKILL.md`](../hash-password/SKILL.md) — the bcrypt setup that `useHashedPassword` calls under the hood; salt rounds, `npm install bcryptjs`.
218
218
  - [`use-repository/SKILL.md`](../use-repository/SKILL.md) — where `create` / `save` calls happen that trigger the transformers.
219
219
  - [`define-resource/SKILL.md`](../define-resource/SKILL.md) — filtering transformed fields (`password`) out of API responses.
220
220
  - [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — schema files live in `src/app/<module>/models/<entity>/<entity>.model.ts`.
@@ -53,7 +53,7 @@ Five lines do the heavy lifting:
53
53
  4. **`defaultOptions`** — applied to every call (`orderBy`, default `limit`, etc.).
54
54
  5. **`new FaqsRepository()`** singleton — import this everywhere; never instantiate again.
55
55
 
56
- The class is intentionally private — only the singleton escapes the module. Scaffold with `pnpm warlock generate.repository <module>/<entity>`.
56
+ The class is intentionally private — only the singleton escapes the module. Scaffold with `npx warlock generate.repository <module>/<entity>`.
57
57
 
58
58
  ## The `filterBy` rules
59
59
 
@@ -47,7 +47,7 @@ Two pieces, always:
47
47
 
48
48
  No separate `*.request.ts` alias file. `RequestHandler<Request<TSchema>>` types `request.validated()` directly off the schema's inferred type.
49
49
 
50
- Scaffold with `pnpm warlock generate.controller <module>/<action> --with-validation`. If the scaffolder emits a `requests/<action>.request.ts` file, delete it — the inline pattern is the convention.
50
+ Scaffold with `npx warlock generate.controller <module>/<action> --with-validation`. If the scaffolder emits a `requests/<action>.request.ts` file, delete it — the inline pattern is the convention.
51
51
 
52
52
  ## The `v.*` factory surface
53
53
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: warlock-doctor
3
- description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `pnpm warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
3
+ description: 'Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `npx warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.'
4
4
  ---
5
5
 
6
6
  # Warlock — `warlock doctor`
@@ -8,7 +8,7 @@ description: 'Run `warlock doctor` — a read-only diagnostics command that chec
8
8
  `warlock doctor` is a read-only preflight. It boots the app far enough to introspect it — loads every config file and bootstrap code so routes and connectors register — but **starts no connectors**, so it never opens a database, cache, or socket connection. It then runs a set of checks and prints a grouped pass / warn / fail report.
9
9
 
10
10
  ```bash
11
- pnpm warlock doctor
11
+ npx warlock doctor
12
12
  ```
13
13
 
14
14
  ```
@@ -111,7 +111,7 @@ if (report.hasFailures) process.exit(report.exitCode);
111
111
  ### CI / pre-release gate
112
112
 
113
113
  ```bash
114
- pnpm warlock doctor || exit 1 # non-zero exit fails the job
114
+ npx warlock doctor || exit 1 # non-zero exit fails the job
115
115
  ```
116
116
 
117
117
  A red `release-hygiene` line catches the classic "bumped `package.json` but forgot the CHANGELOG heading" mistake before a publish.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: warlock-routes
3
- description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`''s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `pnpm warlock routes` / `pnpm warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
3
+ description: 'Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`''s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `npx warlock routes` / `npx warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.'
4
4
  ---
5
5
 
6
6
  # Warlock — `warlock routes`
@@ -8,7 +8,7 @@ description: 'Run `warlock routes` — a read-only command that lists the regist
8
8
  `warlock routes` lists every registered HTTP route as a table. It's the read-only sibling of [`warlock doctor`](../warlock-doctor/SKILL.md): it boots the app far enough to register route modules — but **starts no connectors**, so it never opens a database, cache, or socket connection.
9
9
 
10
10
  ```bash
11
- pnpm warlock routes
11
+ npx warlock routes
12
12
  ```
13
13
 
14
14
  ```
@@ -39,10 +39,10 @@ The `METHOD` column is verb-colored (GET green, POST blue, PUT/PATCH yellow, DEL
39
39
  Optional, case-insensitive, AND-combined:
40
40
 
41
41
  ```bash
42
- pnpm warlock routes --method GET # -m exact HTTP method
43
- pnpm warlock routes --path /users # -p path substring
44
- pnpm warlock routes --name users # -n route-name substring
45
- pnpm warlock routes -m POST -p /users
42
+ npx warlock routes --method GET # -m exact HTTP method
43
+ npx warlock routes --path /users # -p path substring
44
+ npx warlock routes --name users # -n route-name substring
45
+ npx warlock routes -m POST -p /users
46
46
  ```
47
47
 
48
48
  ## JSON output
@@ -50,7 +50,7 @@ pnpm warlock routes -m POST -p /users
50
50
  `--json` (`-j`) emits the normalized rows instead of the table — for `jq`, a CI diff, or a generated API map. Filters apply before serialization.
51
51
 
52
52
  ```bash
53
- pnpm warlock routes --json
53
+ npx warlock routes --json
54
54
  ```
55
55
 
56
56
  ```json
@@ -64,7 +64,7 @@ pnpm warlock routes --json
64
64
  ### Audit which routes are guarded
65
65
 
66
66
  ```bash
67
- pnpm warlock routes --json | jq '[.[] | select(.middleware == 0)]'
67
+ npx warlock routes --json | jq '[.[] | select(.middleware == 0)]'
68
68
  ```
69
69
 
70
70
  Surfaces public routes (no middleware) — a quick check that auth-protected paths actually carry a guard.
@@ -72,7 +72,7 @@ Surfaces public routes (no middleware) — a quick check that auth-protected pat
72
72
  ### Confirm a route registered
73
73
 
74
74
  ```bash
75
- pnpm warlock routes --name users.create
75
+ npx warlock routes --name users.create
76
76
  ```
77
77
 
78
78
  An empty result means the route isn't registered — re-run `warlock dev` and read the boot error (the route-module loader is fail-loud, so a throwing route file aborts boot rather than being silently dropped).
@@ -82,7 +82,7 @@ An empty result means the route isn't registered — re-run `warlock dev` and re
82
82
  Compares the **live dev-server page routes** (`router.list().filter(r => r.isPage)`) against a **snapshot written by the last successful `warlock build`** (`page-routes.manifest.json` in `resolveBuildConfig().outdir`, e.g. `dist/page-routes.manifest.json`). Boots the same diagnostic way as `warlock routes` — route modules registered, no connectors started — then diffs.
83
83
 
84
84
  ```bash
85
- pnpm warlock routes:diff
85
+ npx warlock routes:diff
86
86
  ```
87
87
 
88
88
  ```
@@ -39,7 +39,7 @@ export default command({
39
39
  });
40
40
  ```
41
41
 
42
- Run it: `pnpm warlock users.promote --email=hasan@example.com` (or `pnpm warlock up -e hasan@example.com`).
42
+ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock up -e hasan@example.com`).
43
43
 
44
44
  ## `CLICommandOptions` — the factory input
45
45
 
@@ -36,12 +36,12 @@ export default seeder({
36
36
  Run them:
37
37
 
38
38
  ```bash
39
- pnpm warlock seed # discover + run all
40
- pnpm warlock seed --list # show registry, don't run
41
- pnpm warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
42
- pnpm warlock seed --fresh # truncate every table first, then run all
43
- pnpm warlock seed --drop # undo every tracked record, reset the log
44
- pnpm warlock seed --drop=default-roles # undo just one seeder's records
39
+ npx warlock seed # discover + run all
40
+ npx warlock seed --list # show registry, don't run
41
+ npx warlock seed --path=src/app/roles/seeds/default-roles.seed.ts # one file
42
+ npx warlock seed --fresh # truncate every table first, then run all
43
+ npx warlock seed --drop # undo every tracked record, reset the log
44
+ npx warlock seed --drop=default-roles # undo just one seeder's records
45
45
  ```
46
46
 
47
47
  `--fresh` truncates **every** table in the DB (`datasource.driver.truncateTable(table, { cascade: true })`), including the `seeds` tracking table. After `--fresh`, `once: true` seeds will run again.
@@ -179,8 +179,8 @@ Every record you `track()` is written to a `seed_records` table (created on firs
179
179
  `warlock seed --drop` reads those refs and undoes the seed:
180
180
 
181
181
  ```bash
182
- pnpm warlock seed --drop # undo every tracked record across all seeders
183
- pnpm warlock seed --drop=default-roles # undo just one seeder's records
182
+ npx warlock seed --drop # undo every tracked record across all seeders
183
+ npx warlock seed --drop=default-roles # undo just one seeder's records
184
184
  ```
185
185
 
186
186
  What it does, inside a single transaction: