@cedarjs/pg 0.2.0-beta.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.
- package/README.md +112 -63
- package/dist/cli.cjs +15 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +15 -9
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +54 -11
- package/dist/index.d.mts +54 -11
- package/dist/index.mjs +3 -3
- package/dist/jest-teardown.cjs +1 -1
- package/dist/jest-teardown.mjs +1 -1
- package/dist/jest-template.cjs +5 -3
- package/dist/jest-template.cjs.map +1 -1
- package/dist/jest-template.d.cts +5 -3
- package/dist/jest-template.d.mts +5 -3
- package/dist/jest-template.mjs +5 -3
- package/dist/jest-template.mjs.map +1 -1
- package/dist/jest.cjs +1 -1
- package/dist/jest.mjs +1 -1
- package/dist/{lifecycle-BvIx0xqq.mjs → lifecycle-2BZgSVz2.mjs} +423 -132
- package/dist/lifecycle-2BZgSVz2.mjs.map +1 -0
- package/dist/{lifecycle-BbrvFQvg.cjs → lifecycle-BCeM96tI.cjs} +438 -135
- package/dist/lifecycle-BCeM96tI.cjs.map +1 -0
- package/dist/{naming-C2nGVxPk.d.cts → naming-Df4fz_Fb.d.cts} +3 -2
- package/dist/{naming-C2nGVxPk.d.mts → naming-Df4fz_Fb.d.mts} +3 -2
- package/dist/{status-Dcnpgnlz.mjs → status-B68SGSKx.mjs} +2 -2
- package/dist/{status-Dcnpgnlz.mjs.map → status-B68SGSKx.mjs.map} +1 -1
- package/dist/{status-4Uz6A3LB.cjs → status-Z_BYdxzI.cjs} +2 -2
- package/dist/{status-4Uz6A3LB.cjs.map → status-Z_BYdxzI.cjs.map} +1 -1
- package/dist/{studio-DIFw1qWC.mjs → studio-Bi6SU9jE.mjs} +54 -6
- package/dist/studio-Bi6SU9jE.mjs.map +1 -0
- package/dist/{studio-oJE30_PO.cjs → studio-CdWIdnxr.cjs} +78 -6
- package/dist/studio-CdWIdnxr.cjs.map +1 -0
- package/dist/{template-CXMpODzK.cjs → template-Cd77H7Cr.cjs} +21 -18
- package/dist/template-Cd77H7Cr.cjs.map +1 -0
- package/dist/{template-BiJ-0TIF.mjs → template-DlqHL8Go.mjs} +21 -18
- package/dist/template-DlqHL8Go.mjs.map +1 -0
- package/dist/template-mode-BqV3GjXI.d.cts +48 -0
- package/dist/template-mode-BqV3GjXI.d.mts +48 -0
- package/dist/template-mode-DK5cAlv9.mjs +81 -0
- package/dist/template-mode-DK5cAlv9.mjs.map +1 -0
- package/dist/template-mode-DMO5-z7O.cjs +92 -0
- package/dist/template-mode-DMO5-z7O.cjs.map +1 -0
- package/dist/vite-plus.cjs +9 -9
- package/dist/vite-plus.cjs.map +1 -1
- package/dist/vite-plus.d.cts +17 -2
- package/dist/vite-plus.d.mts +17 -2
- package/dist/vite-plus.mjs +5 -5
- package/dist/vite-plus.mjs.map +1 -1
- package/dist/vitest-template.cjs +6 -3
- package/dist/vitest-template.cjs.map +1 -1
- package/dist/vitest-template.d.cts +6 -3
- package/dist/vitest-template.d.mts +6 -3
- package/dist/vitest-template.mjs +6 -3
- package/dist/vitest-template.mjs.map +1 -1
- package/dist/vitest.cjs +1 -1
- package/dist/vitest.mjs +1 -1
- package/package.json +2 -7
- package/scripts/autopg-version +1 -1
- package/scripts/ci-install-autopg.sh +7 -2
- package/scripts/postinstall.js +41 -13
- package/dist/lease-B3TuX92y.d.mts +0 -24
- package/dist/lease-t4I9JahV.d.cts +0 -24
- package/dist/lifecycle-BbrvFQvg.cjs.map +0 -1
- package/dist/lifecycle-BvIx0xqq.mjs.map +0 -1
- package/dist/nx.cjs +0 -42
- package/dist/nx.cjs.map +0 -1
- package/dist/nx.d.cts +0 -15
- package/dist/nx.d.mts +0 -15
- package/dist/nx.mjs +0 -34
- package/dist/nx.mjs.map +0 -1
- package/dist/studio-DIFw1qWC.mjs.map +0 -1
- package/dist/studio-oJE30_PO.cjs.map +0 -1
- package/dist/tasks-CNHvvlsN.cjs +0 -67
- package/dist/tasks-CNHvvlsN.cjs.map +0 -1
- package/dist/tasks-CjaZRi_G.d.mts +0 -19
- package/dist/tasks-D5iWIdlo.mjs +0 -38
- package/dist/tasks-D5iWIdlo.mjs.map +0 -1
- package/dist/tasks-DdQoP9We.d.cts +0 -19
- package/dist/template-BiJ-0TIF.mjs.map +0 -1
- package/dist/template-CXMpODzK.cjs.map +0 -1
- package/dist/template-mode-BTUsdBtA.mjs +0 -94
- package/dist/template-mode-BTUsdBtA.mjs.map +0 -1
- package/dist/template-mode-CzyVwHsi.d.cts +0 -31
- package/dist/template-mode-CzyVwHsi.d.mts +0 -31
- package/dist/template-mode-JTOxrEgE.cjs +0 -105
- package/dist/template-mode-JTOxrEgE.cjs.map +0 -1
package/dist/vitest-template.cjs
CHANGED
|
@@ -2,10 +2,12 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_template_mode = require("./template-mode-
|
|
5
|
+
const require_template_mode = require("./template-mode-DMO5-z7O.cjs");
|
|
6
6
|
//#region src/adapters/vitest-template.ts
|
|
7
7
|
/**
|
|
8
|
-
* Vitest globalSetup (template mode).
|
|
8
|
+
* Vitest globalSetup (template mode). Drops crashed-run leftovers under this
|
|
9
|
+
* worktree's test role, then acquire → migrate → markTemplate. Returns teardown
|
|
10
|
+
* that disposes TEMPLATE + clones.
|
|
9
11
|
*
|
|
10
12
|
* ```ts
|
|
11
13
|
* // vitest.cedar-global.ts
|
|
@@ -20,7 +22,8 @@ const require_template_mode = require("./template-mode-JTOxrEgE.cjs");
|
|
|
20
22
|
* },
|
|
21
23
|
* })
|
|
22
24
|
*
|
|
23
|
-
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
25
|
+
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).
|
|
26
|
+
* // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.
|
|
24
27
|
* import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
|
|
25
28
|
* await cloneWorkerDatabase();
|
|
26
29
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-template.cjs","names":["setupTemplateMode"],"sources":["../src/adapters/vitest-template.ts"],"sourcesContent":["import {\n cloneWorkerDatabase,\n setupTemplateMode,\n type SetupTemplateModeOptions,\n} from \"./template-mode.ts\";\n\nexport { cloneWorkerDatabase };\nexport type {\n SetupTemplateModeOptions,\n TemplateMigrateFn,\n TemplateMigrateContext,\n} from \"./template-mode.ts\";\n\n/**\n * Vitest globalSetup (template mode). Returns teardown that disposes TEMPLATE + clones.\n *\n * ```ts\n * // vitest.cedar-global.ts\n * import { createGlobalSetup } from \"@cedarjs/pg/vitest/template\";\n * export default createGlobalSetup({ migrate: async ({ databaseUrl }) => {} });\n *\n * // vitest.config.ts\n * export default defineConfig({\n * test: {\n * globalSetup: [\"./vitest.cedar-global.ts\"],\n * setupFiles: [\"./vitest.cedar-worker.ts\"],\n * },\n * })\n *\n * // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
1
|
+
{"version":3,"file":"vitest-template.cjs","names":["setupTemplateMode"],"sources":["../src/adapters/vitest-template.ts"],"sourcesContent":["import {\n cloneWorkerDatabase,\n setupTemplateMode,\n type SetupTemplateModeOptions,\n} from \"./template-mode.ts\";\n\nexport { cloneWorkerDatabase };\nexport type {\n SetupTemplateModeOptions,\n TemplateMigrateFn,\n TemplateMigrateContext,\n} from \"./template-mode.ts\";\n\n/**\n * Vitest globalSetup (template mode). Drops crashed-run leftovers under this\n * worktree's test role, then acquire → migrate → markTemplate. Returns teardown\n * that disposes TEMPLATE + clones.\n *\n * ```ts\n * // vitest.cedar-global.ts\n * import { createGlobalSetup } from \"@cedarjs/pg/vitest/template\";\n * export default createGlobalSetup({ migrate: async ({ databaseUrl }) => {} });\n *\n * // vitest.config.ts\n * export default defineConfig({\n * test: {\n * globalSetup: [\"./vitest.cedar-global.ts\"],\n * setupFiles: [\"./vitest.cedar-worker.ts\"],\n * },\n * })\n *\n * // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).\n * // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.\n * import { cloneWorkerDatabase } from \"@cedarjs/pg/vitest/template\";\n * await cloneWorkerDatabase();\n * ```\n *\n * Stock `@cedarjs/pg/vitest` is one shared test DB only.\n */\nexport function createGlobalSetup(options: SetupTemplateModeOptions) {\n return async () => {\n const result = await setupTemplateMode(options);\n if (result.status !== \"acquired\") {\n return async () => {};\n }\n return async () => {\n await result.dispose();\n };\n };\n}\n\n/** String path without a migrate hook is unsupported — use `createGlobalSetup`. */\nexport default async function setup(): Promise<() => Promise<void>> {\n throw new Error(\n \"@cedarjs/pg/vitest/template requires createGlobalSetup({ migrate }). \" +\n \"Point globalSetup at a local module that exports createGlobalSetup({ migrate }).\",\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kBAAkB,SAAmC;CACnE,OAAO,YAAY;EACjB,MAAM,SAAS,MAAMA,sBAAAA,kBAAkB,OAAO;EAC9C,IAAI,OAAO,WAAW,YACpB,OAAO,YAAY,CAAC;EAEtB,OAAO,YAAY;GACjB,MAAM,OAAO,QAAQ;EACvB;CACF;AACF;;AAGA,eAA8B,QAAsC;CAClE,MAAM,IAAI,MACR,uJAEF;AACF"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMigrateFn, t as SetupTemplateModeOptions } from "./template-mode-
|
|
1
|
+
import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMigrateFn, t as SetupTemplateModeOptions } from "./template-mode-BqV3GjXI.cjs";
|
|
2
2
|
//#region src/adapters/vitest-template.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Vitest globalSetup (template mode).
|
|
4
|
+
* Vitest globalSetup (template mode). Drops crashed-run leftovers under this
|
|
5
|
+
* worktree's test role, then acquire → migrate → markTemplate. Returns teardown
|
|
6
|
+
* that disposes TEMPLATE + clones.
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
9
|
* // vitest.cedar-global.ts
|
|
@@ -16,7 +18,8 @@ import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMig
|
|
|
16
18
|
* },
|
|
17
19
|
* })
|
|
18
20
|
*
|
|
19
|
-
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
21
|
+
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).
|
|
22
|
+
* // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.
|
|
20
23
|
* import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
|
|
21
24
|
* await cloneWorkerDatabase();
|
|
22
25
|
* ```
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMigrateFn, t as SetupTemplateModeOptions } from "./template-mode-
|
|
1
|
+
import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMigrateFn, t as SetupTemplateModeOptions } from "./template-mode-BqV3GjXI.mjs";
|
|
2
2
|
//#region src/adapters/vitest-template.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Vitest globalSetup (template mode).
|
|
4
|
+
* Vitest globalSetup (template mode). Drops crashed-run leftovers under this
|
|
5
|
+
* worktree's test role, then acquire → migrate → markTemplate. Returns teardown
|
|
6
|
+
* that disposes TEMPLATE + clones.
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
9
|
* // vitest.cedar-global.ts
|
|
@@ -16,7 +18,8 @@ import { i as cloneWorkerDatabase, n as TemplateMigrateContext, r as TemplateMig
|
|
|
16
18
|
* },
|
|
17
19
|
* })
|
|
18
20
|
*
|
|
19
|
-
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
21
|
+
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).
|
|
22
|
+
* // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.
|
|
20
23
|
* import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
|
|
21
24
|
* await cloneWorkerDatabase();
|
|
22
25
|
* ```
|
package/dist/vitest-template.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { n as setupTemplateMode, t as cloneWorkerDatabase } from "./template-mode-
|
|
1
|
+
import { n as setupTemplateMode, t as cloneWorkerDatabase } from "./template-mode-DK5cAlv9.mjs";
|
|
2
2
|
//#region src/adapters/vitest-template.ts
|
|
3
3
|
/**
|
|
4
|
-
* Vitest globalSetup (template mode).
|
|
4
|
+
* Vitest globalSetup (template mode). Drops crashed-run leftovers under this
|
|
5
|
+
* worktree's test role, then acquire → migrate → markTemplate. Returns teardown
|
|
6
|
+
* that disposes TEMPLATE + clones.
|
|
5
7
|
*
|
|
6
8
|
* ```ts
|
|
7
9
|
* // vitest.cedar-global.ts
|
|
@@ -16,7 +18,8 @@ import { n as setupTemplateMode, t as cloneWorkerDatabase } from "./template-mod
|
|
|
16
18
|
* },
|
|
17
19
|
* })
|
|
18
20
|
*
|
|
19
|
-
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
21
|
+
* // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).
|
|
22
|
+
* // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.
|
|
20
23
|
* import { cloneWorkerDatabase } from "@cedarjs/pg/vitest/template";
|
|
21
24
|
* await cloneWorkerDatabase();
|
|
22
25
|
* ```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest-template.mjs","names":[],"sources":["../src/adapters/vitest-template.ts"],"sourcesContent":["import {\n cloneWorkerDatabase,\n setupTemplateMode,\n type SetupTemplateModeOptions,\n} from \"./template-mode.ts\";\n\nexport { cloneWorkerDatabase };\nexport type {\n SetupTemplateModeOptions,\n TemplateMigrateFn,\n TemplateMigrateContext,\n} from \"./template-mode.ts\";\n\n/**\n * Vitest globalSetup (template mode). Returns teardown that disposes TEMPLATE + clones.\n *\n * ```ts\n * // vitest.cedar-global.ts\n * import { createGlobalSetup } from \"@cedarjs/pg/vitest/template\";\n * export default createGlobalSetup({ migrate: async ({ databaseUrl }) => {} });\n *\n * // vitest.config.ts\n * export default defineConfig({\n * test: {\n * globalSetup: [\"./vitest.cedar-global.ts\"],\n * setupFiles: [\"./vitest.cedar-worker.ts\"],\n * },\n * })\n *\n * // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here)
|
|
1
|
+
{"version":3,"file":"vitest-template.mjs","names":[],"sources":["../src/adapters/vitest-template.ts"],"sourcesContent":["import {\n cloneWorkerDatabase,\n setupTemplateMode,\n type SetupTemplateModeOptions,\n} from \"./template-mode.ts\";\n\nexport { cloneWorkerDatabase };\nexport type {\n SetupTemplateModeOptions,\n TemplateMigrateFn,\n TemplateMigrateContext,\n} from \"./template-mode.ts\";\n\n/**\n * Vitest globalSetup (template mode). Drops crashed-run leftovers under this\n * worktree's test role, then acquire → migrate → markTemplate. Returns teardown\n * that disposes TEMPLATE + clones.\n *\n * ```ts\n * // vitest.cedar-global.ts\n * import { createGlobalSetup } from \"@cedarjs/pg/vitest/template\";\n * export default createGlobalSetup({ migrate: async ({ databaseUrl }) => {} });\n *\n * // vitest.config.ts\n * export default defineConfig({\n * test: {\n * globalSetup: [\"./vitest.cedar-global.ts\"],\n * setupFiles: [\"./vitest.cedar-worker.ts\"],\n * },\n * })\n *\n * // vitest.cedar-worker.ts — local ESM (pack emits CJS+ESM; top-level await lives here).\n * // Runs per test file: reuses the worker clone, truncated with RESTART IDENTITY first.\n * import { cloneWorkerDatabase } from \"@cedarjs/pg/vitest/template\";\n * await cloneWorkerDatabase();\n * ```\n *\n * Stock `@cedarjs/pg/vitest` is one shared test DB only.\n */\nexport function createGlobalSetup(options: SetupTemplateModeOptions) {\n return async () => {\n const result = await setupTemplateMode(options);\n if (result.status !== \"acquired\") {\n return async () => {};\n }\n return async () => {\n await result.dispose();\n };\n };\n}\n\n/** String path without a migrate hook is unsupported — use `createGlobalSetup`. */\nexport default async function setup(): Promise<() => Promise<void>> {\n throw new Error(\n \"@cedarjs/pg/vitest/template requires createGlobalSetup({ migrate }). \" +\n \"Point globalSetup at a local module that exports createGlobalSetup({ migrate }).\",\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kBAAkB,SAAmC;CACnE,OAAO,YAAY;EACjB,MAAM,SAAS,MAAM,kBAAkB,OAAO;EAC9C,IAAI,OAAO,WAAW,YACpB,OAAO,YAAY,CAAC;EAEtB,OAAO,YAAY;GACjB,MAAM,OAAO,QAAQ;EACvB;CACF;AACF;;AAGA,eAA8B,QAAsC;CAClE,MAAM,IAAI,MACR,uJAEF;AACF"}
|
package/dist/vitest.cjs
CHANGED
package/dist/vitest.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/pg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Worktree-isolated local Postgres for Vite+, Nx, and CedarJS (via autopg)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"autopg",
|
|
@@ -48,11 +48,6 @@
|
|
|
48
48
|
"import": "./dist/vite-plus.mjs",
|
|
49
49
|
"require": "./dist/vite-plus.cjs"
|
|
50
50
|
},
|
|
51
|
-
"./nx": {
|
|
52
|
-
"types": "./dist/nx.d.mts",
|
|
53
|
-
"import": "./dist/nx.mjs",
|
|
54
|
-
"require": "./dist/nx.cjs"
|
|
55
|
-
},
|
|
56
51
|
"./vitest": {
|
|
57
52
|
"types": "./dist/vitest.d.mts",
|
|
58
53
|
"import": "./dist/vitest.mjs",
|
|
@@ -92,7 +87,7 @@
|
|
|
92
87
|
},
|
|
93
88
|
"publishConfig": {
|
|
94
89
|
"access": "public",
|
|
95
|
-
"tag": "
|
|
90
|
+
"tag": "latest"
|
|
96
91
|
},
|
|
97
92
|
"dependencies": {
|
|
98
93
|
"pg": "^8.16.3"
|
package/scripts/autopg-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v3.
|
|
1
|
+
v3.2.2
|
|
@@ -31,10 +31,15 @@ case "$(uname -s)-$(uname -m)" in
|
|
|
31
31
|
;;
|
|
32
32
|
esac
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
# Reuse only the pinned version; a stale binary (cached runner, older image) is replaced.
|
|
35
|
+
EXISTING="$("${HOME}/.local/bin/autopg" --version 2>/dev/null || true)"
|
|
36
|
+
if [ "${EXISTING}" = "autopg ${VERSION#v}" ]; then
|
|
37
|
+
echo "[ci-install-autopg] already present: ${HOME}/.local/bin/autopg (${EXISTING})"
|
|
36
38
|
exit 0
|
|
37
39
|
fi
|
|
40
|
+
if [ -n "${EXISTING}" ]; then
|
|
41
|
+
echo "[ci-install-autopg] replacing ${EXISTING} with ${VERSION}"
|
|
42
|
+
fi
|
|
38
43
|
|
|
39
44
|
TARBALL="autopg-${VERSION#v}-${PLATFORM}.tar.gz"
|
|
40
45
|
URL="https://github.com/${REPO}/releases/download/${VERSION}/${TARBALL}"
|
package/scripts/postinstall.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Idempotent across npm / yarn / pnpm. Never fails the parent install hard
|
|
5
5
|
* when network is unavailable; prints a clear warning instead.
|
|
6
6
|
*
|
|
7
|
-
* Version pin: scripts/autopg-version (single source of truth).
|
|
7
|
+
* Version pin: scripts/autopg-version (single source of truth). An existing
|
|
8
|
+
* older autopg only gets an upgrade hint (runtime warns the same way).
|
|
8
9
|
*
|
|
9
10
|
* - Local / non-CI: pinned upstream install.sh (may start pm2).
|
|
10
11
|
* - CI + CEDAR_PG_INSTALL_AUTOPG=1: binary-only scripts/ci-install-autopg.sh
|
|
@@ -21,25 +22,54 @@ const HERE = dirname(fileURLToPath(import.meta.url));
|
|
|
21
22
|
const AUTOPG_VERSION = readFileSync(join(HERE, "autopg-version"), "utf8").trim();
|
|
22
23
|
const INSTALL_URL = `https://raw.githubusercontent.com/automagik-dev/autopg/${AUTOPG_VERSION}/install.sh`;
|
|
23
24
|
const CI_INSTALL = join(HERE, "ci-install-autopg.sh");
|
|
25
|
+
/** Where ci-install-autopg.sh installs (and replaces stale binaries). */
|
|
26
|
+
const LOCAL_BIN = join(homedir(), ".local", "bin", "autopg");
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
/** Resolved autopg binary (same order as runtime), or null. */
|
|
29
|
+
function findAutopg() {
|
|
30
|
+
if (process.env.AUTOPG_BIN && existsSync(process.env.AUTOPG_BIN)) return process.env.AUTOPG_BIN;
|
|
27
31
|
const which = spawnSync("which", ["autopg"], { encoding: "utf8" });
|
|
28
|
-
if (which.status === 0 && which.stdout.trim()) return
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();
|
|
33
|
+
return existsSync(LOCAL_BIN) ? LOCAL_BIN : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseVersion(text) {
|
|
37
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
38
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Existing autopg older than the pin → hint only. Upgrading restarts the host
|
|
43
|
+
* every worktree shares, so it is never done from `pnpm install`.
|
|
44
|
+
*/
|
|
45
|
+
function warnIfOutdated(bin) {
|
|
46
|
+
const out = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 5000 }).stdout ?? "";
|
|
47
|
+
const have = parseVersion(out);
|
|
48
|
+
const want = parseVersion(AUTOPG_VERSION);
|
|
49
|
+
if (!have || !want) return;
|
|
50
|
+
if ((have[0] - want[0] || have[1] - want[1] || have[2] - want[2]) >= 0) return;
|
|
51
|
+
console.warn(
|
|
52
|
+
`[cedarpg] autopg ${have.join(".")} is older than ${AUTOPG_VERSION}, the version @cedarjs/pg is tested with.\n` +
|
|
53
|
+
" Upgrade (restarts the shared host; open connections from other worktrees drop):\n" +
|
|
54
|
+
` curl -fsSL ${INSTALL_URL} | AUTOPG_VERSION=${AUTOPG_VERSION} bash\n` +
|
|
55
|
+
" autopg update",
|
|
56
|
+
);
|
|
31
57
|
}
|
|
32
58
|
|
|
33
59
|
function main() {
|
|
34
60
|
if (process.env.CEDAR_PG_SKIP_POSTINSTALL === "1") {
|
|
35
61
|
return;
|
|
36
62
|
}
|
|
37
|
-
if (hasAutopg()) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
63
|
const inCi = process.env.CI === "true";
|
|
42
64
|
const forceCiInstall = process.env.CEDAR_PG_INSTALL_AUTOPG === "1";
|
|
65
|
+
const existing = findAutopg();
|
|
66
|
+
// Forced CI install owns ~/.local/bin/autopg: let the installer replace a
|
|
67
|
+
// stale cached binary (it is a no-op when the pinned version is present).
|
|
68
|
+
const ciOwnsExisting = inCi && forceCiInstall && existing === LOCAL_BIN;
|
|
69
|
+
if (existing && !ciOwnsExisting) {
|
|
70
|
+
warnIfOutdated(existing);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
43
73
|
|
|
44
74
|
// Skip in CI unless explicitly requested; CI images often bake autopg
|
|
45
75
|
// or run scripts/ci-install-autopg.sh from the workflow.
|
|
@@ -52,9 +82,7 @@ function main() {
|
|
|
52
82
|
}
|
|
53
83
|
|
|
54
84
|
if (inCi && forceCiInstall) {
|
|
55
|
-
console.log(
|
|
56
|
-
`[cedarpg] autopg not found; CI binary install ${AUTOPG_VERSION} via ci-install-autopg.sh...`,
|
|
57
|
-
);
|
|
85
|
+
console.log(`[cedarpg] CI binary install ${AUTOPG_VERSION} via ci-install-autopg.sh...`);
|
|
58
86
|
const result = spawnSync("bash", [CI_INSTALL], {
|
|
59
87
|
stdio: "inherit",
|
|
60
88
|
env: { ...process.env, AUTOPG_VERSION },
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { t as DbMode } from "./naming-C2nGVxPk.mjs";
|
|
2
|
-
//#region src/core/lease.d.ts
|
|
3
|
-
type Lease = {
|
|
4
|
-
schemaVersion: 1;
|
|
5
|
-
mode: DbMode;
|
|
6
|
-
root: string;
|
|
7
|
-
repoSlug: string;
|
|
8
|
-
worktreeSlug: string;
|
|
9
|
-
pathHash: string;
|
|
10
|
-
databaseName: string;
|
|
11
|
-
roleName: string;
|
|
12
|
-
port: number;
|
|
13
|
-
pid: number;
|
|
14
|
-
createdAt: string;
|
|
15
|
-
};
|
|
16
|
-
/** Narrow-parse a lease; reject incomplete or wrong-version records. */
|
|
17
|
-
declare function parseLease(raw: unknown): Lease | null;
|
|
18
|
-
/** Absolute `.cedarpg/<mode>.env` path (resolves root when omitted). */
|
|
19
|
-
declare function envFilePath(root: string | undefined, mode: DbMode): string;
|
|
20
|
-
declare function readLease(root: string, mode: DbMode): Lease | null;
|
|
21
|
-
declare function isOrphanLease(lease: Lease): boolean;
|
|
22
|
-
//#endregion
|
|
23
|
-
export { readLease as a, parseLease as i, envFilePath as n, isOrphanLease as r, Lease as t };
|
|
24
|
-
//# sourceMappingURL=lease-B3TuX92y.d.mts.map
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { t as DbMode } from "./naming-C2nGVxPk.cjs";
|
|
2
|
-
//#region src/core/lease.d.ts
|
|
3
|
-
type Lease = {
|
|
4
|
-
schemaVersion: 1;
|
|
5
|
-
mode: DbMode;
|
|
6
|
-
root: string;
|
|
7
|
-
repoSlug: string;
|
|
8
|
-
worktreeSlug: string;
|
|
9
|
-
pathHash: string;
|
|
10
|
-
databaseName: string;
|
|
11
|
-
roleName: string;
|
|
12
|
-
port: number;
|
|
13
|
-
pid: number;
|
|
14
|
-
createdAt: string;
|
|
15
|
-
};
|
|
16
|
-
/** Narrow-parse a lease; reject incomplete or wrong-version records. */
|
|
17
|
-
declare function parseLease(raw: unknown): Lease | null;
|
|
18
|
-
/** Absolute `.cedarpg/<mode>.env` path (resolves root when omitted). */
|
|
19
|
-
declare function envFilePath(root: string | undefined, mode: DbMode): string;
|
|
20
|
-
declare function readLease(root: string, mode: DbMode): Lease | null;
|
|
21
|
-
declare function isOrphanLease(lease: Lease): boolean;
|
|
22
|
-
//#endregion
|
|
23
|
-
export { readLease as a, parseLease as i, envFilePath as n, isOrphanLease as r, Lease as t };
|
|
24
|
-
//# sourceMappingURL=lease-t4I9JahV.d.cts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-BbrvFQvg.cjs","names":["PASSWORD_SALT_PREFIX","leaseDir","envPath","resolveWorktreeIdentity","readLease","listRegistryLeases","isOrphanLease"],"sources":["../src/core/naming.ts","../src/core/policy.ts","../src/providers/autopg.ts","../src/providers/host.ts","../src/core/lifecycle.ts"],"sourcesContent":["import type { WorktreeIdentity } from \"./worktree.ts\";\n\nexport type DbMode = \"dev\" | \"test\";\n\nconst PG_MAX = 63;\nconst PREFIX = \"cpg_\";\n\n/**\n * Build an observable Postgres database name:\n * cpg_<repoSlug>_<worktreeSlug>_<mode>_<pathHash8>\n *\n * Never drops `mode` or `pathHash`. Truncates slugs to fit ≤63 chars.\n */\nexport function buildDatabaseName(identity: WorktreeIdentity, mode: DbMode): string {\n const modePart = mode;\n const hashPart = identity.pathHash;\n // PREFIX + slug + '_' + slug + '_' + mode + '_' + hash\n // fixed: PREFIX(4) + 2 underscores around mode/hash segments + mode + hash\n // layout: cpg_<repo>_<wt>_<mode>_<hash>\n const fixed =\n PREFIX.length +\n 1 + // after repo\n 1 + // after worktree\n modePart.length +\n 1 +\n hashPart.length;\n\n let budget = PG_MAX - fixed;\n if (budget < 2) {\n // Extreme truncation: keep mode+hash, minimal slugs\n return `${PREFIX}${modePart}_${hashPart}`.slice(0, PG_MAX);\n }\n\n let repo = identity.repoSlug;\n let wt = identity.worktreeSlug;\n // Split budget roughly evenly, preferring to keep both readable\n while (repo.length + wt.length > budget) {\n if (repo.length >= wt.length && repo.length > 1) {\n repo = repo.slice(0, -1);\n } else if (wt.length > 1) {\n wt = wt.slice(0, -1);\n } else {\n break;\n }\n }\n\n const name = `${PREFIX}${repo}_${wt}_${modePart}_${hashPart}`;\n if (name.length > PG_MAX) {\n return name.slice(0, PG_MAX);\n }\n return name;\n}\n\nexport function buildRoleName(databaseName: string): string {\n const suffix = \"_role\";\n const maxBase = PG_MAX - suffix.length;\n const base = databaseName.slice(0, maxBase);\n return `${base}${suffix}`;\n}\n\n/**\n * Build a worker clone datname from a TEMPLATE database name.\n * Layout: `<template>_c_<suffix>` truncated to ≤63 chars (keeps suffix).\n */\nexport function buildCloneDatabaseName(templateName: string, suffix: string): string {\n const safe = suffix\n .toLowerCase()\n .replace(/[^a-z0-9_]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 24);\n const tag = safe.length > 0 ? safe : \"w\";\n const sep = \"_c_\";\n const maxTemplate = PG_MAX - sep.length - tag.length;\n const base = templateName.slice(0, Math.max(1, maxTemplate));\n return `${base}${sep}${tag}`.slice(0, PG_MAX);\n}\n","/**\n * Shared acquire skip policy for test-runner adapters and Cedar CLI bridges.\n */\n\nexport type AcquireSkip =\n | { skip: false }\n | { skip: true; reason: \"disabled\" }\n | { skip: true; reason: \"external-url\"; databaseUrl: string };\n\nexport type ResolveAcquireSkipInput = {\n /** Candidate URL (TEST_DATABASE_URL for tests, DATABASE_URL for dev). */\n url?: string;\n /** When true, never skip (CEDAR_PG_FORCE=1). */\n force?: boolean;\n /**\n * When true, skip acquire entirely.\n * Defaults from CEDAR_PG=0|false (opt-out adapters). Pass `false` for Cedar opt-in flows.\n */\n disabled?: boolean;\n};\n\n/**\n * Inject DATABASE_URL (and TEST_DATABASE_URL for test mode) from a resolved URL.\n * Single env path for acquire, clone, and external-url skip.\n */\nexport function applyDatabaseUrlEnv(\n databaseUrl: string,\n options: { mode?: \"dev\" | \"test\" } = {},\n): void {\n process.env.DATABASE_URL = databaseUrl;\n if ((options.mode ?? \"test\") === \"test\") {\n process.env.TEST_DATABASE_URL = databaseUrl;\n }\n}\n\nexport type RunIfNeededOptions = ResolveAcquireSkipInput & {\n mode: \"dev\" | \"test\";\n /** Default true (same as acquire / clone host wrappers). */\n setEnv?: boolean;\n};\n\nexport type RunIfNeededSkipped =\n | { status: \"skipped\"; reason: \"disabled\" }\n | { status: \"skipped\"; reason: \"external-url\"; databaseUrl: string };\n\nexport type RunIfNeededResult<T> = RunIfNeededSkipped | { status: \"ran\"; value: T };\n\n/**\n * Shared host skip+env gate for `acquireIfNeeded` / `cloneFromTemplateIfNeeded`.\n * On external-url skip, applies env when `setEnv` is not false.\n */\nexport async function runIfNeeded<T>(\n options: RunIfNeededOptions,\n run: () => Promise<T>,\n): Promise<RunIfNeededResult<T>> {\n const skip = resolveAcquireSkip({\n url: options.url,\n force: options.force,\n disabled: options.disabled,\n });\n if (skip.skip) {\n if (skip.reason === \"external-url\") {\n if (options.setEnv !== false) {\n applyDatabaseUrlEnv(skip.databaseUrl, { mode: options.mode });\n }\n return { status: \"skipped\", reason: \"external-url\", databaseUrl: skip.databaseUrl };\n }\n return { status: \"skipped\", reason: \"disabled\" };\n }\n return { status: \"ran\", value: await run() };\n}\n\n/**\n * True when the URL looks like a cedarpg provisioned database (`cpg_*` name/role).\n * These must never be treated as an external escape hatch; always re-acquire so\n * disposed/stale shell env cannot skip provisioning.\n */\nexport function isCedarPgManagedUrl(url: string | undefined): boolean {\n if (!url || url.startsWith(\"file:\")) return false;\n try {\n const parsed = new URL(url);\n const databaseName = decodeURIComponent(\n (parsed.pathname.replace(/^\\//, \"\").split(\"?\")[0] ?? \"\").trim(),\n );\n const user = decodeURIComponent(parsed.username);\n return databaseName.startsWith(\"cpg_\") || user.startsWith(\"cpg_\");\n } catch {\n return false;\n }\n}\n\n/**\n * True when `url` still looks like an unset dotenv/template value\n * (e.g. `postgresql://{yourMachine}@localhost:5432/app_test` or\n * `postgresql://<user>@localhost/db`), not a real external database.\n */\nfunction isUnsetTemplateUrl(url: string): boolean {\n // `{yourMachine}`, `{user}`, etc.\n if (/\\{[^}]+\\}/.test(url)) return true;\n // `<user>`, `<password>` style templates\n if (/<[^>]+>/.test(url)) return true;\n return false;\n}\n\n/**\n * True when `url` is a real external database and acquire should be skipped.\n * Sqlite `file:` URLs, cedarpg `cpg_*` URLs, and unset template placeholders\n * are not external.\n */\nexport function isExternalDatabaseEscapeHatch(url: string | undefined): boolean {\n if (!url) return false;\n if (url.startsWith(\"file:\")) return false;\n if (isCedarPgManagedUrl(url)) return false;\n if (isUnsetTemplateUrl(url)) return false;\n return true;\n}\n\n/**\n * Decide whether acquire should run.\n * Adapters may call with no args (env defaults). Cedar opt-in should pass `disabled: false`\n * and the relevant `url` (`DATABASE_URL` or `TEST_DATABASE_URL`).\n */\nexport function resolveAcquireSkip(input: ResolveAcquireSkipInput = {}): AcquireSkip {\n const disabled =\n input.disabled ?? (process.env.CEDAR_PG === \"0\" || process.env.CEDAR_PG === \"false\");\n if (disabled) {\n return { skip: true, reason: \"disabled\" };\n }\n\n const force = input.force ?? process.env.CEDAR_PG_FORCE === \"1\";\n if (force) {\n return { skip: false };\n }\n\n const url = input.url !== undefined ? input.url : process.env.TEST_DATABASE_URL;\n if (url && isExternalDatabaseEscapeHatch(url)) {\n return { skip: true, reason: \"external-url\", databaseUrl: url };\n }\n return { skip: false };\n}\n","import { createHash } from \"node:crypto\";\nimport { execFileSync, spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport pg from \"pg\";\nimport { PASSWORD_SALT_PREFIX } from \"../core/constants.ts\";\n\nexport type AutopgDiscovery = {\n port: number;\n adminUrl: string;\n bin: string;\n};\n\nexport const INSTALL_HINT =\n \"autopg is required. Install with:\\n\" +\n \" curl -fsSL https://raw.githubusercontent.com/automagik-dev/autopg/main/install.sh | bash\\n\" +\n \"Then ensure ~/.local/bin is on PATH, or set AUTOPG_BIN.\";\n\n/** Password scheme v2: sha256(PASSWORD_SALT_PREFIX + \"\\\\0\" + roleName) hex[:32]. Frozen for URL rebuild. */\nexport const ROLE_PASSWORD_SCHEME = \"v2\" as const;\n\nfunction candidateBins(): string[] {\n const out: string[] = [];\n if (process.env.AUTOPG_BIN) out.push(process.env.AUTOPG_BIN);\n out.push(\"autopg\");\n out.push(join(homedir(), \".local\", \"bin\", \"autopg\"));\n return out;\n}\n\nexport function resolveAutopgBin(): string | null {\n for (const bin of candidateBins()) {\n if (bin === \"autopg\") {\n const which = spawnSync(\"which\", [\"autopg\"], { encoding: \"utf8\" });\n if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();\n continue;\n }\n if (existsSync(bin)) return bin;\n }\n return null;\n}\n\nexport function requireAutopgBin(): string {\n const bin = resolveAutopgBin();\n if (!bin) {\n throw new Error(INSTALL_HINT);\n }\n return bin;\n}\n\n/**\n * Parse `autopg status --json` → the **registered** port. Throws only when the\n * output is not autopg status JSON.\n *\n * Registration is not liveness: autopg reports a port for a stopped host too,\n * and its `status` string is supervisor-specific (pm2 `online`, systemd-user /\n * launchd differ). Liveness is a TCP accept on the port, proven by the caller —\n * `acquire` does that before it connects.\n */\nexport function parseHostStatus(json: string): { port: number } {\n let parsed: { port?: unknown };\n try {\n parsed = JSON.parse(json) as typeof parsed;\n } catch {\n throw new Error(`autopg status --json returned invalid JSON.\\n${INSTALL_HINT}`);\n }\n if (typeof parsed.port !== \"number\") {\n throw new Error(`autopg status --json missing numeric port.\\n${INSTALL_HINT}`);\n }\n return { port: parsed.port };\n}\n\n/**\n * Admin URL for an autopg host on `port`.\n *\n * Credentials follow autopg's own defaults / env chain (not \"any local Postgres\"):\n * - user: `AUTOPG_PG_USER` / `PGSERVE_PG_USER` / `postgres`\n * - password: `AUTOPG_PG_PASSWORD` / `PGSERVE_PG_PASSWORD` / `postgres`\n *\n * Port is never scanned: callers pass the port from `autopg status` (attach) or the\n * ephemeral recipe (`55432`). That keeps us off unrelated local servers (e.g. brew on 5432).\n */\nexport function adminUrlFor(port: number, env: NodeJS.ProcessEnv = process.env): string {\n const user = encodeURIComponent(env.AUTOPG_PG_USER || env.PGSERVE_PG_USER || \"postgres\");\n const password = encodeURIComponent(\n env.AUTOPG_PG_PASSWORD || env.PGSERVE_PG_PASSWORD || \"postgres\",\n );\n return `postgresql://${user}:${password}@127.0.0.1:${port}/postgres`;\n}\n\n/**\n * Discover the registered autopg host (port + admin URL) via `autopg status --json`.\n * Throws when autopg cannot be queried; does **not** prove a listener — probe TCP\n * (or use `acquire`, which does) before connecting.\n */\nexport function discoverHost(bin = requireAutopgBin()): AutopgDiscovery {\n let status: string;\n try {\n status = execFileSync(bin, [\"status\", \"--json\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to query autopg status.\\n${detail}\\n${INSTALL_HINT}`);\n }\n const { port } = parseHostStatus(status);\n return { port, adminUrl: adminUrlFor(port), bin };\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nasync function withAdminClient<T>(\n adminUrl: string,\n fn: (client: pg.Client) => Promise<T>,\n): Promise<T> {\n const client = new pg.Client({ connectionString: adminUrl });\n await client.connect();\n try {\n return await fn(client);\n } finally {\n await client.end();\n }\n}\n\n/**\n * Deterministic local-only password for an app role (Prisma/TCP need it;\n * autopg hba uses `password` for 127.0.0.1).\n *\n * Keyed by `roleName` (not databaseName) so TEMPLATE clones that reuse the\n * same role keep working when `buildDatabaseUrl` is called with a new database.\n */\nexport function rolePasswordFor(roleName: string): string {\n return createHash(\"sha256\")\n .update(`${PASSWORD_SALT_PREFIX}\\0${roleName}`)\n .digest(\"hex\")\n .slice(0, 32);\n}\n\n/**\n * Idempotently CREATE ROLE + CREATE DATABASE with cedar-pg owned names.\n */\nexport async function ensureDatabase(opts: {\n adminUrl: string;\n databaseName: string;\n roleName: string;\n password?: string;\n}): Promise<void> {\n const password = opts.password ?? rolePasswordFor(opts.roleName);\n await withAdminClient(opts.adminUrl, async (client) => {\n const roleExists = await client.query(\"SELECT 1 FROM pg_roles WHERE rolname = $1\", [\n opts.roleName,\n ]);\n if (roleExists.rowCount === 0) {\n await client.query(\n `CREATE ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`,\n );\n } else {\n await client.query(\n `ALTER ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`,\n );\n }\n\n const dbExists = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [\n opts.databaseName,\n ]);\n if (dbExists.rowCount === 0) {\n await client.query(\n `CREATE DATABASE ${quoteIdent(opts.databaseName)} OWNER ${quoteIdent(opts.roleName)}`,\n );\n } else {\n await client.query(\n `ALTER DATABASE ${quoteIdent(opts.databaseName)} OWNER TO ${quoteIdent(opts.roleName)}`,\n );\n }\n });\n}\n\n/**\n * Mark (or unmark) a database as a PostgreSQL template (`IS_TEMPLATE`).\n * Template DBs cannot be dropped until unset.\n */\nexport async function setDatabaseIsTemplate(opts: {\n adminUrl: string;\n databaseName: string;\n isTemplate: boolean;\n}): Promise<void> {\n const flag = opts.isTemplate ? \"true\" : \"false\";\n await withAdminClient(opts.adminUrl, async (client) => {\n await client.query(`ALTER DATABASE ${quoteIdent(opts.databaseName)} WITH IS_TEMPLATE ${flag}`);\n });\n}\n\n/**\n * CREATE DATABASE … TEMPLATE … OWNER via admin connection.\n * Test roles are LOGIN-only; workers cannot CREATE DATABASE themselves.\n */\nexport async function cloneDatabaseFromTemplate(opts: {\n adminUrl: string;\n templateName: string;\n databaseName: string;\n roleName: string;\n}): Promise<void> {\n await withAdminClient(opts.adminUrl, async (client) => {\n const tmpl = await client.query<{ datistemplate: boolean }>(\n `SELECT datistemplate FROM pg_database WHERE datname = $1`,\n [opts.templateName],\n );\n if (!tmpl.rowCount) {\n throw new Error(`template database not found: ${opts.templateName}`);\n }\n if (!tmpl.rows[0]?.datistemplate) {\n throw new Error(`database is not a TEMPLATE; run markTemplate first: ${opts.templateName}`);\n }\n\n const exists = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [\n opts.databaseName,\n ]);\n if (exists.rowCount && exists.rowCount > 0) {\n throw new Error(`database already exists: ${opts.databaseName}`);\n }\n await client.query(\n `CREATE DATABASE ${quoteIdent(opts.databaseName)} WITH TEMPLATE ${quoteIdent(opts.templateName)} OWNER ${quoteIdent(opts.roleName)}`,\n );\n });\n}\n\nasync function listOwnedDatnames(client: pg.Client, roleName: string): Promise<string[]> {\n const result = await client.query<{ datname: string }>(\n `SELECT datname FROM pg_database\n WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1)\n ORDER BY datname`,\n [roleName],\n );\n return result.rows.map((r) => r.datname);\n}\n\n/** Unset IS_TEMPLATE if needed, terminate backends, DROP DATABASE (no-op if missing). */\nasync function dropOneDatabase(client: pg.Client, databaseName: string): Promise<void> {\n const db = await client.query<{ datistemplate: boolean }>(\n `SELECT datistemplate FROM pg_database WHERE datname = $1`,\n [databaseName],\n );\n if (!db.rowCount || db.rowCount === 0) return;\n if (db.rows[0]?.datistemplate) {\n await client.query(`ALTER DATABASE ${quoteIdent(databaseName)} WITH IS_TEMPLATE false`);\n }\n await client.query(\n `\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = $1 AND pid <> pg_backend_pid()\n `,\n [databaseName],\n );\n await client.query(`DROP DATABASE IF EXISTS ${quoteIdent(databaseName)}`);\n}\n\n/**\n * DROP every database owned by `roleName` on one admin connection, then DROP ROLE.\n * When `preferLast` is owned, it is dropped after the other owned datnames (TEMPLATE after clones).\n * Never invents a DROP target beyond role ownership.\n */\nexport async function dropDatabasesOwnedByRole(opts: {\n adminUrl: string;\n roleName: string;\n preferLast?: string;\n}): Promise<string[]> {\n return withAdminClient(opts.adminUrl, async (client) => {\n const owned = await listOwnedDatnames(client, opts.roleName);\n const ordered = [\n ...owned.filter((name) => name !== opts.preferLast),\n ...owned.filter((name) => name === opts.preferLast),\n ];\n const dropped: string[] = [];\n for (const databaseName of ordered) {\n await dropOneDatabase(client, databaseName);\n dropped.push(databaseName);\n }\n await client.query(`DROP ROLE IF EXISTS ${quoteIdent(opts.roleName)}`);\n return dropped;\n });\n}\n\n/**\n * DROP DATABASE (unset IS_TEMPLATE, force terminate backends) + DROP ROLE\n * when the role owns no remaining databases.\n */\nexport async function dropDatabase(opts: {\n adminUrl: string;\n databaseName: string;\n roleName: string;\n}): Promise<void> {\n await withAdminClient(opts.adminUrl, async (client) => {\n await dropOneDatabase(client, opts.databaseName);\n const owns = await client.query(\n `SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1) LIMIT 1`,\n [opts.roleName],\n );\n if (owns.rowCount === 0) {\n await client.query(`DROP ROLE IF EXISTS ${quoteIdent(opts.roleName)}`);\n }\n });\n}\n\nexport function buildDatabaseUrl(opts: {\n port: number;\n databaseName: string;\n roleName: string;\n password?: string;\n}): string {\n const password = opts.password ?? rolePasswordFor(opts.roleName);\n const user = encodeURIComponent(opts.roleName);\n const pass = encodeURIComponent(password);\n return `postgresql://${user}:${pass}@127.0.0.1:${opts.port}/${opts.databaseName}`;\n}\n","import { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\nimport { existsSync, mkdirSync, readdirSync, rmSync, statfsSync } from \"node:fs\";\nimport { createConnection } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n adminUrlFor,\n discoverHost,\n INSTALL_HINT,\n requireAutopgBin,\n type AutopgDiscovery,\n} from \"./autopg.ts\";\n\n/** How cedar-pg brings up an autopg host when nothing is listening. */\nexport type HostStartPolicy = \"local\" | \"ephemeral\";\n\nexport type EphemeralHostRecipe = {\n dataDir: string;\n port: number;\n postmasterArgs: string[];\n};\n\n/** Inputs for {@link ephemeralHostRecipe} (production uses process defaults). */\nexport type EphemeralRecipeContext = {\n platform?: NodeJS.Platform;\n shmAvailable?: boolean;\n tmpDir?: string;\n uid?: number;\n port?: number;\n};\n\nconst EPHEMERAL_PORT = 55432;\n\nconst HOST_READY_MS = 30_000;\nconst HOST_POLL_MS = 200;\n/** `pm2 restart` of an existing process is quick; 30s is only for first `install` / initdb. */\nconst LOCAL_RESTART_READY_MS = 10_000;\n\n/**\n * autopg verbs that can bring the *registered* local host up, cheapest fix first.\n *\n * `restart` exit 0 is not evidence of a listener: when pm2 is missing or does not\n * list `autopg-server`, autopg still prints \"respawned daemon\" and returns 0.\n * That is why every verb is followed by a TCP wait, not trusted on exit status.\n *\n * `install` covers a never-registered machine (postinstall ships the binary only)\n * and a reboot whose pm2 list is empty (`pm2 start`). On an already-registered\n * host it is a no-op for the server process but may `pm2 start` the autopg UI —\n * so it runs only after `restart` failed to produce a listener.\n */\nconst LOCAL_START = [\n { argv: [\"restart\"], readyMs: LOCAL_RESTART_READY_MS },\n { argv: [\"install\"], readyMs: HOST_READY_MS },\n] as const;\n\n/** Soft minimum free bytes on /dev/shm before RAM-backed ephemeral start (warns only). */\nexport const EPHEMERAL_SHM_MIN_FREE_BYTES = 512 * 1024 * 1024;\n\n/**\n * Hint when ephemeral `--ram` initdb fails for space / leftover dirs under `/dev/shm`.\n * Cloud VMs often ship with a tiny default tmpfs (e.g. 64MB).\n */\nexport const EPHEMERAL_SHM_HINT =\n \"Ephemeral autopg uses /dev/shm with --ram. If initdb fails with Disk quota / No space (Postgres 53100):\\n\" +\n \" 1. Enlarge tmpfs (cloud VMs often default to ~64MB): sudo mount -o remount,size=6G /dev/shm\\n\" +\n \" 2. Clear leftovers from OOM-killed runs (your test data only):\\n\" +\n \" rm -rf /dev/shm/cedar-pg-* /dev/shm/pgserve-* /dev/shm/PostgreSQL.*\";\n\n/**\n * Resolve how to start a host when none is listening.\n *\n * - `CEDAR_PG_EPHEMERAL_HOST=1` → ephemeral owned postmaster\n * - `CEDAR_PG_EPHEMERAL_HOST=0` → never own a postmaster, local autopg only (even in CI)\n * - unset + `CI=true` → ephemeral\n * - otherwise → local\n */\nexport function resolveHostStartPolicy(env: NodeJS.ProcessEnv = process.env): HostStartPolicy {\n const force = env.CEDAR_PG_EPHEMERAL_HOST;\n if (force === \"1\") return \"ephemeral\";\n if (force === \"0\") return \"local\";\n if (env.CI === \"true\") return \"ephemeral\";\n return \"local\";\n}\n\nexport function ephemeralHostRecipe(ctx: EphemeralRecipeContext = {}): EphemeralHostRecipe {\n const platform = ctx.platform ?? process.platform;\n const shmAvailable = ctx.shmAvailable ?? (platform === \"linux\" && existsSync(\"/dev/shm\"));\n const uid = ctx.uid ?? process.getuid?.() ?? 0;\n const port = ctx.port ?? EPHEMERAL_PORT;\n const useRam = platform === \"linux\" && shmAvailable;\n const dataDir = useRam\n ? `/dev/shm/cedar-pg-${uid}`\n : join(ctx.tmpDir ?? tmpdir(), \"cedar-pg-host\");\n\n const postmasterArgs = [\n \"postmaster\",\n ...(useRam ? [\"--ram\"] : []),\n \"--port\",\n String(port),\n \"--socket-dir\",\n dataDir,\n \"--data\",\n dataDir,\n ];\n\n return { dataDir, port, postmasterArgs };\n}\n\nexport function discoveryFromRecipe(bin: string, recipe: EphemeralHostRecipe): AutopgDiscovery {\n return { port: recipe.port, adminUrl: adminUrlFor(recipe.port), bin };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** True when stderr/stdout looks like a /dev/shm quota or ENOSPC failure. */\nexport function looksLikeShmSpaceError(text: string): boolean {\n return /Disk quota exceeded|No space left on device|ENOSPC|\\b53100\\b/i.test(text);\n}\n\nexport function formatHostStartError(detail: string, useRamShm = false): string {\n const hint = useRamShm && looksLikeShmSpaceError(detail) ? `\\n${EPHEMERAL_SHM_HINT}` : \"\";\n return `Failed to start autopg host.\\n${detail}\\n${INSTALL_HINT}${hint}`;\n}\n\n/**\n * Remove leftover ephemeral data dirs under `/dev/shm` (and the recipe dataDir).\n * Safe for OOM-killed CI/cloud runs that leave `cedar-pg-*` / `pgserve-*` /\n * `PostgreSQL.*` filling tmpfs.\n *\n * Only called on ephemeral cold start, i.e. when the recipe port has no listener —\n * never while a host owns those dirs.\n */\nexport function pruneStaleEphemeralDataDirs(opts: {\n dataDir: string;\n /** Parent of RAM dirs; default `/dev/shm` when it exists. */\n shmRoot?: string;\n}): string[] {\n const removed: string[] = [];\n const tryRm = (path: string) => {\n if (!existsSync(path)) return;\n try {\n rmSync(path, { recursive: true, force: true });\n removed.push(path);\n } catch {\n // best-effort: next initdb will surface a clearer error\n }\n };\n\n tryRm(opts.dataDir);\n\n const shmRoot = opts.shmRoot ?? (existsSync(\"/dev/shm\") ? \"/dev/shm\" : undefined);\n if (!shmRoot) return removed;\n\n let entries: string[] = [];\n try {\n entries = readdirSync(shmRoot);\n } catch {\n return removed;\n }\n\n for (const name of entries) {\n if (\n name.startsWith(\"cedar-pg-\") ||\n name.startsWith(\"pgserve-\") ||\n name.startsWith(\"PostgreSQL.\")\n ) {\n tryRm(join(shmRoot, name));\n }\n }\n return removed;\n}\n\n/** Free bytes on `path`, or `null` if unavailable. */\nfunction freeBytesOn(path: string): number | null {\n try {\n const s = statfsSync(path);\n return Number(s.bavail) * Number(s.bsize);\n } catch {\n return null;\n }\n}\n\n/** Run a one-shot autopg verb. Returns failure detail, or `null` on exit 0. */\nfunction runAutopg(bin: string, argv: string[]): string | null {\n const result = spawnSync(bin, argv, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n if (result.error) return result.error.message;\n if (result.status === 0) return null;\n return `exit ${result.status}\\n${(result.stderr || result.stdout || \"\").trim()}`.trim();\n}\n\n/** True when something accepts TCP on 127.0.0.1:port (postmaster live, not just admin.json). */\nfunction canConnect(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ host: \"127.0.0.1\", port }, () => {\n socket.end();\n resolve(true);\n });\n socket.on(\"error\", () => {\n resolve(false);\n });\n });\n}\n\nfunction killOwnedChild(child: ChildProcess): void {\n const pid = child.pid;\n if (pid != null) {\n try {\n process.kill(-pid, \"SIGTERM\");\n } catch {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n // already exited\n }\n }\n }\n child.unref();\n}\n\nexport type WaitForListenerOptions = {\n port: number;\n /** Total grace period; `0` (default) probes exactly once. */\n readyMs?: number;\n pollMs?: number;\n canConnect?: (port: number) => Promise<boolean>;\n now?: () => number;\n sleep?: (ms: number) => Promise<void>;\n /** Return an Error to abort early (spawn error / child exit). */\n failure?: () => Error | null;\n};\n\n/**\n * The single liveness primitive: TCP accept on 127.0.0.1:port.\n *\n * Resolves `true` as soon as something is listening, `false` when the grace\n * period expires, and throws only what {@link WaitForListenerOptions.failure}\n * returns. Used for attach (one probe), local restart, and ephemeral start.\n */\nexport async function waitForListener(opts: WaitForListenerOptions): Promise<boolean> {\n const probe = opts.canConnect ?? canConnect;\n const now = opts.now ?? Date.now;\n const pause = opts.sleep ?? sleep;\n const pollMs = opts.pollMs ?? HOST_POLL_MS;\n const deadline = now() + (opts.readyMs ?? 0);\n\n for (;;) {\n const fail = opts.failure?.();\n if (fail) throw fail;\n if (await probe(opts.port)) return true;\n if (now() >= deadline) return false;\n await pause(pollMs);\n }\n}\n\n/**\n * Attach to the registered autopg host once TCP accepts on its port.\n *\n * Registration is not liveness: a stopped pm2 host still reports a port, and\n * attaching to it is the `ECONNREFUSED 127.0.0.1:25432` bug. `readyMs` > 0 is\n * the grace period after asking autopg to start.\n */\nasync function attachLiveHost(bin: string, readyMs = 0): Promise<AutopgDiscovery | null> {\n let discovered: AutopgDiscovery;\n try {\n discovered = discoverHost(bin);\n } catch {\n return null;\n }\n return (await waitForListener({ port: discovered.port, readyMs })) ? discovered : null;\n}\n\n/**\n * Bring the user's registered autopg host up and attach to it — same port, same\n * `~/.autopg/data`, still there after this process exits. cedar-pg never runs a\n * second local Postgres.\n */\nasync function startLocalHost(bin: string, registeredPort?: number): Promise<AutopgDiscovery> {\n const failures: string[] = [];\n\n for (const { argv, readyMs } of LOCAL_START) {\n const label = `autopg ${argv.join(\" \")}`;\n const failure = runAutopg(bin, [...argv]);\n if (failure) {\n failures.push(`${label}: ${failure}`);\n continue;\n }\n const attached = await attachLiveHost(bin, readyMs);\n if (attached) return attached;\n failures.push(`${label}: no listener within ${readyMs}ms`);\n }\n\n const where =\n registeredPort != null\n ? `autopg host is registered on 127.0.0.1:${registeredPort} but nothing is listening.\\n`\n : \"\";\n throw new Error(formatHostStartError(`${where}${failures.join(\"\\n\")}`));\n}\n\n/**\n * Ephemeral host owned by this process/job: detached `autopg postmaster` with\n * fully ignored stdio (caller exit must not close pipes under the daemon).\n *\n * Never runs `autopg install` — that rewrites `~/.autopg/admin.json` and fails\n * with `supervisor mismatch` next to a local pm2 install. Reuses a listener\n * already on the recipe port, and prunes stale RAM dirs before a cold start.\n */\nasync function startEphemeralHost(bin: string): Promise<AutopgDiscovery> {\n const recipe = ephemeralHostRecipe();\n if (await canConnect(recipe.port)) return discoveryFromRecipe(bin, recipe);\n\n const useRamShm = recipe.postmasterArgs.includes(\"--ram\");\n pruneStaleEphemeralDataDirs({ dataDir: recipe.dataDir });\n\n if (useRamShm) {\n const free = freeBytesOn(\"/dev/shm\");\n if (free != null && free < EPHEMERAL_SHM_MIN_FREE_BYTES) {\n process.stderr.write(\n `[cedar-pg] warning: /dev/shm has ~${Math.round(free / (1024 * 1024))}MB free ` +\n `(recommend ≥${Math.round(EPHEMERAL_SHM_MIN_FREE_BYTES / (1024 * 1024))}MB for --ram).\\n` +\n `${EPHEMERAL_SHM_HINT}\\n`,\n );\n }\n }\n\n mkdirSync(recipe.dataDir, { recursive: true });\n\n const child = spawn(bin, recipe.postmasterArgs, { detached: true, stdio: \"ignore\" });\n let died: Error | null = null;\n child.on(\"error\", (err) => {\n died = new Error(\n formatHostStartError(`Failed to spawn autopg postmaster.\\n${err.message}`, useRamShm),\n );\n });\n child.on(\"exit\", (code, signal) => {\n died ??= new Error(\n formatHostStartError(\n `autopg postmaster exited before ready (code=${code}, signal=${signal}).`,\n useRamShm,\n ),\n );\n });\n\n try {\n const listening = await waitForListener({\n port: recipe.port,\n readyMs: HOST_READY_MS,\n failure: () => died,\n });\n if (!listening) {\n throw new Error(\n formatHostStartError(\n `autopg postmaster did not accept 127.0.0.1:${recipe.port} within ${HOST_READY_MS}ms.`,\n useRamShm,\n ),\n );\n }\n } catch (err) {\n killOwnedChild(child);\n throw err;\n }\n\n // Success: the process/job owns lifetime — drop our handle without killing.\n child.unref();\n return discoveryFromRecipe(bin, recipe);\n}\n\n/**\n * Attach to a listening autopg host, else start one per\n * {@link resolveHostStartPolicy}. Internal: callers use `acquire` / `adminUrl`.\n */\nexport async function ensureHostRunning(bin = requireAutopgBin()): Promise<AutopgDiscovery> {\n let registered: AutopgDiscovery | null = null;\n try {\n registered = discoverHost(bin);\n } catch {\n // no registration — local recovery may `install`\n }\n if (registered && (await waitForListener({ port: registered.port }))) return registered;\n\n return resolveHostStartPolicy() === \"ephemeral\"\n ? startEphemeralHost(bin)\n : startLocalHost(bin, registered?.port);\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { buildDatabaseName, buildRoleName, type DbMode } from \"./naming.ts\";\nimport {\n envPath,\n forgetLease,\n isOrphanLease,\n leaseDir,\n listRegistryLeases,\n readLease,\n writeLease,\n type Lease,\n} from \"./lease.ts\";\nimport { applyDatabaseUrlEnv, runIfNeeded, type ResolveAcquireSkipInput } from \"./policy.ts\";\nimport { resolveWorktreeIdentity } from \"./worktree.ts\";\nimport { buildDatabaseUrl, dropDatabasesOwnedByRole, ensureDatabase } from \"../providers/autopg.ts\";\nimport { ensureHostRunning } from \"../providers/host.ts\";\n\nfunction writeEnvFile(root: string, mode: DbMode, databaseUrl: string): void {\n mkdirSync(leaseDir(root), { recursive: true, mode: 0o700 });\n let body = `DATABASE_URL=${databaseUrl}\\n`;\n if (mode === \"test\") body += `TEST_DATABASE_URL=${databaseUrl}\\n`;\n writeFileSync(envPath(root, mode), body, { mode: 0o600 });\n}\n\nexport function urlFromLease(lease: Lease): string {\n return buildDatabaseUrl({\n port: lease.port,\n databaseName: lease.databaseName,\n roleName: lease.roleName,\n });\n}\n\n/**\n * DROP every DB owned by the lease role (TEMPLATE + clones), then forget lease.\n * Provider owns ordering (clones before leased datname) on one admin connection.\n */\nasync function dropThenForget(lease: Lease, adminUrl: string): Promise<string[]> {\n const dropped = await dropDatabasesOwnedByRole({\n adminUrl,\n roleName: lease.roleName,\n preferLast: lease.databaseName,\n });\n forgetLease(lease);\n return dropped;\n}\n\nexport type AcquireOptions = {\n root?: string;\n mode: DbMode;\n /** Inject DATABASE_URL / TEST_DATABASE_URL into process.env (default true). */\n setEnv?: boolean;\n};\n\nexport type AcquireResult = {\n databaseUrl: string;\n /** Superuser URL for privileged DDL (mark template, CREATE DATABASE … TEMPLATE). */\n adminUrl: string;\n databaseName: string;\n roleName: string;\n repoSlug: string;\n worktreeSlug: string;\n pathHash: string;\n root: string;\n mode: DbMode;\n port: number;\n dispose: () => Promise<void>;\n};\n\n/**\n * Acquire a worktree-scoped database and return connection info.\n *\n * - `dev`: keep DB across restarts.\n * - `test`: DROP when `dispose()` is awaited (callers / test runners own teardown).\n */\nexport async function acquire(options: AcquireOptions): Promise<AcquireResult> {\n const identity = resolveWorktreeIdentity(options.root);\n const mode = options.mode;\n const databaseName = buildDatabaseName(identity, mode);\n const roleName = buildRoleName(databaseName);\n\n const host = await ensureHostRunning();\n await ensureDatabase({\n adminUrl: host.adminUrl,\n databaseName,\n roleName,\n });\n\n const databaseUrl = buildDatabaseUrl({\n port: host.port,\n databaseName,\n roleName,\n });\n\n const lease: Lease = {\n schemaVersion: 1,\n mode,\n root: identity.root,\n repoSlug: identity.repoSlug,\n worktreeSlug: identity.worktreeSlug,\n pathHash: identity.pathHash,\n databaseName,\n roleName,\n port: host.port,\n pid: process.pid,\n createdAt: new Date().toISOString(),\n };\n writeLease(lease);\n writeEnvFile(identity.root, mode, databaseUrl);\n\n if (options.setEnv !== false) {\n applyDatabaseUrlEnv(databaseUrl, { mode });\n }\n\n const disposeFn = async () => {\n await dispose({ root: identity.root, mode });\n };\n\n return {\n databaseUrl,\n adminUrl: host.adminUrl,\n databaseName,\n roleName,\n repoSlug: identity.repoSlug,\n worktreeSlug: identity.worktreeSlug,\n pathHash: identity.pathHash,\n root: identity.root,\n mode,\n port: host.port,\n dispose: disposeFn,\n };\n}\n\nexport type AcquireIfNeededOptions = AcquireOptions & ResolveAcquireSkipInput;\n\nexport type AcquireIfNeededResult =\n | { status: \"skipped\"; reason: \"disabled\" }\n | { status: \"skipped\"; reason: \"external-url\"; databaseUrl: string }\n | ({ status: \"acquired\" } & AcquireResult);\n\n/**\n * Resolve skip policy then acquire. Single entry for hosts (Cedar CLI, Jest, Vitest).\n * On external-url skip, applies DATABASE_URL / TEST_DATABASE_URL when `setEnv` is not false.\n */\nexport async function acquireIfNeeded(\n options: AcquireIfNeededOptions,\n): Promise<AcquireIfNeededResult> {\n const outcome = await runIfNeeded(options, () =>\n acquire({\n root: options.root,\n mode: options.mode,\n setEnv: options.setEnv,\n }),\n );\n if (outcome.status === \"skipped\") return outcome;\n return { status: \"acquired\", ...outcome.value };\n}\n\nexport type DisposeOptions = {\n root?: string;\n mode?: DbMode;\n};\n\nexport type DisposeResult =\n | { dropped: true; databaseName: string; droppedDatabases: string[] }\n | { dropped: false; reason: \"no-lease\" | \"host-unavailable\" };\n\n/**\n * Role-scoped suite teardown: DROP every database owned by the lease role\n * (TEMPLATE + clones), then DROP ROLE and forget the lease. Unsets `IS_TEMPLATE`\n * as needed. This is not per-clone cleanup — use `CloneResult.dropClone` for that.\n * No-ops without a valid lease; never invents a DROP target beyond role ownership.\n * If the host is unavailable, leaves the lease so dispose/gc can retry.\n */\nexport async function dispose(options: DisposeOptions = {}): Promise<DisposeResult> {\n const identity = resolveWorktreeIdentity(options.root);\n const mode = options.mode ?? \"test\";\n const lease = readLease(identity.root, mode);\n if (!lease) return { dropped: false, reason: \"no-lease\" };\n\n let host;\n try {\n host = await ensureHostRunning();\n } catch {\n // Keep lease + registry so a later dispose/gc can still find the DB.\n return { dropped: false, reason: \"host-unavailable\" };\n }\n\n const droppedDatabases = await dropThenForget(lease, host.adminUrl);\n return {\n dropped: true,\n databaseName: lease.databaseName,\n droppedDatabases,\n };\n}\n\n/**\n * Drop databases whose registered worktree root no longer exists on disk.\n * Registry entries are removed only after a successful DROP (owned DBs + lease DB).\n */\nexport async function gc(): Promise<{\n dropped: string[];\n}> {\n const dropped: string[] = [];\n const orphans = listRegistryLeases().filter(isOrphanLease);\n if (orphans.length === 0) return { dropped };\n\n let host;\n try {\n host = await ensureHostRunning();\n } catch {\n return { dropped };\n }\n\n for (const lease of orphans) {\n try {\n const names = await dropThenForget(lease, host.adminUrl);\n dropped.push(...names);\n } catch {\n // Keep registry entry for retry\n }\n }\n return { dropped };\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle-BvIx0xqq.mjs","names":[],"sources":["../src/core/naming.ts","../src/core/policy.ts","../src/providers/autopg.ts","../src/providers/host.ts","../src/core/lifecycle.ts"],"sourcesContent":["import type { WorktreeIdentity } from \"./worktree.ts\";\n\nexport type DbMode = \"dev\" | \"test\";\n\nconst PG_MAX = 63;\nconst PREFIX = \"cpg_\";\n\n/**\n * Build an observable Postgres database name:\n * cpg_<repoSlug>_<worktreeSlug>_<mode>_<pathHash8>\n *\n * Never drops `mode` or `pathHash`. Truncates slugs to fit ≤63 chars.\n */\nexport function buildDatabaseName(identity: WorktreeIdentity, mode: DbMode): string {\n const modePart = mode;\n const hashPart = identity.pathHash;\n // PREFIX + slug + '_' + slug + '_' + mode + '_' + hash\n // fixed: PREFIX(4) + 2 underscores around mode/hash segments + mode + hash\n // layout: cpg_<repo>_<wt>_<mode>_<hash>\n const fixed =\n PREFIX.length +\n 1 + // after repo\n 1 + // after worktree\n modePart.length +\n 1 +\n hashPart.length;\n\n let budget = PG_MAX - fixed;\n if (budget < 2) {\n // Extreme truncation: keep mode+hash, minimal slugs\n return `${PREFIX}${modePart}_${hashPart}`.slice(0, PG_MAX);\n }\n\n let repo = identity.repoSlug;\n let wt = identity.worktreeSlug;\n // Split budget roughly evenly, preferring to keep both readable\n while (repo.length + wt.length > budget) {\n if (repo.length >= wt.length && repo.length > 1) {\n repo = repo.slice(0, -1);\n } else if (wt.length > 1) {\n wt = wt.slice(0, -1);\n } else {\n break;\n }\n }\n\n const name = `${PREFIX}${repo}_${wt}_${modePart}_${hashPart}`;\n if (name.length > PG_MAX) {\n return name.slice(0, PG_MAX);\n }\n return name;\n}\n\nexport function buildRoleName(databaseName: string): string {\n const suffix = \"_role\";\n const maxBase = PG_MAX - suffix.length;\n const base = databaseName.slice(0, maxBase);\n return `${base}${suffix}`;\n}\n\n/**\n * Build a worker clone datname from a TEMPLATE database name.\n * Layout: `<template>_c_<suffix>` truncated to ≤63 chars (keeps suffix).\n */\nexport function buildCloneDatabaseName(templateName: string, suffix: string): string {\n const safe = suffix\n .toLowerCase()\n .replace(/[^a-z0-9_]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 24);\n const tag = safe.length > 0 ? safe : \"w\";\n const sep = \"_c_\";\n const maxTemplate = PG_MAX - sep.length - tag.length;\n const base = templateName.slice(0, Math.max(1, maxTemplate));\n return `${base}${sep}${tag}`.slice(0, PG_MAX);\n}\n","/**\n * Shared acquire skip policy for test-runner adapters and Cedar CLI bridges.\n */\n\nexport type AcquireSkip =\n | { skip: false }\n | { skip: true; reason: \"disabled\" }\n | { skip: true; reason: \"external-url\"; databaseUrl: string };\n\nexport type ResolveAcquireSkipInput = {\n /** Candidate URL (TEST_DATABASE_URL for tests, DATABASE_URL for dev). */\n url?: string;\n /** When true, never skip (CEDAR_PG_FORCE=1). */\n force?: boolean;\n /**\n * When true, skip acquire entirely.\n * Defaults from CEDAR_PG=0|false (opt-out adapters). Pass `false` for Cedar opt-in flows.\n */\n disabled?: boolean;\n};\n\n/**\n * Inject DATABASE_URL (and TEST_DATABASE_URL for test mode) from a resolved URL.\n * Single env path for acquire, clone, and external-url skip.\n */\nexport function applyDatabaseUrlEnv(\n databaseUrl: string,\n options: { mode?: \"dev\" | \"test\" } = {},\n): void {\n process.env.DATABASE_URL = databaseUrl;\n if ((options.mode ?? \"test\") === \"test\") {\n process.env.TEST_DATABASE_URL = databaseUrl;\n }\n}\n\nexport type RunIfNeededOptions = ResolveAcquireSkipInput & {\n mode: \"dev\" | \"test\";\n /** Default true (same as acquire / clone host wrappers). */\n setEnv?: boolean;\n};\n\nexport type RunIfNeededSkipped =\n | { status: \"skipped\"; reason: \"disabled\" }\n | { status: \"skipped\"; reason: \"external-url\"; databaseUrl: string };\n\nexport type RunIfNeededResult<T> = RunIfNeededSkipped | { status: \"ran\"; value: T };\n\n/**\n * Shared host skip+env gate for `acquireIfNeeded` / `cloneFromTemplateIfNeeded`.\n * On external-url skip, applies env when `setEnv` is not false.\n */\nexport async function runIfNeeded<T>(\n options: RunIfNeededOptions,\n run: () => Promise<T>,\n): Promise<RunIfNeededResult<T>> {\n const skip = resolveAcquireSkip({\n url: options.url,\n force: options.force,\n disabled: options.disabled,\n });\n if (skip.skip) {\n if (skip.reason === \"external-url\") {\n if (options.setEnv !== false) {\n applyDatabaseUrlEnv(skip.databaseUrl, { mode: options.mode });\n }\n return { status: \"skipped\", reason: \"external-url\", databaseUrl: skip.databaseUrl };\n }\n return { status: \"skipped\", reason: \"disabled\" };\n }\n return { status: \"ran\", value: await run() };\n}\n\n/**\n * True when the URL looks like a cedarpg provisioned database (`cpg_*` name/role).\n * These must never be treated as an external escape hatch; always re-acquire so\n * disposed/stale shell env cannot skip provisioning.\n */\nexport function isCedarPgManagedUrl(url: string | undefined): boolean {\n if (!url || url.startsWith(\"file:\")) return false;\n try {\n const parsed = new URL(url);\n const databaseName = decodeURIComponent(\n (parsed.pathname.replace(/^\\//, \"\").split(\"?\")[0] ?? \"\").trim(),\n );\n const user = decodeURIComponent(parsed.username);\n return databaseName.startsWith(\"cpg_\") || user.startsWith(\"cpg_\");\n } catch {\n return false;\n }\n}\n\n/**\n * True when `url` still looks like an unset dotenv/template value\n * (e.g. `postgresql://{yourMachine}@localhost:5432/app_test` or\n * `postgresql://<user>@localhost/db`), not a real external database.\n */\nfunction isUnsetTemplateUrl(url: string): boolean {\n // `{yourMachine}`, `{user}`, etc.\n if (/\\{[^}]+\\}/.test(url)) return true;\n // `<user>`, `<password>` style templates\n if (/<[^>]+>/.test(url)) return true;\n return false;\n}\n\n/**\n * True when `url` is a real external database and acquire should be skipped.\n * Sqlite `file:` URLs, cedarpg `cpg_*` URLs, and unset template placeholders\n * are not external.\n */\nexport function isExternalDatabaseEscapeHatch(url: string | undefined): boolean {\n if (!url) return false;\n if (url.startsWith(\"file:\")) return false;\n if (isCedarPgManagedUrl(url)) return false;\n if (isUnsetTemplateUrl(url)) return false;\n return true;\n}\n\n/**\n * Decide whether acquire should run.\n * Adapters may call with no args (env defaults). Cedar opt-in should pass `disabled: false`\n * and the relevant `url` (`DATABASE_URL` or `TEST_DATABASE_URL`).\n */\nexport function resolveAcquireSkip(input: ResolveAcquireSkipInput = {}): AcquireSkip {\n const disabled =\n input.disabled ?? (process.env.CEDAR_PG === \"0\" || process.env.CEDAR_PG === \"false\");\n if (disabled) {\n return { skip: true, reason: \"disabled\" };\n }\n\n const force = input.force ?? process.env.CEDAR_PG_FORCE === \"1\";\n if (force) {\n return { skip: false };\n }\n\n const url = input.url !== undefined ? input.url : process.env.TEST_DATABASE_URL;\n if (url && isExternalDatabaseEscapeHatch(url)) {\n return { skip: true, reason: \"external-url\", databaseUrl: url };\n }\n return { skip: false };\n}\n","import { createHash } from \"node:crypto\";\nimport { execFileSync, spawnSync } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport pg from \"pg\";\nimport { PASSWORD_SALT_PREFIX } from \"../core/constants.ts\";\n\nexport type AutopgDiscovery = {\n port: number;\n adminUrl: string;\n bin: string;\n};\n\nexport const INSTALL_HINT =\n \"autopg is required. Install with:\\n\" +\n \" curl -fsSL https://raw.githubusercontent.com/automagik-dev/autopg/main/install.sh | bash\\n\" +\n \"Then ensure ~/.local/bin is on PATH, or set AUTOPG_BIN.\";\n\n/** Password scheme v2: sha256(PASSWORD_SALT_PREFIX + \"\\\\0\" + roleName) hex[:32]. Frozen for URL rebuild. */\nexport const ROLE_PASSWORD_SCHEME = \"v2\" as const;\n\nfunction candidateBins(): string[] {\n const out: string[] = [];\n if (process.env.AUTOPG_BIN) out.push(process.env.AUTOPG_BIN);\n out.push(\"autopg\");\n out.push(join(homedir(), \".local\", \"bin\", \"autopg\"));\n return out;\n}\n\nexport function resolveAutopgBin(): string | null {\n for (const bin of candidateBins()) {\n if (bin === \"autopg\") {\n const which = spawnSync(\"which\", [\"autopg\"], { encoding: \"utf8\" });\n if (which.status === 0 && which.stdout.trim()) return which.stdout.trim();\n continue;\n }\n if (existsSync(bin)) return bin;\n }\n return null;\n}\n\nexport function requireAutopgBin(): string {\n const bin = resolveAutopgBin();\n if (!bin) {\n throw new Error(INSTALL_HINT);\n }\n return bin;\n}\n\n/**\n * Parse `autopg status --json` → the **registered** port. Throws only when the\n * output is not autopg status JSON.\n *\n * Registration is not liveness: autopg reports a port for a stopped host too,\n * and its `status` string is supervisor-specific (pm2 `online`, systemd-user /\n * launchd differ). Liveness is a TCP accept on the port, proven by the caller —\n * `acquire` does that before it connects.\n */\nexport function parseHostStatus(json: string): { port: number } {\n let parsed: { port?: unknown };\n try {\n parsed = JSON.parse(json) as typeof parsed;\n } catch {\n throw new Error(`autopg status --json returned invalid JSON.\\n${INSTALL_HINT}`);\n }\n if (typeof parsed.port !== \"number\") {\n throw new Error(`autopg status --json missing numeric port.\\n${INSTALL_HINT}`);\n }\n return { port: parsed.port };\n}\n\n/**\n * Admin URL for an autopg host on `port`.\n *\n * Credentials follow autopg's own defaults / env chain (not \"any local Postgres\"):\n * - user: `AUTOPG_PG_USER` / `PGSERVE_PG_USER` / `postgres`\n * - password: `AUTOPG_PG_PASSWORD` / `PGSERVE_PG_PASSWORD` / `postgres`\n *\n * Port is never scanned: callers pass the port from `autopg status` (attach) or the\n * ephemeral recipe (`55432`). That keeps us off unrelated local servers (e.g. brew on 5432).\n */\nexport function adminUrlFor(port: number, env: NodeJS.ProcessEnv = process.env): string {\n const user = encodeURIComponent(env.AUTOPG_PG_USER || env.PGSERVE_PG_USER || \"postgres\");\n const password = encodeURIComponent(\n env.AUTOPG_PG_PASSWORD || env.PGSERVE_PG_PASSWORD || \"postgres\",\n );\n return `postgresql://${user}:${password}@127.0.0.1:${port}/postgres`;\n}\n\n/**\n * Discover the registered autopg host (port + admin URL) via `autopg status --json`.\n * Throws when autopg cannot be queried; does **not** prove a listener — probe TCP\n * (or use `acquire`, which does) before connecting.\n */\nexport function discoverHost(bin = requireAutopgBin()): AutopgDiscovery {\n let status: string;\n try {\n status = execFileSync(bin, [\"status\", \"--json\"], {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n throw new Error(`Failed to query autopg status.\\n${detail}\\n${INSTALL_HINT}`);\n }\n const { port } = parseHostStatus(status);\n return { port, adminUrl: adminUrlFor(port), bin };\n}\n\nfunction quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`;\n}\n\nasync function withAdminClient<T>(\n adminUrl: string,\n fn: (client: pg.Client) => Promise<T>,\n): Promise<T> {\n const client = new pg.Client({ connectionString: adminUrl });\n await client.connect();\n try {\n return await fn(client);\n } finally {\n await client.end();\n }\n}\n\n/**\n * Deterministic local-only password for an app role (Prisma/TCP need it;\n * autopg hba uses `password` for 127.0.0.1).\n *\n * Keyed by `roleName` (not databaseName) so TEMPLATE clones that reuse the\n * same role keep working when `buildDatabaseUrl` is called with a new database.\n */\nexport function rolePasswordFor(roleName: string): string {\n return createHash(\"sha256\")\n .update(`${PASSWORD_SALT_PREFIX}\\0${roleName}`)\n .digest(\"hex\")\n .slice(0, 32);\n}\n\n/**\n * Idempotently CREATE ROLE + CREATE DATABASE with cedar-pg owned names.\n */\nexport async function ensureDatabase(opts: {\n adminUrl: string;\n databaseName: string;\n roleName: string;\n password?: string;\n}): Promise<void> {\n const password = opts.password ?? rolePasswordFor(opts.roleName);\n await withAdminClient(opts.adminUrl, async (client) => {\n const roleExists = await client.query(\"SELECT 1 FROM pg_roles WHERE rolname = $1\", [\n opts.roleName,\n ]);\n if (roleExists.rowCount === 0) {\n await client.query(\n `CREATE ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`,\n );\n } else {\n await client.query(\n `ALTER ROLE ${quoteIdent(opts.roleName)} WITH LOGIN PASSWORD ${quoteLiteral(password)}`,\n );\n }\n\n const dbExists = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [\n opts.databaseName,\n ]);\n if (dbExists.rowCount === 0) {\n await client.query(\n `CREATE DATABASE ${quoteIdent(opts.databaseName)} OWNER ${quoteIdent(opts.roleName)}`,\n );\n } else {\n await client.query(\n `ALTER DATABASE ${quoteIdent(opts.databaseName)} OWNER TO ${quoteIdent(opts.roleName)}`,\n );\n }\n });\n}\n\n/**\n * Mark (or unmark) a database as a PostgreSQL template (`IS_TEMPLATE`).\n * Template DBs cannot be dropped until unset.\n */\nexport async function setDatabaseIsTemplate(opts: {\n adminUrl: string;\n databaseName: string;\n isTemplate: boolean;\n}): Promise<void> {\n const flag = opts.isTemplate ? \"true\" : \"false\";\n await withAdminClient(opts.adminUrl, async (client) => {\n await client.query(`ALTER DATABASE ${quoteIdent(opts.databaseName)} WITH IS_TEMPLATE ${flag}`);\n });\n}\n\n/**\n * CREATE DATABASE … TEMPLATE … OWNER via admin connection.\n * Test roles are LOGIN-only; workers cannot CREATE DATABASE themselves.\n */\nexport async function cloneDatabaseFromTemplate(opts: {\n adminUrl: string;\n templateName: string;\n databaseName: string;\n roleName: string;\n}): Promise<void> {\n await withAdminClient(opts.adminUrl, async (client) => {\n const tmpl = await client.query<{ datistemplate: boolean }>(\n `SELECT datistemplate FROM pg_database WHERE datname = $1`,\n [opts.templateName],\n );\n if (!tmpl.rowCount) {\n throw new Error(`template database not found: ${opts.templateName}`);\n }\n if (!tmpl.rows[0]?.datistemplate) {\n throw new Error(`database is not a TEMPLATE; run markTemplate first: ${opts.templateName}`);\n }\n\n const exists = await client.query(\"SELECT 1 FROM pg_database WHERE datname = $1\", [\n opts.databaseName,\n ]);\n if (exists.rowCount && exists.rowCount > 0) {\n throw new Error(`database already exists: ${opts.databaseName}`);\n }\n await client.query(\n `CREATE DATABASE ${quoteIdent(opts.databaseName)} WITH TEMPLATE ${quoteIdent(opts.templateName)} OWNER ${quoteIdent(opts.roleName)}`,\n );\n });\n}\n\nasync function listOwnedDatnames(client: pg.Client, roleName: string): Promise<string[]> {\n const result = await client.query<{ datname: string }>(\n `SELECT datname FROM pg_database\n WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1)\n ORDER BY datname`,\n [roleName],\n );\n return result.rows.map((r) => r.datname);\n}\n\n/** Unset IS_TEMPLATE if needed, terminate backends, DROP DATABASE (no-op if missing). */\nasync function dropOneDatabase(client: pg.Client, databaseName: string): Promise<void> {\n const db = await client.query<{ datistemplate: boolean }>(\n `SELECT datistemplate FROM pg_database WHERE datname = $1`,\n [databaseName],\n );\n if (!db.rowCount || db.rowCount === 0) return;\n if (db.rows[0]?.datistemplate) {\n await client.query(`ALTER DATABASE ${quoteIdent(databaseName)} WITH IS_TEMPLATE false`);\n }\n await client.query(\n `\n SELECT pg_terminate_backend(pid)\n FROM pg_stat_activity\n WHERE datname = $1 AND pid <> pg_backend_pid()\n `,\n [databaseName],\n );\n await client.query(`DROP DATABASE IF EXISTS ${quoteIdent(databaseName)}`);\n}\n\n/**\n * DROP every database owned by `roleName` on one admin connection, then DROP ROLE.\n * When `preferLast` is owned, it is dropped after the other owned datnames (TEMPLATE after clones).\n * Never invents a DROP target beyond role ownership.\n */\nexport async function dropDatabasesOwnedByRole(opts: {\n adminUrl: string;\n roleName: string;\n preferLast?: string;\n}): Promise<string[]> {\n return withAdminClient(opts.adminUrl, async (client) => {\n const owned = await listOwnedDatnames(client, opts.roleName);\n const ordered = [\n ...owned.filter((name) => name !== opts.preferLast),\n ...owned.filter((name) => name === opts.preferLast),\n ];\n const dropped: string[] = [];\n for (const databaseName of ordered) {\n await dropOneDatabase(client, databaseName);\n dropped.push(databaseName);\n }\n await client.query(`DROP ROLE IF EXISTS ${quoteIdent(opts.roleName)}`);\n return dropped;\n });\n}\n\n/**\n * DROP DATABASE (unset IS_TEMPLATE, force terminate backends) + DROP ROLE\n * when the role owns no remaining databases.\n */\nexport async function dropDatabase(opts: {\n adminUrl: string;\n databaseName: string;\n roleName: string;\n}): Promise<void> {\n await withAdminClient(opts.adminUrl, async (client) => {\n await dropOneDatabase(client, opts.databaseName);\n const owns = await client.query(\n `SELECT 1 FROM pg_database WHERE datdba = (SELECT oid FROM pg_roles WHERE rolname = $1) LIMIT 1`,\n [opts.roleName],\n );\n if (owns.rowCount === 0) {\n await client.query(`DROP ROLE IF EXISTS ${quoteIdent(opts.roleName)}`);\n }\n });\n}\n\nexport function buildDatabaseUrl(opts: {\n port: number;\n databaseName: string;\n roleName: string;\n password?: string;\n}): string {\n const password = opts.password ?? rolePasswordFor(opts.roleName);\n const user = encodeURIComponent(opts.roleName);\n const pass = encodeURIComponent(password);\n return `postgresql://${user}:${pass}@127.0.0.1:${opts.port}/${opts.databaseName}`;\n}\n","import { spawn, spawnSync, type ChildProcess } from \"node:child_process\";\nimport { existsSync, mkdirSync, readdirSync, rmSync, statfsSync } from \"node:fs\";\nimport { createConnection } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport {\n adminUrlFor,\n discoverHost,\n INSTALL_HINT,\n requireAutopgBin,\n type AutopgDiscovery,\n} from \"./autopg.ts\";\n\n/** How cedar-pg brings up an autopg host when nothing is listening. */\nexport type HostStartPolicy = \"local\" | \"ephemeral\";\n\nexport type EphemeralHostRecipe = {\n dataDir: string;\n port: number;\n postmasterArgs: string[];\n};\n\n/** Inputs for {@link ephemeralHostRecipe} (production uses process defaults). */\nexport type EphemeralRecipeContext = {\n platform?: NodeJS.Platform;\n shmAvailable?: boolean;\n tmpDir?: string;\n uid?: number;\n port?: number;\n};\n\nconst EPHEMERAL_PORT = 55432;\n\nconst HOST_READY_MS = 30_000;\nconst HOST_POLL_MS = 200;\n/** `pm2 restart` of an existing process is quick; 30s is only for first `install` / initdb. */\nconst LOCAL_RESTART_READY_MS = 10_000;\n\n/**\n * autopg verbs that can bring the *registered* local host up, cheapest fix first.\n *\n * `restart` exit 0 is not evidence of a listener: when pm2 is missing or does not\n * list `autopg-server`, autopg still prints \"respawned daemon\" and returns 0.\n * That is why every verb is followed by a TCP wait, not trusted on exit status.\n *\n * `install` covers a never-registered machine (postinstall ships the binary only)\n * and a reboot whose pm2 list is empty (`pm2 start`). On an already-registered\n * host it is a no-op for the server process but may `pm2 start` the autopg UI —\n * so it runs only after `restart` failed to produce a listener.\n */\nconst LOCAL_START = [\n { argv: [\"restart\"], readyMs: LOCAL_RESTART_READY_MS },\n { argv: [\"install\"], readyMs: HOST_READY_MS },\n] as const;\n\n/** Soft minimum free bytes on /dev/shm before RAM-backed ephemeral start (warns only). */\nexport const EPHEMERAL_SHM_MIN_FREE_BYTES = 512 * 1024 * 1024;\n\n/**\n * Hint when ephemeral `--ram` initdb fails for space / leftover dirs under `/dev/shm`.\n * Cloud VMs often ship with a tiny default tmpfs (e.g. 64MB).\n */\nexport const EPHEMERAL_SHM_HINT =\n \"Ephemeral autopg uses /dev/shm with --ram. If initdb fails with Disk quota / No space (Postgres 53100):\\n\" +\n \" 1. Enlarge tmpfs (cloud VMs often default to ~64MB): sudo mount -o remount,size=6G /dev/shm\\n\" +\n \" 2. Clear leftovers from OOM-killed runs (your test data only):\\n\" +\n \" rm -rf /dev/shm/cedar-pg-* /dev/shm/pgserve-* /dev/shm/PostgreSQL.*\";\n\n/**\n * Resolve how to start a host when none is listening.\n *\n * - `CEDAR_PG_EPHEMERAL_HOST=1` → ephemeral owned postmaster\n * - `CEDAR_PG_EPHEMERAL_HOST=0` → never own a postmaster, local autopg only (even in CI)\n * - unset + `CI=true` → ephemeral\n * - otherwise → local\n */\nexport function resolveHostStartPolicy(env: NodeJS.ProcessEnv = process.env): HostStartPolicy {\n const force = env.CEDAR_PG_EPHEMERAL_HOST;\n if (force === \"1\") return \"ephemeral\";\n if (force === \"0\") return \"local\";\n if (env.CI === \"true\") return \"ephemeral\";\n return \"local\";\n}\n\nexport function ephemeralHostRecipe(ctx: EphemeralRecipeContext = {}): EphemeralHostRecipe {\n const platform = ctx.platform ?? process.platform;\n const shmAvailable = ctx.shmAvailable ?? (platform === \"linux\" && existsSync(\"/dev/shm\"));\n const uid = ctx.uid ?? process.getuid?.() ?? 0;\n const port = ctx.port ?? EPHEMERAL_PORT;\n const useRam = platform === \"linux\" && shmAvailable;\n const dataDir = useRam\n ? `/dev/shm/cedar-pg-${uid}`\n : join(ctx.tmpDir ?? tmpdir(), \"cedar-pg-host\");\n\n const postmasterArgs = [\n \"postmaster\",\n ...(useRam ? [\"--ram\"] : []),\n \"--port\",\n String(port),\n \"--socket-dir\",\n dataDir,\n \"--data\",\n dataDir,\n ];\n\n return { dataDir, port, postmasterArgs };\n}\n\nexport function discoveryFromRecipe(bin: string, recipe: EphemeralHostRecipe): AutopgDiscovery {\n return { port: recipe.port, adminUrl: adminUrlFor(recipe.port), bin };\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** True when stderr/stdout looks like a /dev/shm quota or ENOSPC failure. */\nexport function looksLikeShmSpaceError(text: string): boolean {\n return /Disk quota exceeded|No space left on device|ENOSPC|\\b53100\\b/i.test(text);\n}\n\nexport function formatHostStartError(detail: string, useRamShm = false): string {\n const hint = useRamShm && looksLikeShmSpaceError(detail) ? `\\n${EPHEMERAL_SHM_HINT}` : \"\";\n return `Failed to start autopg host.\\n${detail}\\n${INSTALL_HINT}${hint}`;\n}\n\n/**\n * Remove leftover ephemeral data dirs under `/dev/shm` (and the recipe dataDir).\n * Safe for OOM-killed CI/cloud runs that leave `cedar-pg-*` / `pgserve-*` /\n * `PostgreSQL.*` filling tmpfs.\n *\n * Only called on ephemeral cold start, i.e. when the recipe port has no listener —\n * never while a host owns those dirs.\n */\nexport function pruneStaleEphemeralDataDirs(opts: {\n dataDir: string;\n /** Parent of RAM dirs; default `/dev/shm` when it exists. */\n shmRoot?: string;\n}): string[] {\n const removed: string[] = [];\n const tryRm = (path: string) => {\n if (!existsSync(path)) return;\n try {\n rmSync(path, { recursive: true, force: true });\n removed.push(path);\n } catch {\n // best-effort: next initdb will surface a clearer error\n }\n };\n\n tryRm(opts.dataDir);\n\n const shmRoot = opts.shmRoot ?? (existsSync(\"/dev/shm\") ? \"/dev/shm\" : undefined);\n if (!shmRoot) return removed;\n\n let entries: string[] = [];\n try {\n entries = readdirSync(shmRoot);\n } catch {\n return removed;\n }\n\n for (const name of entries) {\n if (\n name.startsWith(\"cedar-pg-\") ||\n name.startsWith(\"pgserve-\") ||\n name.startsWith(\"PostgreSQL.\")\n ) {\n tryRm(join(shmRoot, name));\n }\n }\n return removed;\n}\n\n/** Free bytes on `path`, or `null` if unavailable. */\nfunction freeBytesOn(path: string): number | null {\n try {\n const s = statfsSync(path);\n return Number(s.bavail) * Number(s.bsize);\n } catch {\n return null;\n }\n}\n\n/** Run a one-shot autopg verb. Returns failure detail, or `null` on exit 0. */\nfunction runAutopg(bin: string, argv: string[]): string | null {\n const result = spawnSync(bin, argv, {\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n if (result.error) return result.error.message;\n if (result.status === 0) return null;\n return `exit ${result.status}\\n${(result.stderr || result.stdout || \"\").trim()}`.trim();\n}\n\n/** True when something accepts TCP on 127.0.0.1:port (postmaster live, not just admin.json). */\nfunction canConnect(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ host: \"127.0.0.1\", port }, () => {\n socket.end();\n resolve(true);\n });\n socket.on(\"error\", () => {\n resolve(false);\n });\n });\n}\n\nfunction killOwnedChild(child: ChildProcess): void {\n const pid = child.pid;\n if (pid != null) {\n try {\n process.kill(-pid, \"SIGTERM\");\n } catch {\n try {\n child.kill(\"SIGTERM\");\n } catch {\n // already exited\n }\n }\n }\n child.unref();\n}\n\nexport type WaitForListenerOptions = {\n port: number;\n /** Total grace period; `0` (default) probes exactly once. */\n readyMs?: number;\n pollMs?: number;\n canConnect?: (port: number) => Promise<boolean>;\n now?: () => number;\n sleep?: (ms: number) => Promise<void>;\n /** Return an Error to abort early (spawn error / child exit). */\n failure?: () => Error | null;\n};\n\n/**\n * The single liveness primitive: TCP accept on 127.0.0.1:port.\n *\n * Resolves `true` as soon as something is listening, `false` when the grace\n * period expires, and throws only what {@link WaitForListenerOptions.failure}\n * returns. Used for attach (one probe), local restart, and ephemeral start.\n */\nexport async function waitForListener(opts: WaitForListenerOptions): Promise<boolean> {\n const probe = opts.canConnect ?? canConnect;\n const now = opts.now ?? Date.now;\n const pause = opts.sleep ?? sleep;\n const pollMs = opts.pollMs ?? HOST_POLL_MS;\n const deadline = now() + (opts.readyMs ?? 0);\n\n for (;;) {\n const fail = opts.failure?.();\n if (fail) throw fail;\n if (await probe(opts.port)) return true;\n if (now() >= deadline) return false;\n await pause(pollMs);\n }\n}\n\n/**\n * Attach to the registered autopg host once TCP accepts on its port.\n *\n * Registration is not liveness: a stopped pm2 host still reports a port, and\n * attaching to it is the `ECONNREFUSED 127.0.0.1:25432` bug. `readyMs` > 0 is\n * the grace period after asking autopg to start.\n */\nasync function attachLiveHost(bin: string, readyMs = 0): Promise<AutopgDiscovery | null> {\n let discovered: AutopgDiscovery;\n try {\n discovered = discoverHost(bin);\n } catch {\n return null;\n }\n return (await waitForListener({ port: discovered.port, readyMs })) ? discovered : null;\n}\n\n/**\n * Bring the user's registered autopg host up and attach to it — same port, same\n * `~/.autopg/data`, still there after this process exits. cedar-pg never runs a\n * second local Postgres.\n */\nasync function startLocalHost(bin: string, registeredPort?: number): Promise<AutopgDiscovery> {\n const failures: string[] = [];\n\n for (const { argv, readyMs } of LOCAL_START) {\n const label = `autopg ${argv.join(\" \")}`;\n const failure = runAutopg(bin, [...argv]);\n if (failure) {\n failures.push(`${label}: ${failure}`);\n continue;\n }\n const attached = await attachLiveHost(bin, readyMs);\n if (attached) return attached;\n failures.push(`${label}: no listener within ${readyMs}ms`);\n }\n\n const where =\n registeredPort != null\n ? `autopg host is registered on 127.0.0.1:${registeredPort} but nothing is listening.\\n`\n : \"\";\n throw new Error(formatHostStartError(`${where}${failures.join(\"\\n\")}`));\n}\n\n/**\n * Ephemeral host owned by this process/job: detached `autopg postmaster` with\n * fully ignored stdio (caller exit must not close pipes under the daemon).\n *\n * Never runs `autopg install` — that rewrites `~/.autopg/admin.json` and fails\n * with `supervisor mismatch` next to a local pm2 install. Reuses a listener\n * already on the recipe port, and prunes stale RAM dirs before a cold start.\n */\nasync function startEphemeralHost(bin: string): Promise<AutopgDiscovery> {\n const recipe = ephemeralHostRecipe();\n if (await canConnect(recipe.port)) return discoveryFromRecipe(bin, recipe);\n\n const useRamShm = recipe.postmasterArgs.includes(\"--ram\");\n pruneStaleEphemeralDataDirs({ dataDir: recipe.dataDir });\n\n if (useRamShm) {\n const free = freeBytesOn(\"/dev/shm\");\n if (free != null && free < EPHEMERAL_SHM_MIN_FREE_BYTES) {\n process.stderr.write(\n `[cedar-pg] warning: /dev/shm has ~${Math.round(free / (1024 * 1024))}MB free ` +\n `(recommend ≥${Math.round(EPHEMERAL_SHM_MIN_FREE_BYTES / (1024 * 1024))}MB for --ram).\\n` +\n `${EPHEMERAL_SHM_HINT}\\n`,\n );\n }\n }\n\n mkdirSync(recipe.dataDir, { recursive: true });\n\n const child = spawn(bin, recipe.postmasterArgs, { detached: true, stdio: \"ignore\" });\n let died: Error | null = null;\n child.on(\"error\", (err) => {\n died = new Error(\n formatHostStartError(`Failed to spawn autopg postmaster.\\n${err.message}`, useRamShm),\n );\n });\n child.on(\"exit\", (code, signal) => {\n died ??= new Error(\n formatHostStartError(\n `autopg postmaster exited before ready (code=${code}, signal=${signal}).`,\n useRamShm,\n ),\n );\n });\n\n try {\n const listening = await waitForListener({\n port: recipe.port,\n readyMs: HOST_READY_MS,\n failure: () => died,\n });\n if (!listening) {\n throw new Error(\n formatHostStartError(\n `autopg postmaster did not accept 127.0.0.1:${recipe.port} within ${HOST_READY_MS}ms.`,\n useRamShm,\n ),\n );\n }\n } catch (err) {\n killOwnedChild(child);\n throw err;\n }\n\n // Success: the process/job owns lifetime — drop our handle without killing.\n child.unref();\n return discoveryFromRecipe(bin, recipe);\n}\n\n/**\n * Attach to a listening autopg host, else start one per\n * {@link resolveHostStartPolicy}. Internal: callers use `acquire` / `adminUrl`.\n */\nexport async function ensureHostRunning(bin = requireAutopgBin()): Promise<AutopgDiscovery> {\n let registered: AutopgDiscovery | null = null;\n try {\n registered = discoverHost(bin);\n } catch {\n // no registration — local recovery may `install`\n }\n if (registered && (await waitForListener({ port: registered.port }))) return registered;\n\n return resolveHostStartPolicy() === \"ephemeral\"\n ? startEphemeralHost(bin)\n : startLocalHost(bin, registered?.port);\n}\n","import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { buildDatabaseName, buildRoleName, type DbMode } from \"./naming.ts\";\nimport {\n envPath,\n forgetLease,\n isOrphanLease,\n leaseDir,\n listRegistryLeases,\n readLease,\n writeLease,\n type Lease,\n} from \"./lease.ts\";\nimport { applyDatabaseUrlEnv, runIfNeeded, type ResolveAcquireSkipInput } from \"./policy.ts\";\nimport { resolveWorktreeIdentity } from \"./worktree.ts\";\nimport { buildDatabaseUrl, dropDatabasesOwnedByRole, ensureDatabase } from \"../providers/autopg.ts\";\nimport { ensureHostRunning } from \"../providers/host.ts\";\n\nfunction writeEnvFile(root: string, mode: DbMode, databaseUrl: string): void {\n mkdirSync(leaseDir(root), { recursive: true, mode: 0o700 });\n let body = `DATABASE_URL=${databaseUrl}\\n`;\n if (mode === \"test\") body += `TEST_DATABASE_URL=${databaseUrl}\\n`;\n writeFileSync(envPath(root, mode), body, { mode: 0o600 });\n}\n\nexport function urlFromLease(lease: Lease): string {\n return buildDatabaseUrl({\n port: lease.port,\n databaseName: lease.databaseName,\n roleName: lease.roleName,\n });\n}\n\n/**\n * DROP every DB owned by the lease role (TEMPLATE + clones), then forget lease.\n * Provider owns ordering (clones before leased datname) on one admin connection.\n */\nasync function dropThenForget(lease: Lease, adminUrl: string): Promise<string[]> {\n const dropped = await dropDatabasesOwnedByRole({\n adminUrl,\n roleName: lease.roleName,\n preferLast: lease.databaseName,\n });\n forgetLease(lease);\n return dropped;\n}\n\nexport type AcquireOptions = {\n root?: string;\n mode: DbMode;\n /** Inject DATABASE_URL / TEST_DATABASE_URL into process.env (default true). */\n setEnv?: boolean;\n};\n\nexport type AcquireResult = {\n databaseUrl: string;\n /** Superuser URL for privileged DDL (mark template, CREATE DATABASE … TEMPLATE). */\n adminUrl: string;\n databaseName: string;\n roleName: string;\n repoSlug: string;\n worktreeSlug: string;\n pathHash: string;\n root: string;\n mode: DbMode;\n port: number;\n dispose: () => Promise<void>;\n};\n\n/**\n * Acquire a worktree-scoped database and return connection info.\n *\n * - `dev`: keep DB across restarts.\n * - `test`: DROP when `dispose()` is awaited (callers / test runners own teardown).\n */\nexport async function acquire(options: AcquireOptions): Promise<AcquireResult> {\n const identity = resolveWorktreeIdentity(options.root);\n const mode = options.mode;\n const databaseName = buildDatabaseName(identity, mode);\n const roleName = buildRoleName(databaseName);\n\n const host = await ensureHostRunning();\n await ensureDatabase({\n adminUrl: host.adminUrl,\n databaseName,\n roleName,\n });\n\n const databaseUrl = buildDatabaseUrl({\n port: host.port,\n databaseName,\n roleName,\n });\n\n const lease: Lease = {\n schemaVersion: 1,\n mode,\n root: identity.root,\n repoSlug: identity.repoSlug,\n worktreeSlug: identity.worktreeSlug,\n pathHash: identity.pathHash,\n databaseName,\n roleName,\n port: host.port,\n pid: process.pid,\n createdAt: new Date().toISOString(),\n };\n writeLease(lease);\n writeEnvFile(identity.root, mode, databaseUrl);\n\n if (options.setEnv !== false) {\n applyDatabaseUrlEnv(databaseUrl, { mode });\n }\n\n const disposeFn = async () => {\n await dispose({ root: identity.root, mode });\n };\n\n return {\n databaseUrl,\n adminUrl: host.adminUrl,\n databaseName,\n roleName,\n repoSlug: identity.repoSlug,\n worktreeSlug: identity.worktreeSlug,\n pathHash: identity.pathHash,\n root: identity.root,\n mode,\n port: host.port,\n dispose: disposeFn,\n };\n}\n\nexport type AcquireIfNeededOptions = AcquireOptions & ResolveAcquireSkipInput;\n\nexport type AcquireIfNeededResult =\n | { status: \"skipped\"; reason: \"disabled\" }\n | { status: \"skipped\"; reason: \"external-url\"; databaseUrl: string }\n | ({ status: \"acquired\" } & AcquireResult);\n\n/**\n * Resolve skip policy then acquire. Single entry for hosts (Cedar CLI, Jest, Vitest).\n * On external-url skip, applies DATABASE_URL / TEST_DATABASE_URL when `setEnv` is not false.\n */\nexport async function acquireIfNeeded(\n options: AcquireIfNeededOptions,\n): Promise<AcquireIfNeededResult> {\n const outcome = await runIfNeeded(options, () =>\n acquire({\n root: options.root,\n mode: options.mode,\n setEnv: options.setEnv,\n }),\n );\n if (outcome.status === \"skipped\") return outcome;\n return { status: \"acquired\", ...outcome.value };\n}\n\nexport type DisposeOptions = {\n root?: string;\n mode?: DbMode;\n};\n\nexport type DisposeResult =\n | { dropped: true; databaseName: string; droppedDatabases: string[] }\n | { dropped: false; reason: \"no-lease\" | \"host-unavailable\" };\n\n/**\n * Role-scoped suite teardown: DROP every database owned by the lease role\n * (TEMPLATE + clones), then DROP ROLE and forget the lease. Unsets `IS_TEMPLATE`\n * as needed. This is not per-clone cleanup — use `CloneResult.dropClone` for that.\n * No-ops without a valid lease; never invents a DROP target beyond role ownership.\n * If the host is unavailable, leaves the lease so dispose/gc can retry.\n */\nexport async function dispose(options: DisposeOptions = {}): Promise<DisposeResult> {\n const identity = resolveWorktreeIdentity(options.root);\n const mode = options.mode ?? \"test\";\n const lease = readLease(identity.root, mode);\n if (!lease) return { dropped: false, reason: \"no-lease\" };\n\n let host;\n try {\n host = await ensureHostRunning();\n } catch {\n // Keep lease + registry so a later dispose/gc can still find the DB.\n return { dropped: false, reason: \"host-unavailable\" };\n }\n\n const droppedDatabases = await dropThenForget(lease, host.adminUrl);\n return {\n dropped: true,\n databaseName: lease.databaseName,\n droppedDatabases,\n };\n}\n\n/**\n * Drop databases whose registered worktree root no longer exists on disk.\n * Registry entries are removed only after a successful DROP (owned DBs + lease DB).\n */\nexport async function gc(): Promise<{\n dropped: string[];\n}> {\n const dropped: string[] = [];\n const orphans = listRegistryLeases().filter(isOrphanLease);\n if (orphans.length === 0) return { dropped };\n\n let host;\n try {\n host = await ensureHostRunning();\n } catch {\n return { dropped };\n }\n\n for (const lease of orphans) {\n try {\n const names = await dropThenForget(lease, host.adminUrl);\n dropped.push(...names);\n } catch {\n // Keep registry entry for retry\n }\n }\n return { dropped };\n}\n"],"mappings":"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"}
|