@warlock.js/core 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,104 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  > ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an *Upgrading* section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
8
8
 
9
+ ## 4.14.0
10
+
11
+ ### ⚠ Upgrading from 4.13.0 — read this first
12
+
13
+ **Three behaviour changes and one documentation correction. All four touch the test lifecycle; none touch application runtime.**
14
+
15
+ ⛔ **Every existing project must replace its `src/test-setup.ts`.** Three things are wrong with the file 4.13.0 generated:
16
+
17
+ ```ts
18
+ /**
19
+ * Test Setup
20
+ * Runs before EACH test file — not once per worker.
21
+ */
22
+ import { afterAll } from "vitest";
23
+ import { setupTest, teardownTest } from "@warlock.js/core/tests";
24
+
25
+ await setupTest(); // ← was setupTest({ connectors: true })
26
+ afterAll(teardownTest); // ← is new
27
+ ```
28
+
29
+ 1. **`{ connectors: true }` must become a bare `setupTest()`.** Under the new precedence it is an **explicit** value, so it now overrides your `src/config/tests.ts` where it previously deferred to it.
30
+ 2. **`afterAll(teardownTest)` is new and is not optional** — without it nothing ever closes the framework your tests started.
31
+ 3. **The `Per-Worker Test Setup` comment is false.** It always was.
32
+
33
+ **This is the migration step nobody can skip.** `warlock add test` emits the corrected file for new projects.
34
+
35
+ | What changes | How you'll see it | What to do |
36
+ |---|---|---|
37
+ | **`setupTest({ connectors })` now beats `tests.connectors` config** — the precedence flipped | a test file that passes `connectors` explicitly starts a **different connector set** than it did in 4.13.0 | grep for `setupTest({` — a call passing `connectors` was previously **ignored** and is now honoured. **Including the one in your generated setup file** |
38
+ | **A second `setupTest` call with different options now REJECTS** | an error naming the active and the requested selection, where 4.13.0 silently did nothing | call `teardownTest()` first, or don't call `setupTest` again at all |
39
+ | **The generated setup file now registers `afterAll(teardownTest)`** | your test files tear the framework down when they finish, instead of leaving it running | **add it to your existing `src/test-setup.ts`** — see below |
40
+ | **Docs corrected: `setupTest` is called per TEST FILE, not per worker** | no runtime effect on its own — the *invocation* always worked this way | fix the comment in `src/test-setup.ts` as above |
41
+
42
+ ### Added
43
+
44
+ - **`teardownTest()` — the other half of the pair.** `setupTest` has shipped without a counterpart since it was introduced: there was no supported way to close the framework a test file brought up, and the only "reset" available was a module flag that proved nothing about whether ports, sockets, pools or timers had actually closed
45
+
46
+ `teardownTest()` is idempotent when idle, shares one shutdown between concurrent callers, waits for an in-flight setup to settle before closing, and **always clears local state in a `finally`** so a failed shutdown cannot leave the lifecycle claiming to be ready
47
+
48
+ ⚠ **A shutdown failure poisons the lifecycle rather than pretending to recover.** If the shutdown layer reports a rejection, later `setupTest` calls refuse until the Vitest worker is recycled or a retried teardown fully succeeds. **We cannot promise a clean restart after a reported close failure, so we don't.** ⚠ **What it cannot see:** `connectorsManager.shutdown()` catches and logs individual connector failures internally — those never reach this lifecycle and never poison it. Manager-wide error policy is a separate piece of work
49
+
50
+ ### Changed
51
+
52
+ - **BREAKING — an explicit `setupTest({ connectors })` now wins over `tests.connectors` config.** The order was `config > parameter > true`; it is now **`explicit parameter > config > true`**
53
+
54
+ 4.13.0's changelog said this question was open, not settled: *"a per-call override is a contract decision for a later release."* This is that decision. **Call-site intent should beat a project default** — a caller who names a connector set is being specific on purpose, and silently overruling them was the wrong behaviour
55
+
56
+ **"Explicit" means a non-`undefined` value.** `setupTest()`, `setupTest({})` and `setupTest({ connectors: undefined })` **all fall through to config, then to `true`.** The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase project config
57
+
58
+ ⚠ **The generated `src/test-setup.ts` now calls `setupTest()` with no argument**, where it previously passed `{ connectors: true }`. Under the new order, passing `true` explicitly would erase the `tests.connectors` layer for the entire project. **If you edit your setup file, leave the call bare**
59
+
60
+ ⚠ **This is user-visible and it is why the change is marked BREAKING:** an application that sets `tests.connectors` *and* passes `connectors` from any test file will start a different connector set after upgrading
61
+
62
+ - **BREAKING — a conflicting `setupTest` call rejects instead of being ignored.** While a setup is starting or ready, a call with *different* effective options now rejects with an error naming both the active and the requested selection. The same options remain a no-op, and concurrent identical calls share one startup
63
+
64
+ Through 4.13.0 this was a silent early-return on an `isSetupComplete` flag — so `setupTest({ connectors: false })` in a file whose `src/test-setup.ts` had already run **did nothing at all, reported nothing, and started every connector anyway.** Connector arrays are compared as **sets** after deduplication, so caller order never counts as a conflict
65
+
66
+ - **Lifecycle state is now scoped to the worker runtime instead of the module.** `isSetupComplete` was a module-level variable, and **Vitest rebuilds the setup module's registry between test files while the worker process or thread keeps running** — so the flag reset in exactly the situation where live DB connections, pools and timers survive
67
+
68
+ Scope is per **process** under `pool: "forks"` and per **thread** under `pool: "threads"`; it deliberately does not cross thread workers, because `globalThis` is per realm and the resources are per worker too. **The guard's scope now matches the leak's scope in all four `pool` × `isolate` combinations**
69
+
70
+ ### Fixed
71
+
72
+ - **A stranded setup no longer exhausts the heap.** A lifecycle left in the `starting` state sent `teardownTest`'s wait-then-re-enter path into unbounded recursion — **`FATAL ERROR: JavaScript heap out of memory` at 4 GB, killing the worker with 26 tests in that run never executed.** It was found while proving the state machine, not reported by a user, and it would have shipped
73
+
74
+ The setup attempt is now bounded by **`tests.setupTimeout`, defaulting to `120000` ms**, and expiry **poisons** the lifecycle rather than returning it to `idle` — the attempt may have started connectors nobody can now account for. The message names the state, the bound and the remedy:
75
+
76
+ ```
77
+ setupTest() did not finish within 120000ms and is stuck in the "starting" state. The
78
+ lifecycle is now poisoned: whatever that attempt had already started is not known to be
79
+ closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your
80
+ cold start is legitimately slower than this, raise the bound with `tests.setupTimeout`
81
+ in `src/config/tests.ts` — milliseconds, default 120000.
82
+ ```
83
+
84
+ **It bounds the setup attempt, not teardown separately** — `teardownTest()` awaits the same attempt and inherits the bound. **A second teardown-side deadline was tried and rejected during implementation**: it expired instead of the setup's, was swallowed on settle, re-entered and armed a third, and left the stuck setup unbounded after all — reproducing the exact recursion the guard exists to remove
85
+
86
+ ⚠ **An invalid `tests.setupTimeout` throws, naming the value.** Zero, negative and non-numeric fail loudly instead of falling back to the default; a silent fallback hides a typo behind a working suite
87
+
88
+ ⚠ **A stranded lifecycle must fail with a message, not a dead process** — a crash mid-file is indistinguishable from an infrastructure flake, which is the worst way for a framework to report its own bug
89
+
90
+ ⚠ **Scope of the proof, stated because a green here is easy to over-read:** all nine guards were seen to fail under their own mutation, **but every spec injects its scheduler** — the default *value* is tested while the production timer, and whether its `unref` releases the worker, is not. **No spec observes a real hang**; the stuck attempt is a mock gate, not a socket that never returns
91
+
92
+ ### Documentation
93
+
94
+ - **Corrected: `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of the `test-service` and `test-http` skills, both generated LLM projections, the generator's comments and `setupTest`'s own JSDoc described a per-worker lifetime. **Vitest runs `setupFiles` before each test file and their exports are ignored** — measured across all four `pool` × `isolate` combinations, not inferred
95
+
96
+ ⛔ **If your project was generated before 4.14.0, replace the whole file** — see the migration block above. It is not a comment-only change
97
+
98
+ **The lifetime this release commits to is FILE-SCOPED:** the setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing, correct under every pool, every isolation setting, and watch mode**
99
+
100
+ ⚠ **This deliberately declines a faster option.** Holding lifecycle state in the worker runtime makes a worker-scoped lifetime *possible* — bootstrap once, reuse across every file in that worker — and an earlier draft of this release simply left the framework running to get it. **We are not shipping that**, for two reasons neither of which is performance:
101
+
102
+ 1. **Under `pool: "threads"` we cannot honestly claim the runner cleans up.** Vitest tears the thread down while the process lives, and whether Node reclaims that thread's sockets and pools is **unmeasured** — so "the runner owns cleanup by termination" would be a promise we cannot observe being kept
103
+ 2. **In watch mode Vitest reuses workers between reruns**, so there is no recycle and therefore **no cleanup owner at all** between reruns. Declaring watch mode unsupported was the alternative, and a test framework whose lifecycle is undefined in the mode people use all day does not have a lifecycle
104
+
105
+ **The cost is a framework bootstrap per test file — which is exactly what 4.13.0 already paid**, since its module-level flag died with the module registry between files. **Nothing gets slower; an unearned speed-up is simply not being claimed.** A worker-scoped lifetime remains open, and gets taken when the real per-file cost has been measured on a real application and the runner integration is chosen deliberately rather than inherited from whatever the wiring happened to do
106
+
9
107
  ## 4.13.0
10
108
 
11
109
  ### ⚠ Upgrading from 4.12.0 — read this first
@@ -44,14 +44,26 @@ export async function teardown() {
44
44
  const testSetupPath = srcPath("test-setup.ts");
45
45
  if (!await fileExistsAsync(testSetupPath)) {
46
46
  await putFileAsync(testSetupPath, `/**
47
- * Per-Worker Test Setup
47
+ * Test Setup - runs before EVERY test file
48
48
  *
49
- * Runs in EACH Vitest worker thread before tests execute.
50
- * Sets up per-worker database and cache connections.
49
+ * Vitest runs setupFiles before each test file and rebuilds the module
50
+ * registry with it, so this pair boots and closes the test runtime once per
51
+ * test file.
52
+ *
53
+ * setupTest() is called with no options on purpose: an explicit connectors
54
+ * value outranks tests.connectors from src/config/tests.ts, so passing one
55
+ * here would erase your project config. Omitting it leaves the config in
56
+ * charge.
57
+ *
58
+ * afterAll(teardownTest) is the other half of the pair: whoever calls
59
+ * setupTest() owns closing it in the same runtime context.
51
60
  */
52
- import { setupTest } from "@warlock.js/core/tests";
61
+ import { setupTest, teardownTest } from "@warlock.js/core/tests";
62
+ import { afterAll } from "vitest";
63
+
64
+ await setupTest();
53
65
 
