@cedarjs/pg 0.1.0-alpha.0 → 0.2.0-alpha.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 (123) hide show
  1. package/README.md +281 -30
  2. package/dist/cli.cjs +62 -16
  3. package/dist/cli.cjs.map +1 -1
  4. package/dist/cli.mjs +61 -15
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/{constants-B7sCf4fg.cjs → constants-Ct8myrEn.cjs} +4 -4
  7. package/dist/constants-Ct8myrEn.cjs.map +1 -0
  8. package/dist/{constants-YnHDwShD.mjs → constants-NLR0U4NR.mjs} +4 -4
  9. package/dist/constants-NLR0U4NR.mjs.map +1 -0
  10. package/dist/dev-env.cjs +12 -0
  11. package/dist/dev-env.cjs.map +1 -0
  12. package/dist/dev-env.d.cts +1 -0
  13. package/dist/dev-env.d.mts +1 -0
  14. package/dist/dev-env.mjs +14 -0
  15. package/dist/dev-env.mjs.map +1 -0
  16. package/dist/index.cjs +50 -10
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.cts +168 -82
  19. package/dist/index.d.mts +168 -82
  20. package/dist/index.mjs +32 -2
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/jest-teardown.cjs +18 -0
  23. package/dist/jest-teardown.cjs.map +1 -0
  24. package/dist/jest-teardown.d.cts +13 -0
  25. package/dist/jest-teardown.d.mts +14 -0
  26. package/dist/jest-teardown.mjs +18 -0
  27. package/dist/jest-teardown.mjs.map +1 -0
  28. package/dist/jest-template.cjs +41 -0
  29. package/dist/jest-template.cjs.map +1 -0
  30. package/dist/jest-template.d.cts +28 -0
  31. package/dist/jest-template.d.mts +28 -0
  32. package/dist/jest-template.mjs +35 -0
  33. package/dist/jest-template.mjs.map +1 -0
  34. package/dist/jest.cjs +12 -15
  35. package/dist/jest.cjs.map +1 -1
  36. package/dist/jest.d.cts +10 -7
  37. package/dist/jest.d.mts +10 -6
  38. package/dist/jest.mjs +12 -10
  39. package/dist/jest.mjs.map +1 -1
  40. package/dist/lease-B3TuX92y.d.mts +24 -0
  41. package/dist/lease-BSBBpaR4.mjs +201 -0
  42. package/dist/lease-BSBBpaR4.mjs.map +1 -0
  43. package/dist/lease-Bq9wKwQa.cjs +272 -0
  44. package/dist/lease-Bq9wKwQa.cjs.map +1 -0
  45. package/dist/lease-t4I9JahV.d.cts +24 -0
  46. package/dist/lifecycle-BOo6xBjD.mjs +684 -0
  47. package/dist/lifecycle-BOo6xBjD.mjs.map +1 -0
  48. package/dist/lifecycle-DEJ4GgWV.cjs +856 -0
  49. package/dist/lifecycle-DEJ4GgWV.cjs.map +1 -0
  50. package/dist/load-dev-env-7UjMHhw7.mjs +10 -0
  51. package/dist/load-dev-env-7UjMHhw7.mjs.map +1 -0
  52. package/dist/load-dev-env-cII6N4ZP.cjs +15 -0
  53. package/dist/load-dev-env-cII6N4ZP.cjs.map +1 -0
  54. package/dist/load-mode-env-B0qO9DZx.cjs +39 -0
  55. package/dist/load-mode-env-B0qO9DZx.cjs.map +1 -0
  56. package/dist/load-mode-env-C8gy4v9V.mjs +28 -0
  57. package/dist/load-mode-env-C8gy4v9V.mjs.map +1 -0
  58. package/dist/load-test-env-C34wpkIX.mjs +18 -0
  59. package/dist/load-test-env-C34wpkIX.mjs.map +1 -0
  60. package/dist/load-test-env-CkdUjTpV.cjs +23 -0
  61. package/dist/load-test-env-CkdUjTpV.cjs.map +1 -0
  62. package/dist/naming-C2nGVxPk.d.cts +39 -0
  63. package/dist/naming-C2nGVxPk.d.mts +39 -0
  64. package/dist/nx.cjs +26 -17
  65. package/dist/nx.cjs.map +1 -1
  66. package/dist/nx.d.cts +9 -20
  67. package/dist/nx.d.mts +9 -20
  68. package/dist/nx.mjs +20 -18
  69. package/dist/nx.mjs.map +1 -1
  70. package/dist/tasks-Caud9yHr.cjs +67 -0
  71. package/dist/tasks-Caud9yHr.cjs.map +1 -0
  72. package/dist/tasks-CjaZRi_G.d.mts +19 -0
  73. package/dist/tasks-DdQoP9We.d.cts +19 -0
  74. package/dist/tasks-KherHLac.mjs +38 -0
  75. package/dist/tasks-KherHLac.mjs.map +1 -0
  76. package/dist/template-CGw3C1Ob.mjs +88 -0
  77. package/dist/template-CGw3C1Ob.mjs.map +1 -0
  78. package/dist/template-D0gJ_MS2.cjs +105 -0
  79. package/dist/template-D0gJ_MS2.cjs.map +1 -0
  80. package/dist/template-mode-BkEs9LnY.cjs +91 -0
  81. package/dist/template-mode-BkEs9LnY.cjs.map +1 -0
  82. package/dist/template-mode-CcxV_Iju.d.cts +28 -0
  83. package/dist/template-mode-CcxV_Iju.d.mts +28 -0
  84. package/dist/template-mode-IjlFOG4O.mjs +80 -0
  85. package/dist/template-mode-IjlFOG4O.mjs.map +1 -0
  86. package/dist/test-env.cjs +16 -0
  87. package/dist/test-env.cjs.map +1 -0
  88. package/dist/test-env.d.cts +1 -0
  89. package/dist/test-env.d.mts +1 -0
  90. package/dist/test-env.mjs +18 -0
  91. package/dist/test-env.mjs.map +1 -0
  92. package/dist/vite-plus.cjs +8 -33
  93. package/dist/vite-plus.cjs.map +1 -1
  94. package/dist/vite-plus.d.cts +4 -48
  95. package/dist/vite-plus.d.mts +4 -48
  96. package/dist/vite-plus.mjs +5 -30
  97. package/dist/vite-plus.mjs.map +1 -1
  98. package/dist/vitest-template.cjs +48 -0
  99. package/dist/vitest-template.cjs.map +1 -0
  100. package/dist/vitest-template.d.cts +31 -0
  101. package/dist/vitest-template.d.mts +31 -0
  102. package/dist/vitest-template.mjs +42 -0
  103. package/dist/vitest-template.mjs.map +1 -0
  104. package/dist/vitest.cjs +10 -10
  105. package/dist/vitest.cjs.map +1 -1
  106. package/dist/vitest.d.cts +7 -3
  107. package/dist/vitest.d.mts +7 -2
  108. package/dist/vitest.mjs +10 -5
  109. package/dist/vitest.mjs.map +1 -1
  110. package/package.json +37 -13
  111. package/scripts/autopg-version +1 -0
  112. package/scripts/ci-install-autopg.sh +73 -0
  113. package/scripts/postinstall.js +39 -13
  114. package/dist/constants-B7sCf4fg.cjs.map +0 -1
  115. package/dist/constants-YnHDwShD.mjs.map +0 -1
  116. package/dist/lifecycle-D10usEjM.cjs +0 -743
  117. package/dist/lifecycle-D10usEjM.cjs.map +0 -1
  118. package/dist/lifecycle-PbTXNC2Z.mjs +0 -577
  119. package/dist/lifecycle-PbTXNC2Z.mjs.map +0 -1
  120. package/dist/tasks-CW2D0n46.mjs +0 -16
  121. package/dist/tasks-CW2D0n46.mjs.map +0 -1
  122. package/dist/tasks-DZ7hMmGJ.cjs +0 -21
  123. package/dist/tasks-DZ7hMmGJ.cjs.map +0 -1
