@cedarjs/pg 0.2.0-alpha.0 → 0.3.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 (112) hide show
  1. package/README.md +300 -230
  2. package/dist/cli.cjs +99 -43
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.mjs +85 -29
  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 +80 -9
  10. package/dist/index.d.mts +80 -9
  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 +7 -2
  15. package/dist/jest-template.cjs.map +1 -1
  16. package/dist/jest-template.d.cts +7 -2
  17. package/dist/jest-template.d.mts +7 -2
  18. package/dist/jest-template.mjs +7 -2
  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-2BZgSVz2.mjs +1072 -0
  27. package/dist/lifecycle-2BZgSVz2.mjs.map +1 -0
  28. package/dist/{lifecycle-DEJ4GgWV.cjs → lifecycle-BCeM96tI.cjs} +517 -117
  29. package/dist/lifecycle-BCeM96tI.cjs.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/{naming-C2nGVxPk.d.cts → naming-Df4fz_Fb.d.cts} +3 -2
  43. package/dist/{naming-C2nGVxPk.d.mts → naming-Df4fz_Fb.d.mts} +3 -2
  44. package/dist/status-B68SGSKx.mjs +26 -0
  45. package/dist/status-B68SGSKx.mjs.map +1 -0
  46. package/dist/status-Z_BYdxzI.cjs +31 -0
  47. package/dist/status-Z_BYdxzI.cjs.map +1 -0
  48. package/dist/studio-Bi6SU9jE.mjs +221 -0
  49. package/dist/studio-Bi6SU9jE.mjs.map +1 -0
  50. package/dist/studio-CdWIdnxr.cjs +280 -0
  51. package/dist/studio-CdWIdnxr.cjs.map +1 -0
  52. package/dist/{template-D0gJ_MS2.cjs → template-Cd77H7Cr.cjs} +21 -18
  53. package/dist/template-Cd77H7Cr.cjs.map +1 -0
  54. package/dist/{template-CGw3C1Ob.mjs → template-DlqHL8Go.mjs} +21 -18
  55. package/dist/template-DlqHL8Go.mjs.map +1 -0
  56. package/dist/template-mode-BqV3GjXI.d.cts +48 -0
  57. package/dist/template-mode-BqV3GjXI.d.mts +48 -0
  58. package/dist/template-mode-DK5cAlv9.mjs +81 -0
  59. package/dist/template-mode-DK5cAlv9.mjs.map +1 -0
  60. package/dist/template-mode-DMO5-z7O.cjs +92 -0
  61. package/dist/template-mode-DMO5-z7O.cjs.map +1 -0
  62. package/dist/test-env.cjs +1 -1
  63. package/dist/test-env.mjs +1 -1
  64. package/dist/vite-plus.cjs +113 -10
  65. package/dist/vite-plus.cjs.map +1 -1
  66. package/dist/vite-plus.d.cts +41 -2
  67. package/dist/vite-plus.d.mts +41 -2
  68. package/dist/vite-plus.mjs +110 -7
  69. package/dist/vite-plus.mjs.map +1 -1
  70. package/dist/vitest-template.cjs +6 -3
  71. package/dist/vitest-template.cjs.map +1 -1
  72. package/dist/vitest-template.d.cts +6 -3
  73. package/dist/vitest-template.d.mts +6 -3
  74. package/dist/vitest-template.mjs +6 -3
  75. package/dist/vitest-template.mjs.map +1 -1
  76. package/dist/vitest.cjs +1 -1
  77. package/dist/vitest.mjs +1 -1
  78. package/package.json +11 -7
  79. package/scripts/autopg-version +1 -1
  80. package/scripts/ci-install-autopg.sh +7 -2
  81. package/scripts/postinstall.js +41 -13
  82. package/dist/constants-Ct8myrEn.cjs +0 -41
  83. package/dist/constants-Ct8myrEn.cjs.map +0 -1
  84. package/dist/constants-NLR0U4NR.mjs +0 -24
  85. package/dist/constants-NLR0U4NR.mjs.map +0 -1
  86. package/dist/lease-B3TuX92y.d.mts +0 -24
  87. package/dist/lease-BSBBpaR4.mjs.map +0 -1
  88. package/dist/lease-Bq9wKwQa.cjs.map +0 -1
  89. package/dist/lease-t4I9JahV.d.cts +0 -24
  90. package/dist/lifecycle-BOo6xBjD.mjs +0 -684
  91. package/dist/lifecycle-BOo6xBjD.mjs.map +0 -1
  92. package/dist/lifecycle-DEJ4GgWV.cjs.map +0 -1
  93. package/dist/nx.cjs +0 -44
  94. package/dist/nx.cjs.map +0 -1
  95. package/dist/nx.d.cts +0 -15
  96. package/dist/nx.d.mts +0 -15
  97. package/dist/nx.mjs +0 -36
  98. package/dist/nx.mjs.map +0 -1
  99. package/dist/tasks-Caud9yHr.cjs +0 -67
  100. package/dist/tasks-Caud9yHr.cjs.map +0 -1
  101. package/dist/tasks-CjaZRi_G.d.mts +0 -19
  102. package/dist/tasks-DdQoP9We.d.cts +0 -19
  103. package/dist/tasks-KherHLac.mjs +0 -38
  104. package/dist/tasks-KherHLac.mjs.map +0 -1
  105. package/dist/template-CGw3C1Ob.mjs.map +0 -1
  106. package/dist/template-D0gJ_MS2.cjs.map +0 -1
  107. package/dist/template-mode-BkEs9LnY.cjs +0 -91
  108. package/dist/template-mode-BkEs9LnY.cjs.map +0 -1
  109. package/dist/template-mode-CcxV_Iju.d.cts +0 -28
  110. package/dist/template-mode-CcxV_Iju.d.mts +0 -28
  111. package/dist/template-mode-IjlFOG4O.mjs +0 -80
  112. package/dist/template-mode-IjlFOG4O.mjs.map +0 -1
