@cedarjs/pg 0.2.0-alpha.0 → 0.2.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +250 -229
  2. package/dist/cli.cjs +89 -39
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.mjs +73 -23
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/dev-env.cjs +1 -1
  7. package/dist/dev-env.mjs +1 -1
  8. package/dist/index.cjs +9 -8
  9. package/dist/index.d.cts +31 -3
  10. package/dist/index.d.mts +31 -3
  11. package/dist/index.mjs +7 -7
  12. package/dist/jest-teardown.cjs +1 -1
  13. package/dist/jest-teardown.mjs +1 -1
  14. package/dist/jest-template.cjs +4 -1
  15. package/dist/jest-template.cjs.map +1 -1
  16. package/dist/jest-template.d.cts +4 -1
  17. package/dist/jest-template.d.mts +4 -1
  18. package/dist/jest-template.mjs +4 -1
  19. package/dist/jest-template.mjs.map +1 -1
  20. package/dist/jest.cjs +1 -1
  21. package/dist/jest.mjs +1 -1
  22. package/dist/{lease-BSBBpaR4.mjs → lease-DS1SX8U_.mjs} +23 -3
  23. package/dist/lease-DS1SX8U_.mjs.map +1 -0
  24. package/dist/{lease-Bq9wKwQa.cjs → lease-WlmOnNDi.cjs} +41 -3
  25. package/dist/lease-WlmOnNDi.cjs.map +1 -0
  26. package/dist/{lifecycle-DEJ4GgWV.cjs → lifecycle-BbrvFQvg.cjs} +175 -78
  27. package/dist/lifecycle-BbrvFQvg.cjs.map +1 -0
  28. package/dist/{lifecycle-BOo6xBjD.mjs → lifecycle-BvIx0xqq.mjs} +175 -78
  29. package/dist/lifecycle-BvIx0xqq.mjs.map +1 -0
  30. package/dist/{load-dev-env-7UjMHhw7.mjs → load-dev-env-BULkXyen.mjs} +2 -2
  31. package/dist/{load-dev-env-7UjMHhw7.mjs.map → load-dev-env-BULkXyen.mjs.map} +1 -1
  32. package/dist/{load-dev-env-cII6N4ZP.cjs → load-dev-env-Bl6Ddv1U.cjs} +2 -2
  33. package/dist/{load-dev-env-cII6N4ZP.cjs.map → load-dev-env-Bl6Ddv1U.cjs.map} +1 -1
  34. package/dist/{load-mode-env-B0qO9DZx.cjs → load-mode-env-CX9vd56X.cjs} +2 -2
  35. package/dist/{load-mode-env-B0qO9DZx.cjs.map → load-mode-env-CX9vd56X.cjs.map} +1 -1
  36. package/dist/{load-mode-env-C8gy4v9V.mjs → load-mode-env-Ce9ujisN.mjs} +2 -2
  37. package/dist/{load-mode-env-C8gy4v9V.mjs.map → load-mode-env-Ce9ujisN.mjs.map} +1 -1
  38. package/dist/{load-test-env-C34wpkIX.mjs → load-test-env-BqtA6V-d.mjs} +2 -2
  39. package/dist/{load-test-env-C34wpkIX.mjs.map → load-test-env-BqtA6V-d.mjs.map} +1 -1
  40. package/dist/{load-test-env-CkdUjTpV.cjs → load-test-env-Dcgh6YHV.cjs} +2 -2
  41. package/dist/{load-test-env-CkdUjTpV.cjs.map → load-test-env-Dcgh6YHV.cjs.map} +1 -1
  42. package/dist/nx.cjs +12 -14
  43. package/dist/nx.cjs.map +1 -1
  44. package/dist/nx.mjs +10 -12
  45. package/dist/nx.mjs.map +1 -1
  46. package/dist/status-4Uz6A3LB.cjs +31 -0
  47. package/dist/status-4Uz6A3LB.cjs.map +1 -0
  48. package/dist/status-Dcnpgnlz.mjs +26 -0
  49. package/dist/status-Dcnpgnlz.mjs.map +1 -0
  50. package/dist/studio-DIFw1qWC.mjs +173 -0
  51. package/dist/studio-DIFw1qWC.mjs.map +1 -0
  52. package/dist/studio-oJE30_PO.cjs +208 -0
  53. package/dist/studio-oJE30_PO.cjs.map +1 -0
  54. package/dist/{tasks-Caud9yHr.cjs → tasks-CNHvvlsN.cjs} +4 -4
  55. package/dist/{tasks-Caud9yHr.cjs.map → tasks-CNHvvlsN.cjs.map} +1 -1
  56. package/dist/{tasks-KherHLac.mjs → tasks-D5iWIdlo.mjs} +2 -2
  57. package/dist/{tasks-KherHLac.mjs.map → tasks-D5iWIdlo.mjs.map} +1 -1
  58. package/dist/{template-CGw3C1Ob.mjs → template-BiJ-0TIF.mjs} +3 -3
  59. package/dist/{template-CGw3C1Ob.mjs.map → template-BiJ-0TIF.mjs.map} +1 -1
  60. package/dist/{template-D0gJ_MS2.cjs → template-CXMpODzK.cjs} +3 -3
  61. package/dist/{template-D0gJ_MS2.cjs.map → template-CXMpODzK.cjs.map} +1 -1
  62. package/dist/{template-mode-IjlFOG4O.mjs → template-mode-BTUsdBtA.mjs} +27 -13
  63. package/dist/template-mode-BTUsdBtA.mjs.map +1 -0
  64. package/dist/{template-mode-CcxV_Iju.d.cts → template-mode-CzyVwHsi.d.cts} +4 -1
  65. package/dist/{template-mode-CcxV_Iju.d.mts → template-mode-CzyVwHsi.d.mts} +4 -1
  66. package/dist/{template-mode-BkEs9LnY.cjs → template-mode-JTOxrEgE.cjs} +27 -13
  67. package/dist/template-mode-JTOxrEgE.cjs.map +1 -0
  68. package/dist/test-env.cjs +1 -1
  69. package/dist/test-env.mjs +1 -1
  70. package/dist/vite-plus.cjs +107 -4
  71. package/dist/vite-plus.cjs.map +1 -1
  72. package/dist/vite-plus.d.cts +25 -1
  73. package/dist/vite-plus.d.mts +25 -1
  74. package/dist/vite-plus.mjs +108 -5
  75. package/dist/vite-plus.mjs.map +1 -1
  76. package/dist/vitest-template.cjs +1 -1
  77. package/dist/vitest-template.d.cts +1 -1
  78. package/dist/vitest-template.d.mts +1 -1
  79. package/dist/vitest-template.mjs +1 -1
  80. package/dist/vitest.cjs +1 -1
  81. package/dist/vitest.mjs +1 -1
  82. package/package.json +11 -2
  83. package/dist/constants-Ct8myrEn.cjs +0 -41
  84. package/dist/constants-Ct8myrEn.cjs.map +0 -1
  85. package/dist/constants-NLR0U4NR.mjs +0 -24
  86. package/dist/constants-NLR0U4NR.mjs.map +0 -1
  87. package/dist/lease-BSBBpaR4.mjs.map +0 -1
  88. package/dist/lease-Bq9wKwQa.cjs.map +0 -1
  89. package/dist/lifecycle-BOo6xBjD.mjs.map +0 -1
  90. package/dist/lifecycle-DEJ4GgWV.cjs.map +0 -1
  91. package/dist/template-mode-BkEs9LnY.cjs.map +0 -1
  92. package/dist/template-mode-IjlFOG4O.mjs.map +0 -1
