pg-here 0.1.5 → 0.1.9

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 CHANGED
@@ -1,335 +1,92 @@
1
1
  # pg-here
2
2
 
3
- Per-project Postgres instances with instant snapshot & restore to support yolo development methods.
3
+ Run a local PostgreSQL instance in your project folder with one command.
4
4
 
5
- Repository: https://github.com/mayfer/pg-here
6
-
7
- ## Project-Local PostgreSQL Setup
8
-
9
- Each project runs its own isolated PostgreSQL instance. Downloads correct binary automatically for your CPU architecture (x86_64 / arm64). Database lives inside the project folder.
10
-
11
- ### Quick Start
12
-
13
- Start PostgreSQL:
14
-
15
- ```bash
16
- bun run db:up
17
- ```
18
-
19
- Output:
20
- ```
21
- postgresql://postgres:postgres@localhost:55432/postgres
22
- ```
23
-
24
- Connect from your app using that connection string, then Ctrl+C to stop.
25
-
26
- ### One-command start from npm (no local install)
27
-
28
- If you have Bun installed, you can run the published package directly:
5
+ ## 30-second start
29
6
 
30
7
  ```bash
31
8
  bunx pg-here
32
9
  ```
33
10
 
34
- This is the single command you want from any directory.
35
-
36
- If it still resolves an older release, the package `latest` dist-tag is behind:
11
+ Default output:
37
12
 
38
- To force that version and bypass a stale cache:
39
-
40
- ```bash
41
- bunx pg-here@0.1.5
13
+ ```text
14
+ Launching PostgreSQL 18.0.0 into new pg_local/
15
+ psql postgresql://postgres:postgres@localhost:55432/postgres
42
16
  ```
43
17
 
44
- If maintainers want plain `bunx pg-here` to work for everyone, run once:
18
+ If a data folder already exists:
45
19
 
46
- ```bash
47
- npm dist-tag add pg-here@0.1.5 latest
20
+ ```text
21
+ Reusing existing pg_local/data/ with PostgreSQL 18.0.0
22
+ psql postgresql://postgres:postgres@localhost:55432/postgres
48
23
  ```
49
24
 
50
- After that, this should work anywhere:
25
+ If the cached folder version differs:
51
26
 
52
- ```bash
53
- bunx pg-here
27
+ ```text
28
+ Reusing existing pg_local/data/ (pg_local/bin has 18.0.0, running PostgreSQL is 18.0)
29
+ psql postgresql://postgres:postgres@localhost:55432/postgres
54
30
  ```
55
31
 
56
- This starts a local PostgreSQL instance in your current project directory and prints the connection string, then keeps the process alive until you stop it.
32
+ The process stays alive until you stop it.
33
+ Ctrl+C → exits and stops Postgres.
34
+
35
+ ## Defaults (all args optional)
57
36
 
58
- `bunx pg-here` uses these defaults if you pass nothing:
59
- - `username=postgres`
60
- - `password=postgres`
61
- - `database=postgres`
62
- - `port=55432`
37
+ `bunx pg-here`
63
38
 
64
- All args are optional.
39
+ - `username = postgres`
40
+ - `password = postgres`
41
+ - `database = postgres`
42
+ - `port = 55432`
43
+ - `pg-version` = auto
65
44
 
66
- Pass CLI flags just like the local script when you want to override defaults:
45
+ ## Custom run
67
46
 
68
47
  ```bash
69
- bunx pg-here --username postgres --password postgres --database my_app --port 55432
48
+ bunx pg-here --username me --password secret --database my_app --port 55433 --pg-version 17.0.0
70
49
  ```
71
50
 
72
- #### Linux note
73
-
74
- If `bunx pg-here` fails with `error while loading shared libraries`:
75
- - install missing system packages on the host (not in npm), then retry
76
- - restart the command
77
-
78
- Most commonly:
51
+ You can also run locally in this repo:
79
52
 
80
53
  ```bash