package/README.md CHANGED
@@ -1,189 +1,217 @@
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`. Pre-1.0 (`0.x`, `latest` dist-tag). Public APIs may still change between minor versions.
6
6
 
7
- > **Alpha** (`0.2.0-alpha.0`): APIs may change. Install with the `alpha` dist-tag.
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 |
8
11
 
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 |
12
+ ## Install
21
13
 
22
- ## What cedar-pg adds
14
+ Node `>=24`.
23
15
 
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:
16
+ ```bash
17
+ npm install -D @cedarjs/pg
18
+ # pnpm add -D @cedarjs/pg
19
+ # yarn add -D @cedarjs/pg
20
+ ```
25
21
 
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
22
+ `postinstall` installs the pinned autopg binary when it is missing. The pin is `scripts/autopg-version`. To bump autopg, change that file.
30
23
 
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 |
24
+ An autopg that is already installed is never upgraded for you, because upgrading restarts the host every worktree shares. When it is older than the pin, `postinstall` and the first host attach of each process (`acquire`, `cedarpg run`, test setup) print a warning with the upgrade commands. It never fails. `cedarpg run --attach` and skipped acquires (external URL, `CEDAR_PG=0`) do not check. To upgrade, run the install command below, then `autopg update` (autopg's own migrations; on its own it does not download a new binary). `scripts/ci-install-autopg.sh` and the GitHub Action replace any binary that is not exactly the pinned version, since CI hosts are ephemeral.
35
25
 
36
- ## Install
26
+ To install the host by hand (local, non-CI; upstream `install.sh` may use pm2):
37
27
 
38
28
  ```bash
39
- npm install -D @cedarjs/pg@alpha
40
- # or: pnpm add -D @cedarjs/pg@alpha
41
- # or: yarn add -D @cedarjs/pg@alpha
29
+ VER=$(tr -d '[:space:]' < scripts/autopg-version)
30
+ curl -fsSL "https://raw.githubusercontent.com/automagik-dev/autopg/${VER}/install.sh" \
31
+ | AUTOPG_VERSION="$VER" bash
42
32
  ```
43
33
 
44
- ## Database names (observability)
34
+ Once per machine, run the host (`autopg daemon`, or your usual install). Then per worktree:
45
35
 
46
- ```text
47
- cpg_<repo>_<worktree>_<mode>_<pathHash8>
36
+ ```bash
37
+ cedarpg acquire --mode=dev
48
38
  ```
49
39
 
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 |
40
+ Connect with the printed `DATABASE_URL`.
56
41
 
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:
42
+ From another checkout of this repo, pack first, then depend on the build:
61
43
 
62
44
  ```bash
63
- # local / non-CI (upstream install.sh; may use pm2)
64
- VER=$(tr -d '[:space:]' < scripts/autopg-version)
65
- curl -fsSL "https://raw.githubusercontent.com/automagik-dev/autopg/${VER}/install.sh" \
66
- | AUTOPG_VERSION="$VER" bash
45
+ vp pack
46
+ # in the app: yarn add @cedarjs/pg@file:../cedar-pg
47
+ # or install the tarball vp pack writes (name includes the version in package.json)
67
48
  ```
