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.
- package/CHANGELOG.md +63 -0
- package/README.md +75 -0
- package/esm/commands/create-new-app/get-app-path.mjs +1 -1
- package/esm/commands/create-new-app/index.mjs +1 -1
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-new-app/types.d.mts +3 -1
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/database-drivers.mjs.map +1 -1
- package/esm/features/features-map.mjs +12 -0
- package/esm/features/features-map.mjs.map +1 -1
- package/esm/helpers/app.mjs +10 -2
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/helpers/exec.mjs.map +1 -1
- package/esm/helpers/package-manager.mjs +1 -1
- package/esm/helpers/package-manager.mjs.map +1 -1
- package/esm/helpers/project-builder-helpers.mjs +1 -1
- package/esm/helpers/warlock-versions.mjs.map +1 -1
- package/esm/index.d.mts +7 -1
- package/esm/index.mjs +51 -1
- package/esm/index.mjs.map +1 -1
- package/llms-full.txt +3 -1
- package/package.json +2 -2
- package/skills/create-a-warlock-project/SKILL.md +4 -2
- package/templates/warlock/package.json +9 -9
- package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +5 -2
- package/templates/warlock/src/app/auth/controllers/logout.controller.ts +7 -2
- package/templates/warlock/src/app/auth/controllers/me.controller.ts +7 -2
- package/templates/warlock/src/app/auth/main.ts +10 -0
- package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +9 -0
- package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +1 -1
- package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +1 -0
- package/templates/warlock/src/app/users/models/user/user.model.ts +5 -0
- package/templates/warlock/src/app/users/services/login-social.ts +8 -2
- package/templates/warlock/src/config/cache.ts +16 -6
- package/templates/warlock/src/typings.d.ts +82 -0
- package/templates/warlock/tsconfig.json +12 -3
- 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:
|
|
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":"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"}
|
|
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":"
|
|
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`, `
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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`, `
|
|
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": "
|
|
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": "
|
|
39
|
-
"@warlock.js/cache": "
|
|
40
|
-
"@warlock.js/cascade": "
|
|
41
|
-
"@warlock.js/scheduler": "
|
|
42
|
-
"@warlock.js/core": "
|
|
43
|
-
"@warlock.js/fs": "
|
|
44
|
-
"@warlock.js/logger": "
|
|
45
|
-
"@warlock.js/seal": "
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
};
|
|
@@ -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
|
-
|
|
17
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
}
|