54
- await setupTest({ connectors: true });
66
+ afterAll(teardownTest);
55
67
  `);
56
68
  console.log(`${colors.green("✓")} Created src/test-setup.ts`);
57
69
  }
@@ -68,7 +80,7 @@ export default defineConfig({
68
80
  plugins: [lowerStage3Decorators(), mongezVite()],
69
81
  test: {
70
82
  globalSetup: "./src/test-global-setup.ts", // HTTP server - runs once
71
- setupFiles: ["./src/test-setup.ts"], // DB/cache - runs per worker
83
+ setupFiles: ["./src/test-setup.ts"], // DB/cache - runs per test file
72
84
  environment: "node",
73
85
  globals: false,
74
86
  include: ["src/app/**/*.test.ts"],
@@ -1 +1 @@
1
- {"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport {\r\n accessConfigStub,\r\n aiConfigStub,\r\n accessResolverStub,\r\n accessRoleMigrationStub,\r\n accessRoleModelIndexStub,\r\n accessRoleModelStub,\r\n accessUserRoleMigrationStub,\r\n accessUserRoleModelIndexStub,\r\n accessUserRoleModelStub,\r\n communicatorsConfigStub,\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n socketConfigStub,\r\n} from \"./stubs\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\n/**\r\n * The part of a project `tsconfig.json` this action patches.\r\n */\r\ntype ProjectTsConfig = {\r\n include?: string[];\r\n};\r\n\r\n/**\r\n * Build a migration filename timestamp prefix in the framework's\r\n * MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this\r\n * prefix and orders migrations deterministically by it. Pass `offsetSeconds` to\r\n * stamp sibling migrations created in the same scaffold a second apart so they\r\n * never collide and keep a stable relative order.\r\n */\r\nfunction migrationTimestamp(offsetSeconds = 0): string {\r\n const now = new Date(Date.now() + offsetSeconds * 1000);\r\n const pad = (value: number) => String(value).padStart(2, \"0\");\r\n\r\n return (\r\n `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_` +\r\n `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`\r\n );\r\n}\r\n\r\nasync function completeTestInstallation(options: CommandActionData) {\r\n // Create test-global-setup.ts (runs once before all tests)\r\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\r\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\r\n\r\n if (!testGlobalSetupExists) {\r\n await putFileAsync(\r\n testGlobalSetupPath,\r\n `/**\r\n * Global Test Setup\r\n *\r\n * Runs ONCE before all test workers.\r\n * Starts the HTTP server for integration tests.\r\n */\r\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\r\n\r\nexport async function setup() {\r\n await startHttpTestServer();\r\n}\r\n\r\nexport async function teardown() {\r\n await stopHttpTestServer();\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\r\n }\r\n\r\n // Create test-setup.ts (runs per worker thread)\r\n const testSetupPath = srcPath(\"test-setup.ts\");\r\n const testSetupExists = await fileExistsAsync(testSetupPath);\r\n\r\n if (!testSetupExists) {\r\n await putFileAsync(\r\n testSetupPath,\r\n `/**\r\n * Per-Worker Test Setup\r\n *\r\n * Runs in EACH Vitest worker thread before tests execute.\r\n * Sets up per-worker database and cache connections.\r\n */\r\nimport { setupTest } from \"@warlock.js/core/tests\";\r\n\r\nawait setupTest({ connectors: true });\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\r\n }\r\n\r\n // Create vite.config.ts\r\n const viteConfigPath = rootPath(\"vite.config.ts\");\r\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\r\n\r\n if (!viteConfigExists) {\r\n await putFileAsync(\r\n viteConfigPath,\r\n `import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\r\nimport mongezVite from \"@mongez/vite\";\r\nimport { defineConfig } from \"vitest/config\";\r\n\r\nexport default defineConfig({\r\n // lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)\r\n // decorators with esbuild before oxc / the SSR rewrite can mangle them, so\r\n // decorated Cascade models load under Vitest.\r\n plugins: [lowerStage3Decorators(), mongezVite()],\r\n test: {\r\n globalSetup: \"./src/test-global-setup.ts\", // HTTP server - runs once\r\n setupFiles: [\"./src/test-setup.ts\"], // DB/cache - runs per worker\r\n environment: \"node\",\r\n globals: false,\r\n include: [\"src/app/**/*.test.ts\"],\r\n },\r\n});\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\r\n }\r\n}\r\n\r\nasync function completeReactEmailInstallation(_options: CommandActionData) {\r\n // 1. Create emails/ folder with a sample component\r\n const emailsFolderPath = rootPath(\"emails\");\r\n const sampleEmailPath = rootPath(\"emails/welcome-email.tsx\");\r\n\r\n if (!(await fileExistsAsync(sampleEmailPath))) {\r\n await ensureDirectoryAsync(emailsFolderPath);\r\n await putFileAsync(\r\n sampleEmailPath,\r\n `import { Body, Container, Head, Html, Text } from \"@react-email/components\";\r\nimport { Tailwind } from \"@react-email/tailwind\";\r\n\r\ninterface WelcomeEmailProps {\r\n name: string;\r\n}\r\n\r\n/**\r\n * Sample welcome email component.\r\n * Preview with: yarn email:preview\r\n */\r\nexport default function WelcomeEmail({ name }: WelcomeEmailProps) {\r\n return (\r\n <Html>\r\n <Head />\r\n <Tailwind>\r\n <Body className=\"bg-gray-100 font-sans\">\r\n <Container className=\"mx-auto max-w-xl py-8 px-4\">\r\n <Text className=\"text-2xl font-bold text-gray-900\">\r\n Welcome, {name}!\r\n </Text>\r\n <Text className=\"text-gray-600 mt-2\">\r\n You're all set. We're glad to have you on board.\r\n </Text>\r\n </Container>\r\n </Body>\r\n </Tailwind>\r\n </Html>\r\n );\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created emails/welcome-email.tsx`);\r\n }\r\n\r\n // 2. Patch tsconfig.json — add \"emails\" to include if missing\r\n const tsconfigPath = rootPath(\"tsconfig.json\");\r\n const tsconfig = await getJsonFileAsync<ProjectTsConfig>(tsconfigPath);\r\n\r\n if (!tsconfig.include) {\r\n tsconfig.include = [];\r\n }\r\n\r\n if (!tsconfig.include.includes(\"emails\")) {\r\n tsconfig.include.push(\"emails\");\r\n await putJsonFileAsync(tsconfigPath, tsconfig);\r\n console.log(`${colors.green(\"✓\")} Added \"emails\" to tsconfig.json include`);\r\n }\r\n}\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nasync function registerAccessLocale() {\r\n // Register the access locale in the project's shared translations file so a\r\n // denied check returns a real sentence, not the raw \"access.errors.forbidden\"\r\n // key. Append when the file exists, create it otherwise; skip if already there.\r\n const localesPath = srcPath(\"app/shared/utils/locales.ts\");\r\n\r\n const accessLocale = `groupedTranslations(\"access\", {\r\n errors: {\r\n forbidden: {\r\n en: \"You do not have permission to perform this action.\",\r\n ar: \"ليس لديك صلاحية لتنفيذ هذا الإجراء.\",\r\n },\r\n },\r\n});\r\n`;\r\n\r\n if (await fileExistsAsync(localesPath)) {\r\n const current = await getFileAsync(localesPath);\r\n\r\n if (current.includes(`groupedTranslations(\"access\"`)) {\r\n console.log(`${colors.yellowBright(\"access\")} locale already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n // The file uses groupedTranslations already iff it calls it — only inject the\r\n // import when no call is present yet.\r\n const importLine = `import { groupedTranslations } from \"@warlock.js/core\";`;\r\n const prefix = current.includes(\"groupedTranslations(\") ? \"\" : `${importLine}\\n\\n`;\r\n\r\n await putFileAsync(localesPath, `${prefix}${current.trimEnd()}\\n\\n${accessLocale}`);\r\n\r\n console.log(\r\n `${colors.green(\"✓\")} Registered the access locale in src/app/shared/utils/locales.ts`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/shared/utils\"));\r\n\r\n await putFileAsync(\r\n localesPath,\r\n `import { groupedTranslations } from \"@warlock.js/core\";\\n\\n${accessLocale}`,\r\n );\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/app/shared/utils/locales.ts with the access locale`);\r\n}\r\n\r\nasync function scaffoldAccessFiles() {\r\n // The resolver file is the sentinel for \"access already scaffolded\" — its\r\n // presence means the role/user-role model folders and their timestamped\r\n // migrations were created too, so we must not re-emit duplicate migrations on\r\n // a second run.\r\n const resolverPath = srcPath(\"app/access/services/access-resolver.ts\");\r\n\r\n if (await fileExistsAsync(resolverPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/access\")} already scaffolded, skipping resolver + role tables...`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n // 1. Role catalog model folder (model + barrel + migration). The catalog row\r\n // is role name → granted permissions; managed at runtime in the DB.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role\"));\r\n await putFileAsync(srcPath(\"app/access/models/role/role.model.ts\"), accessRoleModelStub);\r\n await putFileAsync(srcPath(\"app/access/models/role/index.ts\"), accessRoleModelIndexStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role/migrations\"));\r\n\r\n // Migration filenames carry a MM-DD-YYYY_HH-MM-SS prefix so cascade infers\r\n // their createdAt and orders them deterministically (the migrate action\r\n // discovers src/app/*/models/*/migrations/*). The two tables are independent\r\n // (no FK between them), but the user-role migration is stamped a second later\r\n // so the relative order is stable.\r\n const roleMigrationFile = `${migrationTimestamp()}-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/role/migrations\", roleMigrationFile),\r\n accessRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/role/migrations/${roleMigrationFile}`,\r\n );\r\n\r\n // 2. UserRole assignment model folder (model + barrel + migration). The model\r\n // statics scope an unresolved tenant to GLOBAL rows only (security\r\n // invariant) — see the stub for the reasoning.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role\"));\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/user-role.model.ts\"),\r\n accessUserRoleModelStub,\r\n );\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/index.ts\"),\r\n accessUserRoleModelIndexStub,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/user-role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role/migrations\"));\r\n\r\n const userRoleMigrationFile = `${migrationTimestamp(1)}-user-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/migrations\", userRoleMigrationFile),\r\n accessUserRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/user-role/migrations/${userRoleMigrationFile}`,\r\n );\r\n\r\n // 3. The DatabaseAccessResolver — the one required config seam, wired into\r\n // config/access.ts by the ejected stub.\r\n await ensureDirectoryAsync(srcPath(\"app/access/services\"));\r\n await putFileAsync(resolverPath, accessResolverStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/services/access-resolver.ts`);\r\n}\r\n\r\nasync function completeAccessInstallation(_options: CommandActionData) {\r\n await registerAccessLocale();\r\n await scaffoldAccessFiles();\r\n}\r\n\r\n/**\r\n * Link a satellite AI package into src/config/ai.ts via a side-effect import.\r\n *\r\n * The satellites (@warlock.js/ai-tools, ai-panoptic, ai-workspace) augment the\r\n * `ai` object on import — they register their runtime surface (ai.tools / ai.mcp,\r\n * ai.workspace, panoptic's ai.config({ panoptic }) wiring) AND the matching TS\r\n * declaration-merging so ai.* members resolve. config-loader runs config/ai.ts at\r\n * boot, so dropping a bare `import \"<specifier>\";` at the top of that file is what\r\n * actually loads the augmentation before the ai connector applies the config.\r\n *\r\n * No-op if config/ai.ts is missing (the `ai` feature ejects it) or the import is\r\n * already present. Inserts right after the `warlock:ai-packages` marker when it\r\n * exists so the satellite imports stay grouped; otherwise prepends to the top.\r\n */\r\nasync function linkAiPackageImport(specifier: string): Promise<void> {\r\n const aiConfigPath = srcPath(\"config/ai.ts\");\r\n\r\n if (!(await fileExistsAsync(aiConfigPath))) {\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(aiConfigPath);\r\n const importLine = `import \"${specifier}\";`;\r\n\r\n if (current.includes(importLine)) {\r\n return;\r\n }\r\n\r\n const marker = \"// >>> warlock:ai-packages (auto-managed) >>>\";\r\n let next: string;\r\n\r\n if (current.includes(marker)) {\r\n next = current.replace(marker, `${marker}\\n${importLine}`);\r\n } else {\r\n next = `${importLine}\\n${current}`;\r\n }\r\n\r\n await putFileAsync(aiConfigPath, next);\r\n\r\n console.log(`${colors.green(\"✓\")} Linked ${specifier} in src/config/ai.ts`);\r\n}\r\n\r\nexport const featuresMap: Record<\r\n string,\r\n {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n }\r\n> = {\r\n \"react-email\": {\r\n description: \"Installs react-email for building email templates with React and Tailwind\",\r\n requires: [\"mail\", \"react\"],\r\n dependencies: {\r\n \"react-email\": \"^5.2.10\",\r\n \"@react-email/components\": \"^1.0.11\",\r\n \"@react-email/render\": \"^2.0.5\",\r\n \"@react-email/tailwind\": \"^2.0.7\",\r\n },\r\n devDependencies: {\r\n \"@react-email/preview-server\": \"5.2.10\",\r\n },\r\n script: {\r\n \"email:preview\": \"npx react-email dev\",\r\n },\r\n onExecuting: completeReactEmailInstallation,\r\n },\r\n react: {\r\n description:\r\n \"Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML\",\r\n dependencies: {\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n },\r\n },\r\n image: {\r\n description: \"Installs sharp for image processing\",\r\n dependencies: {\r\n sharp: \"^0.34.5\",\r\n },\r\n },\r\n mail: {\r\n description: \"Installs nodemailer for sending emails\",\r\n dependencies: {\r\n nodemailer: \"^8.0.5\",\r\n },\r\n devDependencies: {\r\n \"@types/nodemailer\": \"^8.0.0\",\r\n },\r\n },\r\n ses: {\r\n description: \"Installs AWS SES SDK for sending emails via Amazon SES\",\r\n dependencies: {\r\n \"@aws-sdk/client-sesv2\": \"^3.1025.0\",\r\n },\r\n },\r\n mongodb: {\r\n description: \"Installs mongodb driver for database driver (Cascade Package)\",\r\n dependencies: {\r\n mongodb: \"^7.0.0\",\r\n },\r\n },\r\n scheduler: {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": \"~4.0.0\",\r\n },\r\n },\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: {\r\n description: \"Installs pg for Postgres database (Cascade Package)\",\r\n dependencies: {\r\n pg: \"^8.11.0\",\r\n },\r\n },\r\n mysql: {\r\n description: \"Installs mysql2 for MySQL database driver (Cascade Package)\",\r\n dependencies: {\r\n mysql2: \"^3.5.0\",\r\n },\r\n },\r\n redis: {\r\n description: \"Installs redis for Redis cache driver (Cache Package)\",\r\n dependencies: {\r\n redis: \"^4.6.13\",\r\n },\r\n },\r\n s3: {\r\n description: \"Installs AWS SDK for Cloud storage (Storage Package)\",\r\n dependencies: {\r\n \"@aws-sdk/client-s3\": \"^3.955.0\",\r\n \"@aws-sdk/lib-storage\": \"^3.955.0\",\r\n \"@aws-sdk/s3-request-presigner\": \"^3.955.0\",\r\n },\r\n },\r\n test: {\r\n description: \"Installs warlock test for testing\",\r\n onExecuting: completeTestInstallation,\r\n script: {\r\n test: \"vitest run\",\r\n \"test:coverage\": \"vitest run --coverage\",\r\n \"test:ui\": \"vitest --ui\",\r\n \"test:watch\": \"vitest --watch\",\r\n },\r\n devDependencies: {\r\n \"@mongez/vite\": \"^2.0.4\",\r\n vite: \"^8.0.16\",\r\n vitest: \"^4.1.8\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n },\r\n },\r\n herald: {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": \"~4.0.0\",\r\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n },\r\n socket: {\r\n description: \"Installs socket.io for the realtime socket server (Socket Connector)\",\r\n dependencies: {\r\n \"socket.io\": \"^4.8.3\",\r\n },\r\n ejectConfig: {\r\n content: socketConfigStub,\r\n name: \"socket\",\r\n },\r\n },\r\n notifications: {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": \"~4.0.0\",\r\n \"@warlock.js/auth\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n },\r\n access: {\r\n description:\r\n \"Installs @warlock.js/access — authorization (RBAC + ABAC): permission checks, ABAC policies, and roles. Ejects config/access.ts, the DatabaseAccessResolver + Role/UserRole models and migrations into src/app/access, and registers the access locale in src/app/shared/utils/locales.ts\",\r\n dependencies: {\r\n \"@warlock.js/access\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: accessConfigStub,\r\n name: \"access\",\r\n },\r\n onExecuting: completeAccessInstallation,\r\n },\r\n ai: {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: aiConfigStub,\r\n name: \"ai\",\r\n },\r\n },\r\n \"ai-openai\": {\r\n description: \"OpenAI provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-openai\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-google\": {\r\n description: \"Google (Gemini) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-google\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-anthropic\": {\r\n description: \"Anthropic (Claude) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-anthropic\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-bedrock\": {\r\n description: \"AWS Bedrock provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-bedrock\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-ollama\": {\r\n description: \"Ollama provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-ollama\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-tools\": {\r\n description:\r\n \"Installs @warlock.js/ai-tools — ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server, under ai.tools.* / ai.mcp (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-tools\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-tools\"),\r\n },\r\n \"ai-panoptic\": {\r\n description:\r\n \"Installs @warlock.js/ai-panoptic — observability for @warlock.js/ai (collector, exporters, zero-setup local dashboard) via ai.config({ panoptic }) (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-panoptic\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-panoptic\"),\r\n },\r\n \"ai-workspace\": {\r\n description:\r\n \"Installs @warlock.js/ai-workspace — a policy-jailed filesystem + shell workspace for coding agents, as ai.workspace (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-workspace\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-workspace\"),\r\n },\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(featuresMap[feature].description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(packageManager as PackageManager | undefined, dependencies, devDependencies);\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n if (featurePackages.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nasync function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const packageManagerCommand = getAddCommand(packageManager ?? (await detectPackageManager()));\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(dependencies).join(\" \")}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(devDependencies).join(\" \")} -D`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"add-command.action.mjs","names":[],"sources":["../../../../../../../core/src/generations/add-command.action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n ensureDirectoryAsync,\r\n fileExistsAsync,\r\n getFileAsync,\r\n getJsonFileAsync,\r\n putFileAsync,\r\n putJsonFileAsync,\r\n} from \"@warlock.js/fs\";\r\nimport { execSync } from \"node:child_process\";\r\nimport { CommandActionData } from \"../commands/types\";\r\nimport {\r\n detectPackageManager,\r\n getAddCommand,\r\n type PackageManager,\r\n} from \"../updater/package-manager\";\r\nimport { rootPath, srcPath } from \"../utils\";\r\nimport { getWarlockVersion } from \"../utils/framework-vesion\";\r\nimport {\r\n accessConfigStub,\r\n aiConfigStub,\r\n accessResolverStub,\r\n accessRoleMigrationStub,\r\n accessRoleModelIndexStub,\r\n accessRoleModelStub,\r\n accessUserRoleMigrationStub,\r\n accessUserRoleModelIndexStub,\r\n accessUserRoleModelStub,\r\n communicatorsConfigStub,\r\n notificationControllersStub,\r\n notificationMigrationStub,\r\n notificationModelStub,\r\n notificationRoutesStub,\r\n notificationsConfigStub,\r\n socketConfigStub,\r\n} from \"./stubs\";\r\n\r\n/**\r\n * The parts of a project `package.json` this action reads or writes. Deliberately\r\n * partial — it describes what we touch, not the whole manifest.\r\n */\r\ntype ProjectPackageJson = {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n scripts?: Record<string, string>;\r\n};\r\n\r\n/**\r\n * The part of a project `tsconfig.json` this action patches.\r\n */\r\ntype ProjectTsConfig = {\r\n include?: string[];\r\n};\r\n\r\n/**\r\n * Build a migration filename timestamp prefix in the framework's\r\n * MM-DD-YYYY_HH-MM-SS form. Cascade infers a migration's createdAt from this\r\n * prefix and orders migrations deterministically by it. Pass `offsetSeconds` to\r\n * stamp sibling migrations created in the same scaffold a second apart so they\r\n * never collide and keep a stable relative order.\r\n */\r\nfunction migrationTimestamp(offsetSeconds = 0): string {\r\n const now = new Date(Date.now() + offsetSeconds * 1000);\r\n const pad = (value: number) => String(value).padStart(2, \"0\");\r\n\r\n return (\r\n `${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${now.getFullYear()}_` +\r\n `${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`\r\n );\r\n}\r\n\r\nasync function completeTestInstallation(options: CommandActionData) {\r\n // Create test-global-setup.ts (runs once before all tests)\r\n const testGlobalSetupPath = srcPath(\"test-global-setup.ts\");\r\n const testGlobalSetupExists = await fileExistsAsync(testGlobalSetupPath);\r\n\r\n if (!testGlobalSetupExists) {\r\n await putFileAsync(\r\n testGlobalSetupPath,\r\n `/**\r\n * Global Test Setup\r\n *\r\n * Runs ONCE before all test workers.\r\n * Starts the HTTP server for integration tests.\r\n */\r\nimport { startHttpTestServer, stopHttpTestServer } from \"@warlock.js/core/tests\";\r\n\r\nexport async function setup() {\r\n await startHttpTestServer();\r\n}\r\n\r\nexport async function teardown() {\r\n await stopHttpTestServer();\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-global-setup.ts`);\r\n }\r\n\r\n // Create test-setup.ts (runs before EVERY test file)\r\n const testSetupPath = srcPath(\"test-setup.ts\");\r\n const testSetupExists = await fileExistsAsync(testSetupPath);\r\n\r\n if (!testSetupExists) {\r\n await putFileAsync(\r\n testSetupPath,\r\n `/**\r\n * Test Setup - runs before EVERY test file\r\n *\r\n * Vitest runs setupFiles before each test file and rebuilds the module\r\n * registry with it, so this pair boots and closes the test runtime once per\r\n * test file.\r\n *\r\n * setupTest() is called with no options on purpose: an explicit connectors\r\n * value outranks tests.connectors from src/config/tests.ts, so passing one\r\n * here would erase your project config. Omitting it leaves the config in\r\n * charge.\r\n *\r\n * afterAll(teardownTest) is the other half of the pair: whoever calls\r\n * setupTest() owns closing it in the same runtime context.\r\n */\r\nimport { setupTest, teardownTest } from \"@warlock.js/core/tests\";\r\nimport { afterAll } from \"vitest\";\r\n\r\nawait setupTest();\r\n\r\nafterAll(teardownTest);\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/test-setup.ts`);\r\n }\r\n\r\n // Create vite.config.ts\r\n const viteConfigPath = rootPath(\"vite.config.ts\");\r\n const viteConfigExists = await fileExistsAsync(viteConfigPath);\r\n\r\n if (!viteConfigExists) {\r\n await putFileAsync(\r\n viteConfigPath,\r\n `import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\r\nimport mongezVite from \"@mongez/vite\";\r\nimport { defineConfig } from \"vitest/config\";\r\n\r\nexport default defineConfig({\r\n // lowerStage3Decorators MUST come first: it lowers native (@RegisterModel, …)\r\n // decorators with esbuild before oxc / the SSR rewrite can mangle them, so\r\n // decorated Cascade models load under Vitest.\r\n plugins: [lowerStage3Decorators(), mongezVite()],\r\n test: {\r\n globalSetup: \"./src/test-global-setup.ts\", // HTTP server - runs once\r\n setupFiles: [\"./src/test-setup.ts\"], // DB/cache - runs per test file\r\n environment: \"node\",\r\n globals: false,\r\n include: [\"src/app/**/*.test.ts\"],\r\n },\r\n});\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created vite.config.ts`);\r\n }\r\n}\r\n\r\nasync function completeReactEmailInstallation(_options: CommandActionData) {\r\n // 1. Create emails/ folder with a sample component\r\n const emailsFolderPath = rootPath(\"emails\");\r\n const sampleEmailPath = rootPath(\"emails/welcome-email.tsx\");\r\n\r\n if (!(await fileExistsAsync(sampleEmailPath))) {\r\n await ensureDirectoryAsync(emailsFolderPath);\r\n await putFileAsync(\r\n sampleEmailPath,\r\n `import { Body, Container, Head, Html, Text } from \"@react-email/components\";\r\nimport { Tailwind } from \"@react-email/tailwind\";\r\n\r\ninterface WelcomeEmailProps {\r\n name: string;\r\n}\r\n\r\n/**\r\n * Sample welcome email component.\r\n * Preview with: yarn email:preview\r\n */\r\nexport default function WelcomeEmail({ name }: WelcomeEmailProps) {\r\n return (\r\n <Html>\r\n <Head />\r\n <Tailwind>\r\n <Body className=\"bg-gray-100 font-sans\">\r\n <Container className=\"mx-auto max-w-xl py-8 px-4\">\r\n <Text className=\"text-2xl font-bold text-gray-900\">\r\n Welcome, {name}!\r\n </Text>\r\n <Text className=\"text-gray-600 mt-2\">\r\n You're all set. We're glad to have you on board.\r\n </Text>\r\n </Container>\r\n </Body>\r\n </Tailwind>\r\n </Html>\r\n );\r\n}\r\n`,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created emails/welcome-email.tsx`);\r\n }\r\n\r\n // 2. Patch tsconfig.json — add \"emails\" to include if missing\r\n const tsconfigPath = rootPath(\"tsconfig.json\");\r\n const tsconfig = await getJsonFileAsync<ProjectTsConfig>(tsconfigPath);\r\n\r\n if (!tsconfig.include) {\r\n tsconfig.include = [];\r\n }\r\n\r\n if (!tsconfig.include.includes(\"emails\")) {\r\n tsconfig.include.push(\"emails\");\r\n await putJsonFileAsync(tsconfigPath, tsconfig);\r\n console.log(`${colors.green(\"✓\")} Added \"emails\" to tsconfig.json include`);\r\n }\r\n}\r\n\r\nasync function completeNotificationsInstallation(_options: CommandActionData) {\r\n const modelPath = srcPath(\"app/notifications/notification.model.ts\");\r\n\r\n // The model file is the sentinel for \"notifications already scaffolded\" —\r\n // its presence means the migration was created too (timestamped, so we must\r\n // not re-emit a duplicate on a second run).\r\n if (await fileExistsAsync(modelPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/notifications\")} already scaffolded, skipping model + migration...`,\r\n );\r\n return;\r\n }\r\n\r\n // 1. Notification model — extends the package's DatabaseNotification base.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications\"));\r\n await putFileAsync(modelPath, notificationModelStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/notification.model.ts`);\r\n\r\n // 2. Migration — timestamped MM-DD-YYYY_HH-MM-SS prefix so cascade infers its\r\n // createdAt and orders it deterministically (migrate-action discovers\r\n // src/app/*/migrations/*).\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/migrations\"));\r\n\r\n const migrationFile = `${migrationTimestamp()}-notification.migration.ts`;\r\n\r\n await putFileAsync(\r\n srcPath(\"app/notifications/migrations\", migrationFile),\r\n notificationMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/notifications/migrations/${migrationFile}`,\r\n );\r\n\r\n // 3. HTTP surface — the in-app read/dismiss endpoints (routes + controllers),\r\n // gated by authMiddleware. Delete if the app exposes notifications another way.\r\n await ensureDirectoryAsync(srcPath(\"app/notifications/controllers\"));\r\n await putFileAsync(\r\n srcPath(\"app/notifications/controllers/notifications.controller.ts\"),\r\n notificationControllersStub,\r\n );\r\n await putFileAsync(srcPath(\"app/notifications/routes.ts\"), notificationRoutesStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/notifications/routes.ts + controllers`);\r\n}\r\n\r\nasync function registerAccessLocale() {\r\n // Register the access locale in the project's shared translations file so a\r\n // denied check returns a real sentence, not the raw \"access.errors.forbidden\"\r\n // key. Append when the file exists, create it otherwise; skip if already there.\r\n const localesPath = srcPath(\"app/shared/utils/locales.ts\");\r\n\r\n const accessLocale = `groupedTranslations(\"access\", {\r\n errors: {\r\n forbidden: {\r\n en: \"You do not have permission to perform this action.\",\r\n ar: \"ليس لديك صلاحية لتنفيذ هذا الإجراء.\",\r\n },\r\n },\r\n});\r\n`;\r\n\r\n if (await fileExistsAsync(localesPath)) {\r\n const current = await getFileAsync(localesPath);\r\n\r\n if (current.includes(`groupedTranslations(\"access\"`)) {\r\n console.log(`${colors.yellowBright(\"access\")} locale already registered, skipping...`);\r\n\r\n return;\r\n }\r\n\r\n // The file uses groupedTranslations already iff it calls it — only inject the\r\n // import when no call is present yet.\r\n const importLine = `import { groupedTranslations } from \"@warlock.js/core\";`;\r\n const prefix = current.includes(\"groupedTranslations(\") ? \"\" : `${importLine}\\n\\n`;\r\n\r\n await putFileAsync(localesPath, `${prefix}${current.trimEnd()}\\n\\n${accessLocale}`);\r\n\r\n console.log(\r\n `${colors.green(\"✓\")} Registered the access locale in src/app/shared/utils/locales.ts`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/shared/utils\"));\r\n\r\n await putFileAsync(\r\n localesPath,\r\n `import { groupedTranslations } from \"@warlock.js/core\";\\n\\n${accessLocale}`,\r\n );\r\n\r\n console.log(`${colors.green(\"✓\")} Created src/app/shared/utils/locales.ts with the access locale`);\r\n}\r\n\r\nasync function scaffoldAccessFiles() {\r\n // The resolver file is the sentinel for \"access already scaffolded\" — its\r\n // presence means the role/user-role model folders and their timestamped\r\n // migrations were created too, so we must not re-emit duplicate migrations on\r\n // a second run.\r\n const resolverPath = srcPath(\"app/access/services/access-resolver.ts\");\r\n\r\n if (await fileExistsAsync(resolverPath)) {\r\n console.log(\r\n `${colors.yellowBright(\"src/app/access\")} already scaffolded, skipping resolver + role tables...`,\r\n );\r\n\r\n return;\r\n }\r\n\r\n // 1. Role catalog model folder (model + barrel + migration). The catalog row\r\n // is role name → granted permissions; managed at runtime in the DB.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role\"));\r\n await putFileAsync(srcPath(\"app/access/models/role/role.model.ts\"), accessRoleModelStub);\r\n await putFileAsync(srcPath(\"app/access/models/role/index.ts\"), accessRoleModelIndexStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/role/migrations\"));\r\n\r\n // Migration filenames carry a MM-DD-YYYY_HH-MM-SS prefix so cascade infers\r\n // their createdAt and orders them deterministically (the migrate action\r\n // discovers src/app/*/models/*/migrations/*). The two tables are independent\r\n // (no FK between them), but the user-role migration is stamped a second later\r\n // so the relative order is stable.\r\n const roleMigrationFile = `${migrationTimestamp()}-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/role/migrations\", roleMigrationFile),\r\n accessRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/role/migrations/${roleMigrationFile}`,\r\n );\r\n\r\n // 2. UserRole assignment model folder (model + barrel + migration). The model\r\n // statics scope an unresolved tenant to GLOBAL rows only (security\r\n // invariant) — see the stub for the reasoning.\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role\"));\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/user-role.model.ts\"),\r\n accessUserRoleModelStub,\r\n );\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/index.ts\"),\r\n accessUserRoleModelIndexStub,\r\n );\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/models/user-role`);\r\n\r\n await ensureDirectoryAsync(srcPath(\"app/access/models/user-role/migrations\"));\r\n\r\n const userRoleMigrationFile = `${migrationTimestamp(1)}-user-role.migration.ts`;\r\n await putFileAsync(\r\n srcPath(\"app/access/models/user-role/migrations\", userRoleMigrationFile),\r\n accessUserRoleMigrationStub,\r\n );\r\n console.log(\r\n `${colors.green(\"✓\")} Created src/app/access/models/user-role/migrations/${userRoleMigrationFile}`,\r\n );\r\n\r\n // 3. The DatabaseAccessResolver — the one required config seam, wired into\r\n // config/access.ts by the ejected stub.\r\n await ensureDirectoryAsync(srcPath(\"app/access/services\"));\r\n await putFileAsync(resolverPath, accessResolverStub);\r\n console.log(`${colors.green(\"✓\")} Created src/app/access/services/access-resolver.ts`);\r\n}\r\n\r\nasync function completeAccessInstallation(_options: CommandActionData) {\r\n await registerAccessLocale();\r\n await scaffoldAccessFiles();\r\n}\r\n\r\n/**\r\n * Link a satellite AI package into src/config/ai.ts via a side-effect import.\r\n *\r\n * The satellites (@warlock.js/ai-tools, ai-panoptic, ai-workspace) augment the\r\n * `ai` object on import — they register their runtime surface (ai.tools / ai.mcp,\r\n * ai.workspace, panoptic's ai.config({ panoptic }) wiring) AND the matching TS\r\n * declaration-merging so ai.* members resolve. config-loader runs config/ai.ts at\r\n * boot, so dropping a bare `import \"<specifier>\";` at the top of that file is what\r\n * actually loads the augmentation before the ai connector applies the config.\r\n *\r\n * No-op if config/ai.ts is missing (the `ai` feature ejects it) or the import is\r\n * already present. Inserts right after the `warlock:ai-packages` marker when it\r\n * exists so the satellite imports stay grouped; otherwise prepends to the top.\r\n */\r\nasync function linkAiPackageImport(specifier: string): Promise<void> {\r\n const aiConfigPath = srcPath(\"config/ai.ts\");\r\n\r\n if (!(await fileExistsAsync(aiConfigPath))) {\r\n return;\r\n }\r\n\r\n const current = await getFileAsync(aiConfigPath);\r\n const importLine = `import \"${specifier}\";`;\r\n\r\n if (current.includes(importLine)) {\r\n return;\r\n }\r\n\r\n const marker = \"// >>> warlock:ai-packages (auto-managed) >>>\";\r\n let next: string;\r\n\r\n if (current.includes(marker)) {\r\n next = current.replace(marker, `${marker}\\n${importLine}`);\r\n } else {\r\n next = `${importLine}\\n${current}`;\r\n }\r\n\r\n await putFileAsync(aiConfigPath, next);\r\n\r\n console.log(`${colors.green(\"✓\")} Linked ${specifier} in src/config/ai.ts`);\r\n}\r\n\r\nexport const featuresMap: Record<\r\n string,\r\n {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n description: string;\r\n requires?: string[];\r\n script?: Record<string, string>;\r\n onExecuting?: (options: CommandActionData) => Promise<any>;\r\n ejectConfig?: {\r\n content: string;\r\n name: string;\r\n };\r\n }\r\n> = {\r\n \"react-email\": {\r\n description: \"Installs react-email for building email templates with React and Tailwind\",\r\n requires: [\"mail\", \"react\"],\r\n dependencies: {\r\n \"react-email\": \"^5.2.10\",\r\n \"@react-email/components\": \"^1.0.11\",\r\n \"@react-email/render\": \"^2.0.5\",\r\n \"@react-email/tailwind\": \"^2.0.7\",\r\n },\r\n devDependencies: {\r\n \"@react-email/preview-server\": \"5.2.10\",\r\n },\r\n script: {\r\n \"email:preview\": \"npx react-email dev\",\r\n },\r\n onExecuting: completeReactEmailInstallation,\r\n },\r\n react: {\r\n description:\r\n \"Installs React and React dom for rendering React components (non-interactive), useful for sending mails and generating HTML\",\r\n dependencies: {\r\n react: \"^19.2.3\",\r\n \"react-dom\": \"^19.2.3\",\r\n },\r\n devDependencies: {\r\n \"@types/react\": \"^19.2.7\",\r\n \"@types/react-dom\": \"^19.2.3\",\r\n },\r\n },\r\n image: {\r\n description: \"Installs sharp for image processing\",\r\n dependencies: {\r\n sharp: \"^0.34.5\",\r\n },\r\n },\r\n mail: {\r\n description: \"Installs nodemailer for sending emails\",\r\n dependencies: {\r\n nodemailer: \"^8.0.5\",\r\n },\r\n devDependencies: {\r\n \"@types/nodemailer\": \"^8.0.0\",\r\n },\r\n },\r\n ses: {\r\n description: \"Installs AWS SES SDK for sending emails via Amazon SES\",\r\n dependencies: {\r\n \"@aws-sdk/client-sesv2\": \"^3.1025.0\",\r\n },\r\n },\r\n mongodb: {\r\n description: \"Installs mongodb driver for database driver (Cascade Package)\",\r\n dependencies: {\r\n mongodb: \"^7.0.0\",\r\n },\r\n },\r\n scheduler: {\r\n description: \"Installs warlock scheduler for scheduling tasks\",\r\n dependencies: {\r\n \"@warlock.js/scheduler\": \"~4.0.0\",\r\n },\r\n },\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: {\r\n description: \"Installs pg for Postgres database (Cascade Package)\",\r\n dependencies: {\r\n pg: \"^8.11.0\",\r\n },\r\n },\r\n mysql: {\r\n description: \"Installs mysql2 for MySQL database driver (Cascade Package)\",\r\n dependencies: {\r\n mysql2: \"^3.5.0\",\r\n },\r\n },\r\n redis: {\r\n description: \"Installs redis for Redis cache driver (Cache Package)\",\r\n dependencies: {\r\n redis: \"^4.6.13\",\r\n },\r\n },\r\n s3: {\r\n description: \"Installs AWS SDK for Cloud storage (Storage Package)\",\r\n dependencies: {\r\n \"@aws-sdk/client-s3\": \"^3.955.0\",\r\n \"@aws-sdk/lib-storage\": \"^3.955.0\",\r\n \"@aws-sdk/s3-request-presigner\": \"^3.955.0\",\r\n },\r\n },\r\n test: {\r\n description: \"Installs warlock test for testing\",\r\n onExecuting: completeTestInstallation,\r\n script: {\r\n test: \"vitest run\",\r\n \"test:coverage\": \"vitest run --coverage\",\r\n \"test:ui\": \"vitest --ui\",\r\n \"test:watch\": \"vitest --watch\",\r\n },\r\n devDependencies: {\r\n \"@mongez/vite\": \"^2.0.4\",\r\n vite: \"^8.0.16\",\r\n vitest: \"^4.1.8\",\r\n \"@vitest/coverage-v8\": \"^4.1.8\",\r\n },\r\n },\r\n herald: {\r\n description: \"Installs herald for message broker (Herald Package)\",\r\n dependencies: {\r\n \"@warlock.js/herald\": \"~4.0.0\",\r\n amqplib: \"^0.10.0\",\r\n },\r\n devDependencies: {\r\n \"@types/amqplib\": \"^0.10.0\",\r\n },\r\n ejectConfig: {\r\n content: communicatorsConfigStub,\r\n name: \"herald\",\r\n },\r\n },\r\n socket: {\r\n description: \"Installs socket.io for the realtime socket server (Socket Connector)\",\r\n dependencies: {\r\n \"socket.io\": \"^4.8.3\",\r\n },\r\n ejectConfig: {\r\n content: socketConfigStub,\r\n name: \"socket\",\r\n },\r\n },\r\n notifications: {\r\n description:\r\n \"Installs @warlock.js/notifications — multi-channel notifications (mail + in-app database). Pulls the mail feature, ejects config/notifications.ts, and scaffolds the Notification model + migration plus the recipient-scoped read/dismiss routes + controllers into src/app/notifications\",\r\n // The ejected config wires a `mail` channel by default (needs nodemailer,\r\n // via the `mail` feature); the scaffolded routes are gated by\r\n // `authMiddleware`, so `@warlock.js/auth` is pulled in too.\r\n requires: [\"mail\"],\r\n dependencies: {\r\n \"@warlock.js/notifications\": \"~4.0.0\",\r\n \"@warlock.js/auth\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: notificationsConfigStub,\r\n name: \"notifications\",\r\n },\r\n onExecuting: completeNotificationsInstallation,\r\n },\r\n access: {\r\n description:\r\n \"Installs @warlock.js/access — authorization (RBAC + ABAC): permission checks, ABAC policies, and roles. Ejects config/access.ts, the DatabaseAccessResolver + Role/UserRole models and migrations into src/app/access, and registers the access locale in src/app/shared/utils/locales.ts\",\r\n dependencies: {\r\n \"@warlock.js/access\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: accessConfigStub,\r\n name: \"access\",\r\n },\r\n onExecuting: completeAccessInstallation,\r\n },\r\n ai: {\r\n description: \"Installs @warlock.js/ai — the core AI toolkit (agents, tools, workflows)\",\r\n dependencies: {\r\n \"@warlock.js/ai\": \"~4.0.0\",\r\n },\r\n ejectConfig: {\r\n content: aiConfigStub,\r\n name: \"ai\",\r\n },\r\n },\r\n \"ai-openai\": {\r\n description: \"OpenAI provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-openai\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-google\": {\r\n description: \"Google (Gemini) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-google\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-anthropic\": {\r\n description: \"Anthropic (Claude) provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-anthropic\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-bedrock\": {\r\n description: \"AWS Bedrock provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-bedrock\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-ollama\": {\r\n description: \"Ollama provider for @warlock.js/ai (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-ollama\": \"~4.0.0\",\r\n },\r\n },\r\n \"ai-tools\": {\r\n description:\r\n \"Installs @warlock.js/ai-tools — ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + an MCP client/server, under ai.tools.* / ai.mcp (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-tools\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-tools\"),\r\n },\r\n \"ai-panoptic\": {\r\n description:\r\n \"Installs @warlock.js/ai-panoptic — observability for @warlock.js/ai (collector, exporters, zero-setup local dashboard) via ai.config({ panoptic }) (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-panoptic\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-panoptic\"),\r\n },\r\n \"ai-workspace\": {\r\n description:\r\n \"Installs @warlock.js/ai-workspace — a policy-jailed filesystem + shell workspace for coding agents, as ai.workspace (pulls the core ai package)\",\r\n requires: [\"ai\"],\r\n dependencies: {\r\n \"@warlock.js/ai-workspace\": \"~4.0.0\",\r\n },\r\n onExecuting: () => linkAiPackageImport(\"@warlock.js/ai-workspace\"),\r\n },\r\n};\r\n\r\nexport const allowedFeatures = Object.keys(featuresMap);\r\n\r\nfunction resolveFeatures(features: string[], visited = new Set<string>()): string[] {\r\n const resolved: string[] = [];\r\n\r\n for (const feature of features) {\r\n if (visited.has(feature)) continue;\r\n visited.add(feature);\r\n\r\n const def = featuresMap[feature];\r\n\r\n if (def.requires?.length) {\r\n resolved.push(...resolveFeatures(def.requires, visited));\r\n }\r\n\r\n resolved.push(feature);\r\n }\r\n\r\n return resolved;\r\n}\r\n\r\nexport async function addCommandAction(options: CommandActionData) {\r\n const features = options.args;\r\n const { packageManager, list, noInstall } = options.options;\r\n\r\n if (list) {\r\n console.log(\"Available Features:\");\r\n\r\n for (const feature of allowedFeatures) {\r\n console.log(\r\n `- ${colors.yellowBright(feature)}: ${colors.green(featuresMap[feature].description)}`,\r\n );\r\n }\r\n\r\n process.exit(0);\r\n }\r\n\r\n validateFeatures(features);\r\n\r\n const resolvedFeatures = resolveFeatures(features);\r\n\r\n const dependencies: Record<string, string> = {};\r\n const devDependencies: Record<string, string> = {};\r\n const ejectConfigs: Record<string, { content: string; name: string }> = {};\r\n const scripts: Record<string, string> = {};\r\n\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n Object.assign(dependencies, featurePackages.dependencies);\r\n if (featurePackages.devDependencies) {\r\n Object.assign(devDependencies, featurePackages.devDependencies);\r\n }\r\n\r\n if (featurePackages.ejectConfig) {\r\n ejectConfigs[featurePackages.ejectConfig.name] = featurePackages.ejectConfig;\r\n }\r\n\r\n if (featurePackages.script) {\r\n Object.assign(scripts, featurePackages.script);\r\n }\r\n }\r\n\r\n // Pin every @warlock.js/* feature package to the INSTALLED framework version so\r\n // a scaffolded project's features match its core version instead of drifting to\r\n // the feature map's static range.\r\n const frameworkVersion = await getWarlockVersion();\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n dependencies[dependency] = frameworkVersion;\r\n }\r\n }\r\n\r\n const currentPackageJson = await getJsonFileAsync<ProjectPackageJson>(rootPath(\"package.json\"));\r\n\r\n // Fresh templates may omit one of the maps — guard before reading.\r\n currentPackageJson.dependencies = currentPackageJson.dependencies ?? {};\r\n currentPackageJson.devDependencies = currentPackageJson.devDependencies ?? {};\r\n\r\n // Skip anything already present so we never downgrade an existing pin.\r\n for (const dependency of Object.keys(dependencies)) {\r\n if (currentPackageJson.dependencies[dependency]) {\r\n console.log(`${colors.yellowBright(dependency)} is already installed, skipping...`);\r\n delete dependencies[dependency];\r\n }\r\n }\r\n\r\n for (const devDependency of Object.keys(devDependencies)) {\r\n if (currentPackageJson.devDependencies[devDependency]) {\r\n console.log(`${colors.yellowBright(devDependency)} is already installed, skipping...`);\r\n delete devDependencies[devDependency];\r\n }\r\n }\r\n\r\n if (noInstall) {\r\n await recordDependencies(dependencies, devDependencies);\r\n } else {\r\n await installDependencies(packageManager as PackageManager | undefined, dependencies, devDependencies);\r\n }\r\n\r\n for (const [name, config] of Object.entries(ejectConfigs)) {\r\n if (await fileExistsAsync(srcPath(`config/${name}.ts`))) {\r\n console.log(`${colors.yellowBright(name)} config already exists, skipping...`);\r\n continue;\r\n }\r\n\r\n console.log(`Creating ${colors.magenta(name)} config...`);\r\n\r\n await putFileAsync(srcPath(`config/${name}.ts`), config.content);\r\n\r\n console.log(`${colors.green(name)} config created successfully`);\r\n }\r\n\r\n // now loop again over features to execute onExecuting\r\n for (const feature of resolvedFeatures) {\r\n const featurePackages = featuresMap[feature as keyof typeof featuresMap];\r\n if (featurePackages.onExecuting) {\r\n await featurePackages.onExecuting(options);\r\n }\r\n }\r\n\r\n if (Object.keys(scripts).length > 0) {\r\n console.log(`Adding scripts ${colors.magenta(Object.keys(scripts).join(\", \"))}`);\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n packageJson.scripts = { ...(packageJson.scripts ?? {}), ...scripts };\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n console.log(`Scripts added successfully ${colors.green(Object.keys(scripts).join(\", \"))}`);\r\n }\r\n}\r\n\r\n/**\r\n * Install the resolved dependency sets through the project's package manager.\r\n * Runs two passes (prod then dev) so each lands in the correct section.\r\n */\r\nasync function installDependencies(\r\n packageManager: PackageManager | undefined,\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n // `--package-manager` is optional; without it, fall back to the lockfile.\r\n const packageManagerCommand = getAddCommand(packageManager ?? (await detectPackageManager()));\r\n\r\n if (Object.keys(dependencies).length > 0) {\r\n console.log(`Installing dependencies ${colors.magenta(Object.keys(dependencies).join(\", \"))}`);\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(dependencies).join(\" \")}`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dependencies installed successfully ${colors.green(Object.keys(dependencies).join(\", \"))}`,\r\n );\r\n }\r\n\r\n if (Object.keys(devDependencies).length > 0) {\r\n console.log(\r\n `Installing dev dependencies ${colors.magenta(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n\r\n execSync(`${packageManagerCommand} ${Object.keys(devDependencies).join(\" \")} -D`, {\r\n cwd: process.cwd(),\r\n stdio: \"inherit\",\r\n });\r\n\r\n console.log(\r\n `Dev dependencies installed successfully ${colors.green(Object.keys(devDependencies).join(\", \"))}`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Write the resolved dependency sets into package.json without installing.\r\n * Used by `--no-install` so a scaffolder can batch every feature into one\r\n * install pass after the command returns. Versions come from the feature map.\r\n */\r\nasync function recordDependencies(\r\n dependencies: Record<string, string>,\r\n devDependencies: Record<string, string>,\r\n) {\r\n if (Object.keys(dependencies).length === 0 && Object.keys(devDependencies).length === 0) {\r\n return;\r\n }\r\n\r\n const packageJsonPath = rootPath(\"package.json\");\r\n const packageJson = await getJsonFileAsync<ProjectPackageJson>(packageJsonPath);\r\n\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n packageJson.devDependencies = packageJson.devDependencies ?? {};\r\n\r\n Object.assign(packageJson.dependencies, dependencies);\r\n Object.assign(packageJson.devDependencies, devDependencies);\r\n\r\n await putJsonFileAsync(packageJsonPath, packageJson);\r\n\r\n const recorded = [...Object.keys(dependencies), ...Object.keys(devDependencies)];\r\n\r\n console.log(\r\n `Recorded ${colors.green(recorded.join(\", \"))} in package.json (install skipped via --no-install)`,\r\n );\r\n}\r\n\r\nfunction validateFeatures(features: string[]) {\r\n for (const feature of features) {\r\n if (!allowedFeatures.includes(feature)) {\r\n console.log(\r\n `Feature ${colors.redBright(feature)} is not allowed, allowed features are: ${colors.green(allowedFeatures.join(\", \"))}`,\r\n );\r\n process.exit(1);\r\n }\r\n }\r\n}\r\n\r\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { StartHttpTestServerOptions, isTestServerRunning, startHttpTestServer, stopHttpTestServer } from "./start-http-development-server.mjs";
2
2
  import { expectJson, getTestServerUrl, parseJsonResponse, testDelete, testGet, testPatch, testPost, testPut, testRequest } from "./test-helpers.mjs";