package/README.md CHANGED
@@ -1,14 +1,14 @@
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**, powered by [autopg](https://github.com/automagik-dev/autopg).
4
4
 
5
5
  Published on npm as **`@cedarjs/pg`**.
6
6
 
7
- > **Alpha** (`0.1.0-alpha.0`): APIs may change. Install with the `alpha` dist-tag.
7
+ > **Alpha** (`0.2.0-alpha.0`): APIs may change. Install with the `alpha` dist-tag.
8
8
 
9
9
  ## What you get (via autopg)
10
10
 
11
- Under the hood you get an **embedded PostgreSQL server with true concurrent connections** — native PostgreSQL 18 (not WASM), real process forking, no connection locks. It just works: **no credentials needed**, **zero config**, and **auto-provisioned databases** on first use. Any client works (`psql`, `node-postgres`, Prisma, Drizzle, TypeORM).
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
12
 
13
13
  ### Development & testing
14
14
 
@@ -21,7 +21,7 @@ Under the hood you get an **embedded PostgreSQL server with true concurrent conn
21
21
 
22
22
  ## What cedar-pg adds
23
23
 
24
- **cedar-pg** sits on that autopg host and gives each **git worktree** its own database + role, with readable names, leases, and teardown — so parallel checkouts don’t fight over one shared DB:
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
25
 
26
26
  - **1 database per git worktree** (visible in `\l` as `cpg_…`)
27
27
  - **dev** DBs persist across restarts; **test** DBs drop on dispose
@@ -30,7 +30,7 @@ Under the hood you get an **embedded PostgreSQL server with true concurrent conn
30
30
 
31
31
  | Layer | Responsibility |
32
32
  | ------------ | ------------------------------------------------------------------- |
33
- | **autopg** | Embedded Postgres host — concurrent, zero-config, auto-provision |
33
+ | **autopg** | Embedded Postgres host (concurrent, zero-config, auto-provision) |
34
34
  | **cedar-pg** | Per-worktree `CREATE DATABASE` / role, `DATABASE_URL`, dispose + GC |
35
35
 
36
36
  ## Install
@@ -57,14 +57,16 @@ Examples:
57
57
  ## Prerequisites
58
58
 
59
59
  A running [autopg](https://github.com/automagik-dev/autopg) host (installed automatically by `postinstall`, or manually).
60
- Both the install script and binary are pinned to a release tag (currently `v3.0.7`); bump the pin in `scripts/postinstall.js` to upgrade:
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:
61
61
 
62
62
  ```bash
63
- curl -fsSL https://raw.githubusercontent.com/automagik-dev/autopg/v3.0.7/install.sh \
64
- | AUTOPG_VERSION=v3.0.7 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
65
67
  ```
66
68
 
67
- Typical flow: `autopg daemon` (or your usual host install) once per machine → `cedarpg ensure` per worktree → connect with the printed `DATABASE_URL`.
69
+ Typical flow: `autopg daemon` (or your usual host install) once per machine → `cedarpg acquire` per worktree → connect with the printed `DATABASE_URL`.
68
70
 
69
71
  ## Develop this package (Vite+)
70
72
 
@@ -72,8 +74,9 @@ Typical flow: `autopg daemon` (or your usual host install) once per machine →
72
74
  vp install
73
75
  vp check
74
76
  vp test
75
- vp pack # → dist/ (dts + esm + cjs)
76
- vp run smoke # build → npm-pack tarball → install + resolve exports
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
77
80
  ```
78
81
 
79
82
  ## Local consume (without npm)
@@ -84,19 +87,76 @@ vp pack
84
87
 
85
88
  # in your app / Cedar
86
89
  yarn add @cedarjs/pg@file:../cedar-pg
87
- # or: pnpm pack && yarn add ./cedarjs-pg-0.1.0-alpha.0.tgz
90
+ # or: pnpm pack && yarn add ./cedarjs-pg-0.2.0-alpha.0.tgz
88
91
  ```
89
92
 
90
93
  ## CLI
91
94
 
92
95
  ```bash
93
- cedarpg ensure --mode=dev
94
- cedarpg ensure --mode=test --print-env
96
+ cedarpg acquire --mode=dev
97
+ cedarpg acquire --mode=test --print-env
98
+ cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts
99
+ cedarpg run --mode=test -- vitest run
95
100
  cedarpg dispose --mode=test
96
101
  cedarpg print-url --mode=dev
97
102
  cedarpg gc # drop DBs whose worktree root is gone (uses ~/.cedarpg/registry)
98
103
  ```
99
104
 
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.
108
+
109
+ ## Nx consumer adapter
110
+
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.
114
+
115
+ ```ts
116
+ import { cedarPgNxTargets, cedarPgRunCommand, relativeEnvFile } from "@cedarjs/pg/nx";
117
+
118
+ cedarPgNxTargets();
119
+ // { "db:acquire": { command: "cedarpg acquire --mode=dev", cache: false }, … }
120
+
121
+ cedarPgRunCommand("dev", "yarn tsx scripts/apiServer/dev.ts");
122
+ // "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
123
+
124
+ relativeEnvFile("dev"); // ".cedarpg/dev.env"
125
+ ```
126
+
127
+ ```json
128
+ {
129
+ "targets": {
130
+ "dev": {
131
+ "command": "cedarpg run --mode=dev -- yarn tsx scripts/apiServer/dev.ts"
132
+ },
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
+ }
140
+ }
141
+ ```
142
+
143
+ For a db:ready-style migrate hook (same compose shape as Jest `createGlobalSetup`):
144
+
145
+ ```ts
146
+ // tools/db-ready.ts
147
+ import { createAcquireTask } from "@cedarjs/pg";
148
+
149
+ await createAcquireTask({
150
+ mode: "dev",
151
+ afterAcquire: async ({ databaseUrl }) => {
152
+ // prisma migrate deploy / drizzle push / …
153
+ },
154
+ })();
155
+ ```
156
+
157
+ Fallbacks when you cannot wrap with `run`: `loadDevEnv({ overwrite: true })` or
158
+ `import "@cedarjs/pg/dev-env"`. Absolute path helper: `envFilePath(root, mode)`.
159
+
100
160
  ## Vite+ consumer adapter
101
161
 
102
162
  ```ts
@@ -110,12 +170,12 @@ export default defineConfig({
110
170
  ...cedarPgTasks(),
111
171
  test: {
112
172
  command: "vp test",
113
- dependsOn: ["db:ensure-test"],
173
+ dependsOn: ["db:acquire-test"],
114
174
  env: ["DATABASE_URL", "TEST_DATABASE_URL"],
115
175
  },
116
176
  dev: {
117
177
  command: "vp dev",
118
- dependsOn: ["db:ensure"],
178
+ dependsOn: ["db:acquire"],
119
179
  env: ["DATABASE_URL"],
120
180
  },
121
181
  },
@@ -123,31 +183,222 @@ export default defineConfig({
123
183
  });
124
184
  ```
125
185
 
186
+ ## Vitest / Jest adapters
187
+
188
+ ```ts
189
+ // vitest.config.ts
190
+ import { defineConfig } from "vitest/config";
191
+
192
+ export default defineConfig({
193
+ test: {
194
+ globalSetup: ["@cedarjs/pg/vitest"],
195
+ },
196
+ });
197
+ ```
198
+
199
+ ```js
200
+ // jest.config.cjs — standalone apps
201
+ module.exports = {
202
+ globalSetup: require.resolve("@cedarjs/pg/jest"),
203
+ globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
204
+ // Jest globalSetup is a separate process — workers load DATABASE_URL from .cedarpg/test.env
205
+ setupFiles: [require.resolve("@cedarjs/pg/test-env")],
206
+ };
207
+ ```
208
+
209
+ ### Framework hosts (CedarJS, custom globalSetup)
210
+
211
+ If your runner already owns `globalSetup` (e.g. Prisma push/migrate after acquire), **do not** replace it with `@cedarjs/pg/jest`. Compose instead:
212
+
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 })`.
216
+
217
+ ```ts
218
+ // framework globalSetup (sketch)
219
+ import { acquireIfNeeded } from "@cedarjs/pg";
220
+
221
+ if (process.env.CEDAR_PG === "1" || process.env.CEDAR_PG === "true") {
222
+ await acquireIfNeeded({
223
+ root: projectRoot, // e.g. getPaths().base
224
+ mode: "test",
225
+ setEnv: true, // this process (prisma) — workers use @cedarjs/pg/test-env
226
+ url: process.env.TEST_DATABASE_URL,
227
+ force: process.env.CEDAR_PG_FORCE === "1",
228
+ disabled: false, // framework opt-in; adapters alone use CEDAR_PG=0 opt-out
229
+ });
230
+ }
231
+ // … prisma db push / migrate …
232
+ ```
233
+
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
+
126
247
  ## Programmatic API
127
248
 
128
249
  ```ts