81
- # Ubuntu/Debian
82
- sudo apt-get update && sudo apt-get install -y libxml2
83
-
84
- # Fedora/RHEL
85
- sudo dnf install -y libxml2
86
-
87
- # Alpine
88
- sudo apk add libxml2
54
+ bun run db:up
89
55
  ```
90
56
 
91
- If that machine is intentionally minimal (or containerized), use an image with PostgreSQL runtime dependencies.
92
-
93
- ### Programmatic usage
57
+ Same CLI flags are supported.
94
58
 
95
- Use `pg-here` directly from your server startup code and auto-create the app database if missing.
59
+ ## Programmatic
96
60
 
97
61
  ```ts
98
62
  import { startPgHere } from "pg-here";
99
63
 
100
- const pgHere = await startPgHere({
64
+ const pg = await startPgHere({
101
65
  projectDir: process.cwd(),
102
- port: 55432,
103
66
  database: "my_app",
104
67
  createDatabaseIfMissing: true,
105
- postgresVersion: "18.0.0",
106
68
  });
107
69
 
108
- console.log(pgHere.databaseConnectionString);
109
-
110
- // On shutdown:
111
- await pgHere.stop();
70
+ console.log(pg.databaseConnectionString); // psql-ready URL
71
+ await pg.stop();
112
72
  ```
113
73
 
114
- `databaseConnectionString` points to your target DB (`my_app` above).
115
- If the DB does not exist yet, `createDatabaseIfMissing: true` creates it on startup.
116
- Set `postgresVersion` if you want to pin/select a specific PostgreSQL version.
117
- By default, `startPgHere()` installs SIGINT/SIGTERM shutdown hooks that stop Postgres when
118
- your process exits, and `stop()` preserves data (no cluster cleanup/delete).
119
- Use `await pgHere.cleanup()` only when you explicitly want full resource cleanup.
120
- `pg_stat_statements` is enabled automatically (`shared_preload_libraries` + extension creation).
121
- Set `enablePgStatStatements: false` to opt out.
74
+ ## Linux runtime error (quick fix)
122
75
 
123
- ### CLI Options
76
+ If startup fails with missing `libxml2` libraries, install runtime packages and retry:
124
77
 
125
78
  ```bash
126
- # Custom credentials
127
- bun run db:up --username postgres --password postgres
128
-
129
- # Short flags
130
- bun run db:up -u postgres -p postgres
131
-
132
- # Custom port
133
- bun run db:up --port 55433
134
-
135
- # All together
136
- bun run db:up -u postgres -p postgres --database postgres --port 55433
137
-
138
- # Pin postgres version
139
- bun run db:up --pg-version 18.0.0
140
- ```
141
-
142
- **Defaults**: username=`postgres`, password=`postgres`, port=`55432`, pg-version=`PG_VERSION` or pg-embedded default
143
-
144
- ### Project Structure
145
-
146
- ```
147
- project/
148
- pg_local/
149
- data/ # PostgreSQL data cluster (persists between runs)
150
- bin/ # Downloaded PostgreSQL binaries
151
- scripts/
152
- pg-dev.mjs # Runner script
153
- package.json
154
- ```
155
-
156
- ### How It Works
157
-
158
- - PostgreSQL only runs when you execute `bun run db:up`
159
- - Correct architecture binary downloads automatically on first run
160
- - Data persists in `pg_local/data/` across restarts
161
- - Process stops completely on exit (Ctrl+C)
162
- - One instance per project, no system PostgreSQL dependency
163
-
164
- ---
165
-
166
- ## Use this from another project (recommended)
167
-
168
- Keep this repo in one place, and point it at any other project directory when you want a dedicated Postgres instance there.
169
-
170
- Example: your app lives at `/path/to/my-app`, but this repo lives elsewhere.
171
-
172
- ```
173
- # start postgres for that project (one-time init happens automatically)
174
- bun run snapshot snapshot /path/to/my-app/.pg-here
175
-
176
- # list snapshots for that project
177
- bun run snapshot list /path/to/my-app/.pg-here
178
-
179
- # revert that project to a snapshot
180
- bun run revert /path/to/my-app/.pg-here snap_YYYYMMDD_HHMMSS
79
+ sudo apt-get update && sudo apt-get install -y libxml2 libxml2-utils
80
+ sudo dnf install -y libxml2
81
+ sudo apk add libxml2
181
82
  ```
182
83
 
183
- Tips:
184
- - Pick a per-project folder (e.g. `.pg-here`) and reuse it.
185
- - The project directory just needs to be on the same APFS volume for clones to be fast.
186
- - You can also pass the directory with `--project/-p` instead of positional.
187
-
188
- To install dependencies:
84
+ This release already retries startup with a project-local `libxml2` compatibility fallback when needed.
189
85
 
190
- ```bash
191
- bun install
192
- ```
86
+ ## Version pin / stale cache
193
87
 
194
- To run:
88
+ If your environment keeps resolving an older release, force a specific version:
195
89
 
196
90
  ```bash
197
- bun run index.ts
91
+ bunx pg-here@0.1.9
198
92
  ```
199
-
200
- This project was created using `bun init` in bun v1.3.1. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
201
-
202
- ## APFS clone snapshots (macOS) — insanely simple
203
-
204
- ### The 30‑second version
205
-
206
- ```
207
- # take a snapshot for the default project directory
208
- bun run snapshot snapshot
209
-
210
- # list snapshots
211
- bun run snapshot list
212
-
213
- # revert to a snapshot (copy from list output)
214
- bun run snapshot revert snap_YYYYMMDD_HHMMSS
215
-
216
- # or use the alias
217
- bun run revert snap_YYYYMMDD_HHMMSS
218
- ```
219
-
220
- By default, snapshots live under `./pg_projects/default`. You can pass a project directory explicitly if you want.
221
-
222
- ### What this does
223
-
224
- - Uses APFS copy‑on‑write clones (`cp -cR` / `ditto --clone`)
225
- - Stops Postgres during snapshot/revert (cold stop/start)
226
- - Keeps snapshots immutable and restores into new instances
227
-
228
- ### Why it's great
229
-
230
- - Near‑instant snapshot and revert via copy‑on‑write
231
- - Space grows only with changed blocks
232
- - No volume‑wide rollback
233
- - No WAL/PITR complexity
234
- - macOS‑native, zero extra services
235
- - Deterministic failure modes
236
-
237
- ### Operational constraints
238
-
239
- - PostgreSQL must be stopped for snapshot/revert
240
- - Source and destination must be on the same APFS volume
241
- - One cluster per project
242
-
243
- ### Under the hood layout
244
-
245
- ```
246
- ~/pg/proj/
247
- current -> instances/inst_active
248
- instances/
249
- snaps/
250
- ```
251
-
252
- ### Full commands (helper script)
253
-
254
- ```
255
- # snapshot current cluster
256
- bun run snapshot snapshot /path/to/project
257
-
258
- # list snapshots
259
- bun run snapshot list /path/to/project
260
-
261
- # revert to a snapshot
262
- bun run snapshot revert /path/to/project snap_YYYYMMDD_HHMMSS
263
- ```
264
-
265
- Flags (optional):
266
-
267
- ```
268
- --project/-p project directory (same as positional projectDir)
269
- --snap/-s snapshot name (for revert)
270
- --pg-ctl path to pg_ctl (overrides PG_CTL)
271
- ```
272
-
273
- If `pg_ctl` isn't on your `PATH`, set `PG_CTL`. By default the script looks in `./pg_local/bin/*/bin/pg_ctl`.
274
-
275
- ### One‑shot test (does everything end‑to‑end)
276
-
277
- ```
278
- bun run snapshot:test
279
- ```
280
-
281
- This:
282
- 1) Starts a temporary cluster
283
- 2) Writes sample data
284
- 3) Snapshots
285
- 4) Mutates data
286
- 5) Restores
287
- 6) Verifies the original data returns
288
-
289
- Optional flags:
290
-
291
- ```
292
- --project/-p project directory (default: ./pg_projects/apfs_test_TIMESTAMP)
293
- --port postgres port (default: 55433 or PGPORT_SNAPSHOT_TEST)
294
- --pg-version postgres version (default: PG_VERSION or pg-embedded default)
295
- --keep keep the project directory after the test
296
- ```
297
-
298
- ### Bun test integration
299
-
300
- ```
301
- bun test
302
- bun run test:apfs
303
- ```
304
-
305
- Set `SKIP_APFS_TEST=1` to skip the APFS snapshot test.
306
-
307
- ## APFS clone speed benchmark
308
-
309
- Compares clone time between a small and large dataset by seeding a table and cloning the data directory.
310
-
311
- ```
312
- bun run bench:apfs
313
- ```
314
-
315
- Optional flags:
316
-
317
- ```
318
- --project/-p project directory (default: ./pg_projects/bench)
319
- --port postgres port (default: 55434 or PGPORT_BENCH)
320
- --small-rows rows for small dataset (default: 50_000)
321
- --large-rows rows for large dataset (default: 2_000_000)
322
- --row-bytes payload bytes per row (default: 256)
323
- --pg-version postgres version (default: PG_VERSION or pg-embedded default)
324
- ```
325
-
326
- Example:
327
-
328
- ```
329
- bun run bench:apfs --small-rows 100000 --large-rows 5000000 --row-bytes 512
330
- ```
331
-
332
- ### When to choose something else
333
-
334
- - Need online "rewind to 5 minutes ago" repeatedly → base backup + WAL/PITR
335
- - Dataset ≥ ~50 GB with heavy churn → dedicated APFS volume + volume snapshots, or move DB off the laptop
package/bin/pg-here.mjs CHANGED
@@ -3,7 +3,12 @@
3
3
  import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
5
  import { startPgHere } from "../dist/index.js";
6
- import { maybePrintLinuxRuntimeHelp } from "../scripts/cli-error-help.mjs";
6
+ import {
7
+ maybePrintLinuxRuntimeHelp,
8
+ getPreStartPgHereState,
9
+ printPgHereStartupInfo,
10
+ startPgHereWithLibxml2Compat,
11
+ } from "../scripts/cli-error-help.mjs";
7
12
 
8
13
  const argv = await yargs(hideBin(process.argv))
9
14
  .version(false)
@@ -33,8 +38,9 @@ const argv = await yargs(hideBin(process.argv))
33
38
  .parse();
34
39
 
35
40
  let pg;
36
- try {
37
- pg = await startPgHere({
41
+ const preStartState = getPreStartPgHereState(process.cwd());
42
+ const startInstance = () =>
43
+ startPgHere({
38
44
  projectDir: process.cwd(),
39
45
  port: argv.port,
40
46
  username: argv.username,
@@ -42,6 +48,9 @@ try {
42
48
  database: argv.database,
43
49
  postgresVersion: argv["pg-version"],
44
50
  });
51
+
52
+ try {
53
+ pg = await startPgHereWithLibxml2Compat(startInstance, process.cwd());
45
54
  } catch (error) {
46
55
  if (process.platform === "linux") {
47
56
  maybePrintLinuxRuntimeHelp(error);
@@ -49,5 +58,11 @@ try {
49
58
  throw error;
50
59
  }
51
60
 
52
- console.log(pg.databaseConnectionString);
61
+ printPgHereStartupInfo({
62
+ connectionString: pg.databaseConnectionString,
63
+ instance: pg.instance,
64
+ preStartState,
65
+ requestedVersion: argv["pg-version"],
66
+ });
67
+
53
68
  setInterval(() => {}, 1 << 30);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pg-here",
3
- "version": "0.1.5",
3
+ "version": "0.1.9",
4
4
  "description": "Per-project embedded PostgreSQL with programmatic startup and auto DB creation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,42 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import {
3
+ accessSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ readlinkSync,
8
+ readdirSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ F_OK,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
2
14
 
3
- const LIBXML_LIBRARY = /libxml2\.so\.2/;
15
+ const LIBXML2_SONAME = "libxml2.so.2";
16
+ const LIBXML2_ALTERNATE_SONAME = "libxml2.so.16";
17
+ const LIBXML2_COMPAT_DIR = "pg_local/runtime-libs";
18
+
19
+ const LIB_PATHS = [
20
+ "/usr/lib/x86_64-linux-gnu",
21
+ "/usr/lib/i386-linux-gnu",
22
+ "/usr/lib",
23
+ "/lib/x86_64-linux-gnu",
24
+ "/lib/i386-linux-gnu",
25
+ "/lib",
26
+ "/usr/local/lib",
27
+ ];
28
+
29
+ const PG_LOCAL_DIR = "pg_local";
30
+ const PG_LOCAL_DATA_DIR = "data";
31
+ const PG_LOCAL_BIN_DIR = "bin";
32
+ const PG_VERSION_FILE = "PG_VERSION";
33
+ const VERSION_DIR_RE = /^\d+\.\d+(?:\.\d+)?$/;
4
34
 
5
35
  export function maybePrintLinuxRuntimeHelp(error) {
6
36
  const message = String(error?.message ?? error);
7
37
  const missingLibsFromError = extractMissingLibrariesFromMessage(message);
8
38
  const missingLibsFromBinary = extractMissingLibrariesFromBinaryPath(message);
39
+ const binPath = extractBinaryPathFromError(message);
9
40
 
10
41
  const missingLibs = [...new Set([...missingLibsFromError, ...missingLibsFromBinary])];
11
42
 
@@ -18,20 +49,248 @@ export function maybePrintLinuxRuntimeHelp(error) {
18
49
  console.error(`Missing libraries: ${missingLibs.join(", ")}`);
19
50
  console.error();
20
51
  console.error("Install system packages for your distro and retry:");
21
- console.error(" Ubuntu/Debian: sudo apt-get update && sudo apt-get install -y libxml2");
22
- console.error(" Fedora/RHEL: sudo dnf install -y libxml2");
23
- console.error(" Alpine: sudo apk add libxml2");
24
52
  console.error(
25
- `If your distro requires different package names, install packages that provide: ${missingLibs.join(
26
- ", "
27
- )}`
53
+ " Ubuntu/Debian: sudo apt-get update && sudo apt-get install -y libxml2 libxml2-dev libxml2-utils"
54
+ );
55
+ console.error(
56
+ " Fedora/RHEL: sudo dnf install -y libxml2"
57
+ );
58
+ console.error(
59
+ " Alpine: sudo apk add libxml2"
60
+ );
61
+ console.error();
62
+ console.error(" Quick checks:");
63
+ console.error(
64
+ " find /usr/lib /usr/local/lib /lib -name 'libxml2.so.2*' 2>/dev/null | head"
65
+ );
66
+ if (binPath) {
67
+ console.error(` ldd ${binPath} | grep libxml2`);
68
+ }
69
+ console.error(" sudo ldconfig && sudo ldconfig -p | grep libxml2.so.2");
70
+ if (binPath.includes("/bin/bin/postgres")) {
71
+ console.error(" Your local pg_local cache looks partially provisioned (bin/bin/postgres path).");
72
+ console.error(" Try removing it and retrying:");
73
+ console.error(" rm -rf pg_local && bunx pg-here@0.1.9");
74
+ }
75
+ console.error(
76
+ `If your distro requires different package names, install packages that provide: ${missingLibs.join(", ")}`
28
77
  );
78
+ printPotentialSymlinkHint();
79
+
80
+ if (hasLibxml2CompatibilityNeed(error)) {
81
+ const fallback = findLibraryPath(LIBXML2_ALTERNATE_SONAME);
82
+ console.error();
83
+ console.error("Compatibility note:");
84
+ if (fallback) {
85
+ console.error(
86
+ `This host only provides ${LIBXML2_ALTERNATE_SONAME} at ${fallback}, not ${LIBXML2_SONAME}.`
87
+ );
88
+ console.error(
89
+ "This release now retries startup with a project-local symlink fallback automatically."
90
+ );
91
+ console.error(
92
+ `If that does not work, you can retry with a global symlink (requires sudo):\n sudo ln -sfn ${fallback} /usr/local/lib/${LIBXML2_SONAME} && sudo ldconfig`
93
+ );
94
+ } else {
95
+ console.error(
96
+ `Expected ${LIBXML2_SONAME} was not found and no ${LIBXML2_ALTERNATE_SONAME} fallback was discovered.`
97
+ );
98
+ console.error(
99
+ `Your host may be too minimal or custom; install a PostgreSQL-ready runtime stack for your distro.`
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ export function hasLibxml2CompatibilityNeed(error) {
106
+ const message = String(error?.message ?? error);
107
+ const missingFromMessage = extractMissingLibrariesFromMessage(message);
108
+ const missingFromBinary = extractMissingLibrariesFromBinaryPath(message);
109
+ const missing = [...missingFromMessage, ...missingFromBinary];
110
+ return missing.includes(LIBXML2_SONAME) || message.includes("libxml2.so.2");
111
+ }
112
+
113
+ export function getPreStartPgHereState(projectDir) {
114
+ const normalizedProjectDir = typeof projectDir === "string" && projectDir ? projectDir : process.cwd();
115
+ const dataDir = join(normalizedProjectDir, PG_LOCAL_DIR, PG_LOCAL_DATA_DIR);
116
+ const binDir = join(normalizedProjectDir, PG_LOCAL_DIR, PG_LOCAL_BIN_DIR);
117
+ const hasData = existsSync(dataDir);
118
+ const hasPgVersionFile = existsSync(join(dataDir, PG_VERSION_FILE));
119
+ const installedVersions = getInstalledPostgresVersions(binDir);
120
+
121
+ return {
122
+ dataDir,
123
+ hasData,
124
+ hasPgVersionFile,
125
+ installedVersions,
126
+ installedVersion: installedVersions[0] ?? "",
127
+ };
128
+ }
129
+
130
+ export function printPgHereStartupInfo({
131
+ connectionString,
132
+ instance,
133
+ preStartState,
134
+ requestedVersion,
135
+ }) {
136
+ const { hasData, installedVersion } = preStartState ?? {};
137
+ const startedVersion = getPostgresInstanceVersion(instance);
138
+
139
+ const displayVersion =
140
+ startedVersion || requestedVersion || "default";
141
+ const dataPath = `${PG_LOCAL_DIR}/${PG_LOCAL_DATA_DIR}/`;
142
+ const firstLine = hasData
143
+ ? getExistingDataStatusLine({ installedVersion, startedVersion, requestedVersion, dataPath })
144
+ : `Launching PostgreSQL ${displayVersion} into new ${PG_LOCAL_DIR}/`;
29
145
 
30
- if (LIBXML_LIBRARY.test(message)) {
31
- console.error("Hint: this project does not ship all optional shared objects in every Linux image.");
146
+ console.log(firstLine);
147
+ if (typeof connectionString === "string" && connectionString.length > 0) {
148
+ console.log(`psql ${connectionString}`);
32
149
  }
33
150
  }
34
151
 
152
+ function getExistingDataStatusLine({
153
+ installedVersion,
154
+ startedVersion,
155
+ requestedVersion,
156
+ dataPath,
157
+ }) {
158
+ const runVersion = startedVersion || requestedVersion || "default";
159
+ if (installedVersion && startedVersion && installedVersion !== startedVersion) {
160
+ return `Reusing existing ${dataPath} (pg_local/bin has ${installedVersion}, running PostgreSQL is ${startedVersion})`;
161
+ }
162
+
163
+ if (installedVersion && startedVersion && installedVersion === startedVersion) {
164
+ return `Reusing existing ${dataPath} with PostgreSQL ${runVersion}`;
165
+ }
166
+
167
+ return `Reusing existing ${dataPath} with PostgreSQL ${runVersion}`;
168
+ }
169
+
170
+ function getPostgresInstanceVersion(instance) {
171
+ try {
172
+ if (typeof instance?.getPostgreSqlVersion === "function") {
173
+ return instance.getPostgreSqlVersion();
174
+ }
175
+ } catch {
176
+ return "";
177
+ }
178
+
179
+ return "";
180
+ }
181
+
182
+ function compareSemVerDesc(left, right) {
183
+ const leftParts = left.split(".").map((segment) => Number.parseInt(segment, 10) || 0);
184
+ const rightParts = right.split(".").map((segment) => Number.parseInt(segment, 10) || 0);
185
+ const maxLength = Math.max(leftParts.length, rightParts.length);
186
+ for (let i = 0; i < maxLength; i += 1) {
187
+ const leftValue = leftParts[i] ?? 0;
188
+ const rightValue = rightParts[i] ?? 0;
189
+ if (leftValue === rightValue) {
190
+ continue;
191
+ }
192
+ return rightValue - leftValue;
193
+ }
194
+
195
+ return 0;
196
+ }
197
+
198
+ export async function startPgHereWithLibxml2Compat(start, workingDir) {
199
+ try {
200
+ return await start();
201
+ } catch (error) {
202
+ if (!hasLibxml2CompatibilityNeed(error)) {
203
+ throw error;
204
+ }
205
+
206
+ const patched = ensureLibxml2Compatibility(workingDir);
207
+ if (!patched) {
208
+ throw error;
209
+ }
210
+
211
+ return await start();
212
+ }
213
+ }
214
+
215
+ function getInstalledPostgresVersions(baseDir) {
216
+ if (!existsSync(baseDir)) {
217
+ return [];
218
+ }
219
+
220
+ let entries = [];
221
+ try {
222
+ entries = readdirSync(baseDir, { withFileTypes: true });
223
+ } catch {
224
+ return [];
225
+ }
226
+
227
+ return entries
228
+ .filter((entry) => entry.isDirectory() && VERSION_DIR_RE.test(entry.name))
229
+ .map((entry) => entry.name)
230
+ .sort(compareSemVerDesc);
231
+ }
232
+
233
+
234
+ export function ensureLibxml2Compatibility(workingDir) {
235
+ if (process.platform !== "linux") {
236
+ return false;
237
+ }
238
+
239
+ const projectDir = typeof workingDir === "string" && workingDir ? workingDir : process.cwd();
240
+ const compatDir = join(projectDir, LIBXML2_COMPAT_DIR);
241
+ const compatLib = join(compatDir, LIBXML2_SONAME);
242
+
243
+ if (findLibraryPath(LIBXML2_SONAME)) {
244
+ return false;
245
+ }
246
+
247
+ const fallback = findLibraryPath(LIBXML2_ALTERNATE_SONAME);
248
+ if (!fallback) {
249
+ return false;
250
+ }
251
+
252
+ try {
253
+ mkdirSync(compatDir, { recursive: true });
254
+ if (existsSync(compatLib)) {
255
+ const existing = readCompatLink(compatLib);
256
+ if (existing === fallback) {
257
+ ensureLdLibraryPath(compatDir);
258
+ return true;
259
+ }
260
+
261
+ rmSync(compatLib, { force: true });
262
+ }
263
+
264
+ symlinkSync(fallback, compatLib);
265
+ ensureLdLibraryPath(compatDir);
266
+ return true;
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+
272
+ function readCompatLink(path) {
273
+ try {
274
+ const info = lstatSync(path);
275
+ if (!info.isSymbolicLink()) {
276
+ return "";
277
+ }
278
+ return readlinkSync(path);
279
+ } catch {
280
+ return "";
281
+ }
282
+ }
283
+
284
+ function ensureLdLibraryPath(path) {
285
+ const currentPath = process.env.LD_LIBRARY_PATH ?? "";
286
+ const paths = currentPath.split(":").filter(Boolean);
287
+ if (paths.includes(path)) {
288
+ return;
289
+ }
290
+
291
+ process.env.LD_LIBRARY_PATH = path + (currentPath ? `:${currentPath}` : "");
292
+ }
293
+
35
294
  function extractMissingLibrariesFromMessage(message) {
36
295
  const regex = /([A-Za-z0-9._-]+\.so(?:\.\d+)*)\b/g;
37
296
  const matches = [...message.matchAll(regex)].map((match) => match[1]);
@@ -55,3 +314,68 @@ function extractMissingLibrariesFromBinaryPath(message) {
55
314
  (match) => match[1]
56
315
  );
57
316
  }
317
+
318
+ function extractBinaryPathFromError(message) {
319
+ const pathMatch = message.match(/(\/[^\s"]*\/bin\/postgres)/);
320
+ return pathMatch ? pathMatch[1] : "";
321
+ }
322
+
323
+ function hasLibxmlVersion16Only() {
324
+ const candidates = ["/usr/lib/x86_64-linux-gnu", "/usr/lib", "/lib/x86_64-linux-gnu", "/lib"];
325
+ for (const dir of candidates) {
326
+ try {
327
+ accessSync(`${dir}/libxml2.so.2`, F_OK);
328
+ return false;
329
+ } catch {
330
+ // continue
331
+ }
332
+
333
+ try {
334
+ accessSync(`${dir}/libxml2.so.16`, F_OK);
335
+ if (hasLibxmlVersion16AtPath(`${dir}/libxml2.so.16`)) {
336
+ return true;
337
+ }
338
+ } catch {}
339
+ }
340
+
341
+ return false;
342
+ }
343
+
344
+ function hasLibxmlVersion16AtPath(path) {
345
+ return typeof path === "string" && path.endsWith("/libxml2.so.16");
346
+ }
347
+
348
+ function printPotentialSymlinkHint() {
349
+ const result = spawnSync("sh", [
350
+ "-c",
351
+ "find /usr/lib /usr/local/lib /lib -name 'libxml2.so.2*' 2>/dev/null",
352
+ ], { encoding: "utf8" });
353
+
354
+ if (result.status !== 0) {
355
+ return;
356
+ }
357
+
358
+ const matches = (result.stdout ?? "").split("\n").filter(Boolean);
359
+ const exact = matches.find((item) => item.endsWith("/libxml2.so.2"));
360
+ if (!exact) {
361
+ console.error();
362
+ console.error("Hint:");
363
+ if (hasLibxmlVersion16Only()) {
364
+ console.error("This host appears to provide only libxml2.so.16. Compatibility is not guaranteed.");
365
+ } else {
366
+ console.error("No libxml2.so.2 file was found in the standard library paths.");
367
+ }
368
+ }
369
+ }
370
+
371
+ function findLibraryPath(name) {
372
+ const args = [...LIB_PATHS, "-name", `${name}*`, "-print"];
373
+ const result = spawnSync("find", args, { encoding: "utf8" });
374
+ const matches = (result.stdout ?? "").split("\n").filter(Boolean);
375
+ if (matches.length === 0) {
376
+ return "";
377
+ }
378
+
379
+ const exact = matches.find((item) => item.endsWith(`/${name}`));
380
+ return exact ?? matches[0];
381
+ }
@@ -1,7 +1,12 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  import { startPgHere } from "../index.ts";
4
- import { maybePrintLinuxRuntimeHelp } from "./cli-error-help.mjs";
4
+ import {
5
+ maybePrintLinuxRuntimeHelp,
6
+ getPreStartPgHereState,
7
+ printPgHereStartupInfo,
8
+ startPgHereWithLibxml2Compat,
9
+ } from "./cli-error-help.mjs";
5
10
 
6
11
  const argv = await yargs(hideBin(process.argv))
7
12
  .version(false)
@@ -31,8 +36,9 @@ const argv = await yargs(hideBin(process.argv))
31
36
  .parse();
32
37
 
33
38
  let pg;
34
- try {
35
- pg = await startPgHere({
39
+ const preStartState = getPreStartPgHereState(process.cwd());
40
+ const startInstance = () =>
41
+ startPgHere({
36
42
  projectDir: process.cwd(),
37
43
  port: argv.port,
38
44
  username: argv.username,
@@ -40,6 +46,9 @@ try {
40
46
  database: argv.database,
41
47
  postgresVersion: argv["pg-version"],
42
48
  });
49
+
50
+ try {
51
+ pg = await startPgHereWithLibxml2Compat(startInstance, process.cwd());
43
52
  } catch (error) {
44
53
  if (process.platform === "linux") {
45
54
  maybePrintLinuxRuntimeHelp(error);
@@ -47,8 +56,12 @@ try {
47
56
  throw error;
48
57
  }
49
58
 
50
- // print connection string for tooling
51
- console.log(pg.databaseConnectionString);
59
+ printPgHereStartupInfo({
60
+ connectionString: pg.databaseConnectionString,
61
+ instance: pg.instance,
62
+ preStartState,
63
+ requestedVersion: argv["pg-version"],
64
+ });
52
65
 
53
66
  // keep this process alive; Ctrl-C stops postgres
54
67
  setInterval(() => {}, 1 << 30);