3
- import { setupTest } from "./vitest-setup.mjs";
4
- export { StartHttpTestServerOptions, expectJson, getTestServerUrl, isTestServerRunning, parseJsonResponse, setupTest, startHttpTestServer, stopHttpTestServer, testDelete, testGet, testPatch, testPost, testPut, testRequest };
3
+ import { TestConnectorsSelection } from "./test-connectors-selection.mjs";
4
+ import { TestLifecycleError, TestSetupOptions, setupTest, teardownTest } from "./vitest-setup.mjs";
5
+ export { StartHttpTestServerOptions, type TestConnectorsSelection, TestLifecycleError, TestSetupOptions, expectJson, getTestServerUrl, isTestServerRunning, parseJsonResponse, setupTest, startHttpTestServer, stopHttpTestServer, teardownTest, testDelete, testGet, testPatch, testPost, testPut, testRequest };
@@ -1,5 +1,5 @@
1
1
  import { isTestServerRunning, startHttpTestServer, stopHttpTestServer } from "./start-http-development-server.mjs";
2
2
  import { expectJson, getTestServerUrl, parseJsonResponse, testDelete, testGet, testPatch, testPost, testPut, testRequest } from "./test-helpers.mjs";
3
- import { setupTest } from "./vitest-setup.mjs";
3
+ import { TestLifecycleError, setupTest, teardownTest } from "./vitest-setup.mjs";
4
4
 