68
49
 
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+)
50
+ Every push to `main` and every PR also builds a preview on [pkg.pr.new](https://pkg.pr.new) (not the npm registry):
72
51
 
73
52
  ```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
53
+ pnpm add -D https://pkg.pr.new/@cedarjs/pg@<commit-sha-or-pr-number>
80
54
  ```
81
55
 
82
- ## Local consume (without npm)
56
+ ## Acquire a database
83
57
 
84
- ```bash
85
- # in this repo
86
- vp pack
58
+ `dev` databases persist across restarts. `test` databases drop on `dispose`.
87
59
 
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
60
+ Names look like this (visible in `psql` `\l`):
61
+
62
+ ```text
63
+ cpg_<repo>_<worktree>_<mode>_<pathHash8>
91
64
  ```
92
65
 
66
+ Worktree state lives in `.cedarpg`. Import `STATE_DIRNAME` instead of hardcoding that string. `gc` uses `~/.cedarpg/registry`.
67
+
93
68
  ## CLI
94
69
 
95
70
  ```bash
96
71
  cedarpg acquire --mode=dev
97
72
  cedarpg acquire --mode=test --print-env
98
- cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts
73
+ cedarpg run --mode=dev -- prisma migrate deploy
99
74
  cedarpg run --mode=test -- vitest run
75
+ cedarpg run --attach --mode=dev -- node dist/server.js # existing lease only, no DDL
100
76
  cedarpg dispose --mode=test
101
77
  cedarpg print-url --mode=dev
102
- cedarpg gc # drop DBs whose worktree root is gone (uses ~/.cedarpg/registry)
78
+ cedarpg status --mode=dev
79
+ cedarpg studio --mode=dev # Prisma or Drizzle Studio (--prisma / --drizzle)
80
+ cedarpg gc # drop DBs whose worktree root is gone
103
81
  ```
104
82
 
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.
83
+ `status` and `studio` are read-only. They do not acquire. `studio` walks from cwd up to the worktree.
108
84
 
109
- ## Nx consumer adapter
85
+ `cedarpg run` acquires (idempotent role and database DDL), 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.
110
86
 
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.
87
+ `cedarpg run --attach` skips the acquire. It reads the lease a prior `acquire` / `run` wrote, sets the same child env, and execs. It never runs DDL and never starts or revives the host. It fails (nonzero, before the child starts) when there is no lease or nothing is listening on the leased port.
114
88
 
115
- ```ts
116
- import { cedarPgNxTargets, cedarPgRunCommand, relativeEnvFile } from "@cedarjs/pg/nx";
89
+ If two targets run `cedarpg acquire` or plain `cedarpg run` on the same worktree, role and database DDL can race. Acquire once, then wrap children with `run --attach`. Any number of those can run concurrently.
117
90
 
118
- cedarPgNxTargets();
119
- // { "db:acquire": { command: "cedarpg acquire --mode=dev", cache: false }, … }
91
+ ## Vite+
120
92
 
121
- cedarPgRunCommand("dev", "yarn tsx scripts/apiServer/dev.ts");
122
- // "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
93
+ ```ts
94
+ // vite.config.ts
95
+ import { defineConfig } from "vite-plus";
96
+ import { cedarPgTasks, cedarPgDev } from "@cedarjs/pg/vite-plus";
123
97
 
124
- relativeEnvFile("dev"); // ".cedarpg/dev.env"
98
+ export default defineConfig({
99
+ plugins: [cedarPgDev()],
100
+ run: {
101
+ tasks: {
102
+ ...cedarPgTasks(),
103
+ test: {
104
+ command: "vp test",
105
+ dependsOn: ["db:acquire-test"],
106
+ env: ["DATABASE_URL", "TEST_DATABASE_URL"],
107
+ },
108
+ dev: {
109
+ command: "cedarpg run --attach --mode=dev -- vp dev",
110
+ dependsOn: ["db:acquire"],
111
+ cache: false,
112
+ },
113
+ },
114
+ },
115
+ });
125
116
  ```
126
117
 
127
- ```json
118
+ Like Nx `dependsOn`, Vite+ `dependsOn` does not forward env, and task `env` only fingerprints and passes through variables already set in the `vp` process. So `dev` acquires once via `db:acquire`, then `cedarpg run --attach` hands the lease `DATABASE_URL` to `vp dev`. `test` needs no wrapper: the stock Vitest setup acquires and sets the URL itself.
119
+
120
+ `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`):
121
+
122
+ | Key | Action |
123
+ | --- | ---------------------------------------------------------------------- |
124
+ | `d` | Reprint status (name, port, `DATABASE_URL`, env file) |
125
+ | `s` | Open Prisma Studio or Drizzle Kit Studio with the lease `DATABASE_URL` |
126
+
127
+ 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.
128
+
129
+ 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.
130
+
131
+ ## Nx
132
+
133
+ Nx `dependsOn` does not forward env from one target to the next, so the setup has two parts:
134
+
135
+ 1. **One `db:ready` target** acquires the worktree database and migrates it. It is the only target that runs role/database DDL.
136
+ 2. **Children** (`dev`, `serve`, workers, e2e) depend on `db:ready` and pick up the lease URL in one of two ways. They never acquire again: two acquires on the same worktree race DDL.
137
+
138
+ ```jsonc
139
+ // project.json (workspace root)
128
140
  {
129
141
  "targets": {
130
- "dev": {
131
- "command": "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
142
+ "db:ready": {
143
+ "command": "cedarpg run --mode=dev -- prisma migrate deploy",
144
+ "cache": false,
132
145
  },
133
- "db:acquire": { "command": "cedarpg acquire --mode=dev" },
134
- "serve": {
135
- "dependsOn": ["db:acquire"],
136
- "command": "node dist/server.js",
137
- "options": { "envFile": ".cedarpg/dev.env" }
138
- }
139
- }
146
+ "db:status": { "command": "cedarpg status --mode=dev", "cache": false },
147
+ "db:url": { "command": "cedarpg print-url --mode=dev", "cache": false },
148
+ "db:studio": { "command": "cedarpg studio --mode=dev", "cache": false },
149
+ },
140
150
  }
141
151
  ```
142
152
 
143
- For a db:ready-style migrate hook (same compose shape as Jest `createGlobalSetup`):
153
+ Swap in your migrate command (`drizzle-kit migrate`, …). Add `--force` (`cedarpg run --mode=dev --force -- …`) when a shell or `.env` `DATABASE_URL` would otherwise trip the [external-URL escape hatch](#environment-variables). For a migrate step written in TypeScript, call `createAcquireTask` from a script instead:
144
154
 
145
155
  ```ts
146
- // tools/db-ready.ts
156
+ // tools/db-ready.ts → "command": "tsx tools/db-ready.ts"
147
157
  import { createAcquireTask } from "@cedarjs/pg";
148
158
 
149
159
  await createAcquireTask({
150
160
  mode: "dev",
161
+ force: true,
151
162
  afterAcquire: async ({ databaseUrl }) => {
152
- // prisma migrate deploy / drizzle push / …
163
+ // run your migrations against databaseUrl
153
164
  },
154
165
  })();
155
166
  ```
156
167
 
157
- Fallbacks when you cannot wrap with `run`: `loadDevEnv({ overwrite: true })` or
158
- `import "@cedarjs/pg/dev-env"`. Absolute path helper: `envFilePath(root, mode)`.
168
+ `db:status`, `db:url`, and `db:studio` are optional local helpers. They read the lease and never acquire. `studio` opens Prisma Studio or Drizzle Kit Studio, whichever it finds.
159
169
 
160
- ## Vite+ consumer adapter
170
+ ### Children: preload or attach
161
171
 
162
- ```ts
163
- // vite.config.ts
164
- import { defineConfig } from "vite-plus";
165
- import { cedarPgTasks } from "@cedarjs/pg/vite-plus";
172
+ Pick one per target. Both give the child the `DATABASE_URL` that `db:ready` leased, overriding a `DATABASE_URL` from `.env`.
166
173
 
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"],
174
+ ```jsonc
175
+ {
176
+ "targets": {
177
+ // Preload: Node loads .cedarpg/dev.env with overwrite before your code runs.
178
+ "serve": {
179
+ "dependsOn": ["db:ready"],
180
+ "command": "node --require @cedarjs/pg/dev-env dist/server.js",
181
+ },
182
+ "dev": {
183
+ "dependsOn": ["db:ready"],
184
+ "executor": "nx:run-commands",
185
+ "options": {
186
+ "command": "tsx watch src/server.ts",
187
+ "env": { "NODE_OPTIONS": "--require @cedarjs/pg/dev-env" },
180
188
  },
181
189
  },
190
+ // Attach: cedarpg sets the env, then execs any command (Node or not).
191
+ "worker": {
192
+ "dependsOn": ["db:ready"],
193
+ "command": "cedarpg run --attach --mode=dev -- node dist/worker.js",
194
+ },
182
195
  },
183
- });
196
+ }
184
197
  ```
185
198
 
186
- ## Vitest / Jest adapters
199
+ - **Preload** (`@cedarjs/pg/dev-env`, or `loadDevEnv({ overwrite: true })` at the top of your entry) costs nothing extra but only works for Node processes. With no lease it is a no-op, and the process keeps its ambient URL.
200
+ - **Attach** (`cedarpg run --attach --mode=dev -- <cmd>`) works for any command. It reads the lease and checks the port, and fails before the child starts when either is missing. It never runs DDL and never starts the host, so any number of attached children can start at once.
201
+
202
+ Avoid Nx `envFile: ".cedarpg/dev.env"`. An ambient `.env` `DATABASE_URL` wins over it.
203
+
204
+ ### Tests
205
+
206
+ Tests do not go through `db:ready`. The Jest TEMPLATE adapter acquires its own `test` lease, migrates once, clones per worker, and drops everything in teardown. Point Jest at it (see [TEMPLATE clones → Jest](#jest)) and make the Nx `test` target a plain runner command:
207
+
208
+ ```jsonc
209
+ { "targets": { "test": { "command": "jest --config jest.config.cjs" } } }
210
+ ```
211
+
212
+ ## Vitest and Jest
213
+
214
+ 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
215
 
188
216
  ```ts
189
217
  // vitest.config.ts
@@ -196,142 +224,87 @@ export default defineConfig({
196
224
  });
197
225
  ```
198
226
 
227
+ 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.
228
+
199
229
  ```js
200
- // jest.config.cjs — standalone apps
230
+ // jest.config.cjs
201
231
  module.exports = {
202
232
  globalSetup: require.resolve("@cedarjs/pg/jest"),
203
233
  globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
204
- // Jest globalSetup is a separate process — workers load DATABASE_URL from .cedarpg/test.env
234
+ // Jest globalSetup is a separate process. Workers load DATABASE_URL from .cedarpg/test.env.
205
235
  setupFiles: [require.resolve("@cedarjs/pg/test-env")],
206
236
  };
207
237
  ```
208
238
 
209
- ### Framework hosts (CedarJS, custom globalSetup)
239
+ `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).
240
+
241
+ ## CedarJS and custom globalSetup
210
242
 
211
- If your runner already owns `globalSetup` (e.g. Prisma push/migrate after acquire), **do not** replace it with `@cedarjs/pg/jest`. Compose instead:
243
+ If the runner already owns `globalSetup` (Prisma push or migrate after acquire), do not replace it with `@cedarjs/pg/jest`. Compose:
212
244
 
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 })`.
245
+ 1. In `globalSetup`, call `acquireIfNeeded` when opted in, then migrate.
246
+ 2. Add `setupFiles: [require.resolve("@cedarjs/pg/test-env")]` so Jest workers see `DATABASE_URL`.
247
+ 3. In `globalTeardown`, call `dispose({ mode: "test", root })`.
216
248
 
217
249
  ```ts
218
- // framework globalSetup (sketch)
219
250
  import { acquireIfNeeded } from "@cedarjs/pg";
220
251
 
221
252
  if (process.env.CEDAR_PG === "1" || process.env.CEDAR_PG === "true") {
222
253
  await acquireIfNeeded({
223
254
  root: projectRoot, // e.g. getPaths().base
224
255
  mode: "test",
225
- setEnv: true, // this process (prisma) — workers use @cedarjs/pg/test-env
256
+ setEnv: true, // this process (prisma). Workers use @cedarjs/pg/test-env.
226
257
  url: process.env.TEST_DATABASE_URL,
227
258
  force: process.env.CEDAR_PG_FORCE === "1",
228
- disabled: false, // framework opt-in; adapters alone use CEDAR_PG=0 opt-out
259
+ disabled: false, // framework opt-in. Stock adapters use CEDAR_PG=0 opt-out.
229
260
  });
230
261
  }
231
- // … prisma db push / migrate …
232
262
  ```
233
263
 
234
- ```js
235
- // jest-preset
236
- setupFiles: [require.resolve("@cedarjs/pg/test-env")],
237
- ```
238
-
239
- Use exported `STATE_DIRNAME` (`.cedarpg`) / `loadTestEnv` / `loadDevEnv` /
240
- `envFilePath(root, mode)` instead of hardcoding the lease dir.
241
-
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).
246
-
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.
264
+ ## TEMPLATE clones
288
265
 
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
- ```
266
+ Migrate stays app-owned via `createGlobalSetup({ migrate })`. The adapter then marks TEMPLATE and clones per worker.
300
267
 
301
- See [`.github/actions/setup-autopg`](.github/actions/setup-autopg/README.md) for inputs (`version`, `cache`, `token`) and outputs.
268
+ Point `globalSetup` at a local module that calls `createGlobalSetup`. String-resolving the package entry without a migrate hook throws.
302
269
 
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.
270
+ Each setup starts clean. Before migrate, `createGlobalSetup` drops every database owned by this worktree's test role: a TEMPLATE and worker clones left behind by a crashed or killed run, even if its lease file is gone. `migrate` therefore always runs against an empty database, and you do not need your own pre-cleanup. Only this worktree's `cpg_*_test_*` role is touched, never other databases on the shared host. Core API: `acquire({ mode: "test", fresh: true })`.
304
271
 
305
- ### Migrate-once + TEMPLATE clones (Jest / Vitest)
272
+ `cloneWorkerDatabase()` gives each worker one clone, `<template>_c_<JEST_WORKER_ID | VITEST_POOL_ID | pid>`, shared by every test file that worker runs. The first file creates it. Later files find it in Postgres and reuse it. Nothing is cached in memory, so this holds under Jest's per-file `globalThis` and module registry. If the clone name exists but belongs to a different role, the call fails with a clear error rather than reusing it.
306
273
 
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.
274
+ Each file then starts from empty tables. By default `cloneWorkerDatabase()` runs one `TRUNCATE ... RESTART IDENTITY` over every user table in the clone, so rows from earlier files in the worker are gone and `serial` / identity IDs restart at 1. Tests that expect a first row with `id = 1` behave the same whichever file a worker runs first. It truncates on every call, the first file included, so rows that `migrate` seeded into the TEMPLATE (including bookkeeping tables such as `_prisma_migrations`) are cleared too. It skips system schemas, temp tables, and tables an extension owns (for example PostGIS `spatial_ref_sys`). If skip policy uses an external URL or `CEDAR_PG=0` is set, nothing is cloned and nothing is truncated.
308
275
 
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.
276
+ To keep the clone as the previous file left it and reset on your own, pass `cloneWorkerDatabase({ reset: "none" })`. Resets between tests inside one file stay app-owned.
310
277
 
311
- **Jest (template mode):**
278
+ ### Jest
312
279
 
313
280
  ```js
314
281
  // jest.cedar-global.cjs
315
282
  const { createGlobalSetup } = require("@cedarjs/pg/jest/template");
316
283
  module.exports = createGlobalSetup({
317
284
  migrate: async ({ databaseUrl }) => {
318
- // prisma migrate reset / drizzle push / etc.
285
+ // prisma migrate reset, drizzle push
319
286
  },
320
287
  });
321
288
 
322
289
  // jest.config.cjs
290
+ // When .env has a real TEST_DATABASE_URL, set FORCE once here so it
291
+ // inherits into globalSetup and workers (dotenv will not override existing keys).
292
+ process.env.CEDAR_PG_FORCE = "1";
293
+
323
294
  module.exports = {
324
295
  globalSetup: "<rootDir>/jest.cedar-global.cjs",
325
296
  globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
297
+ // Runs once per test file; every file in a worker reuses that worker's clone,
298
+ // truncated with RESTART IDENTITY first.
326
299
  setupFilesAfterEnv: ["<rootDir>/jest.cedar-worker.cjs"],
327
300
  };
328
301
 
329
- // jest.cedar-worker.cjs — once per worker process
302
+ // jest.cedar-worker.cjs
330
303
  const { cloneWorkerDatabase } = require("@cedarjs/pg/jest/template");
331
304
  beforeAll(() => cloneWorkerDatabase());
332
305
  ```
333
306
 
334
- **Vitest (template mode):**
307
+ ### Vitest
335
308
 
336
309
  ```ts
337
310
  // vitest.cedar-global.ts
@@ -350,12 +323,14 @@ export default defineConfig({
350
323
  },
351
324
  });
352
325
 
353
- // vitest.cedar-worker.ts — once per worker process (ESM top-level await)
326
+ // vitest.cedar-worker.ts
354
327
  import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
355
328
  await cloneWorkerDatabase();
356
329
  ```
357
330
 
358
- **Programmatic** (core API — no runner adapters):
331
+ ### Core API
332
+
333
+ No runner adapters.
359
334
 
360
335
  ```ts
361
336
  import { acquire, markTemplate, cloneFromTemplate, dispose } from "@cedarjs/pg";
@@ -369,36 +344,131 @@ const worker = await cloneFromTemplate({
369
344
  name: "1",
370
345
  setEnv: true,
371
346
  });
372
- // … tests …
373
347
  await worker.dropClone(); // optional: drop one clone only
374
- await dispose({ root: acquired.root, mode: "test" }); // role-scoped: TEMPLATE + all clones + role
348
+ await dispose({ root: acquired.root, mode: "test" }); // TEMPLATE + all clones + role
349
+ ```
350
+
351
+ `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.
352
+
353
+ `setEnv` defaults to false on `cloneFromTemplate`. It defaults to true on `cloneFromTemplateIfNeeded` (same as `acquireIfNeeded`). Worker adapters call `cloneFromTemplateIfNeeded` via `cloneWorkerDatabase`, with `reuse: true`.
354
+
355
+ An explicit `name` that already exists fails with `database already exists` unless you pass `reuse: true`. Then a clone owned by the lease role is kept as is.
356
+
357
+ `dispose` is role-scoped suite teardown, not `dropClone`. It unsets `IS_TEMPLATE` and drops every database owned by the lease role.
358
+
359
+ ## Host and CI
360
+
361
+ `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.
362
+
363
+ When nothing is listening, cedar-pg brings up the registered local host. It runs `autopg restart`, then (if still dark) detached `autopg postmaster` on the registered port and `~/.autopg/data`. `autopg restart` (autopg ≥ v3.2) drives pm2 only and exits 0 once ready; without pm2, or under systemd-user / launchd, it exits 1 and cedar-pg moves on. Either way TCP accept is the gate. It does not run `autopg install` on an already-registered host (that path wants pm2). `install` is only for a never-registered machine. Same port, same data dir — not a second Postgres. The data dir and socket dir come from `autopg status --json`. They are never guessed: if autopg reports no `dataDir`, revive is skipped. If a live `postmaster.pid` already owns the data dir (pm2 still recovering, or a concurrent acquire), cedar-pg waits for that postmaster instead of starting a competitor. The revived postmaster has no supervisor and lives until reboot or crash. Its output is appended to `cedarpg-postmaster.log` in autopg's `logsDir` (`~/.autopg/logs`). If revive still produces no listener, `acquire` fails with what it tried.
364
+
365
+ Callers use `acquire` (and `adminUrl`). There is no public host-options object. Ephemeral behavior is env-driven.
366
+
367
+ ```ts
368
+ import { acquire } from "@cedarjs/pg";
369
+
370
+ // CI=true: detached postmaster (--ram on Linux /dev/shm). Does not rewrite ~/.autopg.
371
+ const { databaseUrl } = await acquire({ mode: "test" });
372
+ ```
373
+
374
+ | Signal | Effect (attach always wins when something is listening) |
375
+ | --------------------------- | -------------------------------------------------------------------------------------------- |
376
+ | `CEDAR_PG_EPHEMERAL_HOST=1` | Ephemeral owned postmaster on 55432 |
377
+ | `CEDAR_PG_EPHEMERAL_HOST=0` | Never start the ephemeral 55432 postmaster (even in CI). Local registered host only, or fail |
378
+ | unset and `CI=true` | Ephemeral |
379
+ | unset | Local: `autopg restart`, then registered `postmaster` if still dark, then fail |
380
+
381
+ Ephemeral recipe (not configurable via cedar-pg):
382
+
383
+ - Detached `autopg postmaster --port 55432 --socket-dir DIR --data DIR`
384
+ - Does not run `autopg install` (that rewrites `~/.autopg/admin.json` and conflicts with a local pm2 host)
385
+ - Linux when `/dev/shm` exists: also `--ram` and `DIR=/dev/shm/cedar-pg-<uid>`
386
+ - Otherwise: disk `DIR` under the OS temp dir
387
+ - Ready when TCP accepts on the recipe port
388
+ - Before cold-start, if the recipe port is not live, cedar-pg removes its own leftover data dir (`/dev/shm/cedar-pg-<uid>`, or `$TMPDIR/cedar-pg-host` without `--ram`) from an OOM-killed run. It never touches other `/dev/shm` entries: `PostgreSQL.*` segments belong to every running Postgres, the registered host included.
389
+
390
+ 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.
391
+
392
+ 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).
393
+
394
+ ### GitHub Actions
395
+
396
+ 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).
397
+
398
+ ```yaml
399
+ - uses: actions/checkout@v6
400
+ # In cedar-pg:
401
+ - uses: ./.github/actions/setup-autopg
402
+ # From another repo (pin to a tag when publishing the action):
403
+ # - uses: cedarjs/cedar-pg/.github/actions/setup-autopg@main
404
+ ```
405
+
406
+ 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.
407
+
408
+ 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.
409
+
410
+ ```yaml
411
+ - name: Ensure autopg binary
412
+ run: |
413
+ set -euo pipefail
414
+ echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
415
+ export PATH="${HOME}/.local/bin:${PATH}"
416
+ bash node_modules/@cedarjs/pg/scripts/ci-install-autopg.sh
417
+ env:
418
+ GH_TOKEN: ${{ github.token }}
419
+ ```
420
+
421
+ ## Environment variables
422
+
423
+ | Var | Meaning |
424
+ | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
425
+ | `AUTOPG_BIN` | Path to the autopg binary |
426
+ | `AUTOPG_PG_USER`, `AUTOPG_PG_PASSWORD` | Autopg superuser for admin URL (default user `postgres`, password `postgres`) |
427
+ | `CEDAR_PG=0` | Disable auto-acquire in adapters |
428
+ | `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) |
429
+ | `CEDAR_PG_FORCE=1` | Ignore the external-URL escape hatch (adapters, `cedarpg run --force`) |
430
+ | `CEDAR_PG_EPHEMERAL_HOST` | `1` owned postmaster. `0` never own one (even in CI). Unset and `CI=true` means ephemeral |
431
+ | `CEDAR_PG_REGISTRY_DIR` | Override the global lease registry (for `gc`) |
432
+ | `CEDAR_PG_SKIP_POSTINSTALL=1` | Skip the autopg install hook |
433
+ | `CEDAR_PG_INSTALL_AUTOPG=1` | Under `CI=true`, run binary-only `ci-install-autopg.sh` from postinstall |
434
+
435
+ `force`, `overwrite`, and `run` are different knobs. Do not collapse them.
436
+
437
+ ## Programmatic API
438
+
439
+ ```ts
440
+ import { acquire, loadDevEnv } from "@cedarjs/pg";
441
+
442
+ const { databaseUrl, adminUrl, databaseName, dispose } = await acquire({ mode: "test" });
443
+ await dispose();
444
+
445
+ loadDevEnv({ overwrite: true }); // override .env DATABASE_URL from .cedarpg/dev.env
446
+ ```
447
+
448
+ 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).
449
+
450
+ ## Develop this package
451
+
452
+ Vite+, Node `>=24` (`.node-version`), `pnpm@11`. Contributor rules: [AGENTS.md](AGENTS.md). User-visible API changes: [CHANGELOG.md](CHANGELOG.md).
453
+
454
+ ```bash
455
+ vp install
456
+ vp check
457
+ vp test
458
+ vp pack # dist/ (dts + esm + cjs)
459
+ vp run smoke # pack, then tarball install, then resolve exports
460
+ vp run smoke:pg # pack, then Vitest and Jest adapters against real ephemeral Postgres
375
461
  ```