129
- import { ensure, dispose } from "@cedarjs/pg";
250
+ import { acquire, dispose, loadTestEnv, loadDevEnv, envFilePath, STATE_DIRNAME } from "@cedarjs/pg";
130
251
 
131
- const { databaseUrl, databaseName, dispose: drop } = await ensure({ mode: "test" });
252
+ const { databaseUrl, adminUrl, databaseName, dispose: drop } = await acquire({ mode: "test" });
132
253
  // … tests …
133
254
  await drop();
255
+
256
+ loadDevEnv({ overwrite: true }); // override .env DATABASE_URL from .cedarpg/dev.env
134
257
  ```
135
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):**
312
+
313
+ ```js
314
+ // jest.cedar-global.cjs
315
+ const { createGlobalSetup } = require("@cedarjs/pg/jest/template");
316
+ module.exports = createGlobalSetup({
317
+ migrate: async ({ databaseUrl }) => {
318
+ // prisma migrate reset / drizzle push / etc.
319
+ },
320
+ });
321
+
322
+ // jest.config.cjs
323
+ module.exports = {
324
+ globalSetup: "<rootDir>/jest.cedar-global.cjs",
325
+ globalTeardown: require.resolve("@cedarjs/pg/jest-teardown"),
326
+ setupFilesAfterEnv: ["<rootDir>/jest.cedar-worker.cjs"],
327
+ };
328
+
329
+ // jest.cedar-worker.cjs — once per worker process
330
+ const { cloneWorkerDatabase } = require("@cedarjs/pg/jest/template");
331
+ beforeAll(() => cloneWorkerDatabase());
332
+ ```
333
+
334
+ **Vitest (template mode):**
335
+
336
+ ```ts
337
+ // vitest.cedar-global.ts
338
+ import { createGlobalSetup } from "@cedarjs/pg/vitest/template";
339
+ export default createGlobalSetup({
340
+ migrate: async ({ databaseUrl }) => {
341
+ // migrate once
342
+ },
343
+ });
344
+
345
+ // vitest.config.ts
346
+ export default defineConfig({
347
+ test: {
348
+ globalSetup: ["./vitest.cedar-global.ts"],
349
+ setupFiles: ["./vitest.cedar-worker.ts"],
350
+ },
351
+ });
352
+
353
+ // vitest.cedar-worker.ts — once per worker process (ESM top-level await)
354
+ import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
355
+ await cloneWorkerDatabase();
356
+ ```
357
+
358
+ **Programmatic** (core API — no runner adapters):
359
+
360
+ ```ts
361
+ import { acquire, markTemplate, cloneFromTemplate, dispose } from "@cedarjs/pg";
362
+
363
+ const acquired = await acquire({ mode: "test" });
364
+ await migrate({ databaseUrl: acquired.databaseUrl, adminUrl: acquired.adminUrl });
365
+ await markTemplate({ root: acquired.root, mode: "test", adminUrl: acquired.adminUrl });
366
+ const worker = await cloneFromTemplate({
367
+ root: acquired.root,
368
+ mode: "test",
369
+ name: "1",
370
+ setEnv: true,
371
+ });
372
+ // … tests …
373
+ await worker.dropClone(); // optional: drop one clone only
374
+ await dispose({ root: acquired.root, mode: "test" }); // role-scoped: TEMPLATE + all clones + role
375
+ ```
376
+
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
+
136
382
  ## Env
137
383
 
138
- | Var | Meaning |
139
- | ----------------------------- | ------------------------------------------------------------------- |
140
- | `AUTOPG_BIN` | Path to autopg |
141
- | `CEDAR_PG=0` | Disable auto-ensure in adapters |
142
- | `TEST_DATABASE_URL` | Escape hatch — skip ensure for external DBs (not `cpg_*` / `file:`) |
143
- | `CEDAR_PG_FORCE=1` | Ignore external-URL escape hatch |
144
- | `CEDAR_PG_REGISTRY_DIR` | Override global lease registry (for `gc`) |
145
- | `CEDAR_PG_SKIP_POSTINSTALL=1` | Skip autopg install hook |
146
- | `CEDAR_PG_INSTALL_AUTOPG=1` | Force autopg install in CI |
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 |
147
395
 
148
396
  ## Alpha caveats
149
397
 
150
398
  - Public API may change before `0.1.0`.
151
- - End-to-end Postgres flows assume a working local `autopg` host; CI unit tests do not start Postgres.
152
- - State lives in product-owned `.cedarpg` (worktree + `~/.cedarpg/registry`) — not under autopg's `~/.autopg/`, not a generic `.pg`.
153
- - Password salt (`cedar-pg\\0`, scheme v1) is an opaque crypto constant; bump the scheme id to change it.
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.
package/dist/cli.cjs CHANGED
@@ -1,24 +1,32 @@
1
1
  #!/usr/bin/env node
2
- const require_lifecycle = require("./lifecycle-D10usEjM.cjs");
3
- const require_constants = require("./constants-B7sCf4fg.cjs");
2
+ const require_lifecycle = require("./lifecycle-DEJ4GgWV.cjs");
3
+ const require_constants = require("./constants-Ct8myrEn.cjs");
4
+ const require_lease = require("./lease-Bq9wKwQa.cjs");
5
+ let node_child_process = require("node:child_process");
4
6
  //#region src/cli.ts
5
7
  function printHelp() {
6
- process.stdout.write(`${require_constants.CLI_NAME} — worktree-isolated local Postgres (via autopg)
8
+ process.stdout.write(`${require_constants.CLI_NAME}: worktree-isolated local Postgres (via autopg)
7
9
 
8
10
  Usage:
9
- ${require_constants.CLI_NAME} ensure --mode=dev|test [--root <path>] [--json] [--print-env]
11
+ ${require_constants.CLI_NAME} acquire --mode=dev|test [--root <path>] [--force] [--json] [--print-env]
12
+ ${require_constants.CLI_NAME} run --mode=dev|test [--root <path>] [--force] -- <cmd…>
10
13
  ${require_constants.CLI_NAME} dispose [--mode=dev|test] [--root <path>]
11
14
  ${require_constants.CLI_NAME} gc [--json]
12
15
  ${require_constants.CLI_NAME} print-url [--mode=dev|test] [--root <path>]
13
16
  ${require_constants.CLI_NAME} --help
14
17
 
15
18
  Modes:
16
- dev Keep DB across restarts (default for ensure if omitted: dev)
19
+ dev Keep DB across restarts (default when --mode omitted)
17
20
  test Drop DB on dispose / test teardown
18
21
 
22
+ run:
23
+ Acquire, set DATABASE_URL (+ TEST_DATABASE_URL in test) on the child, exec <cmd…>.
24
+ --force sets CEDAR_PG_FORCE (escape hatch); child env overwrite is always on.
25
+
19
26
  Env:
20
- AUTOPG_BIN Path to autopg binary
21
- CEDAR_PG=0 Disable adapters that auto-ensure
27
+ AUTOPG_BIN Path to autopg binary
28
+ CEDAR_PG=0 Disable adapters that auto-acquire
29
+ CEDAR_PG_FORCE=1 Ignore external-URL escape hatch (same as --force)
22
30
  `);
