create-warlock 5.0.2 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +75 -0
  3. package/esm/commands/create-new-app/get-app-path.mjs +1 -1
  4. package/esm/commands/create-new-app/index.mjs +1 -1
  5. package/esm/commands/create-new-app/index.mjs.map +1 -1
  6. package/esm/commands/create-new-app/types.d.mts +3 -1
  7. package/esm/commands/create-warlock-app/index.mjs.map +1 -1
  8. package/esm/features/database-drivers.mjs.map +1 -1
  9. package/esm/features/features-map.mjs +12 -0
  10. package/esm/features/features-map.mjs.map +1 -1
  11. package/esm/helpers/app.mjs +10 -2
  12. package/esm/helpers/app.mjs.map +1 -1
  13. package/esm/helpers/exec.mjs.map +1 -1
  14. package/esm/helpers/package-manager.mjs +1 -1
  15. package/esm/helpers/package-manager.mjs.map +1 -1
  16. package/esm/helpers/project-builder-helpers.mjs +1 -1
  17. package/esm/helpers/warlock-versions.mjs.map +1 -1
  18. package/esm/index.d.mts +7 -1
  19. package/esm/index.mjs +51 -1
  20. package/esm/index.mjs.map +1 -1
  21. package/llms-full.txt +3 -1
  22. package/package.json +2 -2
  23. package/skills/create-a-warlock-project/SKILL.md +4 -2
  24. package/templates/warlock/package.json +9 -9
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +5 -2
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +7 -2
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -2
  28. package/templates/warlock/src/app/auth/main.ts +10 -0
  29. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +9 -0
  30. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +1 -1
  31. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
  32. package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
  33. package/templates/warlock/src/app/users/services/login-social.ts +8 -2
  34. package/templates/warlock/src/config/cache.ts +16 -6
  35. package/templates/warlock/src/typings.d.ts +82 -0
  36. package/templates/warlock/tsconfig.json +12 -3
  37. package/templates/warlock/yarn.lock +0 -2332