376
462
 
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.
463
+ ## Troubleshooting
464
+
465
+ | Symptom | Fix |
466
+ | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
467
+ | `ECONNREFUSED 127.0.0.1:25432` on `cedarpg acquire` | autopg is registered but not listening. cedar-pg attaches only after TCP accepts. It runs `autopg restart`, then detached `autopg postmaster` on the registered port/data if still dark — including when pm2 is missing and `restart` exits 1. A revived postmaster logs to `~/.autopg/logs/cedarpg-postmaster.log`. `status=stopped` with `runtime.live=true` is healthy once TCP accepts; cedar-pg will not `install` pm2. If revive fails, the error lists what was tried. |
468
+ | `database already exists: ..._c_<workerId>` in Jest | Upgrade `@cedarjs/pg`. Older releases cached the clone on `globalThis`, which Jest resets for every test file. `cloneWorkerDatabase` now reuses the worker's clone from Postgres (`reuse: true`), and template setup drops crashed-run leftovers first. If the error says `owned by <other role>`, a database outside this worktree's lease has that name: drop it or pass another `name`. |
469
+ | 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`. |
470
+ | `Disk quota exceeded` / `No space left on device` / Postgres `53100` on ephemeral start | Enlarge `/dev/shm` (`sudo mount -o remount,size=6G /dev/shm`). Cold-start already removes cedar-pg's own leftover `/dev/shm/cedar-pg-<uid>` when the recipe port is dead. Do not `rm /dev/shm/PostgreSQL.*`: a live Postgres (your registered host included) then fails every new connection with `58P01 could not open shared memory segment`, and only a restart of that host recovers it. |
471
+ | `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. |
472
+ | Nx child still uses `.env` `DATABASE_URL` | `dependsOn` does not forward acquire env. Preload `@cedarjs/pg/dev-env` (`node --require` / `NODE_OPTIONS`), or wrap with `cedarpg run --attach --mode=dev -- <cmd>`. Not Nx `envFile`. |
473
+ | Role or DB errors under parallel Nx targets | Children are running plain `cedarpg run` (or `acquire`), so each one runs DDL. Keep the acquire in one `db:ready`, and switch the children to `cedarpg run --attach`. |
474
+ | `no dev lease ...; attach never acquires` from `cedarpg run --attach` | Nothing has acquired this worktree yet. Make the target `dependsOn` your `db:ready` (or run `cedarpg acquire --mode=dev`). On `nothing is listening`, re-run `db:ready` / `acquire` to bring the host back; attach never starts it. |