@cedarjs/pg 0.1.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/cli.cjs +139 -0
  4. package/dist/cli.cjs.map +1 -0
  5. package/dist/cli.d.cts +1 -0
  6. package/dist/cli.d.mts +1 -0
  7. package/dist/cli.mjs +140 -0
  8. package/dist/cli.mjs.map +1 -0
  9. package/dist/constants-B7sCf4fg.cjs +41 -0
  10. package/dist/constants-B7sCf4fg.cjs.map +1 -0
  11. package/dist/constants-YnHDwShD.mjs +24 -0
  12. package/dist/constants-YnHDwShD.mjs.map +1 -0
  13. package/dist/index.cjs +26 -0
  14. package/dist/index.d.cts +201 -0
  15. package/dist/index.d.mts +201 -0
  16. package/dist/index.mjs +2 -0
  17. package/dist/jest.cjs +28 -0
  18. package/dist/jest.cjs.map +1 -0
  19. package/dist/jest.d.cts +14 -0
  20. package/dist/jest.d.mts +14 -0
  21. package/dist/jest.mjs +23 -0
  22. package/dist/jest.mjs.map +1 -0
  23. package/dist/lifecycle-D10usEjM.cjs +743 -0
  24. package/dist/lifecycle-D10usEjM.cjs.map +1 -0
  25. package/dist/lifecycle-PbTXNC2Z.mjs +577 -0
  26. package/dist/lifecycle-PbTXNC2Z.mjs.map +1 -0
  27. package/dist/nx.cjs +35 -0
  28. package/dist/nx.cjs.map +1 -0
  29. package/dist/nx.d.cts +26 -0
  30. package/dist/nx.d.mts +26 -0
  31. package/dist/nx.mjs +34 -0
  32. package/dist/nx.mjs.map +1 -0
  33. package/dist/tasks-CW2D0n46.mjs +16 -0
  34. package/dist/tasks-CW2D0n46.mjs.map +1 -0
  35. package/dist/tasks-DZ7hMmGJ.cjs +21 -0
  36. package/dist/tasks-DZ7hMmGJ.cjs.map +1 -0
  37. package/dist/vite-plus.cjs +65 -0
  38. package/dist/vite-plus.cjs.map +1 -0
  39. package/dist/vite-plus.d.cts +51 -0
  40. package/dist/vite-plus.d.mts +51 -0
  41. package/dist/vite-plus.mjs +61 -0
  42. package/dist/vite-plus.mjs.map +1 -0
  43. package/dist/vitest.cjs +33 -0
  44. package/dist/vitest.cjs.map +1 -0
  45. package/dist/vitest.d.cts +17 -0
  46. package/dist/vitest.d.mts +17 -0
  47. package/dist/vitest.mjs +28 -0
  48. package/dist/vitest.mjs.map +1 -0
  49. package/package.json +96 -0
  50. package/scripts/postinstall.js +61 -0