@@ -1 +1 @@
1
- {"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/package-manager.ts"],"sourcesContent":["import { exec, execSync } from \"child_process\";\r\nimport { promisify } from \"util\";\r\nimport detectPackageManager from \"which-pm-runs\";\r\n\r\nconst execAsync = promisify(exec);\r\n\r\nlet detectedPackageManager: string | undefined;\r\nlet cachedSystemManagers: string[] | undefined;\r\nlet cachedPreferredManager: string | undefined;\r\n\r\n/**\r\n * The only package managers `--pm` may select. This value reaches `spawn()`\r\n * as the executable to run and is spliced into the generated\r\n * `package.json`'s scripts — an allow-list here is a hard security\r\n * boundary, not just input hygiene, so it stays a fixed literal list\r\n * rather than anything derived from user input or the running environment.\r\n */\r\nexport const ALLOWED_PACKAGE_MANAGERS = [\"npm\", \"yarn\", \"pnpm\", \"bun\"] as const;\r\n\r\nexport type AllowedPackageManager = (typeof ALLOWED_PACKAGE_MANAGERS)[number];\r\n\r\n/** Whether `value` is one of the allow-listed package managers. */\r\nexport function isValidPackageManager(\r\n value: string,\r\n): value is AllowedPackageManager {\r\n return (ALLOWED_PACKAGE_MANAGERS as readonly string[]).includes(value);\r\n}\r\n\r\nexport function getPackageManager() {\r\n if (detectedPackageManager) {\r\n return detectedPackageManager;\r\n }\r\n\r\n return getPreferredPackageManager();\r\n}\r\n\r\n/**\r\n * Check if a package manager is installed\r\n */\r\nfunction isInstalled(manager: string): boolean {\r\n try {\r\n execSync(`${manager} --version`, { stdio: \"ignore\" });\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Check if a package manager is installed (async)\r\n */\r\nasync function checkManager(manager: string): Promise<boolean> {\r\n try {\r\n await execAsync(`${manager} --version`);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Detect available package managers asynchronously and cache results\r\n */\r\nexport async function detectPackageManagers() {\r\n const managers = [\"npm\"];\r\n const checks = [checkManager(\"yarn\"), checkManager(\"pnpm\")];\r\n\r\n const [hasYarn, hasPnpm] = await Promise.all(checks);\r\n\r\n if (hasYarn) managers.push(\"yarn\");\r\n if (hasPnpm) managers.push(\"pnpm\");\r\n\r\n cachedSystemManagers = managers;\r\n\r\n // Determine preference\r\n const runningPm = detectPackageManager()?.name;\r\n if (runningPm && runningPm !== \"npm\") {\r\n cachedPreferredManager = runningPm;\r\n } else if (hasYarn) {\r\n cachedPreferredManager = \"yarn\";\r\n } else if (hasPnpm) {\r\n cachedPreferredManager = \"pnpm\";\r\n } else {\r\n cachedPreferredManager = \"npm\";\r\n }\r\n}\r\n\r\n/**\r\n * Get available package managers on the system\r\n */\r\nexport function getSystemPackageManagers(): string[] {\r\n if (cachedSystemManagers) return cachedSystemManagers;\r\n\r\n const managers = [\"npm\"]; // npm is assumed to be always available\r\n\r\n if (isInstalled(\"yarn\")) {\r\n managers.push(\"yarn\");\r\n }\r\n\r\n if (isInstalled(\"pnpm\")) {\r\n managers.push(\"pnpm\");\r\n }\r\n\r\n return managers;\r\n}\r\n\r\n/**\r\n * Get the preferred package manager based on priority\r\n */\r\nexport function getPreferredPackageManager(): string {\r\n if (cachedPreferredManager) return cachedPreferredManager;\r\n\r\n // Priority 1: The manager currently running the script\r\n const runningPm = detectPackageManager()?.name;\r\n if (runningPm && runningPm !== \"npm\") return runningPm;\r\n\r\n // Priority 2: Yarn (if installed)\r\n if (isInstalled(\"yarn\")) return \"yarn\";\r\n\r\n // Priority 3: pnpm (if installed)\r\n if (isInstalled(\"pnpm\")) return \"pnpm\";\r\n\r\n // Priority 4: npm (default)\r\n return \"npm\";\r\n}\r\n\r\nexport function setPackageManager(packageManager: string) {\r\n detectedPackageManager = packageManager;\r\n}\r\n\r\nexport function installCommand() {\r\n return `${getPackageManager()} install`;\r\n}\r\n\r\nexport function startCommand() {\r\n if (getPackageManager() === \"npm\") return \"npm run dev\";\r\n\r\n return `${getPackageManager()} dev`;\r\n}\r\n\r\nexport function runPackageManagerCommand(command: string) {\r\n const packageManager = getPackageManager();\r\n\r\n if (packageManager === \"npm\") return `npm run ${command}`;\r\n\r\n return `${packageManager} ${command}`;\r\n}\r\n"],"mappings":";;;;;AAIA,MAAM,YAAY,UAAU,IAAI;AAEhC,IAAI;AACJ,IAAI;AACJ,IAAI;;;;;;;;AASJ,MAAa,2BAA2B;CAAC;CAAO;CAAQ;CAAQ;AAAK;;AAKrE,SAAgB,sBACd,OACgC;CAChC,OAAQ,yBAA+C,SAAS,KAAK;AACvE;AAEA,SAAgB,oBAAoB;CAClC,IAAI,wBACF,OAAO;CAGT,OAAO,2BAA2B;AACpC;;;;AAKA,SAAS,YAAY,SAA0B;CAC7C,IAAI;EACF,SAAS,GAAG,QAAQ,aAAa,EAAE,OAAO,SAAS,CAAC;EACpD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;AAKA,eAAe,aAAa,SAAmC;CAC7D,IAAI;EACF,MAAM,UAAU,GAAG,QAAQ,WAAW;EACtC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;AAKA,eAAsB,wBAAwB;CAC5C,MAAM,WAAW,CAAC,KAAK;CACvB,MAAM,SAAS,CAAC,aAAa,MAAM,GAAG,aAAa,MAAM,CAAC;CAE1D,MAAM,CAAC,SAAS,WAAW,MAAM,QAAQ,IAAI,MAAM;CAEnD,IAAI,SAAS,SAAS,KAAK,MAAM;CACjC,IAAI,SAAS,SAAS,KAAK,MAAM;CAEjC,uBAAuB;CAGvB,MAAM,YAAY,qBAAqB,CAAC,EAAE;CAC1C,IAAI,aAAa,cAAc,OAC7B,yBAAyB;MACpB,IAAI,SACT,yBAAyB;MACpB,IAAI,SACT,yBAAyB;MAEzB,yBAAyB;AAE7B;;;;AAKA,SAAgB,2BAAqC;CACnD,IAAI,sBAAsB,OAAO;CAEjC,MAAM,WAAW,CAAC,KAAK;CAEvB,IAAI,YAAY,MAAM,GACpB,SAAS,KAAK,MAAM;CAGtB,IAAI,YAAY,MAAM,GACpB,SAAS,KAAK,MAAM;CAGtB,OAAO;AACT;;;;AAKA,SAAgB,6BAAqC;CACnD,IAAI,wBAAwB,OAAO;CAGnC,MAAM,YAAY,qBAAqB,CAAC,EAAE;CAC1C,IAAI,aAAa,cAAc,OAAO,OAAO;CAG7C,IAAI,YAAY,MAAM,GAAG,OAAO;CAGhC,IAAI,YAAY,MAAM,GAAG,OAAO;CAGhC,OAAO;AACT;AAEA,SAAgB,kBAAkB,gBAAwB;CACxD,yBAAyB;AAC3B;AAYA,SAAgB,yBAAyB,SAAiB;CACxD,MAAM,iBAAiB,kBAAkB;CAEzC,IAAI,mBAAmB,OAAO,OAAO,WAAW;CAEhD,OAAO,GAAG,eAAe,GAAG;AAC9B"}
1
+ {"version":3,"file":"package-manager.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/package-manager.ts"],"sourcesContent":["import { exec, execSync } from \"child_process\";\r\nimport { promisify } from \"util\";\r\nimport detectPackageManager from \"which-pm-runs\";\r\n\r\nconst execAsync = promisify(exec);\r\n\r\nlet detectedPackageManager: string | undefined;\r\nlet cachedSystemManagers: string[] | undefined;\r\nlet cachedPreferredManager: string | undefined;\r\n\r\n/**\r\n * The only package managers `--pm` may select. This value reaches `spawn()`\r\n * as the executable to run and is spliced into the generated\r\n * `package.json`'s scripts — an allow-list here is a hard security\r\n * boundary, not just input hygiene, so it stays a fixed literal list\r\n * rather than anything derived from user input or the running environment.\r\n */\r\nexport const ALLOWED_PACKAGE_MANAGERS = [\"npm\", \"yarn\", \"pnpm\", \"bun\"] as const;\r\n\r\nexport type AllowedPackageManager = (typeof ALLOWED_PACKAGE_MANAGERS)[number];\r\n\r\n/** Whether `value` is one of the allow-listed package managers. */\r\nexport function isValidPackageManager(\r\n value: string,\r\n): value is AllowedPackageManager {\r\n return (ALLOWED_PACKAGE_MANAGERS as readonly string[]).includes(value);\r\n}\r\n\r\nexport function getPackageManager() {\r\n if (detectedPackageManager) {\r\n return detectedPackageManager;\r\n }\r\n\r\n return getPreferredPackageManager();\r\n}\r\n\r\n/**\r\n * Check if a package manager is installed\r\n */\r\nfunction isInstalled(manager: string): boolean {\r\n try {\r\n execSync(`${manager} --version`, { stdio: \"ignore\" });\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Check if a package manager is installed (async)\r\n */\r\nasync function checkManager(manager: string): Promise<boolean> {\r\n try {\r\n await execAsync(`${manager} --version`);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Detect available package managers asynchronously and cache results\r\n */\r\nexport async function detectPackageManagers() {\r\n const managers = [\"npm\"];\r\n const checks = [checkManager(\"yarn\"), checkManager(\"pnpm\")];\r\n\r\n const [hasYarn, hasPnpm] = await Promise.all(checks);\r\n\r\n if (hasYarn) managers.push(\"yarn\");\r\n if (hasPnpm) managers.push(\"pnpm\");\r\n\r\n cachedSystemManagers = managers;\r\n\r\n // Determine preference\r\n const runningPm = detectPackageManager()?.name;\r\n if (runningPm && runningPm !== \"npm\") {\r\n cachedPreferredManager = runningPm;\r\n } else if (hasYarn) {\r\n cachedPreferredManager = \"yarn\";\r\n } else if (hasPnpm) {\r\n cachedPreferredManager = \"pnpm\";\r\n } else {\r\n cachedPreferredManager = \"npm\";\r\n }\r\n}\r\n\r\n/**\r\n * Get available package managers on the system\r\n */\r\nexport function getSystemPackageManagers(): string[] {\r\n if (cachedSystemManagers) return cachedSystemManagers;\r\n\r\n const managers = [\"npm\"]; // npm is assumed to be always available\r\n\r\n if (isInstalled(\"yarn\")) {\r\n managers.push(\"yarn\");\r\n }\r\n\r\n if (isInstalled(\"pnpm\")) {\r\n managers.push(\"pnpm\");\r\n }\r\n\r\n return managers;\r\n}\r\n\r\n/**\r\n * Get the preferred package manager based on priority\r\n */\r\nexport function getPreferredPackageManager(): string {\r\n if (cachedPreferredManager) return cachedPreferredManager;\r\n\r\n // Priority 1: The manager currently running the script\r\n const runningPm = detectPackageManager()?.name;\r\n if (runningPm && runningPm !== \"npm\") return runningPm;\r\n\r\n // Priority 2: pnpm (if installed) — the framework's own package manager, so a\r\n // scaffolded app defaults to the same tooling Warlock itself is developed with.\r\n if (isInstalled(\"pnpm\")) return \"pnpm\";\r\n\r\n // Priority 3: Yarn (if installed)\r\n if (isInstalled(\"yarn\")) return \"yarn\";\r\n\r\n // Priority 4: npm (default)\r\n return \"npm\";\r\n}\r\n\r\nexport function setPackageManager(packageManager: string) {\r\n detectedPackageManager = packageManager;\r\n}\r\n\r\nexport function installCommand() {\r\n return `${getPackageManager()} install`;\r\n}\r\n\r\nexport function startCommand() {\r\n if (getPackageManager() === \"npm\") return \"npm run dev\";\r\n\r\n return `${getPackageManager()} dev`;\r\n}\r\n\r\nexport function runPackageManagerCommand(command: string) {\r\n const packageManager = getPackageManager();\r\n\r\n if (packageManager === \"npm\") return `npm run ${command}`;\r\n\r\n return `${packageManager} ${command}`;\r\n}\r\n"],"mappings":";;;;;AAIA,MAAM,YAAY,UAAU,IAAI;AAEhC,IAAI;AACJ,IAAI;AACJ,IAAI;;;;;;;;AASJ,MAAa,2BAA2B;CAAC;CAAO;CAAQ;CAAQ;AAAK;;AAKrE,SAAgB,sBACd,OACgC;CAChC,OAAQ,yBAA+C,SAAS,KAAK;AACvE;AAEA,SAAgB,oBAAoB;CAClC,IAAI,wBACF,OAAO;CAGT,OAAO,2BAA2B;AACpC;;;;AAKA,SAAS,YAAY,SAA0B;CAC7C,IAAI;EACF,SAAS,GAAG,QAAQ,aAAa,EAAE,OAAO,SAAS,CAAC;EACpD,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;AAKA,eAAe,aAAa,SAAmC;CAC7D,IAAI;EACF,MAAM,UAAU,GAAG,QAAQ,WAAW;EACtC,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;;;AAKA,eAAsB,wBAAwB;CAC5C,MAAM,WAAW,CAAC,KAAK;CACvB,MAAM,SAAS,CAAC,aAAa,MAAM,GAAG,aAAa,MAAM,CAAC;CAE1D,MAAM,CAAC,SAAS,WAAW,MAAM,QAAQ,IAAI,MAAM;CAEnD,IAAI,SAAS,SAAS,KAAK,MAAM;CACjC,IAAI,SAAS,SAAS,KAAK,MAAM;CAEjC,uBAAuB;CAGvB,MAAM,YAAY,qBAAqB,GAAG;CAC1C,IAAI,aAAa,cAAc,OAC7B,yBAAyB;MACpB,IAAI,SACT,yBAAyB;MACpB,IAAI,SACT,yBAAyB;MAEzB,yBAAyB;AAE7B;;;;AAKA,SAAgB,2BAAqC;CACnD,IAAI,sBAAsB,OAAO;CAEjC,MAAM,WAAW,CAAC,KAAK;CAEvB,IAAI,YAAY,MAAM,GACpB,SAAS,KAAK,MAAM;CAGtB,IAAI,YAAY,MAAM,GACpB,SAAS,KAAK,MAAM;CAGtB,OAAO;AACT;;;;AAKA,SAAgB,6BAAqC;CACnD,IAAI,wBAAwB,OAAO;CAGnC,MAAM,YAAY,qBAAqB,GAAG;CAC1C,IAAI,aAAa,cAAc,OAAO,OAAO;CAI7C,IAAI,YAAY,MAAM,GAAG,OAAO;CAGhC,IAAI,YAAY,MAAM,GAAG,OAAO;CAGhC,OAAO;AACT;AAEA,SAAgB,kBAAkB,gBAAwB;CACxD,yBAAyB;AAC3B;AAYA,SAAgB,yBAAyB,SAAiB;CACxD,MAAM,iBAAiB,kBAAkB;CAEzC,IAAI,mBAAmB,OAAO,OAAO,WAAW;CAEhD,OAAO,GAAG,eAAe,GAAG;AAC9B"}
@@ -1,9 +1,9 @@
1
1
  import { executeCommand } from "./exec.mjs";
2
2
  import "./package-manager.mjs";
3
3
  import "./paths.mjs";
4
+ import { copyDirectory, getFile, getJsonFile, putFile, putJsonFile, renameFile } from "@warlock.js/fs";
4
5
  import "@clack/prompts";
5
6
  import { colors } from "@mongez/copper";
6
- import { copyDirectory, getFile, getJsonFile, putFile, putJsonFile, renameFile } from "@warlock.js/fs";
7
7
  import path from "path";