package/README.md CHANGED
@@ -1,95 +1,62 @@
1
1
  # cedar-pg
2
2
 
3
- Worktree-isolated local Postgres for **Vite+**, **Nx**, and **CedarJS**, powered by [autopg](https://github.com/automagik-dev/autopg).
3
+ Worktree-isolated local Postgres for Vite+, Nx, and CedarJS. The host is [autopg](https://github.com/automagik-dev/autopg) (embedded PostgreSQL 18). cedar-pg creates one database and role per git worktree so parallel checkouts do not share a DB.
4
4
 
5
- Published on npm as **`@cedarjs/pg`**.
5
+ Published as `@cedarjs/pg`. CLI: `cedarpg`. Beta (`0.2.0-beta.0`, `beta` dist-tag). Public APIs may still change.
6
6
 
7
- > **Alpha** (`0.2.0-alpha.0`): APIs may change. Install with the `alpha` dist-tag.
8
-
9
- ## What you get (via autopg)
10
-
11
- autopg runs embedded PostgreSQL 18 (not WASM) with real concurrent connections. No credentials, zero config, and databases are provisioned on first use. Any client works (`psql`, `node-postgres`, Prisma, Drizzle, TypeORM).
12
-
13
- ### Development & testing
14
-
15
- | Use case | What you get |
16
- | ----------------------- | ------------------------------------------ |
17
- | **Local development** | PostgreSQL without Docker |
18
- | **Integration testing** | Real PostgreSQL, not mocks |
19
- | **CI/CD pipelines** | Fresh databases per test run |
20
- | **E2E testing** | Isolated database for Playwright / Cypress |
21
-
22
- ## What cedar-pg adds
23
-
24
- **cedar-pg** gives each **git worktree** its own database and role on that autopg host (readable names, leases, teardown) so parallel checkouts do not share one DB:
25
-
26
- - **1 database per git worktree** (visible in `\l` as `cpg_…`)
27
- - **dev** DBs persist across restarts; **test** DBs drop on dispose
28
- - First-class **Vite+ Task** + **Nx** / Vitest / Jest adapters
29
- - `postinstall` ensures the pinned `autopg` binary when missing
30
-
31
- | Layer | Responsibility |
32
- | ------------ | ------------------------------------------------------------------- |
33
- | **autopg** | Embedded Postgres host (concurrent, zero-config, auto-provision) |
34
- | **cedar-pg** | Per-worktree `CREATE DATABASE` / role, `DATABASE_URL`, dispose + GC |
7
+ | Layer | Owns |
8
+ | -------- | --------------------------------------------------------------------------------- |
9
+ | autopg | Postgres process, port, concurrent connections |
10
+ | cedar-pg | Per-worktree `CREATE DATABASE` and role, lease files, `DATABASE_URL`, dispose, GC |
35
11
 
36
12
  ## Install
37
13
 
38
- ```bash
39
- npm install -D @cedarjs/pg@alpha
40
- # or: pnpm add -D @cedarjs/pg@alpha
41
- # or: yarn add -D @cedarjs/pg@alpha
42
- ```
43
-
44
- ## Database names (observability)
14
+ Node `>=24`.
45
15
 
46
- ```text
47
- cpg_<repo>_<worktree>_<mode>_<pathHash8>
16
+ ```bash
17
+ npm install -D @cedarjs/pg@beta
18
+ # pnpm add -D @cedarjs/pg@beta
19
+ # yarn add -D @cedarjs/pg@beta
48
20
  ```
49
21
 
50
- Examples:
51
-
52
- | Name | Meaning |
53
- | ----------------------------------- | -------------------------- |
54
- | `cpg_cedar_cedar_dev_a1b2c3d4` | main `cedar` checkout, dev |
55
- | `cpg_cedar_feat_auth_test_e5f67890` | worktree `feat-auth`, test |
22
+ `postinstall` installs the pinned autopg binary when it is missing. The pin is `scripts/autopg-version`. To bump autopg, change that file.
56
23
 
57
- ## Prerequisites
58
-
59
- A running [autopg](https://github.com/automagik-dev/autopg) host (installed automatically by `postinstall`, or manually).
60
- The release pin lives in **`scripts/autopg-version`** (single source of truth for postinstall, CI binary install, and docs). Bump that file to upgrade:
24
+ To install the host by hand (local, non-CI; upstream `install.sh` may use pm2):
61
25
 
62
26
  ```bash
63
- # local / non-CI (upstream install.sh; may use pm2)
64
27
  VER=$(tr -d '[:space:]' < scripts/autopg-version)
65
28
  curl -fsSL "https://raw.githubusercontent.com/automagik-dev/autopg/${VER}/install.sh" \
66
29
  | AUTOPG_VERSION="$VER" bash
67
30
  ```
68
31
 
69
- Typical flow: `autopg daemon` (or your usual host install) once per machine → `cedarpg acquire` per worktree → connect with the printed `DATABASE_URL`.
70
-
71
- ## Develop this package (Vite+)
32
+ Once per machine, run the host (`autopg daemon`, or your usual install). Then per worktree:
72
33
 
73
34
  ```bash
74
- vp install
75
- vp check
76
- vp test
77
- vp pack # → dist/ (dts + esm + cjs)
78
- vp run smoke # build → npm-pack tarball → install + resolve exports
79
- vp run smoke:pg # pack → Vitest + Jest adapters against real ephemeral Postgres
35
+ cedarpg acquire --mode=dev
80
36
  ```
81
37
 
82
- ## Local consume (without npm)
38
+ Connect with the printed `DATABASE_URL`.
39
+
40
+ From another checkout of this repo, pack first, then depend on the build:
83
41
 
84
42
  ```bash
85
- # in this repo
86
43
  vp pack
44
+ # in the app: yarn add @cedarjs/pg@file:../cedar-pg
45
+ # or install the tarball vp pack writes (name includes the version in package.json)
46
+ ```
87
47
 
88
- # in your app / Cedar
89
- yarn add @cedarjs/pg@file:../cedar-pg
90
- # or: pnpm pack && yarn add ./cedarjs-pg-0.2.0-alpha.0.tgz
48
+ ## Acquire a database
49
+
50
+ `dev` databases persist across restarts. `test` databases drop on `dispose`.
51
+
52
+ Names look like this (visible in `psql` `\l`):
53
+
54
+ ```text
55
+ cpg_<repo>_<worktree>_<mode>_<pathHash8>
91
56
  ```
92
57
 
58
+ Worktree state lives in `.cedarpg`. Import `STATE_DIRNAME` instead of hardcoding that string. `gc` uses `~/.cedarpg/registry`.
59
+
93
60
  ## CLI
94
61
 
95
62
  ```bash
@@ -99,24 +66,69 @@ cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts
99
66
  cedarpg run --mode=test -- vitest run
100
67
  cedarpg dispose --mode=test
101
68
  cedarpg print-url --mode=dev
102
- cedarpg gc # drop DBs whose worktree root is gone (uses ~/.cedarpg/registry)
69
+ cedarpg status --mode=dev
70
+ cedarpg studio --mode=dev # Prisma or Drizzle Studio (--prisma / --drizzle)
71
+ cedarpg gc # drop DBs whose worktree root is gone
72
+ ```
73
+
74
+ `status` and `studio` are read-only. They do not acquire. `studio` walks from cwd up to the worktree.
75
+
76
+ `cedarpg run` acquires or attaches the lease, then execs the command. The child always gets `DATABASE_URL` from the lease. In test mode it also gets `TEST_DATABASE_URL`. `--force` only sets `CEDAR_PG_FORCE=1`, so nested adapters do not treat an ambient URL as an escape hatch.
77
+
78
+ If two targets run `cedarpg acquire` or `cedarpg run` on the same worktree, role and database DDL can race. Use one acquire, then `run` wrappers.
79
+
80
+ ## Vite+
81
+
82
+ ```ts
83
+ // vite.config.ts
84
+ import { defineConfig } from "vite-plus";
85
+ import { cedarPgTasks, cedarPgDev } from "@cedarjs/pg/vite-plus";
86
+
87
+ export default defineConfig({
88
+ plugins: [cedarPgDev()],
89
+ run: {
90
+ tasks: {
91
+ ...cedarPgTasks(),
92
+ test: {
93
+ command: "vp test",
94
+ dependsOn: ["db:acquire-test"],
95
+ env: ["DATABASE_URL", "TEST_DATABASE_URL"],
96
+ },
97
+ dev: {
98
+ command: "vp dev",
99
+ dependsOn: ["db:acquire"],
100
+ env: ["DATABASE_URL"],
101
+ },
102
+ },
103
+ },
104
+ });
103
105
  ```
104
106
 
105
- `cedarpg run` acquires (or attaches the lease), force-sets `DATABASE_URL` (and
106
- `TEST_DATABASE_URL` in test mode) in the **child** process, then execs the command.
107
- Use it for Nx / e2e / API wrappers — local `.env` URLs do not win inside the child.
107
+ `cedarPgDev()` does not acquire. Keep `dependsOn: ["db:acquire"]`. On listen it prints a status panel (TTY, non-CI). Vite CLI shortcuts (`key` then Enter; also listed under `h`):
108
+
109
+ | Key | Action |
110
+ | --- | ---------------------------------------------------------------------- |
111
+ | `d` | Reprint status (name, port, `DATABASE_URL`, env file) |
112
+ | `s` | Open Prisma Studio or Drizzle Kit Studio with the lease `DATABASE_URL` |
113
+
114
+ Options: `cedarPgDev({ mode, root, cwd, studio: "prisma" | "drizzle" | false })`. Studio walks from `cwd` (default Vite `config.root`) up to the worktree, so an Nx `apps/...` package is found without moving the lease. `prisma` wins when both ORMs sit in the same package.
108
115
 
109
- ## Nx consumer adapter
116
+ Shortcuts bind on Vite 8 and vite-plus. Vite 7 and Cedar print the listen panel only. Use `cedarpg status` and `cedarpg studio` for Nx and other non-Vite hosts.
110
117
 
111
- Nx `dependsOn` alone does not forward env from an acquire task into dependents
112
- (Vite+ `env: [...]` does). **Canonical fix:** wrap the child with `cedarpg run`.
113
- Secondary: point Nx `envFile` at `.cedarpg/<mode>.env` after acquire.
118
+ ## Nx
119
+
120
+ Nx `dependsOn` does not forward env from an acquire task into dependents. Vite+ `env: [...]` does. Canonical shape:
121
+
122
+ 1. One `db:ready` (or `createAcquireTask`) that acquires and migrates.
123
+ 2. Wrap API, dev, and e2e children with `cedarpg run --mode=dev --force -- <cmd>`.
124
+
125
+ Pointing Nx `envFile` at `.cedarpg/<mode>.env` after acquire still loses to an ambient `.env` unless you also force or overwrite.
114
126
 
115
127
  ```ts
116
128
  import { cedarPgNxTargets, cedarPgRunCommand, relativeEnvFile } from "@cedarjs/pg/nx";
117
129
 
118
130
  cedarPgNxTargets();
119
- // { "db:acquire": { command: "cedarpg acquire --mode=dev", cache: false }, … }
131
+ // { "db:acquire": { command: "cedarpg acquire --mode=dev", cache: false }, ... }
120
132
 
121
133
  cedarPgRunCommand("dev", "yarn tsx scripts/apiServer/dev.ts");
122
134
  // "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
@@ -127,20 +139,20 @@ relativeEnvFile("dev"); // ".cedarpg/dev.env"
127
139
  ```json
128
140
  {
129
141
  "targets": {
142
+ "db:ready": { "command": "tsx tools/db-ready.ts", "cache": false },
130
143
  "dev": {
131
- "command": "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
144
+ "dependsOn": ["db:ready"],
145
+ "command": "cedarpg run --mode=dev --force -- yarn tsx scripts/apiServer/dev.ts"
132
146
  },
133
- "db:acquire": { "command": "cedarpg acquire --mode=dev" },
134
147
  "serve": {
135
- "dependsOn": ["db:acquire"],
136
- "command": "node dist/server.js",
137
- "options": { "envFile": ".cedarpg/dev.env" }
148
+ "dependsOn": ["db:ready"],
149
+ "command": "cedarpg run --mode=dev --force -- node dist/server.js"
138
150
  }
139
151
  }
140
152
  }
141
153
  ```
142
154
 
143
- For a db:ready-style migrate hook (same compose shape as Jest `createGlobalSetup`):
155
+ Migrate hook (same compose shape as Jest `createGlobalSetup`):
144
156
 
145
157
  ```ts
146
158
  // tools/db-ready.ts
@@ -148,42 +160,19 @@ import { createAcquireTask } from "@cedarjs/pg";
148
160
 
149
161
  await createAcquireTask({
150
162
  mode: "dev",
163
+ // Need this when .env already has DATABASE_URL
164
+ force: true,
151
165
  afterAcquire: async ({ databaseUrl }) => {
152
- // prisma migrate deploy / drizzle push / …
166
+ // prisma migrate deploy, drizzle push, ...
153
167
  },
154
168
  })();
155
169
  ```
156
170
 
157
- Fallbacks when you cannot wrap with `run`: `loadDevEnv({ overwrite: true })` or
158
- `import "@cedarjs/pg/dev-env"`. Absolute path helper: `envFilePath(root, mode)`.
171
+ If you cannot wrap with `run`, use `loadDevEnv({ overwrite: true })` or `import "@cedarjs/pg/dev-env"`. Absolute path helper: `envFilePath(root, mode)`.
159
172
 
160
- ## Vite+ consumer adapter
161
-
162
- ```ts
163
- // vite.config.ts
164
- import { defineConfig } from "vite-plus";
165
- import { cedarPgTasks } from "@cedarjs/pg/vite-plus";
166
-
167
- export default defineConfig({
168
- run: {
169
- tasks: {
170
- ...cedarPgTasks(),
171
- test: {
172
- command: "vp test",
173
- dependsOn: ["db:acquire-test"],
174
- env: ["DATABASE_URL", "TEST_DATABASE_URL"],
175
- },
176
- dev: {
177
- command: "vp dev",
178
- dependsOn: ["db:acquire"],
179
- env: ["DATABASE_URL"],
180
- },
181
- },
182
- },
183
- });
184
- ```
173
+ ## Vitest and Jest
185
174
 
186
- ## Vitest / Jest adapters
175
+ Stock `@cedarjs/pg/vitest` and `@cedarjs/pg/jest` only acquire and dispose. One shared test DB. They do not migrate, and they do not clone per worker. For migrate-once plus clones, see [TEMPLATE clones](#template-clones).
187
176
 
188
177
  ```ts
189
178
  // vitest.config.ts
@@ -196,142 +185,80 @@ export default defineConfig({
196
185
  });
197
186
  ```
198
187
 
188
+ Vitest runs `globalSetup` in the main process, then workers inherit `process.env`. Add `setupFiles: ["@cedarjs/pg/test-env"]` only if your pool does not inherit env.
189
+
199
190
  ```js
200
- // jest.config.cjs — standalone apps
191
+ // jest.config.cjs
201
192
  module.exports = {
202
193
  globalSetup: require.resolve("@cedarjs/pg/jest"),
203
194
  globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
204
- // Jest globalSetup is a separate process — workers load DATABASE_URL from .cedarpg/test.env
195
+ // Jest globalSetup is a separate process. Workers load DATABASE_URL from .cedarpg/test.env.
205
196
  setupFiles: [require.resolve("@cedarjs/pg/test-env")],
206
197
  };
207
198
  ```
208
199
 
209
- ### Framework hosts (CedarJS, custom globalSetup)
200
+ `loadTestEnv` and `loadDevEnv` fill undefined keys by default. Pass `{ overwrite: true }` (or import `@cedarjs/pg/dev-env`) when a local `.env` URL should lose to cedar-pg. That is not the same as `CEDAR_PG_FORCE` or `{ force: true }` (external-URL escape hatch).
201
+
202
+ ## CedarJS and custom globalSetup
210
203
 
211
- If your runner already owns `globalSetup` (e.g. Prisma push/migrate after acquire), **do not** replace it with `@cedarjs/pg/jest`. Compose instead:
204
+ If the runner already owns `globalSetup` (Prisma push or migrate after acquire), do not replace it with `@cedarjs/pg/jest`. Compose:
212
205
 
213
- 1. In your `globalSetup`: call `acquireIfNeeded` when opted in, then run migrations.
214
- 2. Add `setupFiles: [require.resolve('@cedarjs/pg/test-env')]` so Jest **workers** see `DATABASE_URL`.
215
- 3. In your `globalTeardown`: call `dispose({ mode: 'test', root })`.
206
+ 1. In `globalSetup`, call `acquireIfNeeded` when opted in, then migrate.
207
+ 2. Add `setupFiles: [require.resolve("@cedarjs/pg/test-env")]` so Jest workers see `DATABASE_URL`.
208
+ 3. In `globalTeardown`, call `dispose({ mode: "test", root })`.
216
209
 
217
210
  ```ts
218
- // framework globalSetup (sketch)
219
211
  import { acquireIfNeeded } from "@cedarjs/pg";
220
212
 
221
213
  if (process.env.CEDAR_PG === "1" || process.env.CEDAR_PG === "true") {
222
214
  await acquireIfNeeded({
223
215
  root: projectRoot, // e.g. getPaths().base
224
216
  mode: "test",
225
- setEnv: true, // this process (prisma) — workers use @cedarjs/pg/test-env
217
+ setEnv: true, // this process (prisma). Workers use @cedarjs/pg/test-env.
226
218
  url: process.env.TEST_DATABASE_URL,
227
219
  force: process.env.CEDAR_PG_FORCE === "1",
228
- disabled: false, // framework opt-in; adapters alone use CEDAR_PG=0 opt-out
220
+ disabled: false, // framework opt-in. Stock adapters use CEDAR_PG=0 opt-out.
229
221
  });
230
222
  }
231
- // … prisma db push / migrate …
232
223
  ```
233
224
 
234
- ```js
235
- // jest-preset
236
- setupFiles: [require.resolve("@cedarjs/pg/test-env")],
237
- ```
225
+ ## TEMPLATE clones
238
226
 
239
- Use exported `STATE_DIRNAME` (`.cedarpg`) / `loadTestEnv` / `loadDevEnv` /
240
- `envFilePath(root, mode)` instead of hardcoding the lease dir.
227
+ Migrate stays app-owned via `createGlobalSetup({ migrate })`. The adapter then marks TEMPLATE and clones per worker.
241
228
 
242
- `loadTestEnv` / `loadDevEnv` only fill **undefined** keys by default. Pass
243
- `{ overwrite: true }` (or import `@cedarjs/pg/dev-env`) when a local `.env`
244
- `DATABASE_URL` / `TEST_DATABASE_URL` should lose to cedar-pg. That is not the
245
- same as `CEDAR_PG_FORCE` / acquire `{ force }` (external-URL escape hatch).
229
+ Point `globalSetup` at a local module that calls `createGlobalSetup`. String-resolving the package entry without a migrate hook throws.
246
230
 
247
- ## Programmatic API
248
-
249
- ```ts
250
- import { acquire, dispose, loadTestEnv, loadDevEnv, envFilePath, STATE_DIRNAME } from "@cedarjs/pg";
251
-
252
- const { databaseUrl, adminUrl, databaseName, dispose: drop } = await acquire({ mode: "test" });
253
- // … tests …
254
- await drop();
255
-
256
- loadDevEnv({ overwrite: true }); // override .env DATABASE_URL from .cedarpg/dev.env
257
- ```
258
-
259
- ### Host startup (CI ephemeral)
260
-
261
- By default cedar-pg **attaches** to a live autopg host (`autopg status`). If none is live it runs bare `autopg install` (pm2) — fine for local machines, hostile to GitHub Actions (no pm2) and slower than RAM-backed CI.
262
-
263
- In CI, cedar-pg starts an **opinionated ephemeral host** automatically when `CI=true` (or when forced). Callers just use `acquire` — no host options bag:
264
-
265
- ```ts
266
- import { acquire } from "@cedarjs/pg";
267
-
268
- // CI=true → install --no-pm2 --no-ui + detached postmaster (--ram on Linux /dev/shm)
269
- const { databaseUrl } = await acquire({ mode: "test" });
270
- ```
271
-
272
- | Signal | Effect |
273
- | --------------------------- | ------------------------------------------------------------------- |
274
- | `CEDAR_PG_EPHEMERAL_HOST=1` | Prefer ephemeral start when **no** host is live (attach still wins) |
275
- | `CEDAR_PG_EPHEMERAL_HOST=0` | Force local attach / pm2 install (even if `CI=true`) |
276
- | unset + `CI=true` | Prefer ephemeral when no host is live |
277
- | otherwise | Local: attach if live, else bare `autopg install` |
278
-
279
- Ephemeral recipe (not configurable via cedar-pg):
280
-
281
- - `autopg install --no-pm2 --no-ui --port 55432 --data DIR`
282
- - detached `autopg postmaster --port 55432 --socket-dir DIR --data DIR`
283
- - Linux when `/dev/shm` exists → also `--ram` and `DIR=/dev/shm/cedar-pg-<uid>`
284
- - otherwise → disk `DIR` under the OS temp dir (still owned, no pm2)
285
- - Ready when TCP accepts on the recipe port (not merely `autopg status` after install)
286
-
287
- If a host is already live, cedar-pg attaches and does not start another. The **CI job owns** ephemeral postmaster lifetime (runner teardown / `/dev/shm`); there is no cedar-pg host dispose API.
288
-
289
- ### CI setup (GitHub Actions)
290
-
291
- Prefer the composite action (cache + attested binary install, no pm2). Version defaults to this repo’s `scripts/autopg-version`:
292
-
293
- ```yaml
294
- - uses: actions/checkout@v6
295
- # In cedar-pg:
296
- - uses: ./.github/actions/setup-autopg
297
- # From another repo (pin to a tag when publishing the action):
298
- # - uses: cedarjs/cedar-pg/.github/actions/setup-autopg@main
299
- ```
300
-
301
- See [`.github/actions/setup-autopg`](.github/actions/setup-autopg/README.md) for inputs (`version`, `cache`, `token`) and outputs.
302
-
303
- The action runs `scripts/ci-install-autopg.sh` under the hood. For published-package consumers under `CI=true` without the Action, set `CEDAR_PG_INSTALL_AUTOPG=1` so `postinstall` runs that same script (not upstream `install.sh`) — that flag alone is not enough when the package manager disables lifecycle scripts (`--ignore-scripts`, `YARN_ENABLE_SCRIPTS=false`, etc.). Prefer this Action, or bake the binary into the image.
304
-
305
- ### Migrate-once + TEMPLATE clones (Jest / Vitest)
306
-
307
- Stock `@cedarjs/pg/jest` and `@cedarjs/pg/vitest` only run `acquireIfNeeded` + `dispose` (one shared test DB). They are **not** a full replacement for Redwood-style globalSetup that migrates once and clones per worker. For that, use template mode.
308
-
309
- Migrate stays app-owned via `createGlobalSetup({ migrate })`, then the adapter marks TEMPLATE and clones per worker. Point `globalSetup` at a **local** module that calls `createGlobalSetup` — string-resolving the package entry without a migrate hook throws.
310
-
311
- **Jest (template mode):**
231
+ ### Jest
312
232
 
313
233
  ```js
314
234
  // jest.cedar-global.cjs
315
235
  const { createGlobalSetup } = require("@cedarjs/pg/jest/template");
316
236
  module.exports = createGlobalSetup({
317
237
  migrate: async ({ databaseUrl }) => {
318
- // prisma migrate reset / drizzle push / etc.
238
+ // prisma migrate reset, drizzle push
319
239
  },
320
240
  });
321
241
 
322
242
  // jest.config.cjs
243
+ // When .env has a real TEST_DATABASE_URL, set FORCE once here so it
244
+ // inherits into globalSetup and workers (dotenv will not override existing keys).
245
+ process.env.CEDAR_PG_FORCE = "1";
246
+
323
247
  module.exports = {
324
248
  globalSetup: "<rootDir>/jest.cedar-global.cjs",
325
249
  globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
250
+ // Prefer setupFilesAfterEnv so you can use beforeAll (Jest globals).
326
251
  setupFilesAfterEnv: ["<rootDir>/jest.cedar-worker.cjs"],
327
252
  };
328
253
 
329
- // jest.cedar-worker.cjs — once per worker process
254
+ // jest.cedar-worker.cjs
330
255
  const { cloneWorkerDatabase } = require("@cedarjs/pg/jest/template");
331
256
  beforeAll(() => cloneWorkerDatabase());
332
257
  ```
333
258
 
334
- **Vitest (template mode):**
259
+ `cloneWorkerDatabase` memos on `globalThis`, so Jest `setupFiles` (module reload per file) still shares one clone per worker.
260
+
261
+ ### Vitest
335
262
 
336
263
  ```ts
337
264
  // vitest.cedar-global.ts
@@ -350,12 +277,14 @@ export default defineConfig({
350
277
  },
351
278
  });
352
279
 
353
- // vitest.cedar-worker.ts — once per worker process (ESM top-level await)
280
+ // vitest.cedar-worker.ts
354
281
  import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
355
282
  await cloneWorkerDatabase();
356
283
  ```
357
284
 
358
- **Programmatic** (core API — no runner adapters):
285
+ ### Core API
286
+
287
+ No runner adapters.
359
288
 
360
289
  ```ts
361
290
  import { acquire, markTemplate, cloneFromTemplate, dispose } from "@cedarjs/pg";
@@ -369,36 +298,128 @@ const worker = await cloneFromTemplate({
369
298
  name: "1",
370
299
  setEnv: true,
371
300
  });
372
- // … tests …
373
301
  await worker.dropClone(); // optional: drop one clone only
374
- await dispose({ root: acquired.root, mode: "test" }); // role-scoped: TEMPLATE + all clones + role
302
+ await dispose({ root: acquired.root, mode: "test" }); // TEMPLATE + all clones + role
303
+ ```
304
+
305
+ `acquire` returns `adminUrl` for migrate hooks and privileged DDL. `markTemplate` and `cloneFromTemplate` accept it, or rediscover the host when it is omitted. `cloneFromTemplate` uses the admin connection (`CREATE DATABASE ... TEMPLATE`). Test roles stay `LOGIN`-only.
306
+
307
+ `setEnv` defaults to false on `cloneFromTemplate`. It defaults to true on `cloneFromTemplateIfNeeded` (same as `acquireIfNeeded`). Worker adapters call `cloneFromTemplateIfNeeded` via `cloneWorkerDatabase`.
308
+
309
+ `dispose` is role-scoped suite teardown, not `dropClone`. It unsets `IS_TEMPLATE` and drops every database owned by the lease role.
310
+
311
+ ## Host and CI
312
+
313
+ `acquire` attaches when TCP accepts on the port from `autopg status --json`. Registration is not liveness. An installed-but-stopped host still reports a port.
314
+
315
+ When nothing is listening, cedar-pg brings up the registered local host. It runs `autopg restart`, then `autopg install` if the host was never registered. It attaches once TCP accepts. Same port, same `~/.autopg/data`. It does not start a second local Postgres. If neither verb produces a listener, `acquire` fails with what it tried.
316
+
317
+ Callers use `acquire` (and `adminUrl`). There is no public host-options object. Ephemeral behavior is env-driven.
318
+
319
+ ```ts
320
+ import { acquire } from "@cedarjs/pg";
321
+
322
+ // CI=true: detached postmaster (--ram on Linux /dev/shm). Does not rewrite ~/.autopg.
323
+ const { databaseUrl } = await acquire({ mode: "test" });
324
+ ```
325
+
326
+ | Signal | Effect (attach always wins when something is listening) |
327
+ | --------------------------- | ----------------------------------------------------------------------------- |
328
+ | `CEDAR_PG_EPHEMERAL_HOST=1` | Ephemeral owned postmaster on 55432 |
329
+ | `CEDAR_PG_EPHEMERAL_HOST=0` | Never own a postmaster (even when `CI=true`). Local autopg host only, or fail |
330
+ | unset and `CI=true` | Ephemeral |
331
+ | unset | Local: `autopg restart`, then `autopg install`, then fail |
332
+
333
+ Ephemeral recipe (not configurable via cedar-pg):
334
+
335
+ - Detached `autopg postmaster --port 55432 --socket-dir DIR --data DIR`
336
+ - Does not run `autopg install` (that rewrites `~/.autopg/admin.json` and conflicts with a local pm2 host)
337
+ - Linux when `/dev/shm` exists: also `--ram` and `DIR=/dev/shm/cedar-pg-<uid>`
338
+ - Otherwise: disk `DIR` under the OS temp dir
339
+ - Ready when TCP accepts on the recipe port
340
+ - Before cold-start, if the recipe port is not live, cedar-pg prunes leftover `/dev/shm/cedar-pg-*`, `pgserve-*`, and `PostgreSQL.*` (OOM-killed runs filling tmpfs). Safe on isolated CI VMs. On shared self-hosted runners another job's leftovers could match those globs.
341
+
342
+ If TCP already accepts on the discovered autopg port, or in ephemeral mode on 55432, cedar-pg attaches and does not start another. The CI job owns ephemeral postmaster lifetime (runner teardown, `/dev/shm`). There is no cedar-pg host dispose API.
343
+
344
+ Cloud VMs often ship `/dev/shm` at about 64MB, too small for `--ram`. Remount before tests if needed (`sudo mount -o remount,size=6G /dev/shm`). See [Troubleshooting](#troubleshooting).
345
+
346
+ ### GitHub Actions
347
+
348
+ Prefer the composite action (cache plus attested binary install, no pm2). Version defaults to this repo's `scripts/autopg-version`. Inputs and outputs: [`.github/actions/setup-autopg`](.github/actions/setup-autopg/README.md).
349
+
350
+ ```yaml
351
+ - uses: actions/checkout@v6
352
+ # In cedar-pg:
353
+ - uses: ./.github/actions/setup-autopg
354
+ # From another repo (pin to a tag when publishing the action):
355
+ # - uses: cedarjs/cedar-pg/.github/actions/setup-autopg@main
356
+ ```
357
+
358
+ The action runs `scripts/ci-install-autopg.sh`. For published-package consumers under `CI=true` without the Action, set `CEDAR_PG_INSTALL_AUTOPG=1` so `postinstall` runs that script (not upstream `install.sh`). That flag is not enough when the package manager disables lifecycle scripts (`--ignore-scripts`, `YARN_ENABLE_SCRIPTS=false`). Prefer the Action, or bake the binary into the image.
359
+
360
+ Yarn Berry or ignore-scripts, when you cannot use the Action. Requires a real `node_modules` tree (`nodeLinker: node-modules` or pnpm). Default Yarn PnP has no `node_modules/@cedarjs/pg/...` path. Resolve via `yarn node` or `require.resolve`, or prefer the Action.
361
+
362
+ ```yaml
363
+ - name: Ensure autopg binary
364
+ run: |
365
+ set -euo pipefail
366
+ echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
367
+ export PATH="${HOME}/.local/bin:${PATH}"
368
+ bash node_modules/@cedarjs/pg/scripts/ci-install-autopg.sh
369
+ env:
370
+ GH_TOKEN: ${{ github.token }}
371
+ ```
372
+
373
+ ## Environment variables
374
+
375
+ | Var | Meaning |
376
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
377
+ | `AUTOPG_BIN` | Path to the autopg binary |
378
+ | `AUTOPG_PG_USER`, `AUTOPG_PG_PASSWORD` | Autopg superuser for admin URL (default user `postgres`, password `postgres`) |
379
+ | `CEDAR_PG=0` | Disable auto-acquire in adapters |
380
+ | `TEST_DATABASE_URL` | Default escape hatch for `acquireIfNeeded`. Skip acquire for a real external DB (not `cpg_*`, `file:`, or `{...}` or `<...>` placeholders). Callers may pass `url` (`DATABASE_URL` is common in dev) |
381
+ | `CEDAR_PG_FORCE=1` | Ignore the external-URL escape hatch (adapters, `cedarpg acquire --force`, `cedarpg run --force`) |
382
+ | `CEDAR_PG_EPHEMERAL_HOST` | `1` owned postmaster. `0` never own one (even in CI). Unset and `CI=true` means ephemeral |
383
+ | `CEDAR_PG_REGISTRY_DIR` | Override the global lease registry (for `gc`) |
384
+ | `CEDAR_PG_SKIP_POSTINSTALL=1` | Skip the autopg install hook |
385
+ | `CEDAR_PG_INSTALL_AUTOPG=1` | Under `CI=true`, run binary-only `ci-install-autopg.sh` from postinstall |
386
+
387
+ `force`, `overwrite`, and `run` are different knobs. Do not collapse them.
388
+
389
+ ## Programmatic API
390
+
391
+ ```ts
392
+ import { acquire, loadDevEnv } from "@cedarjs/pg";
393
+
394
+ const { databaseUrl, adminUrl, databaseName, dispose } = await acquire({ mode: "test" });
395
+ await dispose();
396
+
397
+ loadDevEnv({ overwrite: true }); // override .env DATABASE_URL from .cedarpg/dev.env
398
+ ```
399
+
400
+ Unit tests in this repo do not start Postgres. CI runs `vp run smoke:pg` for Vitest and Jest adapters against real Postgres (ephemeral cold-start when the runner has no live host; attach wins otherwise).
401
+
402
+ ## Develop this package
403
+
404
+ Vite+, Node `>=24` (`.node-version`), `pnpm@11`. Contributor rules: [AGENTS.md](AGENTS.md). User-visible API changes: [CHANGELOG.md](CHANGELOG.md).
405
+
406
+ ```bash
407
+ vp install
408
+ vp check
409
+ vp test
410
+ vp pack # dist/ (dts + esm + cjs)
411
+ vp run smoke # pack, then tarball install, then resolve exports
412
+ vp run smoke:pg # pack, then Vitest and Jest adapters against real ephemeral Postgres
375
413
  ```
376
414
 
377
- `acquire` returns `adminUrl` for migrate hooks / privileged DDL; `markTemplate` / `cloneFromTemplate` accept it or rediscover the host when omitted.
378
- `cloneFromTemplate` uses the admin connection internally (`CREATE DATABASE … TEMPLATE`); test roles stay `LOGIN`-only. `setEnv` defaults to false on `cloneFromTemplate`; `cloneFromTemplateIfNeeded` defaults true (same as `acquireIfNeeded`).
379
- Worker adapters call `cloneFromTemplateIfNeeded` (shared skip policy via `runIfNeeded`) via `cloneWorkerDatabase`.
380
- `dispose` is role-scoped suite teardown (not `dropClone`): unsets `IS_TEMPLATE` and drops every database owned by the lease role.
381
-
382
- ## Env
383
-
384
- | Var | Meaning |
385
- | ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
386
- | `AUTOPG_BIN` | Path to autopg |
387
- | `AUTOPG_PG_USER` / `_PASSWORD` | Autopg superuser for admin URL (default `postgres` / `postgres`) |
388
- | `CEDAR_PG=0` | Disable auto-acquire in adapters |
389
- | `TEST_DATABASE_URL` | Escape hatch: skip acquire for real external DBs (not `cpg_*` / `file:` / `{…}` / `<…>` template placeholders) |
390
- | `CEDAR_PG_FORCE=1` | Ignore external-URL escape hatch (adapters + `cedarpg acquire --force` / `run --force`) |
391
- | `CEDAR_PG_EPHEMERAL_HOST` | `1` force / `0` disable ephemeral host (auto when `CI=true`) |
392
- | `CEDAR_PG_REGISTRY_DIR` | Override global lease registry (for `gc`) |
393
- | `CEDAR_PG_SKIP_POSTINSTALL=1` | Skip autopg install hook |
394
- | `CEDAR_PG_INSTALL_AUTOPG=1` | Under `CI=true`, run binary-only `ci-install-autopg.sh` from postinstall |
395
-
396
- ## Alpha caveats
397
-
398
- - Public API may change before `0.1.0`.
399
- - End-to-end Postgres flows assume a working local `autopg` host; unit tests do not start Postgres.
400
- CI runs `vp run smoke:pg` for Vitest/Jest adapters against real Postgres
401
- (ephemeral cold-start when the runner has no live host; attach-wins otherwise).
402
- - State lives in product-owned `.cedarpg` (worktree + `~/.cedarpg/registry`), not under autopg's `~/.autopg/` or a generic `.pg`.
403
- - Role passwords are derived from `roleName` (`cedar-pg\\0` + roleName, scheme v2) so TEMPLATE clones that reuse a role keep working; bump the scheme id to change the derivation.
404
- - Test TEMPLATE flow: `acquire` → app migrate → `markTemplate` → `cloneFromTemplate` → role-scoped `dispose`. Optional `@cedarjs/pg/jest/template` + `@cedarjs/pg/vitest/template` adapters orchestrate that pipeline via `createGlobalSetup({ migrate })`; migrate stays app-owned.
415
+ ## Troubleshooting
416
+
417
+ | Symptom | Fix |
418
+ | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
419
+ | `ECONNREFUSED 127.0.0.1:25432` on `cedarpg acquire` | autopg is registered but not listening. cedar-pg runs `autopg restart` (then `install`) and attaches once TCP accepts. `restart` exiting 0 is not proof. When pm2 is missing it still prints "respawned daemon". If both verbs fail, install pm2 or fix the supervisor (`pm2 logs autopg-server`). |
420
+ | `database already exists: ..._c_<workerId>` in Jest | Use current `@cedarjs/pg` (`cloneWorkerDatabase` memos on `globalThis`). Prefer `setupFilesAfterEnv` + `beforeAll`. Avoid passing bare `JEST_WORKER_ID` as an explicit `name`. |
421
+ | Acquire skipped. Tests hit shared or stale Postgres | A real `.env` `TEST_DATABASE_URL` (or a `url` the caller passed) trips the escape hatch. Set `CEDAR_PG_FORCE=1` once in `jest.config.js`, or `force: true`, or `cedarpg run --force`. |
422
+ | `Disk quota exceeded` / `No space left on device` / Postgres `53100` on ephemeral start | Enlarge `/dev/shm` (`sudo mount -o remount,size=6G /dev/shm`). On isolated runners only: `rm -rf /dev/shm/cedar-pg-* /dev/shm/pgserve-* /dev/shm/PostgreSQL.*`. Cold-start also prunes these when the recipe port is dead. |
423
+ | `autopg: command not found` in CI with Yarn `YARN_ENABLE_SCRIPTS=false` | `CEDAR_PG_INSTALL_AUTOPG=1` is not enough when lifecycle scripts are off. With `nodeLinker: node-modules`, run `bash node_modules/@cedarjs/pg/scripts/ci-install-autopg.sh` and put `~/.local/bin` on `PATH` (or use `setup-autopg`). PnP: resolve the script path via Yarn, or prefer the Action. |
424
+ | Nx child still uses `.env` `DATABASE_URL` | `dependsOn` does not forward acquire env. Wrap with `cedarpg run --mode=dev --force -- <cmd>`, or `loadDevEnv({ overwrite: true })`. |
425
+ | Role or DB errors under parallel Nx targets | Do not run concurrent `acquire` or `run` on the same worktree. One `db:ready`, then `run` wrappers. |