5
- export { expectJson, getTestServerUrl, isTestServerRunning, parseJsonResponse, setupTest, startHttpTestServer, stopHttpTestServer, testDelete, testGet, testPatch, testPost, testPut, testRequest };
5
+ export { TestLifecycleError, expectJson, getTestServerUrl, isTestServerRunning, parseJsonResponse, setupTest, startHttpTestServer, stopHttpTestServer, teardownTest, testDelete, testGet, testPatch, testPost, testPut, testRequest };
@@ -0,0 +1,14 @@
1
+ import { ConnectorName } from "../connectors/types.mjs";
2
+ //#region ../core/src/tests/test-connectors-selection.d.ts
3
+ /**
4
+ * What a test runtime was asked to start.
5
+ *
6
+ * - `false` — bootstrap the runtime, start no connectors.
7
+ * - `true` — start the framework's default connector set.
8
+ * - an array — start exactly those. Order is not promised; lifecycle priority
9
+ * decides the real boot/shutdown order.
10
+ */
11
+ type TestConnectorsSelection = boolean | ConnectorName[];
12
+ //#endregion
13
+ export { TestConnectorsSelection };
14
+ //# sourceMappingURL=test-connectors-selection.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-connectors-selection.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-connectors-selection.ts"],"mappings":";;;;;AAqBA;;;;AAA6D;KAAjD,uBAAA,aAAoC,aAAa"}
@@ -0,0 +1,72 @@
1
+ import { config } from "../config/config-getter.mjs";
2
+ import "../config/index.mjs";
3
+
4
+ //#region ../core/src/tests/test-connectors-selection.ts
5
+ /**
6
+ * Classify the caller's `connectors` option as explicit or deferred.
7
+ */
8
+ function readRequestedConnectors(selection) {
9
+ if (selection === void 0) return { isExplicit: false };
10
+ return {
11
+ isExplicit: true,
12
+ selection
13
+ };
14
+ }
15
+ /**
16
+ * Read the `tests.connectors` config layer, or `undefined` when the project has
17
+ * not configured one.
18
+ *
19
+ * Only meaningful once config files are loaded — before that, every project
20
+ * looks like a project without a `tests` config.
21
+ */
22
+ function readConfiguredConnectors() {
23
+ return config.get("tests", {})?.connectors;
24
+ }
25
+ /**
26
+ * Apply the ratified precedence:
27
+ *
28
+ * explicit non-`undefined` setupTest option > tests.connectors config > true
29
+ *
30
+ * ⚠ This is the reverse of 4.13, where config won over the parameter. Explicit
31
+ * call-site intent beats a project default; a call that supplied nothing is not
32
+ * intent, which is why "omitted" and "explicitly `undefined`" both defer.
33
+ */
34
+ function resolveEffectiveConnectors(requested) {
35
+ if (requested.isExplicit) return requested.selection;
36
+ const configured = readConfiguredConnectors();
37
+ if (configured !== void 0) return configured;
38
+ return true;
39
+ }
40
+ /**
41
+ * Deduplicate a connector list and put it in a stable order.
42
+ *
43
+ * Two callers naming the same connectors in a different order asked for the same
44
+ * thing, so the lifecycle must not treat them as a conflict.
45
+ */
46
+ function normalizeConnectorNames(names) {
47
+ return [...new Set(names)].sort();
48
+ }
49
+ /**
50
+ * Compare two selections by normalized semantics — arrays as deduplicated sets,
51
+ * never by caller order.
52
+ */
53
+ function isSameConnectorsSelection(left, right) {
54
+ if (Array.isArray(left) && Array.isArray(right)) {
55
+ const normalizedLeft = normalizeConnectorNames(left);
56
+ const normalizedRight = normalizeConnectorNames(right);
57
+ return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((name, index) => name === normalizedRight[index]);
58
+ }
59
+ return left === right;
60
+ }
61
+ /**
62
+ * Render a selection for an error message a caller can act on.
63
+ */
64
+ function describeConnectorsSelection(selection) {
65
+ if (selection === true) return "the default connector set (`connectors: true`)";
66
+ if (selection === false) return "no connectors (`connectors: false`)";
67
+ return `\`connectors: [${normalizeConnectorNames(selection).map((name) => `"${name}"`).join(", ")}]\``;
68
+ }
69
+
70
+ //#endregion
71
+ export { describeConnectorsSelection, isSameConnectorsSelection, normalizeConnectorNames, readRequestedConnectors, resolveEffectiveConnectors };
72
+ //# sourceMappingURL=test-connectors-selection.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-connectors-selection.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-connectors-selection.ts"],"sourcesContent":["/**\n * Effective connector selection for the worker test lifecycle.\n *\n * Owns one decision: given what the caller passed and what the project\n * configured, which connectors does `setupTest` actually start? The precedence\n * is ratified in `contracts/2026-08-12-test-worker-lifecycle.md`:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\nimport type { ConnectorName } from \"../connectors\";\n\n/**\n * What a test runtime was asked to start.\n *\n * - `false` — bootstrap the runtime, start no connectors.\n * - `true` — start the framework's default connector set.\n * - an array — start exactly those. Order is not promised; lifecycle priority\n * decides the real boot/shutdown order.\n */\nexport type TestConnectorsSelection = boolean | ConnectorName[];\n\n/**\n * A caller's request, before the lower precedence layers are consulted.\n *\n * The distinction that matters is \"did the caller supply a value at all\", not\n * \"what value\" — `setupTest({ connectors: undefined })` must fall through to\n * project config exactly like `setupTest()` does, so an optional variable\n * holding `undefined` can never silently erase a project's configuration.\n */\nexport type RequestedTestConnectors =\n | { readonly isExplicit: true; readonly selection: TestConnectorsSelection }\n | { readonly isExplicit: false };\n\n/**\n * Classify the caller's `connectors` option as explicit or deferred.\n */\nexport function readRequestedConnectors(\n selection: TestConnectorsSelection | undefined,\n): RequestedTestConnectors {\n if (selection === undefined) {\n return { isExplicit: false };\n }\n\n return { isExplicit: true, selection };\n}\n\n/**\n * Read the `tests.connectors` config layer, or `undefined` when the project has\n * not configured one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n */\nexport function readConfiguredConnectors(): TestConnectorsSelection | undefined {\n // The default matters: `config.get` resolves an absent key to its default, and\n // ITS default is `null` — not `{}`. `warlock add test` does not generate\n // `src/config/tests.ts`, so reading `.connectors` off the result threw\n // \"Cannot read properties of null\" on the generated default path.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n\n // Cast at the config boundary: `GenericObject` values are untyped, and this is\n // the one place the untyped value becomes a typed selection.\n return testsConfig?.connectors as TestConnectorsSelection | undefined;\n}\n\n/**\n * Apply the ratified precedence:\n *\n * explicit non-`undefined` setupTest option > tests.connectors config > true\n *\n * ⚠ This is the reverse of 4.13, where config won over the parameter. Explicit\n * call-site intent beats a project default; a call that supplied nothing is not\n * intent, which is why \"omitted\" and \"explicitly `undefined`\" both defer.\n */\nexport function resolveEffectiveConnectors(\n requested: RequestedTestConnectors,\n): TestConnectorsSelection {\n if (requested.isExplicit) {\n return requested.selection;\n }\n\n const configured = readConfiguredConnectors();\n\n if (configured !== undefined) {\n return configured;\n }\n\n return true;\n}\n\n/**\n * Deduplicate a connector list and put it in a stable order.\n *\n * Two callers naming the same connectors in a different order asked for the same\n * thing, so the lifecycle must not treat them as a conflict.\n */\nexport function normalizeConnectorNames(names: ConnectorName[]): ConnectorName[] {\n return [...new Set(names)].sort();\n}\n\n/**\n * Compare two selections by normalized semantics — arrays as deduplicated sets,\n * never by caller order.\n */\nexport function isSameConnectorsSelection(\n left: TestConnectorsSelection,\n right: TestConnectorsSelection,\n): boolean {\n if (Array.isArray(left) && Array.isArray(right)) {\n const normalizedLeft = normalizeConnectorNames(left);\n const normalizedRight = normalizeConnectorNames(right);\n\n return (\n normalizedLeft.length === normalizedRight.length &&\n normalizedLeft.every((name, index) => name === normalizedRight[index])\n );\n }\n\n return left === right;\n}\n\n/**\n * Render a selection for an error message a caller can act on.\n */\nexport function describeConnectorsSelection(selection: TestConnectorsSelection): string {\n if (selection === true) {\n return \"the default connector set (`connectors: true`)\";\n }\n\n if (selection === false) {\n return \"no connectors (`connectors: false`)\";\n }\n\n const names = normalizeConnectorNames(selection)\n .map((name) => `\"${name}\"`)\n .join(\", \");\n\n return `\\`connectors: [${names}]\\``;\n}\n"],"mappings":";;;;;;;AAsCA,SAAgB,wBACd,WACyB;CACzB,IAAI,cAAc,QAChB,OAAO,EAAE,YAAY,MAAM;CAG7B,OAAO;EAAE,YAAY;EAAM;CAAU;AACvC;;;;;;;;AASA,SAAgB,2BAAgE;CAS9E,OAJoB,OAAO,IAAmB,SAAS,CAAC,CAIvC,CAAC,EAAE;AACtB;;;;;;;;;;AAWA,SAAgB,2BACd,WACyB;CACzB,IAAI,UAAU,YACZ,OAAO,UAAU;CAGnB,MAAM,aAAa,yBAAyB;CAE5C,IAAI,eAAe,QACjB,OAAO;CAGT,OAAO;AACT;;;;;;;AAQA,SAAgB,wBAAwB,OAAyC;CAC/E,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK;AAClC;;;;;AAMA,SAAgB,0BACd,MACA,OACS;CACT,IAAI,MAAM,QAAQ,IAAI,KAAK,MAAM,QAAQ,KAAK,GAAG;EAC/C,MAAM,iBAAiB,wBAAwB,IAAI;EACnD,MAAM,kBAAkB,wBAAwB,KAAK;EAErD,OACE,eAAe,WAAW,gBAAgB,UAC1C,eAAe,OAAO,MAAM,UAAU,SAAS,gBAAgB,MAAM;CAEzE;CAEA,OAAO,SAAS;AAClB;;;;AAKA,SAAgB,4BAA4B,WAA4C;CACtF,IAAI,cAAc,MAChB,OAAO;CAGT,IAAI,cAAc,OAChB,OAAO;CAOT,OAAO,kBAJO,wBAAwB,SAAS,CAAC,CAC7C,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAC1B,KAAK,IAEqB,EAAE;AACjC"}
@@ -0,0 +1,27 @@
1
+ //#region ../core/src/tests/test-lifecycle-state.ts
2
+ /**
3
+ * Where the registry hangs off the runtime context.
4
+ *
5
+ * @internal Exported for specs, which must be able to reach the same slot the
6
+ * implementation uses — a spec that could only reset module state would be
7
+ * testing the very assumption this module rejects. Deliberately not re-exported
8
+ * from `src/tests/index.ts`.
9
+ */
10
+ const TEST_LIFECYCLE_REGISTRY_KEY = Symbol.for("@warlock.js/core:tests:lifecycle");
11
+ /**
12
+ * Read the runtime context's lifecycle registry, creating it on first use.
13
+ *
14
+ * @internal
15
+ */
16
+ function getTestLifecycleRegistry() {
17
+ const host = globalThis;
18
+ const registry = host[TEST_LIFECYCLE_REGISTRY_KEY];
19
+ if (registry) return registry;
20
+ const freshRegistry = { state: "idle" };
21
+ host[TEST_LIFECYCLE_REGISTRY_KEY] = freshRegistry;
22
+ return freshRegistry;
23
+ }
24
+
25
+ //#endregion
26
+ export { getTestLifecycleRegistry };
27
+ //# sourceMappingURL=test-lifecycle-state.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-lifecycle-state.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-lifecycle-state.ts"],"sourcesContent":["/**\n * Lifecycle bookkeeping for the worker test runtime.\n *\n * Owns one thing: WHERE the state lives. `setupTest` / `teardownTest` own what\n * the transitions mean.\n *\n * The storage rule is the defect this module exists to fix. A module-scoped\n * `let` is rebuilt for every test file — measured across `forks|threads` ×\n * `isolate true|false` in `proofs/2026-08-12-nova-vitest-setupfiles-lifetime.md`\n * — while the worker itself, and every socket, pool and timer it holds, can\n * survive that rebuild. State that resets precisely where live resources do not\n * is not a guard; it is a lie about what is running.\n *\n * `globalThis` is the right scope for it: one per process for a fork worker, one\n * per thread for a thread worker, never shared between distinct workers — which\n * is exactly the boundary the resources themselves live inside.\n */\nimport type {\n RequestedTestConnectors,\n TestConnectorsSelection,\n} from \"./test-connectors-selection\";\nimport type { TestTimeoutScheduler } from \"./test-setup-timeout\";\n\n/**\n * The transitions the lifecycle serializes through.\n *\n * ```\n * idle -> starting -> ready -> stopping -> idle\n * \\-> poisoned\n * ```\n *\n * `poisoned` is reached when the shutdown layer reports a failure: the runtime\n * is neither up nor provably down, so the next `setupTest` must refuse rather\n * than stack a second runtime on top of leaked resources.\n *\n * @internal No state API is public.\n */\nexport type TestLifecycleState = \"idle\" | \"starting\" | \"ready\" | \"stopping\" | \"poisoned\";\n\n/**\n * The single in-flight setup attempt that concurrent `setupTest` callers share.\n *\n * @internal\n */\nexport type TestSetupAttempt = {\n /**\n * What the caller that opened the attempt asked for, before config was read.\n */\n readonly requested: RequestedTestConnectors;\n\n /**\n * Settles once the attempt has consulted project config, and therefore doubles\n * as the \"config is readable now\" barrier a later caller needs before it can\n * resolve its own config-derived selection and compare the two. Rejects with\n * the startup error when the attempt fails before reaching that point.\n */\n readonly effectiveSelection: Promise<TestConnectorsSelection>;\n\n /**\n * The attempt itself. Resolves when the runtime is ready, rejects with the\n * original startup error.\n */\n readonly completion: Promise<void>;\n};\n\n/**\n * @internal\n */\nexport type TestLifecycleRegistry = {\n state: TestLifecycleState;\n activeSelection?: TestConnectorsSelection;\n setupAttempt?: TestSetupAttempt;\n teardownAttempt?: Promise<void>;\n\n /**\n * How the setup bound is scheduled. Specs replace it to prove the guard in\n * milliseconds rather than outliving a real two-minute timeout; production\n * leaves it unset and gets {@link scheduleRealTimeout}.\n *\n * It lives here, on the runtime-context registry, for the same reason the\n * state does: a spec that could only reach a module-level slot would be\n * relying on the very rebuild this module exists to survive.\n */\n scheduleTimeout?: TestTimeoutScheduler;\n\n /**\n * Overrides `tests.setupTimeout` and the default. Internal, for specs — the\n * public surface stays the `setupTest` / `teardownTest` pair.\n */\n setupTimeoutOverride?: number;\n};\n\n/**\n * Where the registry hangs off the runtime context.\n *\n * @internal Exported for specs, which must be able to reach the same slot the\n * implementation uses — a spec that could only reset module state would be\n * testing the very assumption this module rejects. Deliberately not re-exported\n * from `src/tests/index.ts`.\n */\nexport const TEST_LIFECYCLE_REGISTRY_KEY = Symbol.for(\"@warlock.js/core:tests:lifecycle\");\n\ntype TestLifecycleHost = typeof globalThis & {\n [TEST_LIFECYCLE_REGISTRY_KEY]?: TestLifecycleRegistry;\n};\n\n/**\n * Read the runtime context's lifecycle registry, creating it on first use.\n *\n * @internal\n */\nexport function getTestLifecycleRegistry(): TestLifecycleRegistry {\n const host = globalThis as TestLifecycleHost;\n const registry = host[TEST_LIFECYCLE_REGISTRY_KEY];\n\n if (registry) {\n return registry;\n }\n\n const freshRegistry: TestLifecycleRegistry = { state: \"idle\" };\n\n host[TEST_LIFECYCLE_REGISTRY_KEY] = freshRegistry;\n\n return freshRegistry;\n}\n"],"mappings":";;;;;;;;;AAoGA,MAAa,8BAA8B,OAAO,IAAI,kCAAkC;;;;;;AAWxF,SAAgB,2BAAkD;CAChE,MAAM,OAAO;CACb,MAAM,WAAW,KAAK;CAEtB,IAAI,UACF,OAAO;CAGT,MAAM,gBAAuC,EAAE,OAAO,OAAO;CAE7D,KAAK,+BAA+B;CAEpC,OAAO;AACT"}
@@ -0,0 +1,53 @@
1
+ import { config } from "../config/config-getter.mjs";
2
+ import "../config/index.mjs";
3
+
4
+ //#region ../core/src/tests/test-setup-timeout.ts
5
+ /**
6
+ * Two minutes. Chosen to sit far above any healthy bootstrap and far below the
7
+ * point where a human stops watching — a stuck worker should announce itself
8
+ * while someone is still looking at the terminal.
9
+ */
10
+ const DEFAULT_TEST_SETUP_TIMEOUT = 12e4;
11
+ /**
12
+ * The real scheduler.
13
+ *
14
+ * `unref` matters: a pending hang guard must never be the reason a worker stays
15
+ * alive after its tests have finished.
16
+ *
17
+ * @internal
18
+ */
19
+ const scheduleRealTimeout = (timeout, onExpiry) => {
20
+ const timer = setTimeout(onExpiry, timeout);
21
+ timer.unref?.();
22
+ return { cancel: () => clearTimeout(timer) };
23
+ };
24
+ /**
25
+ * Read `tests.setupTimeout`, or `undefined` when the project has not set one.
26
+ *
27
+ * Only meaningful once config files are loaded — before that, every project
28
+ * looks like a project without a `tests` config.
29
+ *
30
+ * @throws when the key is present but is not a positive finite number of
31
+ * milliseconds. Falling back to the default would silently erase a project's
32
+ * configuration, which is the same defect the connector precedence rules out.
33
+ */
34
+ function readConfiguredSetupTimeout() {
35
+ const configured = config.get("tests", {})?.setupTimeout;
36
+ if (configured === void 0) return;
37
+ if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) throw new Error(`tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`);
38
+ return configured;
39
+ }
40
+ /**
41
+ * The message a caller sees when the bound expires.
42
+ *
43
+ * Names three things, because a hang guard that only says "timed out" sends the
44
+ * reader to the framework's source: what state the lifecycle is stuck in, what
45
+ * bound it exceeded, and how to raise that bound.
46
+ */
47
+ function describeExpiredSetup(timeout) {
48
+ return `setupTest() did not finish within ${timeout}ms and is stuck in the "starting" state. The lifecycle is now poisoned: whatever that attempt had already started is not known to be closed, so later setupTest() calls refuse until the Vitest worker is recycled. If your cold start is legitimately slower than this, raise the bound with \`tests.setupTimeout\` in \`src/config/tests.ts\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`;
49
+ }
50
+
51
+ //#endregion
52
+ export { DEFAULT_TEST_SETUP_TIMEOUT, describeExpiredSetup, readConfiguredSetupTimeout, scheduleRealTimeout };
53
+ //# sourceMappingURL=test-setup-timeout.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-setup-timeout.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-setup-timeout.ts"],"sourcesContent":["/**\n * The setup bound for the worker test lifecycle.\n *\n * Owns one decision: how long a `setupTest` attempt may run before the\n * lifecycle gives up on it. `vitest-setup.ts` owns what giving up MEANS.\n *\n * This is a HANG GUARD, not a startup-performance deadline. A cold database, a\n * container that has just been started, a first-run migration — all of those are\n * legitimately slow and none of them are a stuck lifecycle. The default is\n * therefore generous on purpose: it exists so that a setup which will never\n * finish fails with a sentence a caller can act on, instead of stranding the\n * lifecycle in `starting` where `teardownTest`'s wait-then-re-enter path spins\n * until the worker dies of heap exhaustion.\n */\nimport type { GenericObject } from \"@mongez/reinforcements\";\nimport { config } from \"../config\";\n\n/**\n * Two minutes. Chosen to sit far above any healthy bootstrap and far below the\n * point where a human stops watching — a stuck worker should announce itself\n * while someone is still looking at the terminal.\n */\nexport const DEFAULT_TEST_SETUP_TIMEOUT = 120_000;\n\n/**\n * A scheduled expiry the lifecycle can cancel when the attempt settles first.\n *\n * @internal\n */\nexport type TestTimeoutHandle = {\n cancel: () => void;\n};\n\n/**\n * How the lifecycle schedules its expiry.\n *\n * Injectable so a spec can prove the bound in milliseconds instead of outliving\n * a real one — see {@link TestLifecycleRegistry.scheduleTimeout}. Deliberately\n * internal: the public API stays the `setupTest` / `teardownTest` pair.\n *\n * @internal\n */\nexport type TestTimeoutScheduler = (\n timeout: number,\n onExpiry: () => void,\n) => TestTimeoutHandle;\n\n/**\n * The real scheduler.\n *\n * `unref` matters: a pending hang guard must never be the reason a worker stays\n * alive after its tests have finished.\n *\n * @internal\n */\nexport const scheduleRealTimeout: TestTimeoutScheduler = (timeout, onExpiry) => {\n const timer = setTimeout(onExpiry, timeout);\n\n timer.unref?.();\n\n return { cancel: () => clearTimeout(timer) };\n};\n\n/**\n * Read `tests.setupTimeout`, or `undefined` when the project has not set one.\n *\n * Only meaningful once config files are loaded — before that, every project\n * looks like a project without a `tests` config.\n *\n * @throws when the key is present but is not a positive finite number of\n * milliseconds. Falling back to the default would silently erase a project's\n * configuration, which is the same defect the connector precedence rules out.\n */\nexport function readConfiguredSetupTimeout(): number | undefined {\n // `config.get` resolves an absent key to its default, and ITS default is\n // `null` — not `{}` — so the explicit `{}` is what keeps this from throwing\n // \"Cannot read properties of null\" on a project with no `src/config/tests.ts`.\n const testsConfig = config.get<GenericObject>(\"tests\", {});\n const configured = testsConfig?.setupTimeout as unknown;\n\n if (configured === undefined) {\n return undefined;\n }\n\n if (typeof configured !== \"number\" || !Number.isFinite(configured) || configured <= 0) {\n throw new Error(\n `tests.setupTimeout must be a positive number of milliseconds, but it is ${JSON.stringify(configured)}. Remove the key to use the default of ${DEFAULT_TEST_SETUP_TIMEOUT}ms.`,\n );\n }\n\n return configured;\n}\n\n/**\n * The message a caller sees when the bound expires.\n *\n * Names three things, because a hang guard that only says \"timed out\" sends the\n * reader to the framework's source: what state the lifecycle is stuck in, what\n * bound it exceeded, and how to raise that bound.\n */\nexport function describeExpiredSetup(timeout: number): string {\n return (\n `setupTest() did not finish within ${timeout}ms and is stuck in the \"starting\" state. ` +\n \"The lifecycle is now poisoned: whatever that attempt had already started is not known to be \" +\n \"closed, so later setupTest() calls refuse until the Vitest worker is recycled. \" +\n `If your cold start is legitimately slower than this, raise the bound with \\`tests.setupTimeout\\` ` +\n `in \\`src/config/tests.ts\\` — milliseconds, default ${DEFAULT_TEST_SETUP_TIMEOUT}.`\n );\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAa,6BAA6B;;;;;;;;;AAiC1C,MAAa,uBAA6C,SAAS,aAAa;CAC9E,MAAM,QAAQ,WAAW,UAAU,OAAO;CAE1C,MAAM,QAAQ;CAEd,OAAO,EAAE,cAAc,aAAa,KAAK,EAAE;AAC7C;;;;;;;;;;;AAYA,SAAgB,6BAAiD;CAK/D,MAAM,aADc,OAAO,IAAmB,SAAS,CAAC,CAC3B,CAAC,EAAE;CAEhC,IAAI,eAAe,QACjB;CAGF,IAAI,OAAO,eAAe,YAAY,CAAC,OAAO,SAAS,UAAU,KAAK,cAAc,GAClF,MAAM,IAAI,MACR,2EAA2E,KAAK,UAAU,UAAU,EAAE,yCAAyC,2BAA2B,IAC5K;CAGF,OAAO;AACT;;;;;;;;AASA,SAAgB,qBAAqB,SAAyB;CAC5D,OACE,qCAAqC,QAAQ,0WAIS,2BAA2B;AAErF"}