8
8
 
9
9
  //#region ../create-warlock/src/helpers/project-builder-helpers.ts
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-versions.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/warlock-versions.ts"],"sourcesContent":["/**\n * Resolving the version to stamp onto the generated project's `@warlock.js/*`\n * dependencies.\n *\n * ## Why this file exists\n *\n * The scaffolder used to stamp its OWN version onto every sibling package\n * (`\"@warlock.js/core\": \"4.16.2\"`). That is only correct while the scaffolder's\n * version is published — and it usually is not: the release tooling bumps the\n * source version on every build, including `--no-publish` builds, so between\n * publishes the working tree carries a version that exists nowhere on the\n * registry. Every project scaffolded in that window pinned eight dependencies\n * to a version npm cannot resolve, and the install died with `ETARGET`.\n *\n * ## The rule\n *\n * Never write a dependency version we have not established exists. Resolution\n * order, per package:\n *\n * 1. the scaffolder's own version, IF the registry has it published — this\n * preserves the lockstep guarantee the pin was introduced for;\n * 2. otherwise the registry's `latest` — the newest thing that actually\n * exists, with a note explaining the substitution;\n * 3. otherwise (registry unreachable) a caret range floored to the major,\n * e.g. `^4.0.0` — always satisfiable by any published 4.x, and the\n * scaffold-time install writes a lockfile that freezes the result anyway.\n *\n * A package that resolves to nothing (404 — never published) is reported, not\n * papered over: it is the difference between \"your install failed\" and \"the\n * feature you asked for does not exist yet\".\n */\n\nimport { getJsonFile } from \"@warlock.js/fs\";\nimport { packageRoot, template } from \"./paths\";\n\nexport type VersionSource =\n \"own-version\" | \"registry-latest\" | \"range-fallback\";\n\nexport type ResolvedVersion = {\n package: string;\n /** The exact version or range to write into the generated package.json. */\n version: string;\n source: VersionSource;\n};\n\nexport type VersionResolution = {\n /** package name -> version/range to stamp. */\n versions: Record<string, string>;\n /** Human-readable notes worth showing before the install runs. */\n notes: string[];\n /** Packages the registry does not know about at all. */\n unpublished: string[];\n /** True when the registry could not be reached and ranges were guessed. */\n offline: boolean;\n};\n\nconst REGISTRY_TIMEOUT_MS = 6_000;\n\n/**\n * Registry to query. `npm_config_registry` is set by npm/npx when the\n * scaffolder runs through them, so a private mirror is honoured for free.\n */\nfunction registryUrl(): string {\n const registry =\n process.env.npm_config_registry?.trim() || \"https://registry.npmjs.org\";\n\n return registry.replace(/\\/+$/, \"\");\n}\n\n/** `4.16.2` -> `^4.0.0`; anything unparseable -> `latest`. */\nexport function fallbackRange(version: string): string {\n const major = /^\\s*v?(\\d+)\\./.exec(version)?.[1];\n\n return major ? `^${major}.0.0` : \"latest\";\n}\n\ntype Packument = {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n};\n\n/**\n * Fetch the abbreviated packument for a package. Returns `undefined` when the\n * registry cannot be reached (network / timeout) and `null` when the registry\n * answers that the package does not exist.\n */\nasync function fetchPackument(\n packageName: string,\n): Promise<Packument | null | undefined> {\n const url = `${registryUrl()}/${packageName.replace(\"/\", \"%2F\")}`;\n\n try {\n const response = await fetch(url, {\n headers: { accept: \"application/vnd.npm.install-v1+json\" },\n signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),\n });\n\n // 404 (public) and 401 (scoped package the registry hides) both mean the\n // same thing to us: there is nothing here to install.\n if (response.status === 404 || response.status === 401) return null;\n\n if (!response.ok) return undefined;\n\n return (await response.json()) as Packument;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve one package against the registry. Pure aside from the fetch, so the\n * decision table above is readable in one place.\n */\nasync function resolvePackage(\n packageName: string,\n ownVersion: string,\n): Promise<ResolvedVersion & { published: boolean; reachable: boolean }> {\n const packument = await fetchPackument(packageName);\n\n if (packument === undefined) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: false,\n };\n }\n\n if (packument === null) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: false,\n reachable: true,\n };\n }\n\n if (packument.versions?.[ownVersion]) {\n return {\n package: packageName,\n version: ownVersion,\n source: \"own-version\",\n published: true,\n reachable: true,\n };\n }\n\n const latest = packument[\"dist-tags\"]?.latest;\n\n if (latest) {\n return {\n package: packageName,\n version: latest,\n source: \"registry-latest\",\n published: true,\n reachable: true,\n };\n }\n\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: true,\n };\n}\n\n/**\n * Every `@warlock.js/*` dependency the template declares. Read from the\n * template rather than the copied project so resolution can start before (or\n * in parallel with) the copy.\n */\nexport function templateWarlockDependencies(): string[] {\n const templatePackageJson = getJsonFile(\n `${template(\"warlock\")}/package.json`,\n ) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n\n const names = new Set<string>();\n\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n for (const name of Object.keys(templatePackageJson[field] ?? {})) {\n if (name.startsWith(\"@warlock.js/\")) names.add(name);\n }\n }\n\n return [...names];\n}\n\n/** The scaffolder's own published version, i.e. the lockstep candidate. */\nexport function scaffolderVersion(): string {\n return (getJsonFile(packageRoot(\"package.json\")) as { version: string })\n .version;\n}\n\nlet cached: Promise<VersionResolution> | undefined;\n\n/**\n * Resolve the versions to stamp, once per process.\n *\n * Never throws and never blocks a scaffold: the worst case is the caret\n * fallback plus a note saying so.\n */\nexport function resolveWarlockVersions(\n packages: string[] = templateWarlockDependencies(),\n ownVersion: string = scaffolderVersion(),\n): Promise<VersionResolution> {\n if (cached) return cached;\n\n cached = (async (): Promise<VersionResolution> => {\n const resolutions = await Promise.all(\n packages.map(name => resolvePackage(name, ownVersion)),\n );\n\n const versions: Record<string, string> = {};\n const notes: string[] = [];\n const unpublished: string[] = [];\n const substituted: ResolvedVersion[] = [];\n\n let offline = false;\n\n for (const resolution of resolutions) {\n versions[resolution.package] = resolution.version;\n\n if (!resolution.reachable) offline = true;\n if (!resolution.published) unpublished.push(resolution.package);\n if (resolution.source === \"registry-latest\") substituted.push(resolution);\n }\n\n if (offline) {\n notes.push(\n `Could not reach the npm registry — pinning @warlock.js/* to ${fallbackRange(ownVersion)} instead of an exact version.`,\n );\n }\n\n if (substituted.length > 0) {\n const latest = substituted[0].version;\n\n notes.push(\n `create-warlock ${ownVersion} is not published yet — pinning @warlock.js/* to the latest published version (${latest}).`,\n );\n }\n\n if (unpublished.length > 0) {\n notes.push(\n `Not published on the registry: ${unpublished.join(\", \")} — the install will fail until they are released.`,\n );\n }\n\n return { versions, notes, unpublished, offline };\n })();\n\n return cached;\n}\n\n/** Test/CLI seam: forget the memoized resolution. */\nexport function resetWarlockVersionsCache() {\n cached = undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAM,sBAAsB;;;;;AAM5B,SAAS,cAAsB;CAI7B,QAFE,QAAQ,IAAI,qBAAqB,KAAK,KAAK,6BAE9B,CAAC,QAAQ,QAAQ,EAAE;AACpC;;AAGA,SAAgB,cAAc,SAAyB;CACrD,MAAM,QAAQ,gBAAgB,KAAK,OAAO,CAAC,GAAG;CAE9C,OAAO,QAAQ,IAAI,MAAM,QAAQ;AACnC;;;;;;AAYA,eAAe,eACb,aACuC;CACvC,MAAM,MAAM,GAAG,YAAY,EAAE,GAAG,YAAY,QAAQ,KAAK,KAAK;CAE9D,IAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,SAAS,EAAE,QAAQ,sCAAsC;GACzD,QAAQ,YAAY,QAAQ,mBAAmB;EACjD,CAAC;EAID,IAAI,SAAS,WAAW,OAAO,SAAS,WAAW,KAAK,OAAO;EAE/D,IAAI,CAAC,SAAS,IAAI,OAAO;EAEzB,OAAQ,MAAM,SAAS,KAAK;CAC9B,QAAQ;EACN;CACF;AACF;;;;;AAMA,eAAe,eACb,aACA,YACuE;CACvE,MAAM,YAAY,MAAM,eAAe,WAAW;CAElD,IAAI,cAAc,QAChB,OAAO;EACL,SAAS;EACT,SAAS,cAAc,UAAU;EACjC,QAAQ;EACR,WAAW;EACX,WAAW;CACb;CAGF,IAAI,cAAc,MAChB,OAAO;EACL,SAAS;EACT,SAAS,cAAc,UAAU;EACjC,QAAQ;EACR,WAAW;EACX,WAAW;CACb;CAGF,IAAI,UAAU,WAAW,aACvB,OAAO;EACL,SAAS;EACT,SAAS;EACT,QAAQ;EACR,WAAW;EACX,WAAW;CACb;CAGF,MAAM,SAAS,UAAU,YAAY,EAAE;CAEvC,IAAI,QACF,OAAO;EACL,SAAS;EACT,SAAS;EACT,QAAQ;EACR,WAAW;EACX,WAAW;CACb;CAGF,OAAO;EACL,SAAS;EACT,SAAS,cAAc,UAAU;EACjC,QAAQ;EACR,WAAW;EACX,WAAW;CACb;AACF;;;;;;AAOA,SAAgB,8BAAwC;CACtD,MAAM,sBAAsB,YAC1B,GAAG,SAAS,SAAS,EAAE,cACzB;CAKA,MAAM,wBAAQ,IAAI,IAAY;CAE9B,KAAK,MAAM,SAAS,CAAC,gBAAgB,iBAAiB,GACpD,KAAK,MAAM,QAAQ,OAAO,KAAK,oBAAoB,UAAU,CAAC,CAAC,GAC7D,IAAI,KAAK,WAAW,cAAc,GAAG,MAAM,IAAI,IAAI;CAIvD,OAAO,CAAC,GAAG,KAAK;AAClB;;AAGA,SAAgB,oBAA4B;CAC1C,OAAQ,YAAY,YAAY,cAAc,CAAC,CAAC,CAC7C;AACL;AAEA,IAAI;;;;;;;AAQJ,SAAgB,uBACd,WAAqB,4BAA4B,GACjD,aAAqB,kBAAkB,GACX;CAC5B,IAAI,QAAQ,OAAO;CAEnB,UAAU,YAAwC;EAChD,MAAM,cAAc,MAAM,QAAQ,IAChC,SAAS,KAAI,SAAQ,eAAe,MAAM,UAAU,CAAC,CACvD;EAEA,MAAM,WAAmC,CAAC;EAC1C,MAAM,QAAkB,CAAC;EACzB,MAAM,cAAwB,CAAC;EAC/B,MAAM,cAAiC,CAAC;EAExC,IAAI,UAAU;EAEd,KAAK,MAAM,cAAc,aAAa;GACpC,SAAS,WAAW,WAAW,WAAW;GAE1C,IAAI,CAAC,WAAW,WAAW,UAAU;GACrC,IAAI,CAAC,WAAW,WAAW,YAAY,KAAK,WAAW,OAAO;GAC9D,IAAI,WAAW,WAAW,mBAAmB,YAAY,KAAK,UAAU;EAC1E;EAEA,IAAI,SACF,MAAM,KACJ,+DAA+D,cAAc,UAAU,EAAE,8BAC3F;EAGF,IAAI,YAAY,SAAS,GAAG;GAC1B,MAAM,SAAS,YAAY,EAAE,CAAC;GAE9B,MAAM,KACJ,kBAAkB,WAAW,iFAAiF,OAAO,GACvH;EACF;EAEA,IAAI,YAAY,SAAS,GACvB,MAAM,KACJ,kCAAkC,YAAY,KAAK,IAAI,EAAE,kDAC3D;EAGF,OAAO;GAAE;GAAU;GAAO;GAAa;EAAQ;CACjD,EAAC,CAAE;CAEH,OAAO;AACT"}
1
+ {"version":3,"file":"warlock-versions.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/warlock-versions.ts"],"sourcesContent":["/**\n * Resolving the version to stamp onto the generated project's `@warlock.js/*`\n * dependencies.\n *\n * ## Why this file exists\n *\n * The scaffolder used to stamp its OWN version onto every sibling package\n * (`\"@warlock.js/core\": \"4.16.2\"`). That is only correct while the scaffolder's\n * version is published — and it usually is not: the release tooling bumps the\n * source version on every build, including `--no-publish` builds, so between\n * publishes the working tree carries a version that exists nowhere on the\n * registry. Every project scaffolded in that window pinned eight dependencies\n * to a version npm cannot resolve, and the install died with `ETARGET`.\n *\n * ## The rule\n *\n * Never write a dependency version we have not established exists. Resolution\n * order, per package:\n *\n * 1. the scaffolder's own version, IF the registry has it published — this\n * preserves the lockstep guarantee the pin was introduced for;\n * 2. otherwise the registry's `latest` — the newest thing that actually\n * exists, with a note explaining the substitution;\n * 3. otherwise (registry unreachable) a caret range floored to the major,\n * e.g. `^4.0.0` — always satisfiable by any published 4.x, and the\n * scaffold-time install writes a lockfile that freezes the result anyway.\n *\n * A package that resolves to nothing (404 — never published) is reported, not\n * papered over: it is the difference between \"your install failed\" and \"the\n * feature you asked for does not exist yet\".\n */\n\nimport { getJsonFile } from \"@warlock.js/fs\";\nimport { packageRoot, template } from \"./paths\";\n\nexport type VersionSource =\n \"own-version\" | \"registry-latest\" | \"range-fallback\";\n\nexport type ResolvedVersion = {\n package: string;\n /** The exact version or range to write into the generated package.json. */\n version: string;\n source: VersionSource;\n};\n\nexport type VersionResolution = {\n /** package name -> version/range to stamp. */\n versions: Record<string, string>;\n /** Human-readable notes worth showing before the install runs. */\n notes: string[];\n /** Packages the registry does not know about at all. */\n unpublished: string[];\n /** True when the registry could not be reached and ranges were guessed. */\n offline: boolean;\n};\n\nconst REGISTRY_TIMEOUT_MS = 6_000;\n\n/**\n * Registry to query. `npm_config_registry` is set by npm/npx when the\n * scaffolder runs through them, so a private mirror is honoured for free.\n */\nfunction registryUrl(): string {\n const registry =\n process.env.npm_config_registry?.trim() || \"https://registry.npmjs.org\";\n\n return registry.replace(/\\/+$/, \"\");\n}\n\n/** `4.16.2` -> `^4.0.0`; anything unparseable -> `latest`. */\nexport function fallbackRange(version: string): string {\n const major = /^\\s*v?(\\d+)\\./.exec(version)?.[1];\n\n return major ? `^${major}.0.0` : \"latest\";\n}\n\ntype Packument = {\n \"dist-tags\"?: Record<string, string>;\n versions?: Record<string, unknown>;\n};\n\n/**\n * Fetch the abbreviated packument for a package. Returns `undefined` when the\n * registry cannot be reached (network / timeout) and `null` when the registry\n * answers that the package does not exist.\n */\nasync function fetchPackument(\n packageName: string,\n): Promise<Packument | null | undefined> {\n const url = `${registryUrl()}/${packageName.replace(\"/\", \"%2F\")}`;\n\n try {\n const response = await fetch(url, {\n headers: { accept: \"application/vnd.npm.install-v1+json\" },\n signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),\n });\n\n // 404 (public) and 401 (scoped package the registry hides) both mean the\n // same thing to us: there is nothing here to install.\n if (response.status === 404 || response.status === 401) return null;\n\n if (!response.ok) return undefined;\n\n return (await response.json()) as Packument;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolve one package against the registry. Pure aside from the fetch, so the\n * decision table above is readable in one place.\n */\nasync function resolvePackage(\n packageName: string,\n ownVersion: string,\n): Promise<ResolvedVersion & { published: boolean; reachable: boolean }> {\n const packument = await fetchPackument(packageName);\n\n if (packument === undefined) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: false,\n };\n }\n\n if (packument === null) {\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: false,\n reachable: true,\n };\n }\n\n if (packument.versions?.[ownVersion]) {\n return {\n package: packageName,\n version: ownVersion,\n source: \"own-version\",\n published: true,\n reachable: true,\n };\n }\n\n const latest = packument[\"dist-tags\"]?.latest;\n\n if (latest) {\n return {\n package: packageName,\n version: latest,\n source: \"registry-latest\",\n published: true,\n reachable: true,\n };\n }\n\n return {\n package: packageName,\n version: fallbackRange(ownVersion),\n source: \"range-fallback\",\n published: true,\n reachable: true,\n };\n}\n\n/**\n * Every `@warlock.js/*` dependency the template declares. Read from the\n * template rather than the copied project so resolution can start before (or\n * in parallel with) the copy.\n */\nexport function templateWarlockDependencies(): string[] {\n const templatePackageJson = getJsonFile(\n `${template(\"warlock\")}/package.json`,\n ) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n\n const names = new Set<string>();\n\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\n for (const name of Object.keys(templatePackageJson[field] ?? {})) {\n if (name.startsWith(\"@warlock.js/\")) names.add(name);\n }\n }\n\n return [...names];\n}\n\n/** The scaffolder's own published version, i.e. the lockstep candidate. */\nexport function scaffolderVersion(): string {\n return (getJsonFile(packageRoot(\"package.json\")) as { version: string })\n .version;\n}\n\nlet cached: Promise<VersionResolution> | undefined;\n\n/**\n * Resolve the versions to stamp, once per process.\n *\n * Never throws and never blocks a scaffold: the worst case is the caret\n * fallback plus a note saying so.\n */\nexport function resolveWarlockVersions(\n packages: string[] = templateWarlockDependencies(),\n ownVersion: string = scaffolderVersion(),\n): Promise<VersionResolution> {\n if (cached) return cached;\n\n cached = (async (): Promise<VersionResolution> => {\n const resolutions = await Promise.all(\n packages.map(name => resolvePackage(name, ownVersion)),\n );\n\n const versions: Record<string, string> = {};\n const notes: string[] = [];\n const unpublished: string[] = [];\n const substituted: ResolvedVersion[] = [];\n\n let offline = false;\n\n for (const resolution of resolutions) {\n versions[resolution.package] = resolution.version;\n\n if (!resolution.reachable) offline = true;\n if (!resolution.published) unpublished.push(resolution.package);\n if (resolution.source === \"registry-latest\") substituted.push(resolution);\n }\n\n if (offline) {\n notes.push(\n `Could not reach the npm registry — pinning @warlock.js/* to ${fallbackRange(ownVersion)} instead of an exact version.`,\n );\n }\n\n if (substituted.length > 0) {\n const latest = substituted[0].version;\n\n notes.push(\n `create-warlock ${ownVersion} is not published yet — pinning @warlock.js/* to the latest published version (${latest}).`,\n );\n }\n\n if (unpublished.length > 0) {\n notes.push(\n `Not published on the registry: ${unpublished.join(\", \")} — the install will fail until they are released.`,\n );\n }\n\n return { versions, notes, unpublished, offline };\n })();\n\n return cached;\n}\n\n/** Test/CLI seam: forget the memoized resolution. */\nexport function resetWarlockVersionsCache() {\n cached = undefined;\n}\n"],"mappings":"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"}
package/esm/index.d.mts CHANGED
@@ -8,7 +8,13 @@ import { CliFlags } from "./commands/create-new-app/types.mjs";
8
8
  * create-warlock my-app --db=postgres --features=test,herald --ai=openai,anthropic --yes