@@ -0,0 +1,743 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ const require_constants = require("./constants-B7sCf4fg.cjs");
24
+ let node_fs = require("node:fs");
25
+ let node_path = require("node:path");
26
+ let node_os = require("node:os");
27
+ let node_crypto = require("node:crypto");
28
+ let node_child_process = require("node:child_process");
29
+ let pg = require("pg");
30
+ pg = __toESM(pg, 1);
31
+ //#region src/core/naming.ts
32
+ const PG_MAX = 63;
33
+ const PREFIX = "cpg_";
34
+ /**
35
+ * Build an observable Postgres database name:
36
+ * cpg_<repoSlug>_<worktreeSlug>_<mode>_<pathHash8>
37
+ *
38
+ * Never drops `mode` or `pathHash`. Truncates slugs to fit ≤63 chars.
39
+ */
40
+ function buildDatabaseName(identity, mode) {
41
+ const modePart = mode;
42
+ const hashPart = identity.pathHash;
43
+ const fixed = 6 + modePart.length + 1 + hashPart.length;
44
+ let budget = PG_MAX - fixed;
45
+ if (budget < 2) return `${PREFIX}${modePart}_${hashPart}`.slice(0, PG_MAX);
46
+ let repo = identity.repoSlug;
47
+ let wt = identity.worktreeSlug;
48
+ while (repo.length + wt.length > budget) if (repo.length >= wt.length && repo.length > 1) repo = repo.slice(0, -1);
49
+ else if (wt.length > 1) wt = wt.slice(0, -1);
50
+ else break;
51
+ const name = `${PREFIX}${repo}_${wt}_${modePart}_${hashPart}`;
52
+ if (name.length > PG_MAX) return name.slice(0, PG_MAX);
53
+ return name;
54
+ }
55
+ function buildRoleName(databaseName) {
56
+ const suffix = "_role";
57
+ const maxBase = PG_MAX - 5;
58
+ return `${databaseName.slice(0, maxBase)}${suffix}`;
59
+ }
60
+ //#endregion
61
+ //#region src/core/lease.ts
62
+ function isDbMode(value) {
63
+ return value === "dev" || value === "test";
64
+ }
65
+ function isNonEmptyString(value) {
66
+ return typeof value === "string" && value.length > 0;
67
+ }
68
+ /** Narrow-parse a lease; reject incomplete or wrong-version records. */
69
+ function parseLease(raw) {
70
+ if (!raw || typeof raw !== "object") return null;
71
+ const o = raw;
72
+ if (o.schemaVersion !== 1) return null;
73
+ if (!isDbMode(o.mode)) return null;
74
+ if (!isNonEmptyString(o.root)) return null;
75
+ if (!isNonEmptyString(o.repoSlug)) return null;
76
+ if (!isNonEmptyString(o.worktreeSlug)) return null;
77
+ if (!isNonEmptyString(o.pathHash)) return null;
78
+ if (!isNonEmptyString(o.databaseName)) return null;
79
+ if (!isNonEmptyString(o.roleName)) return null;
80
+ if (typeof o.port !== "number") return null;
81
+ if (typeof o.pid !== "number") return null;
82
+ if (!isNonEmptyString(o.createdAt)) return null;
83
+ return {
84
+ schemaVersion: 1,
85
+ mode: o.mode,
86
+ root: o.root,
87
+ repoSlug: o.repoSlug,
88
+ worktreeSlug: o.worktreeSlug,
89
+ pathHash: o.pathHash,
90
+ databaseName: o.databaseName,
91
+ roleName: o.roleName,
92
+ port: o.port,
93
+ pid: o.pid,
94
+ createdAt: o.createdAt
95
+ };
96
+ }
97
+ function leaseDir(root) {
98
+ return (0, node_path.join)(root, require_constants.STATE_DIRNAME);
99
+ }
100
+ function leasePath(root, mode) {
101
+ return (0, node_path.join)(leaseDir(root), `${mode}.json`);
102
+ }
103
+ /** Durable registry outside worktrees so GC can find orphans after a checkout is deleted. */
104
+ function registryDir() {
105
+ return process.env.CEDAR_PG_REGISTRY_DIR || (0, node_path.join)((0, node_os.homedir)(), ".cedarpg", "registry");
106
+ }
107
+ function registryPath(databaseName) {
108
+ return (0, node_path.join)(registryDir(), `${databaseName}.json`);
109
+ }
110
+ function writeJsonAtomic(file, value) {
111
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(file), {
112
+ recursive: true,
113
+ mode: 448
114
+ });
115
+ const tmp = `${file}.tmp.${process.pid}`;
116
+ (0, node_fs.writeFileSync)(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
117
+ (0, node_fs.renameSync)(tmp, file);
118
+ }
119
+ function registerLease(lease) {
120
+ writeJsonAtomic(registryPath(lease.databaseName), lease);
121
+ }
122
+ function unregisterLease(databaseName) {
123
+ try {
124
+ (0, node_fs.unlinkSync)(registryPath(databaseName));
125
+ } catch {}
126
+ }
127
+ function readLease(root, mode) {
128
+ const file = leasePath(root, mode);
129
+ if (!(0, node_fs.existsSync)(file)) return null;
130
+ try {
131
+ return parseLease(JSON.parse((0, node_fs.readFileSync)(file, "utf8")));
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ /**
137
+ * Persist lease. Registry is written first (GC source of truth), then the worktree cache.
138
+ */
139
+ function writeLease(lease) {
140
+ registerLease(lease);
141
+ (0, node_fs.mkdirSync)(leaseDir(lease.root), {
142
+ recursive: true,
143
+ mode: 448
144
+ });
145
+ writeJsonAtomic(leasePath(lease.root, lease.mode), lease);
146
+ }
147
+ /**
148
+ * Remove registry entry by databaseName and best-effort delete the worktree lease file.
149
+ * Call only after a successful DROP (or when intentionally abandoning tracking).
150
+ */
151
+ function forgetLease(lease) {
152
+ unregisterLease(lease.databaseName);
153
+ try {
154
+ (0, node_fs.unlinkSync)(leasePath(lease.root, lease.mode));
155
+ } catch {}
156
+ }
157
+ /** All registry entries (used by gc). */
158
+ function listRegistryLeases() {
159
+ const dir = registryDir();
160
+ if (!(0, node_fs.existsSync)(dir)) return [];
161
+ const out = [];
162
+ for (const name of (0, node_fs.readdirSync)(dir)) {
163
+ if (!name.endsWith(".json")) continue;
164
+ try {
165
+ const lease = parseLease(JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(dir, name), "utf8")));
166
+ if (lease) out.push(lease);
167
+ } catch {}
168
+ }
169
+ return out;
170
+ }
171
+ function isOrphanLease(lease) {
172
+ return !(0, node_fs.existsSync)(lease.root);
173
+ }
174
+ //#endregion
175
+ //#region src/core/policy.ts
176
+ /**
177
+ * True when the URL looks like a cedarpg provisioned database (`cpg_*` name/role).
178
+ * These must never be treated as an external escape hatch — always re-ensure so
179
+ * disposed/stale shell env cannot skip provisioning.
180
+ */
181
+ function isCedarPgManagedUrl(url) {
182
+ if (!url || url.startsWith("file:")) return false;
183
+ try {
184
+ const parsed = new URL(url);
185
+ const databaseName = decodeURIComponent((parsed.pathname.replace(/^\//, "").split("?")[0] ?? "").trim());
186
+ const user = decodeURIComponent(parsed.username);
187
+ return databaseName.startsWith("cpg_") || user.startsWith("cpg_");
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ /**
193
+ * True when `url` is a real external database and ensure should be skipped.
194
+ * Sqlite `file:` URLs and cedarpg `cpg_*` URLs are not external.
195
+ */
196
+ function isExternalDatabaseEscapeHatch(url) {
197
+ if (!url) return false;
198
+ if (url.startsWith("file:")) return false;
199
+ if (isCedarPgManagedUrl(url)) return false;
200
+ return true;
201
+ }
202
+ /**
203
+ * Decide whether ensure should run.
204
+ * Adapters may call with no args (env defaults). Cedar opt-in should pass `disabled: false`
205
+ * and the relevant `url` (`DATABASE_URL` or `TEST_DATABASE_URL`).
206
+ */
207
+ function resolveEnsureSkip(input = {}) {
208
+ if (input.disabled ?? (process.env.CEDAR_PG === "0" || process.env.CEDAR_PG === "false")) return {
209
+ skip: true,
210
+ reason: "disabled"
211
+ };
212
+ if (input.force ?? process.env.CEDAR_PG_FORCE === "1") return { skip: false };
213
+ const url = input.url !== void 0 ? input.url : process.env.TEST_DATABASE_URL;
214
+ if (url && isExternalDatabaseEscapeHatch(url)) return {
215
+ skip: true,
216
+ reason: "external-url",
217
+ databaseUrl: url
218
+ };
219
+ return { skip: false };
220
+ }
221
+ //#endregion
222
+ //#region src/core/worktree.ts
223
+ function git(args, cwd) {
224
+ try {
225
+ return (0, node_child_process.execFileSync)("git", args, {
226
+ cwd,
227
+ encoding: "utf8",
228
+ stdio: [
229
+ "ignore",
230
+ "pipe",
231
+ "ignore"
232
+ ]
233
+ }).trim();
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ function sanitizeSlug(input) {
239
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
240
+ }
241
+ /**
242
+ * Resolve the project / worktree root.
243
+ * Explicit `root` wins (app project path). Otherwise prefer git toplevel, then cwd.
244
+ */
245
+ function resolveRoot(root) {
246
+ if (root) return (0, node_path.resolve)(root);
247
+ const start = (0, node_path.resolve)(process.cwd());
248
+ const toplevel = git(["rev-parse", "--show-toplevel"], start);
249
+ if (toplevel) return toplevel;
250
+ return start;
251
+ }
252
+ /**
253
+ * Identify repo + worktree for observable DB naming.
254
+ */
255
+ function resolveWorktreeIdentity(root) {
256
+ const absRoot = resolveRoot(root);
257
+ const pathHash = (0, node_crypto.createHash)("sha256").update(absRoot).digest("hex").slice(0, 8);
258
+ const commonDir = git(["rev-parse", "--git-common-dir"], absRoot);
259
+ let repoSlug = sanitizeSlug((0, node_path.basename)(absRoot));
260
+ if (commonDir) {
261
+ const commonAbs = (0, node_path.resolve)(absRoot, commonDir);
262
+ const mainRoot = (0, node_path.basename)(commonAbs) === ".git" ? (0, node_path.dirname)(commonAbs) : commonAbs;
263
+ const fromGit = sanitizeSlug((0, node_path.basename)((0, node_path.basename)(mainRoot) === ".git" ? (0, node_path.dirname)(mainRoot) : mainRoot));
264
+ if (fromGit) repoSlug = fromGit;
265
+ }
266
+ let worktreeSlug = sanitizeSlug((0, node_path.basename)(absRoot));
267
+ const gitFile = (0, node_path.resolve)(absRoot, ".git");
268
+ if ((0, node_fs.existsSync)(gitFile)) try {
269
+ const match = (0, node_fs.readFileSync)(gitFile, "utf8").trim().match(/gitdir:\s*(.+)$/m);
270
+ if (match?.[1]) {
271
+ const parts = (0, node_path.resolve)(absRoot, match[1].trim()).split(/[/\\]/);
272
+ const wtIdx = parts.lastIndexOf("worktrees");
273
+ if (wtIdx >= 0 && parts[wtIdx + 1]) {
274
+ const fromWt = sanitizeSlug(parts[wtIdx + 1]);
275
+ if (fromWt) worktreeSlug = fromWt;
276
+ }
277
+ }
278
+ } catch {}
279
+ if (!repoSlug) repoSlug = "repo";
280
+ if (!worktreeSlug) worktreeSlug = "wt";
281
+ return {
282
+ root: absRoot,
283
+ repoSlug,
284
+ worktreeSlug,
285
+ pathHash
286
+ };
287
+ }
288
+ //#endregion
289
+ //#region src/providers/autopg.ts
290
+ const INSTALL_HINT = "autopg is required. Install with:\n curl -fsSL https://raw.githubusercontent.com/automagik-dev/autopg/main/install.sh | bash\nThen ensure ~/.local/bin is on PATH, or set AUTOPG_BIN.";
291
+ /** Password scheme v1: sha256(PASSWORD_SALT_PREFIX + "\\0" + databaseName) hex[:32]. Frozen for URL rebuild. */
292
+ const ROLE_PASSWORD_SCHEME = "v1";
293
+ function candidateBins() {
294
+ const out = [];
295
+ if (process.env.AUTOPG_BIN) out.push(process.env.AUTOPG_BIN);
296
+ out.push("autopg");
297
+ out.push((0, node_path.join)((0, node_os.homedir)(), ".local", "bin", "autopg"));
298
+ return out;
299
+ }
300
+ function resolveAutopgBin() {
301
+ for (const bin of candidateBins()) {
302
+ if (bin === "autopg") {
303
+ const which = (0, node_child_process.spawnSync)("which", ["autopg"], { encoding: "utf8" });
304
+ if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
305
+ continue;
306
+ }
307
+ if ((0, node_fs.existsSync)(bin)) return bin;
308
+ }
309
+ return null;
310
+ }
311
+ function requireAutopgBin() {
312
+ const bin = resolveAutopgBin();
313
+ if (!bin) throw new Error(INSTALL_HINT);
314
+ return bin;
315
+ }
316
+ /**
317
+ * Parse `autopg status --json` output. Requires a numeric port and running !== false.
318
+ */
319
+ function parseHostStatus(json) {
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(json);
323
+ } catch {
324
+ throw new Error(`autopg status --json returned invalid JSON.\n${INSTALL_HINT}`);
325
+ }
326
+ if (typeof parsed.port !== "number") throw new Error(`autopg status --json missing numeric port.\n${INSTALL_HINT}`);
327
+ if (parsed.running === false) throw new Error(`autopg host is not running.\n${INSTALL_HINT}`);
328
+ return { port: parsed.port };
329
+ }
330
+ function adminUrlFor(port) {
331
+ return `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`;
332
+ }
333
+ /**
334
+ * Discover a live autopg host via `autopg status --json`. Throws if the host is not proven live.
335
+ */
336
+ function discoverHost(bin = requireAutopgBin()) {
337
+ let status;
338
+ try {
339
+ status = (0, node_child_process.execFileSync)(bin, ["status", "--json"], {
340
+ encoding: "utf8",
341
+ stdio: [
342
+ "ignore",
343
+ "pipe",
344
+ "pipe"
345
+ ]
346
+ });
347
+ } catch (err) {
348
+ const detail = err instanceof Error ? err.message : String(err);
349
+ throw new Error(`Failed to query autopg status.\n${detail}\n${INSTALL_HINT}`);
350
+ }
351
+ const { port } = parseHostStatus(status);
352
+ return {
353
+ port,
354
+ adminUrl: adminUrlFor(port),
355
+ bin
356
+ };
357
+ }
358
+ /**
359
+ * Ensure the host postmaster is up. Runs `autopg install` only after status fails, then re-discovers.
360
+ */
361
+ function ensureHostRunning(bin = requireAutopgBin()) {
362
+ try {
363
+ return discoverHost(bin);
364
+ } catch {}
365
+ const install = (0, node_child_process.spawnSync)(bin, ["install"], {
366
+ encoding: "utf8",
367
+ stdio: [
368
+ "ignore",
369
+ "pipe",
370
+ "pipe"
371
+ ]
372
+ });
373
+ if (install.status !== 0) throw new Error(`Failed to start autopg host (exit ${install.status}).\n${install.stderr || install.stdout || ""}\n${INSTALL_HINT}`);
374
+ return discoverHost(bin);
375
+ }
376
+ function quoteIdent(name) {
377
+ return `"${name.replace(/"/g, "\"\"")}"`;
378
+ }
379
+ function quoteLiteral(value) {
380
+ return `'${value.replace(/'/g, "''")}'`;
381
+ }
382
+ /**
383
+ * Deterministic local-only password for an app role (Prisma/TCP need it;
384
+ * autopg hba uses `password` for 127.0.0.1).
385
+ */
386
+ function rolePasswordFor(databaseName) {
387
+ return (0, node_crypto.createHash)("sha256").update(`${require_constants.PASSWORD_SALT_PREFIX}\0${databaseName}`).digest("hex").slice(0, 32);
388
+ }
389
+ /**
390
+ * Idempotently CREATE ROLE + CREATE DATABASE with cedar-pg owned names.
391
+ */
392
+ async function ensureDatabase(opts) {
393
+ const password = opts.password ?? rolePasswordFor(opts.databaseName);
394
+ const client = new pg.default.Client({ connectionString: opts.adminUrl });
395
+ await client.connect();
396
+ try {
397
+ if ((await client.query("SELECT 1 FROM pg_roles WHERE rolname = $1", [opts.roleName])).rowCount === 0) await client.query(`CREATE ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`);
398
+ else await client.query(`ALTER ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`);
399
+ if ((await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [opts.databaseName])).rowCount === 0) await client.query(`CREATE DATABASE ${quoteIdent(opts.databaseName)} OWNER ${quoteIdent(opts.roleName)}`);
400
+ else await client.query(`ALTER DATABASE ${quoteIdent(opts.databaseName)} OWNER TO ${quoteIdent(opts.roleName)}`);
401
+ } finally {
402
+ await client.end();
403
+ }
404
+ }
405
+ /**
406
+ * DROP DATABASE (force terminate backends) + DROP ROLE.
407
+ */
408
+ async function dropDatabase(opts) {
409
+ const client = new pg.default.Client({ connectionString: opts.adminUrl });
410
+ await client.connect();
411
+ try {
412
+ await client.query(`
413
+ SELECT pg_terminate_backend(pid)
414
+ FROM pg_stat_activity
415
+ WHERE datname = $1 AND pid <> pg_backend_pid()
416
+ `, [opts.databaseName]);
417
+ await client.query(`DROP DATABASE IF EXISTS ${quoteIdent(opts.databaseName)}`);
418
+ if ((await client.query(`SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1) LIMIT 1`, [opts.roleName])).rowCount === 0) await client.query(`DROP ROLE IF EXISTS ${quoteIdent(opts.roleName)}`);
419
+ } finally {
420
+ await client.end();
421
+ }
422
+ }
423
+ function buildDatabaseUrl(opts) {
424
+ const password = opts.password ?? rolePasswordFor(opts.databaseName);
425
+ return `postgresql://${encodeURIComponent(opts.roleName)}:${encodeURIComponent(password)}@127.0.0.1:${opts.port}/${opts.databaseName}`;
426
+ }
427
+ //#endregion
428
+ //#region src/core/lifecycle.ts
429
+ function writeEnvFile(root, mode, databaseUrl) {
430
+ const dir = leaseDir(root);
431
+ (0, node_fs.mkdirSync)(dir, {
432
+ recursive: true,
433
+ mode: 448
434
+ });
435
+ let body = `DATABASE_URL=${databaseUrl}\n`;
436
+ if (mode === "test") body += `TEST_DATABASE_URL=${databaseUrl}\n`;
437
+ (0, node_fs.writeFileSync)((0, node_path.join)(dir, `${mode}.env`), body, { mode: 384 });
438
+ }
439
+ function urlFromLease(lease) {
440
+ return buildDatabaseUrl({
441
+ port: lease.port,
442
+ databaseName: lease.databaseName,
443
+ roleName: lease.roleName
444
+ });
445
+ }
446
+ /**
447
+ * DROP the database, then forget lease/registry. Never forget without a successful DROP.
448
+ */
449
+ async function dropThenForget(lease, adminUrl) {
450
+ await dropDatabase({
451
+ adminUrl,
452
+ databaseName: lease.databaseName,
453
+ roleName: lease.roleName
454
+ });
455
+ forgetLease(lease);
456
+ }
457
+ /**
458
+ * Ensure a worktree-scoped database exists and return connection info.
459
+ *
460
+ * - `dev`: keep DB across restarts.
461
+ * - `test`: DROP when `dispose()` is awaited (callers / test runners own teardown).
462
+ */
463
+ async function ensure(options) {
464
+ const identity = resolveWorktreeIdentity(options.root);
465
+ const mode = options.mode;
466
+ const databaseName = buildDatabaseName(identity, mode);
467
+ const roleName = buildRoleName(databaseName);
468
+ const host = ensureHostRunning();
469
+ await ensureDatabase({
470
+ adminUrl: host.adminUrl,
471
+ databaseName,
472
+ roleName
473
+ });
474
+ const databaseUrl = buildDatabaseUrl({
475
+ port: host.port,
476
+ databaseName,
477
+ roleName
478
+ });
479
+ writeLease({
480
+ schemaVersion: 1,
481
+ mode,
482
+ root: identity.root,
483
+ repoSlug: identity.repoSlug,
484
+ worktreeSlug: identity.worktreeSlug,
485
+ pathHash: identity.pathHash,
486
+ databaseName,
487
+ roleName,
488
+ port: host.port,
489
+ pid: process.pid,
490
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
491
+ });
492
+ writeEnvFile(identity.root, mode, databaseUrl);
493
+ if (options.setEnv !== false) {
494
+ process.env.DATABASE_URL = databaseUrl;
495
+ if (mode === "test") process.env.TEST_DATABASE_URL = databaseUrl;
496
+ }
497
+ const disposeFn = async () => {
498
+ await dispose({
499
+ root: identity.root,
500
+ mode
501
+ });
502
+ };
503
+ return {
504
+ databaseUrl,
505
+ databaseName,
506
+ roleName,
507
+ repoSlug: identity.repoSlug,
508
+ worktreeSlug: identity.worktreeSlug,
509
+ pathHash: identity.pathHash,
510
+ root: identity.root,
511
+ mode,
512
+ port: host.port,
513
+ dispose: disposeFn
514
+ };
515
+ }
516
+ /**
517
+ * Resolve skip policy then ensure. Single entry for hosts (Cedar CLI, Jest, Vitest).
518
+ * On external-url skip, sets DATABASE_URL when `setEnv` is not false.
519
+ */
520
+ async function ensureIfNeeded(options) {
521
+ const skip = resolveEnsureSkip({
522
+ url: options.url,
523
+ force: options.force,
524
+ disabled: options.disabled
525
+ });
526
+ if (skip.skip) {
527
+ if (skip.reason === "external-url") {
528
+ if (options.setEnv !== false) process.env.DATABASE_URL = skip.databaseUrl;
529
+ return {
530
+ status: "skipped",
531
+ reason: "external-url",
532
+ databaseUrl: skip.databaseUrl
533
+ };
534
+ }
535
+ return {
536
+ status: "skipped",
537
+ reason: "disabled"
538
+ };
539
+ }
540
+ return {
541
+ status: "ensured",
542
+ ...await ensure({
543
+ root: options.root,
544
+ mode: options.mode,
545
+ setEnv: options.setEnv
546
+ })
547
+ };
548
+ }
549
+ /**
550
+ * DROP the worktree database for the given mode (default: test).
551
+ * No-ops without a valid lease (never invents a name to DROP).
552
+ * If the host is unavailable, leaves the lease so dispose/gc can retry.
553
+ */
554
+ async function dispose(options = {}) {
555
+ const identity = resolveWorktreeIdentity(options.root);
556
+ const mode = options.mode ?? "test";
557
+ const lease = readLease(identity.root, mode);
558
+ if (!lease) return {
559
+ dropped: false,
560
+ reason: "no-lease"
561
+ };
562
+ let host;
563
+ try {
564
+ host = ensureHostRunning();
565
+ } catch {
566
+ return {
567
+ dropped: false,
568
+ reason: "host-unavailable"
569
+ };
570
+ }
571
+ await dropThenForget(lease, host.adminUrl);
572
+ return {
573
+ dropped: true,
574
+ databaseName: lease.databaseName
575
+ };
576
+ }
577
+ /**
578
+ * Drop databases whose registered worktree root no longer exists on disk.
579
+ * Registry entries are removed only after a successful DROP.
580
+ */
581
+ async function gc() {
582
+ const dropped = [];
583
+ const orphans = listRegistryLeases().filter(isOrphanLease);
584
+ if (orphans.length === 0) return { dropped };
585
+ let host;
586
+ try {
587
+ host = ensureHostRunning();
588
+ } catch {
589
+ return { dropped };
590
+ }
591
+ for (const lease of orphans) try {
592
+ await dropThenForget(lease, host.adminUrl);
593
+ dropped.push(lease.databaseName);
594
+ } catch {}
595
+ return { dropped };
596
+ }
597
+ //#endregion
598
+ Object.defineProperty(exports, "INSTALL_HINT", {
599
+ enumerable: true,
600
+ get: function() {
601
+ return INSTALL_HINT;
602
+ }
603
+ });
604
+ Object.defineProperty(exports, "ROLE_PASSWORD_SCHEME", {
605
+ enumerable: true,
606
+ get: function() {
607
+ return ROLE_PASSWORD_SCHEME;
608
+ }
609
+ });
610
+ Object.defineProperty(exports, "buildDatabaseName", {
611
+ enumerable: true,
612
+ get: function() {
613
+ return buildDatabaseName;
614
+ }
615
+ });
616
+ Object.defineProperty(exports, "buildDatabaseUrl", {
617
+ enumerable: true,
618
+ get: function() {
619
+ return buildDatabaseUrl;
620
+ }
621
+ });
622
+ Object.defineProperty(exports, "buildRoleName", {
623
+ enumerable: true,
624
+ get: function() {
625
+ return buildRoleName;
626
+ }
627
+ });
628
+ Object.defineProperty(exports, "discoverHost", {
629
+ enumerable: true,
630
+ get: function() {
631
+ return discoverHost;
632
+ }
633
+ });
634
+ Object.defineProperty(exports, "dispose", {
635
+ enumerable: true,
636
+ get: function() {
637
+ return dispose;
638
+ }
639
+ });
640
+ Object.defineProperty(exports, "ensure", {
641
+ enumerable: true,
642
+ get: function() {
643
+ return ensure;
644
+ }
645
+ });
646
+ Object.defineProperty(exports, "ensureHostRunning", {
647
+ enumerable: true,
648
+ get: function() {
649
+ return ensureHostRunning;
650
+ }
651
+ });
652
+ Object.defineProperty(exports, "ensureIfNeeded", {
653
+ enumerable: true,
654
+ get: function() {
655
+ return ensureIfNeeded;
656
+ }
657
+ });
658
+ Object.defineProperty(exports, "gc", {
659
+ enumerable: true,
660
+ get: function() {
661
+ return gc;
662
+ }
663
+ });
664
+ Object.defineProperty(exports, "isCedarPgManagedUrl", {
665
+ enumerable: true,
666
+ get: function() {
667
+ return isCedarPgManagedUrl;
668
+ }
669
+ });
670
+ Object.defineProperty(exports, "isExternalDatabaseEscapeHatch", {
671
+ enumerable: true,
672
+ get: function() {
673
+ return isExternalDatabaseEscapeHatch;
674
+ }
675
+ });
676
+ Object.defineProperty(exports, "isOrphanLease", {
677
+ enumerable: true,
678
+ get: function() {
679
+ return isOrphanLease;
680
+ }
681
+ });
682
+ Object.defineProperty(exports, "parseHostStatus", {
683
+ enumerable: true,
684
+ get: function() {
685
+ return parseHostStatus;
686
+ }
687
+ });
688
+ Object.defineProperty(exports, "parseLease", {
689
+ enumerable: true,
690
+ get: function() {
691
+ return parseLease;
692
+ }
693
+ });
694
+ Object.defineProperty(exports, "readLease", {
695
+ enumerable: true,
696
+ get: function() {
697
+ return readLease;
698
+ }
699
+ });
700
+ Object.defineProperty(exports, "requireAutopgBin", {
701
+ enumerable: true,
702
+ get: function() {
703
+ return requireAutopgBin;
704
+ }
705
+ });
706
+ Object.defineProperty(exports, "resolveAutopgBin", {
707
+ enumerable: true,
708
+ get: function() {
709
+ return resolveAutopgBin;
710
+ }
711
+ });
712
+ Object.defineProperty(exports, "resolveEnsureSkip", {
713
+ enumerable: true,
714
+ get: function() {
715
+ return resolveEnsureSkip;
716
+ }
717
+ });
718
+ Object.defineProperty(exports, "resolveRoot", {
719
+ enumerable: true,
720
+ get: function() {
721
+ return resolveRoot;
722
+ }
723
+ });
724
+ Object.defineProperty(exports, "resolveWorktreeIdentity", {
725
+ enumerable: true,
726
+ get: function() {
727
+ return resolveWorktreeIdentity;
728
+ }
729
+ });
730
+ Object.defineProperty(exports, "rolePasswordFor", {
731
+ enumerable: true,
732
+ get: function() {
733
+ return rolePasswordFor;
734
+ }
735
+ });
736
+ Object.defineProperty(exports, "urlFromLease", {
737
+ enumerable: true,
738
+ get: function() {
739
+ return urlFromLease;
740
+ }
741
+ });
742
+
743
+ //# sourceMappingURL=lifecycle-D10usEjM.cjs.map