23
31
  }
24
32
  function parseMode(value) {
@@ -29,19 +37,40 @@ function parseArgs(argv) {
29
37
  const out = {};
30
38
  const rest = [...argv];
31
39
  out.cmd = rest.shift();
32
- for (let i = 0; i < rest.length; i++) {
33
- const a = rest[i];
40
+ const dash = rest.indexOf("--");
41
+ let flagArgs = rest;
42
+ if (dash >= 0) {
43
+ flagArgs = rest.slice(0, dash);
44
+ out.child = rest.slice(dash + 1);
45
+ }
46
+ for (let i = 0; i < flagArgs.length; i++) {
47
+ const a = flagArgs[i];
34
48
  if (a === "--help" || a === "-h") out.help = true;
35
49
  else if (a === "--json") out.json = true;
36
50
  else if (a === "--print-env") out.printEnv = true;
51
+ else if (a === "--force") out.force = true;
37
52
  else if (a.startsWith("--mode=")) out.mode = parseMode(a.slice(7));
38
- else if (a === "--mode") out.mode = parseMode(rest[++i]);
53
+ else if (a === "--mode") out.mode = parseMode(flagArgs[++i]);
39
54
  else if (a.startsWith("--root=")) out.root = a.slice(7);
40
- else if (a === "--root") out.root = rest[++i];
55
+ else if (a === "--root") out.root = flagArgs[++i];
41
56
  else throw new Error(`unknown argument: ${a}`);
42
57
  }
43
58
  return out;
44
59
  }
60
+ function runChild(command, env) {
61
+ const [file, ...args] = command;
62
+ if (!file) throw new Error(`run requires a command after -- (e.g. ${require_constants.CLI_NAME} run --mode=dev -- yarn dev)`);
63
+ return new Promise((resolve, reject) => {
64
+ const child = (0, node_child_process.spawn)(file, args, {
65
+ stdio: "inherit",
66
+ env
67
+ });
68
+ child.on("error", reject);
69
+ child.on("exit", (code, signal) => {
70
+ resolve(signal ? 1 : code ?? 1);
71
+ });
72
+ });
73
+ }
45
74
  async function main() {
46
75
  const argv = process.argv.slice(2);
47
76
  if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
@@ -54,9 +83,10 @@ async function main() {
54
83
  return 0;
55
84
  }
56
85
  try {
57
- if (args.cmd === "ensure") {
86
+ if (args.cmd === "acquire") {
87
+ if (args.force) process.env.CEDAR_PG_FORCE = "1";
58
88
  const mode = args.mode ?? "dev";
59
- const result = await require_lifecycle.ensure({
89
+ const result = await require_lifecycle.acquire({
60
90
  root: args.root,
61
91
  mode,
62
92
  setEnv: true
@@ -67,6 +97,7 @@ async function main() {
67
97
  }
68
98
  if (args.json) process.stdout.write(`${JSON.stringify({
69
99
  databaseUrl: result.databaseUrl,
100
+ adminUrl: result.adminUrl,
70
101
  databaseName: result.databaseName,
71
102
  roleName: result.roleName,
72
103
  repoSlug: result.repoSlug,
@@ -82,6 +113,21 @@ async function main() {
82
113
  }
83
114
  return 0;
84
115
  }
116
+ if (args.cmd === "run") {
117
+ if (args.force) process.env.CEDAR_PG_FORCE = "1";
118
+ const mode = args.mode ?? "dev";
119
+ const result = await require_lifecycle.acquire({
120
+ root: args.root,
121
+ mode,
122
+ setEnv: true
123
+ });
124
+ const childEnv = {
125
+ ...process.env,
126
+ DATABASE_URL: result.databaseUrl
127
+ };
128
+ if (mode === "test") childEnv.TEST_DATABASE_URL = result.databaseUrl;
129
+ return await runChild(args.child ?? [], childEnv);
130
+ }
85
131
  if (args.cmd === "dispose") {
86
132
  const mode = args.mode ?? "test";
87
133
  const result = await require_lifecycle.dispose({
@@ -96,7 +142,7 @@ async function main() {
96
142
  process.stdout.write(`${require_constants.CLI_NAME}: nothing to dispose for ${mode} (no lease)\n`);
97
143
  return 0;
98
144
  }
99
- process.stderr.write(`${require_constants.CLI_NAME}: could not dispose ${mode} — autopg host unavailable (lease kept for retry)\n`);
145
+ process.stderr.write(`${require_constants.CLI_NAME}: could not dispose ${mode}: autopg host unavailable (lease kept for retry)\n`);
100
146
  return 1;
101
147
  }
102
148
  if (args.cmd === "gc") {
@@ -110,9 +156,9 @@ async function main() {
110
156
  }
111
157
  if (args.cmd === "print-url") {
112
158
  const mode = args.mode ?? "dev";
113
- const lease = require_lifecycle.readLease(require_lifecycle.resolveWorktreeIdentity(args.root).root, mode);
159
+ const lease = require_lease.readLease(require_lease.resolveWorktreeIdentity(args.root).root, mode);
114
160
  if (!lease) {
115
- process.stderr.write(`${require_constants.CLI_NAME}: no ${mode} lease — run \`${require_constants.CLI_NAME} ensure --mode=${mode}\` first\n`);
161
+ process.stderr.write(`${require_constants.CLI_NAME}: no ${mode} lease; run \`${require_constants.CLI_NAME} acquire --mode=${mode}\` first\n`);
116
162
  return 2;
117
163
  }
118
164
  process.stdout.write(`${require_lifecycle.urlFromLease(lease)}\n`);
package/dist/cli.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.cjs","names":["CLI_NAME","ensure","dispose","gc","readLease","resolveWorktreeIdentity","urlFromLease"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { CLI_NAME } from \"./core/constants.ts\";\nimport { ensure, dispose, gc, urlFromLease } from \"./core/lifecycle.ts\";\nimport { resolveWorktreeIdentity } from \"./core/worktree.ts\";\nimport { readLease } from \"./core/lease.ts\";\nimport type { DbMode } from \"./core/naming.ts\";\n\nfunction printHelp(): void {\n process.stdout.write(`${CLI_NAME} — worktree-isolated local Postgres (via autopg)\n\nUsage:\n ${CLI_NAME} ensure --mode=dev|test [--root <path>] [--json] [--print-env]\n ${CLI_NAME} dispose [--mode=dev|test] [--root <path>]\n ${CLI_NAME} gc [--json]\n ${CLI_NAME} print-url [--mode=dev|test] [--root <path>]\n ${CLI_NAME} --help\n\nModes:\n dev Keep DB across restarts (default for ensure if omitted: dev)\n test Drop DB on dispose / test teardown\n\nEnv:\n AUTOPG_BIN Path to autopg binary\n CEDAR_PG=0 Disable adapters that auto-ensure\n`);\n}\n\nfunction parseMode(value: string | undefined): DbMode {\n if (value === \"dev\" || value === \"test\") return value;\n throw new Error(\"--mode must be dev or test\");\n}\n\nfunction parseArgs(argv: string[]) {\n const out: {\n cmd?: string;\n mode?: DbMode;\n root?: string;\n json?: boolean;\n printEnv?: boolean;\n help?: boolean;\n } = {};\n const rest = [...argv];\n out.cmd = rest.shift();\n for (let i = 0; i < rest.length; i++) {\n const a = rest[i]!;\n if (a === \"--help\" || a === \"-h\") out.help = true;\n else if (a === \"--json\") out.json = true;\n else if (a === \"--print-env\") out.printEnv = true;\n else if (a.startsWith(\"--mode=\")) out.mode = parseMode(a.slice(7));\n else if (a === \"--mode\") out.mode = parseMode(rest[++i]);\n else if (a.startsWith(\"--root=\")) out.root = a.slice(7);\n else if (a === \"--root\") out.root = rest[++i];\n else throw new Error(`unknown argument: ${a}`);\n }\n return out;\n}\n\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n if (argv.length === 0 || argv[0] === \"help\" || argv[0] === \"--help\" || argv[0] === \"-h\") {\n printHelp();\n return 0;\n }\n const args = parseArgs(argv);\n if (args.help) {\n printHelp();\n return 0;\n }\n\n try {\n if (args.cmd === \"ensure\") {\n const mode = args.mode ?? \"dev\";\n const result = await ensure({\n root: args.root,\n mode,\n setEnv: true,\n });\n if (args.printEnv) {\n process.stdout.write(`DATABASE_URL=${result.databaseUrl}\\n`);\n if (mode === \"test\") {\n process.stdout.write(`TEST_DATABASE_URL=${result.databaseUrl}\\n`);\n }\n }\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify(\n {\n databaseUrl: result.databaseUrl,\n databaseName: result.databaseName,\n roleName: result.roleName,\n repoSlug: result.repoSlug,\n worktreeSlug: result.worktreeSlug,\n pathHash: result.pathHash,\n mode: result.mode,\n port: result.port,\n root: result.root,\n },\n null,\n 2,\n )}\\n`,\n );\n } else if (!args.printEnv) {\n process.stdout.write(\n `${CLI_NAME}: ${result.databaseName} (${result.repoSlug}/${result.worktreeSlug} ${result.mode})\\n`,\n );\n process.stdout.write(`${result.databaseUrl}\\n`);\n }\n return 0;\n }\n\n if (args.cmd === \"dispose\") {\n const mode = args.mode ?? \"test\";\n const result = await dispose({ root: args.root, mode });\n if (result.dropped) {\n process.stdout.write(`${CLI_NAME}: disposed ${mode} (${result.databaseName})\\n`);\n return 0;\n }\n if (result.reason === \"no-lease\") {\n process.stdout.write(`${CLI_NAME}: nothing to dispose for ${mode} (no lease)\\n`);\n return 0;\n }\n process.stderr.write(\n `${CLI_NAME}: could not dispose ${mode} — autopg host unavailable (lease kept for retry)\\n`,\n );\n return 1;\n }\n\n if (args.cmd === \"gc\") {\n const result = await gc();\n if (args.json) {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n } else {\n process.stdout.write(`${CLI_NAME} gc: dropped ${result.dropped.length} database(s)\\n`);\n for (const name of result.dropped) process.stdout.write(` ${name}\\n`);\n }\n return 0;\n }\n\n if (args.cmd === \"print-url\") {\n const mode = args.mode ?? \"dev\";\n const identity = resolveWorktreeIdentity(args.root);\n const lease = readLease(identity.root, mode);\n if (!lease) {\n process.stderr.write(\n `${CLI_NAME}: no ${mode} lease — run \\`${CLI_NAME} ensure --mode=${mode}\\` first\\n`,\n );\n return 2;\n }\n process.stdout.write(`${urlFromLease(lease)}\\n`);\n return 0;\n }\n\n process.stderr.write(`${CLI_NAME}: unknown command ${args.cmd}\\n`);\n printHelp();\n return 64;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`${CLI_NAME}: ${message}\\n`);\n return 1;\n }\n}\n\nmain()\n .then((code) => {\n process.exit(code);\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`${CLI_NAME}: ${message}\\n`);\n process.exit(1);\n });\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.cjs","names":["CLI_NAME","acquire","dispose","gc","readLease","resolveWorktreeIdentity","urlFromLease"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport { CLI_NAME } from \"./core/constants.ts\";\nimport { acquire, dispose, gc, urlFromLease } from \"./core/lifecycle.ts\";\nimport { resolveWorktreeIdentity } from \"./core/worktree.ts\";\nimport { readLease } from \"./core/lease.ts\";\nimport type { DbMode } from \"./core/naming.ts\";\n\nfunction printHelp(): void {\n process.stdout.write(`${CLI_NAME}: worktree-isolated local Postgres (via autopg)\n\nUsage:\n ${CLI_NAME} acquire --mode=dev|test [--root <path>] [--force] [--json] [--print-env]\n ${CLI_NAME} run --mode=dev|test [--root <path>] [--force] -- <cmd…>\n ${CLI_NAME} dispose [--mode=dev|test] [--root <path>]\n ${CLI_NAME} gc [--json]\n ${CLI_NAME} print-url [--mode=dev|test] [--root <path>]\n ${CLI_NAME} --help\n\nModes:\n dev Keep DB across restarts (default when --mode omitted)\n test Drop DB on dispose / test teardown\n\nrun:\n Acquire, set DATABASE_URL (+ TEST_DATABASE_URL in test) on the child, exec <cmd…>.\n --force sets CEDAR_PG_FORCE (escape hatch); child env overwrite is always on.\n\nEnv:\n AUTOPG_BIN Path to autopg binary\n CEDAR_PG=0 Disable adapters that auto-acquire\n CEDAR_PG_FORCE=1 Ignore external-URL escape hatch (same as --force)\n`);\n}\n\nfunction parseMode(value: string | undefined): DbMode {\n if (value === \"dev\" || value === \"test\") return value;\n throw new Error(\"--mode must be dev or test\");\n}\n\ntype ParsedArgs = {\n cmd?: string;\n mode?: DbMode;\n root?: string;\n json?: boolean;\n printEnv?: boolean;\n force?: boolean;\n help?: boolean;\n child?: string[];\n};\n\nfunction parseArgs(argv: string[]): ParsedArgs {\n const out: ParsedArgs = {};\n const rest = [...argv];\n out.cmd = rest.shift();\n\n const dash = rest.indexOf(\"--\");\n let flagArgs = rest;\n if (dash >= 0) {\n flagArgs = rest.slice(0, dash);\n out.child = rest.slice(dash + 1);\n }\n\n for (let i = 0; i < flagArgs.length; i++) {\n const a = flagArgs[i]!;\n if (a === \"--help\" || a === \"-h\") out.help = true;\n else if (a === \"--json\") out.json = true;\n else if (a === \"--print-env\") out.printEnv = true;\n else if (a === \"--force\") out.force = true;\n else if (a.startsWith(\"--mode=\")) out.mode = parseMode(a.slice(7));\n else if (a === \"--mode\") out.mode = parseMode(flagArgs[++i]);\n else if (a.startsWith(\"--root=\")) out.root = a.slice(7);\n else if (a === \"--root\") out.root = flagArgs[++i];\n else throw new Error(`unknown argument: ${a}`);\n }\n return out;\n}\n\nfunction runChild(command: string[], env: NodeJS.ProcessEnv): Promise<number> {\n const [file, ...args] = command;\n if (!file) {\n throw new Error(\n `run requires a command after -- (e.g. ${CLI_NAME} run --mode=dev -- yarn dev)`,\n );\n }\n return new Promise((resolve, reject) => {\n const child = spawn(file, args, { stdio: \"inherit\", env });\n child.on(\"error\", reject);\n child.on(\"exit\", (code, signal) => {\n resolve(signal ? 1 : (code ?? 1));\n });\n });\n}\n\nasync function main(): Promise<number> {\n const argv = process.argv.slice(2);\n if (argv.length === 0 || argv[0] === \"help\" || argv[0] === \"--help\" || argv[0] === \"-h\") {\n printHelp();\n return 0;\n }\n const args = parseArgs(argv);\n if (args.help) {\n printHelp();\n return 0;\n }\n\n try {\n if (args.cmd === \"acquire\") {\n if (args.force) process.env.CEDAR_PG_FORCE = \"1\";\n const mode = args.mode ?? \"dev\";\n const result = await acquire({\n root: args.root,\n mode,\n setEnv: true,\n });\n if (args.printEnv) {\n process.stdout.write(`DATABASE_URL=${result.databaseUrl}\\n`);\n if (mode === \"test\") {\n process.stdout.write(`TEST_DATABASE_URL=${result.databaseUrl}\\n`);\n }\n }\n if (args.json) {\n process.stdout.write(\n `${JSON.stringify(\n {\n databaseUrl: result.databaseUrl,\n adminUrl: result.adminUrl,\n databaseName: result.databaseName,\n roleName: result.roleName,\n repoSlug: result.repoSlug,\n worktreeSlug: result.worktreeSlug,\n pathHash: result.pathHash,\n mode: result.mode,\n port: result.port,\n root: result.root,\n },\n null,\n 2,\n )}\\n`,\n );\n } else if (!args.printEnv) {\n process.stdout.write(\n `${CLI_NAME}: ${result.databaseName} (${result.repoSlug}/${result.worktreeSlug} ${result.mode})\\n`,\n );\n process.stdout.write(`${result.databaseUrl}\\n`);\n }\n return 0;\n }\n\n if (args.cmd === \"run\") {\n if (args.force) process.env.CEDAR_PG_FORCE = \"1\";\n const mode = args.mode ?? \"dev\";\n const result = await acquire({\n root: args.root,\n mode,\n setEnv: true,\n });\n const childEnv: NodeJS.ProcessEnv = {\n ...process.env,\n DATABASE_URL: result.databaseUrl,\n };\n if (mode === \"test\") childEnv.TEST_DATABASE_URL = result.databaseUrl;\n return await runChild(args.child ?? [], childEnv);\n }\n\n if (args.cmd === \"dispose\") {\n const mode = args.mode ?? \"test\";\n const result = await dispose({ root: args.root, mode });\n if (result.dropped) {\n process.stdout.write(`${CLI_NAME}: disposed ${mode} (${result.databaseName})\\n`);\n return 0;\n }\n if (result.reason === \"no-lease\") {\n process.stdout.write(`${CLI_NAME}: nothing to dispose for ${mode} (no lease)\\n`);\n return 0;\n }\n process.stderr.write(\n `${CLI_NAME}: could not dispose ${mode}: autopg host unavailable (lease kept for retry)\\n`,\n );\n return 1;\n }\n\n if (args.cmd === \"gc\") {\n const result = await gc();\n if (args.json) {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n } else {\n process.stdout.write(`${CLI_NAME} gc: dropped ${result.dropped.length} database(s)\\n`);\n for (const name of result.dropped) process.stdout.write(` ${name}\\n`);\n }\n return 0;\n }\n\n if (args.cmd === \"print-url\") {\n const mode = args.mode ?? \"dev\";\n const identity = resolveWorktreeIdentity(args.root);\n const lease = readLease(identity.root, mode);\n if (!lease) {\n process.stderr.write(\n `${CLI_NAME}: no ${mode} lease; run \\`${CLI_NAME} acquire --mode=${mode}\\` first\\n`,\n );\n return 2;\n }\n process.stdout.write(`${urlFromLease(lease)}\\n`);\n return 0;\n }\n\n process.stderr.write(`${CLI_NAME}: unknown command ${args.cmd}\\n`);\n printHelp();\n return 64;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`${CLI_NAME}: ${message}\\n`);\n return 1;\n }\n}\n\nmain()\n .then((code) => {\n process.exit(code);\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`${CLI_NAME}: ${message}\\n`);\n process.exit(1);\n });\n"],"mappings":"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"}