9
9
  */
10
10
  declare function parseFlags(argv: string[]): CliFlags;
11
+ /**
12
+ * Read this package's own `version` field. A plain JSON read — no network
13
+ * call, no write, nothing else touched — so `--version` stays a pure,
14
+ * side-effect-free exit.
15
+ */
16
+ declare function packageVersion(): string;
11
17
  declare function createApp(): void;
12
18
  //#endregion
13
- export { createApp as default, parseFlags };
19
+ export { createApp as default, packageVersion, parseFlags };
14
20
  //# sourceMappingURL=index.d.mts.map
package/esm/index.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import { NO_DATABASE } from "./features/database-drivers.mjs";
2
+ import { packageRoot } from "./helpers/paths.mjs";
2
3
  import createNewApp from "./commands/create-new-app/index.mjs";
4
+ import { getJsonFile } from "@warlock.js/fs";
3
5
 
4
6
  //#region ../create-warlock/src/index.ts
5
7
  const valueFlags = [
@@ -9,6 +11,28 @@ const valueFlags = [
9
11
  "features",
10
12
  "ai"
11
13
  ];
14
+ const HELP_TEXT = `
15
+ create-warlock — scaffold a new Warlock.js project
16
+
17
+ Usage
18
+ $ create-warlock [project-name] [options]
19
+
20
+ Options
21
+ --name Project name (or pass it as the first positional arg)
22
+ --db=<driver> Database driver (e.g. postgres, mongodb)
23
+ --no-db Skip database selection entirely
24
+ --features=<list> Comma-separated feature keys (e.g. test,herald)
25
+ --ai=<list> Comma-separated AI provider keys (e.g. openai,anthropic)
26
+ --pm=<manager> Package manager to use (npm, yarn, pnpm)
27
+ --git / --no-git Force-enable or force-disable git initialization
28
+ --jwt / --no-jwt Force-enable or force-disable JWT secret generation
29
+ -y, --yes Skip prompts and accept defaults for anything unset
30
+ -h, --help Show this help message and exit
31
+ -v, --version Show the installed create-warlock version and exit
32
+
33
+ Example
34
+ $ create-warlock my-app --db=postgres --features=test,herald --yes
35
+ `;
12
36
  /**
13
37
  * Parse the scaffolder's own CLI flags for non-interactive mode.
14
38
  *
@@ -35,6 +59,14 @@ function parseFlags(argv) {
35
59
  }
36
60
  }
37
61
  switch (key) {
62
+ case "help":
63
+ case "h":
64
+ flags.help = true;
65
+ break;
66
+ case "version":
67
+ case "v":
68
+ flags.version = true;
69
+ break;
38
70
  case "yes":
39
71
  case "y":
40
72
  flags.yes = true;
@@ -78,8 +110,26 @@ function splitList(value) {
78
110
  if (!value) return [];
79
111
  return value.split(",").map((item) => item.trim()).filter(Boolean);
80
112
  }
113
+ /**
114
+ * Read this package's own `version` field. A plain JSON read — no network
115
+ * call, no write, nothing else touched — so `--version` stays a pure,
116
+ * side-effect-free exit.
117
+ */
118
+ function packageVersion() {
119
+ return getJsonFile(packageRoot("package.json")).version;
120
+ }
81
121
  function createApp() {
82
122
  const flags = parseFlags(process.argv.slice(2));
123
+ if (flags.help) {
124
+ console.log(HELP_TEXT);
125
+ process.exit(0);
126
+ return;
127
+ }
128
+ if (flags.version) {
129
+ console.log(packageVersion());
130
+ process.exit(0);
131
+ return;
132
+ }
83
133
  Promise.resolve(createNewApp(flags)).catch((error) => {
84
134
  console.error();
85
135
  console.error(` create-warlock failed: ${error?.message ?? String(error)}`);
@@ -89,5 +139,5 @@ function createApp() {
89
139
  }
90
140
 
91
141
  //#endregion
92
- export { createApp as default, parseFlags };
142
+ export { createApp as default, packageVersion, parseFlags };
93
143
  //# sourceMappingURL=index.mjs.map
package/esm/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../create-warlock/src/index.ts"],"sourcesContent":["import createNewApp from \"./commands/create-new-app\";\nimport { CliFlags } from \"./commands/create-new-app/types\";\nimport { NO_DATABASE } from \"./features/database-drivers\";\n\nconst valueFlags = [\"name\", \"db\", \"pm\", \"features\", \"ai\"];\n\n/**\n * Parse the scaffolder's own CLI flags for non-interactive mode.\n *\n * @example\n * create-warlock my-app --db=postgres --features=test,herald --ai=openai,anthropic --yes\n */\nexport function parseFlags(argv: string[]): CliFlags {\n const flags: CliFlags = {};\n const positionals: string[] = [];\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n\n if (!arg.startsWith(\"-\")) {\n positionals.push(arg);\n continue;\n }\n\n const equalIndex = arg.indexOf(\"=\");\n const key = (equalIndex === -1 ? arg : arg.slice(0, equalIndex)).replace(/^-+/, \"\");\n let value: string | undefined = equalIndex === -1 ? undefined : arg.slice(equalIndex + 1);\n\n // Value-taking flags may use either `--key=value` or `--key value`.\n if (valueFlags.includes(key) && value === undefined) {\n const next = argv[i + 1];\n\n if (next && !next.startsWith(\"-\")) {\n value = next;\n i++;\n }\n }\n\n switch (key) {\n case \"yes\":\n case \"y\":\n flags.yes = true;\n break;\n case \"git\":\n flags.git = true;\n break;\n case \"no-git\":\n flags.git = false;\n break;\n case \"jwt\":\n flags.jwt = true;\n break;\n case \"no-jwt\":\n flags.jwt = false;\n break;\n case \"name\":\n flags.name = value;\n break;\n case \"db\":\n flags.db = value;\n break;\n case \"no-db\":\n // Opt out of a database entirely — equivalent to `--db=none`.\n flags.db = NO_DATABASE;\n break;\n case \"pm\":\n flags.pm = value;\n break;\n case \"features\":\n flags.features = splitList(value);\n break;\n case \"ai\":\n flags.ai = splitList(value);\n break;\n }\n }\n\n if (!flags.name && positionals.length > 0) {\n flags.name = positionals[0];\n }\n\n return flags;\n}\n\nfunction splitList(value: string | undefined): string[] {\n if (!value) return [];\n\n return value\n .split(\",\")\n .map(item => item.trim())\n .filter(Boolean);\n}\n\nexport default function createApp() {\n const flags = parseFlags(process.argv.slice(2));\n\n // An unexpected throw must surface as a readable error AND a non-zero exit\n // code — never as a stack trace the user scrolls past on the way to a green\n // banner (there is no banner after this point).\n Promise.resolve(createNewApp(flags)).catch((error: unknown) => {\n console.error();\n console.error(\n ` create-warlock failed: ${(error as Error)?.message ?? String(error)}`,\n );\n console.error();\n\n process.exit(1);\n });\n}\n"],"mappings":";;;;AAIA,MAAM,aAAa;CAAC;CAAQ;CAAM;CAAM;CAAY;AAAI;;;;;;;AAQxD,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAkB,CAAC;CACzB,MAAM,cAAwB,CAAC;CAE/B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EAEjB,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG;GACxB,YAAY,KAAK,GAAG;GACpB;EACF;EAEA,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,OAAO,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU,EAAC,CAAE,QAAQ,OAAO,EAAE;EAClF,IAAI,QAA4B,eAAe,KAAK,SAAY,IAAI,MAAM,aAAa,CAAC;EAGxF,IAAI,WAAW,SAAS,GAAG,KAAK,UAAU,QAAW;GACnD,MAAM,OAAO,KAAK,IAAI;GAEtB,IAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;IACjC,QAAQ;IACR;GACF;EACF;EAEA,QAAQ,KAAR;GACE,KAAK;GACL,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,OAAO;IACb;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IAEH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,WAAW,UAAU,KAAK;IAChC;GACF,KAAK;IACH,MAAM,KAAK,UAAU,KAAK;IAC1B;EACJ;CACF;CAEA,IAAI,CAAC,MAAM,QAAQ,YAAY,SAAS,GACtC,MAAM,OAAO,YAAY;CAG3B,OAAO;AACT;AAEA,SAAS,UAAU,OAAqC;CACtD,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,OAAO,MACJ,MAAM,GAAG,CAAC,CACV,KAAI,SAAQ,KAAK,KAAK,CAAC,CAAC,CACxB,OAAO,OAAO;AACnB;AAEA,SAAwB,YAAY;CAClC,MAAM,QAAQ,WAAW,QAAQ,KAAK,MAAM,CAAC,CAAC;CAK9C,QAAQ,QAAQ,aAAa,KAAK,CAAC,CAAC,CAAC,OAAO,UAAmB;EAC7D,QAAQ,MAAM;EACd,QAAQ,MACN,4BAA6B,OAAiB,WAAW,OAAO,KAAK,GACvE;EACA,QAAQ,MAAM;EAEd,QAAQ,KAAK,CAAC;CAChB,CAAC;AACH"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../create-warlock/src/index.ts"],"sourcesContent":["import { getJsonFile } from \"@warlock.js/fs\";\nimport createNewApp from \"./commands/create-new-app\";\nimport { CliFlags } from \"./commands/create-new-app/types\";\nimport { NO_DATABASE } from \"./features/database-drivers\";\nimport { packageRoot } from \"./helpers/paths\";\n\nconst valueFlags = [\"name\", \"db\", \"pm\", \"features\", \"ai\"];\n\nconst HELP_TEXT = `\n create-warlock — scaffold a new Warlock.js project\n\n Usage\n $ create-warlock [project-name] [options]\n\n Options\n --name Project name (or pass it as the first positional arg)\n --db=<driver> Database driver (e.g. postgres, mongodb)\n --no-db Skip database selection entirely\n --features=<list> Comma-separated feature keys (e.g. test,herald)\n --ai=<list> Comma-separated AI provider keys (e.g. openai,anthropic)\n --pm=<manager> Package manager to use (npm, yarn, pnpm)\n --git / --no-git Force-enable or force-disable git initialization\n --jwt / --no-jwt Force-enable or force-disable JWT secret generation\n -y, --yes Skip prompts and accept defaults for anything unset\n -h, --help Show this help message and exit\n -v, --version Show the installed create-warlock version and exit\n\n Example\n $ create-warlock my-app --db=postgres --features=test,herald --yes\n`;\n\n/**\n * Parse the scaffolder's own CLI flags for non-interactive mode.\n *\n * @example\n * create-warlock my-app --db=postgres --features=test,herald --ai=openai,anthropic --yes\n */\nexport function parseFlags(argv: string[]): CliFlags {\n const flags: CliFlags = {};\n const positionals: string[] = [];\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n\n if (!arg.startsWith(\"-\")) {\n positionals.push(arg);\n continue;\n }\n\n const equalIndex = arg.indexOf(\"=\");\n const key = (equalIndex === -1 ? arg : arg.slice(0, equalIndex)).replace(/^-+/, \"\");\n let value: string | undefined = equalIndex === -1 ? undefined : arg.slice(equalIndex + 1);\n\n // Value-taking flags may use either `--key=value` or `--key value`.\n if (valueFlags.includes(key) && value === undefined) {\n const next = argv[i + 1];\n\n if (next && !next.startsWith(\"-\")) {\n value = next;\n i++;\n }\n }\n\n switch (key) {\n case \"help\":\n case \"h\":\n flags.help = true;\n break;\n case \"version\":\n case \"v\":\n flags.version = true;\n break;\n case \"yes\":\n case \"y\":\n flags.yes = true;\n break;\n case \"git\":\n flags.git = true;\n break;\n case \"no-git\":\n flags.git = false;\n break;\n case \"jwt\":\n flags.jwt = true;\n break;\n case \"no-jwt\":\n flags.jwt = false;\n break;\n case \"name\":\n flags.name = value;\n break;\n case \"db\":\n flags.db = value;\n break;\n case \"no-db\":\n // Opt out of a database entirely — equivalent to `--db=none`.\n flags.db = NO_DATABASE;\n break;\n case \"pm\":\n flags.pm = value;\n break;\n case \"features\":\n flags.features = splitList(value);\n break;\n case \"ai\":\n flags.ai = splitList(value);\n break;\n }\n }\n\n if (!flags.name && positionals.length > 0) {\n flags.name = positionals[0];\n }\n\n return flags;\n}\n\nfunction splitList(value: string | undefined): string[] {\n if (!value) return [];\n\n return value\n .split(\",\")\n .map(item => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Read this package's own `version` field. A plain JSON read — no network\n * call, no write, nothing else touched — so `--version` stays a pure,\n * side-effect-free exit.\n */\nexport function packageVersion(): string {\n return (getJsonFile(packageRoot(\"package.json\")) as { version: string })\n .version;\n}\n\nexport default function createApp() {\n const flags = parseFlags(process.argv.slice(2));\n\n // `--help` and `--version` must exit before anything that touches the\n // filesystem, the network, or a prompt — they win over every other flag,\n // including a positional project name.\n if (flags.help) {\n console.log(HELP_TEXT);\n process.exit(0);\n return;\n }\n\n if (flags.version) {\n console.log(packageVersion());\n process.exit(0);\n return;\n }\n\n // An unexpected throw must surface as a readable error AND a non-zero exit\n // code — never as a stack trace the user scrolls past on the way to a green\n // banner (there is no banner after this point).\n Promise.resolve(createNewApp(flags)).catch((error: unknown) => {\n console.error();\n console.error(\n ` create-warlock failed: ${(error as Error)?.message ?? String(error)}`,\n );\n console.error();\n\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;AAMA,MAAM,aAAa;CAAC;CAAQ;CAAM;CAAM;CAAY;AAAI;AAExD,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BlB,SAAgB,WAAW,MAA0B;CACnD,MAAM,QAAkB,CAAC;CACzB,MAAM,cAAwB,CAAC;CAE/B,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EAEjB,IAAI,CAAC,IAAI,WAAW,GAAG,GAAG;GACxB,YAAY,KAAK,GAAG;GACpB;EACF;EAEA,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,OAAO,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU,GAAG,QAAQ,OAAO,EAAE;EAClF,IAAI,QAA4B,eAAe,KAAK,SAAY,IAAI,MAAM,aAAa,CAAC;EAGxF,IAAI,WAAW,SAAS,GAAG,KAAK,UAAU,QAAW;GACnD,MAAM,OAAO,KAAK,IAAI;GAEtB,IAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,GAAG;IACjC,QAAQ;IACR;GACF;EACF;EAEA,QAAQ,KAAR;GACE,KAAK;GACL,KAAK;IACH,MAAM,OAAO;IACb;GACF,KAAK;GACL,KAAK;IACH,MAAM,UAAU;IAChB;GACF,KAAK;GACL,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,MAAM;IACZ;GACF,KAAK;IACH,MAAM,OAAO;IACb;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IAEH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,KAAK;IACX;GACF,KAAK;IACH,MAAM,WAAW,UAAU,KAAK;IAChC;GACF,KAAK;IACH,MAAM,KAAK,UAAU,KAAK;IAC1B;EACJ;CACF;CAEA,IAAI,CAAC,MAAM,QAAQ,YAAY,SAAS,GACtC,MAAM,OAAO,YAAY;CAG3B,OAAO;AACT;AAEA,SAAS,UAAU,OAAqC;CACtD,IAAI,CAAC,OAAO,OAAO,CAAC;CAEpB,OAAO,MACJ,MAAM,GAAG,EACT,KAAI,SAAQ,KAAK,KAAK,CAAC,EACvB,OAAO,OAAO;AACnB;;;;;;AAOA,SAAgB,iBAAyB;CACvC,OAAQ,YAAY,YAAY,cAAc,CAAC,EAC5C;AACL;AAEA,SAAwB,YAAY;CAClC,MAAM,QAAQ,WAAW,QAAQ,KAAK,MAAM,CAAC,CAAC;CAK9C,IAAI,MAAM,MAAM;EACd,QAAQ,IAAI,SAAS;EACrB,QAAQ,KAAK,CAAC;EACd;CACF;CAEA,IAAI,MAAM,SAAS;EACjB,QAAQ,IAAI,eAAe,CAAC;EAC5B,QAAQ,KAAK,CAAC;EACd;CACF;CAKA,QAAQ,QAAQ,aAAa,KAAK,CAAC,EAAE,OAAO,UAAmB;EAC7D,QAAQ,MAAM;EACd,QAAQ,MACN,4BAA6B,OAAiB,WAAW,OAAO,KAAK,GACvE;EACA,QAAQ,MAAM;EAEd,QAAQ,KAAK,CAAC;CAChB,CAAC;AACH"}
package/llms-full.txt CHANGED
@@ -61,6 +61,8 @@ Pass `--yes` (or `-y`) to skip every prompt and build from flags with defaults.
61
61
 
62
62
  Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--features` / `--ai` keys fail fast before any install.
63
63
 
64
+ `--help`/`-h` and `--version`/`-v` short-circuit before anything else — no prompt, filesystem write, or network call, and `--help` wins even over a positional project name. `--version` prints the installed `create-warlock` version.
65
+
64
66
  ## Valid keys
65
67
 
66
68
  **Database drivers** (`--db`):
@@ -72,7 +74,7 @@ Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--feature
72
74
  | `mysql` | 3306 | coming soon (disabled in the wizard) |
73
75
  | `none` | — | opt out — no driver, no driver package, `src/config/database.ts` removed |
74
76
 
75
- **Features** (`--features`): `react`, `react-email`, `mail`, `ses`, `image`, `s3`, `redis`, `scheduler`, `herald`, `socket`, `swagger`, `postman`, `test`.
77
+ **Features** (`--features`): `react`, `react-email`, `mail`, `ses`, `image`, `s3`, `redis`, `scheduler`, `herald`, `socket`, `test`, `access`, `web`, `tailwind`, `shadcn`, `notifications`.
76
78
 
77
79
  **AI providers / packages** (`--ai`): `ai-openai`, `ai-google`, `ai-anthropic`, `ai-bedrock`, `ai-ollama`, plus the capability packages `ai-tools`, `ai-panoptic`, `ai-workspace`. Any pick auto-pulls the core `@warlock.js/ai` package.
78
80
 
package/package.json CHANGED
@@ -12,13 +12,13 @@
12
12
  "dependencies": {
13
13
  "@clack/prompts": "^0.7.0",
14
14
  "@mongez/copper": "^2.1.2",
15
- "@warlock.js/fs": "5.0.2",
15
+ "@warlock.js/fs": "5.2.2",
16
16
  "@mongez/reinforcements": "^4.0.1",
17
17
  "cross-spawn": "^7.0.3",
18
18
  "rimraf": "^6.0.1",
19
19
  "which-pm-runs": "^1.1.0"
20
20
  },
21
- "version": "5.0.2",
21
+ "version": "5.2.2",
22
22
  "type": "module",
23
23
  "main": "./esm/index.mjs",
24
24
  "module": "./esm/index.mjs",
@@ -11,7 +11,7 @@ description: 'Scaffold a brand-new Warlock.js project with `create-warlock` —
11
11
 
12
12
  ```bash
13
13
  # Interactive wizard (recommended for humans)
14
- yarn create warlock
14
+ pnpm create warlock
15
15
  # or: npm create warlock@latest / pnpm create warlock / npx create-warlock
16
16
 
17
17
  # Non-interactive (CI, agents, reproducible setups) — one command scaffolds the whole app
@@ -53,6 +53,8 @@ Pass `--yes` (or `-y`) to skip every prompt and build from flags with defaults.
53
53
 
54
54
  Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--features` / `--ai` keys fail fast before any install.
55
55
 
56
+ `--help`/`-h` and `--version`/`-v` short-circuit before anything else — no prompt, filesystem write, or network call, and `--help` wins even over a positional project name. `--version` prints the installed `create-warlock` version.
57
+
56
58
  ## Valid keys
57
59
 
58
60
  **Database drivers** (`--db`):
@@ -64,7 +66,7 @@ Value flags accept either `--db=postgres` or `--db postgres`. Unknown `--feature
64
66
  | `mysql` | 3306 | coming soon (disabled in the wizard) |
65
67
  | `none` | — | opt out — no driver, no driver package, `src/config/database.ts` removed |
66
68
 
67
- **Features** (`--features`): `react`, `react-email`, `mail`, `ses`, `image`, `s3`, `redis`, `scheduler`, `herald`, `socket`, `swagger`, `postman`, `test`.
69
+ **Features** (`--features`): `react`, `react-email`, `mail`, `ses`, `image`, `s3`, `redis`, `scheduler`, `herald`, `socket`, `test`, `access`, `web`, `tailwind`, `shadcn`, `notifications`.
68
70
 
69
71
  **AI providers / packages** (`--ai`): `ai-openai`, `ai-google`, `ai-anthropic`, `ai-bedrock`, `ai-ollama`, plus the capability packages `ai-tools`, `ai-panoptic`, `ai-workspace`. Any pick auto-pulls the core `@warlock.js/ai` package.
70
72
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "wow2",
2
+ "name": "name-set-by-scaffolder",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "@mongez/reinforcements": "^4.0.1",
36
36
  "@mongez/localization": "^3.4.7",
37
37
  "@mongez/supportive-is": "^2.1.4",
38
- "@warlock.js/auth": "4.15.0",
39
- "@warlock.js/cache": "4.15.0",
40
- "@warlock.js/cascade": "4.15.0",
41
- "@warlock.js/scheduler": "4.15.0",
42
- "@warlock.js/core": "4.15.0",
43
- "@warlock.js/fs": "4.15.0",
44
- "@warlock.js/logger": "4.15.0",
45
- "@warlock.js/seal": "4.15.0",
38
+ "@warlock.js/auth": "0.0.0-set-by-scaffolder",
39
+ "@warlock.js/cache": "0.0.0-set-by-scaffolder",
40
+ "@warlock.js/cascade": "0.0.0-set-by-scaffolder",
41
+ "@warlock.js/scheduler": "0.0.0-set-by-scaffolder",
42
+ "@warlock.js/core": "0.0.0-set-by-scaffolder",
43
+ "@warlock.js/fs": "0.0.0-set-by-scaffolder",
44
+ "@warlock.js/logger": "0.0.0-set-by-scaffolder",
45
+ "@warlock.js/seal": "0.0.0-set-by-scaffolder",
46
46
  "dayjs": "^1.11.19"
47
47
  },
48
48
  "devDependencies": {
@@ -1,11 +1,14 @@
1
- import { t, type RequestHandler } from "@warlock.js/core";
1
+ import { t } from "@warlock.js/core";
2
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
3
  import { logoutAllService } from "../services/auth.service";
3
4
 
4
5
  /**
5
6
  * Logout from all devices controller
6
7
  * POST /auth/logout-all
8
+ *
9
+ * Guarded route — see the note on `logoutController`.
7
10
  */
8
- export const logoutAllController: RequestHandler = async ({ request, response }) => {
11
+ export const logoutAllController: GuardedRequestHandler = async ({ request, response }) => {
9
12
  await logoutAllService(request.user);
10
13
 
11
14
  return response.success({
@@ -1,11 +1,16 @@
1
- import { t, type RequestHandler } from "@warlock.js/core";
1
+ import { t } from "@warlock.js/core";
2
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
3
  import { logoutService } from "../services/auth.service";
3
4
 
4
5
  /**
5
6
  * Logout controller
6
7
  * POST /auth/logout
8
+ *
9
+ * Typed as `GuardedRequestHandler` because the route sits inside `guarded()`
10
+ * (see `../routes.ts`). A plain `RequestHandler` types `request.user` as
11
+ * `RequestUser | undefined`, which is not assignable to `logoutService`.
7
12
  */
8
- export const logoutController: RequestHandler = async ({ request, response }) => {
13
+ export const logoutController: GuardedRequestHandler = async ({ request, response }) => {
9
14
  await logoutService(request.user);
10
15
 
11
16
  return response.success({
@@ -1,10 +1,15 @@
1
- import { type RequestHandler } from "@warlock.js/core";
1
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
2
 
3
3
  /**
4
4
  * Get current user controller
5
5
  * GET /auth/me
6
+ *
7
+ * Guarded route. As a plain `RequestHandler` this compiled without complaint
8
+ * while serializing `RequestUser | undefined` — i.e. the empty interface `{}` —
9
+ * so the response body was typed as nothing at all and no error was raised.
10
+ * `GuardedRequestHandler` types it as the app's `User` model.
6
11
  */
7
- export const meController: RequestHandler = async ({ request, response }) => {
12
+ export const meController: GuardedRequestHandler = async ({ request, response }) => {
8
13
  return response.success({
9
14
  user: request.user,
10
15
  });
@@ -7,3 +7,13 @@ scheduler.newJob("cleanup-expired-otps", cleanupExpiredOtpsService).everyHour();
7
7
 
8
8
  // Cleanup expired refresh tokens every hour
9
9
  scheduler.newJob("cleanup-expired-tokens", () => authService.cleanupExpiredTokens()).everyHour();
10
+
11
+ // Registering jobs does not schedule them — without this, a fresh project's very
12
+ // first `warlock dev` printed "scheduler.start() was never called" and neither
13
+ // cleanup ever ran.
14
+ //
15
+ // Safe to call here: `start()` throws on an empty scheduler, and the two jobs
16
+ // above are registered directly before it. Jobs registered later by other
17
+ // modules still run — `addJob` prepares them when the scheduler is already
18
+ // running.
19
+ scheduler.start();
@@ -1,6 +1,15 @@
1
1
  import { type RequestHandler } from "@warlock.js/core";
2
2
  import { HomePageComponent } from "../components/HomePageComponent";
3
3
 
4
+ /**
5
+ * React-rendered welcome page — kept only when the `react` feature is selected.
6
+ *
7
+ * This file and its plain-JSON sibling (`home-page.controller.ts`) are a
8
+ * mutually exclusive PAIR: both export `homePageController`, and the scaffolder
9
+ * (`configureHomePage`) deletes exactly one at generation time, so a real
10
+ * project never contains both. They coexist here in the template on purpose —
11
+ * this is not resolver-order ambiguity, and neither file is dead code.
12
+ */
4
13
  export const homePageController: RequestHandler = async ({ response }) => {
5
14
  return response.render(<HomePageComponent />);
6
15
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sample test to verify test infrastructure
3
3
  */
4
- import { testGet } from "@warlock.js/core";
4
+ import { testGet } from "@warlock.js/core/tests";
5
5
  import { describe, expect, it } from "vitest";
6
6
 
7
7
  describe("Test Infrastructure", () => {
@@ -7,5 +7,6 @@ export default Migration.create(User, {
7
7
  password: string(255),
8
8
  image: string(500).nullable(),
9
9
  imageMetadata: json().nullable(),
10
+ lastLogin: timestamp().nullable(),
10
11
  deletedAt: timestamp().nullable(),
11
12
  });
@@ -9,6 +9,11 @@ export const userSchema = v.object({
9
9
  email: v.email().requiredIfEmpty("id"),
10
10
  image: v.string(),
11
11
  password: v.string().min(6).requiredIfEmpty("id").addTransformer(useHashedPassword()),
12
+ // Written by the social-login handler (`app/users/services/login-social.ts`).
13
+ // A field only reachable through `save({ merge })` still has to be declared
14
+ // here — `merge` is typed against this schema, so an undeclared key is a
15
+ // compile error, not a silent write.
16
+ lastLogin: v.date(),
12
17
  });
13
18
 
14
19
  export type UserSchema = Infer<typeof userSchema>;
@@ -13,8 +13,14 @@ const loginSocial: GuardedRequestHandler = async ({ request, response }) => {
13
13
 
14
14
  const auth = await user.generateAccessToken();
15
15
 
16
- user.save({
17
- lastLogin: new Date(),
16
+ // `save()` takes WriterOptions — the fields to write go under `merge`, typed
17
+ // against `userSchema`. Passing `{ lastLogin }` at the top level was not a
18
+ // write at all: it was an unknown option, and the value went nowhere.
19
+ // Awaited, so the write is not a floating promise racing the response.
20
+ await user.save({
21
+ merge: {
22
+ lastLogin: new Date(),
23
+ },
18
24
  });
19
25
 
20
26
  return response.success({
@@ -7,6 +7,22 @@ import {
7
7
  } from "@warlock.js/cache";
8
8
  import { DatabaseCacheDriver, env, useRequestStore } from "@warlock.js/core";
9
9
 
10
+ /**
11
+ * Namespace every cache key by the caller's domain, so two tenants hitting the
12
+ * same app never read each other's cached values.
13
+ *
14
+ * This used to branch on `request.client` first. That property does not exist:
15
+ * it survived from v4, where `Request` carried a `[key: string]: any` index
16
+ * signature that made any property name compile. v5 removed the index signature
17
+ * and the branch became a type error.
18
+ *
19
+ * It was deleted rather than renamed. The obvious "fix" — pointing it at
20
+ * `request.locals.client` — compiles and is worse than the bug: nothing in the
21
+ * framework populates `request.locals`, so the branch would be permanently
22
+ * `undefined` and silently dead. `originDomain` below is real (it is derived
23
+ * from the `Origin` header) and already covers the multi-tenant case, so the
24
+ * scaffold prefixes on that and models nothing the framework does not provide.
25
+ */
10
26
  const globalPrefix = () => {
11
27
  const { request } = useRequestStore();
12
28
 
@@ -14,12 +30,6 @@ const globalPrefix = () => {
14
30
 
15
31
  if (!request) return cachePrefix;
16
32
 
17
- if (request.client) {
18
- cachePrefix = `${cachePrefix}.${request.client.get("username")}`;
19
-
20
- return cachePrefix;
21
- }
22
-
23
33
  const domain = request.originDomain || request.header("domain") || request.input("domain");
24
34
 
25
35
  if (!domain) return cachePrefix;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Project-wide type augmentations.
3
+ *
4
+ * This is where the app teaches the framework about ITS OWN types. Warlock's
5
+ * `Request` class no longer carries a `[key: string]: any` index signature —
6
+ * attaching arbitrary properties used to compile silently and hide real bugs
7
+ * behind `any` — so the sanctioned way to extend a request is to merge extra
8
+ * members into the framework's own interfaces from here.
9
+ *
10
+ * ---------------------------------------------------------------------------
11
+ * WHY THIS FILE HAS `export {}` AT THE BOTTOM
12
+ * ---------------------------------------------------------------------------
13
+ * `declare module "x" { ... }` means two completely different things depending
14
+ * on whether the enclosing file is a module:
15
+ *
16
+ * - In a SCRIPT file (no top-level `import`/`export`), it declares an AMBIENT
17
+ * module — it REPLACES the real typings of `@warlock.js/core` with whatever
18
+ * is inside the braces, and every existing export vanishes.
19
+ * - In a MODULE file (has a top-level `import`/`export`), it is a MODULE
20
+ * AUGMENTATION — it merges into the real typings, which is what we want.
21
+ *
22
+ * The trailing `export {}` is what makes this file a module. Do not delete it,
23
+ * and do not "clean it up" as an unused statement.
24
+ *
25
+ * ---------------------------------------------------------------------------
26
+ * WHY THESE ARE `interface` AND NOT `type`
27
+ * ---------------------------------------------------------------------------
28
+ * This project's standard is "prefer `type` over `interface`". These
29
+ * declarations are a NAMED EXCEPTION to that standard: declaration merging is
30
+ * an interface-only feature. A `type RequestUser = { ... }` here does not merge
31
+ * with the framework's declaration — it is a duplicate-identifier error. Every
32
+ * augmentation in this file must stay an `interface`.
33
+ */
34
+
35
+ declare module "@warlock.js/core" {
36
+ /**
37
+ * Arbitrary per-request data — `request.locals`.
38
+ *
39
+ * Populate it from a middleware and read it downstream in the same request.
40
+ *
41
+ * NOTE: the framework itself never writes to `request.locals`; it only
42
+ * initializes it to `{}` once per request. Anything you declare here is a
43
+ * promise YOU have to keep in a middleware, otherwise the property typechecks
44
+ * and is `undefined` forever at runtime. Declare a field here only once
45
+ * something actually assigns it.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * interface RequestLocals {
50
+ * // set by a middleware that resolves the tenant from the request host
51
+ * tenant?: Tenant;
52
+ * }
53
+ * ```
54
+ */
55
+ interface RequestLocals {}
56
+
57
+ /**
58
+ * The authenticated user — `request.user`.
59
+ *
60
+ * The auth middleware assigns the resolved user model here. Declaring its
61
+ * shape gives unguarded handlers (ones that may or may not have a user) a
62
+ * real type instead of `{}`.
63
+ *
64
+ * This does NOT make `request.user` non-optional: the framework declares it
65
+ * as `user?: RequestUser`, and augmentation can add members but cannot remove
66
+ * the `?`. For routes that are actually behind the auth guard, type the
67
+ * handler as `GuardedRequestHandler` (see
68
+ * `src/app/auth/requests/guarded.request.ts`) — that narrows `request.user`
69
+ * to the app's `User` model AND drops the `| undefined`.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * interface RequestUser {
74
+ * id: string | number;
75
+ * email: string;
76
+ * }
77
+ * ```
78
+ */
79
+ interface RequestUser {}
80
+ }
81
+
82
+ export {};
@@ -10,18 +10,27 @@
10
10
  "downlevelIteration": true,
11
11
  "strict": true,
12
12
  "forceConsistentCasingInFileNames": true,
13
- "typeRoots": ["./src/typings.d.ts"],
14
13
  "noFallthroughCasesInSwitch": true,
15
14
  "module": "ESNext",
16
15
  "jsx": "react-jsx",
17
16
  "isolatedModules": true,
18
- "moduleResolution": "node",
17
+ // "bundler", not "node". The framework builds this project with esbuild, and
18
+ // every `@warlock.js/*` package ships an `exports` map with subpath entries
19
+ // (e.g. `@warlock.js/core/tests`). Classic "node" resolution ignores
20
+ // `exports` entirely, so those subpaths resolved to nothing and any import
21
+ // of one failed to typecheck even though it ran fine at runtime.
22
+ "moduleResolution": "bundler",
19
23
  "resolveJsonModule": true,
20
24
  "paths": {
21
25
  "app/*": ["./src/app/*"]
22
26
  },
23
27
  "noEmit": true
24
28
  },
25
- "include": ["src", "warlock.config.ts", ".warlock/typings/*.d.ts"],
29
+ // `src/typings.d.ts` is listed explicitly even though "src" already covers it:
30
+ // it is the project's module-augmentation file, and a dangling reference to it
31
+ // here is exactly the bug this replaced (it used to sit under `typeRoots`,
32
+ // which takes DIRECTORIES of @types packages, not files — so the entry pointed
33
+ // at a path that did not exist and did nothing).
34
+ "include": ["src", "src/typings.d.ts", "warlock.config.ts", ".warlock/typings/*.d.ts"],
26
35
  "exclude": ["node_modules"]
27
36
  }