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 +42 -285
- package/bin/pg-here.mjs +19 -4
- package/package.json +1 -1
- package/scripts/cli-error-help.mjs +333 -9
- package/scripts/pg-dev.mjs +18 -5
package/README.md
CHANGED
|
@@ -1,335 +1,92 @@
|
|
|
1
1
|
# pg-here
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run a local PostgreSQL instance in your project folder with one command.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
If it still resolves an older release, the package `latest` dist-tag is behind:
|
|
11
|
+
Default output:
|
|
37
12
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
18
|
+
If a data folder already exists:
|
|
45
19
|
|
|
46
|
-
```
|
|
47
|
-
|
|
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
|
-
|
|
25
|
+
If the cached folder version differs:
|
|
51
26
|
|
|
52
|
-
```
|
|
53
|
-
|
|
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
|
-
|
|
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`
|
|
59
|
-
- `username=postgres`
|
|
60
|
-
- `password=postgres`
|
|
61
|
-
- `database=postgres`
|
|
62
|
-
- `port=55432`
|
|
37
|
+
`bunx pg-here`
|
|
63
38
|
|
|
64
|
-
|
|
39
|
+
- `username = postgres`
|
|
40
|
+
- `password = postgres`
|
|
41
|
+
- `database = postgres`
|
|
42
|
+
- `port = 55432`
|
|
43
|
+
- `pg-version` = auto
|
|
65
44
|
|
|
66
|
-
|
|
45
|
+
## Custom run
|
|
67
46
|
|
|
68
47
|
```bash
|
|
69
|
-
bunx pg-here --username
|
|
48
|
+
bunx pg-here --username me --password secret --database my_app --port 55433 --pg-version 17.0.0
|
|
70
49
|
```
|
|
71
50
|
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
### Programmatic usage
|
|
57
|
+
Same CLI flags are supported.
|
|
94
58
|
|
|
95
|
-
|
|
59
|
+
## Programmatic
|
|
96
60
|
|
|
97
61
|
```ts
|
|
98
62
|
import { startPgHere } from "pg-here";
|
|
99
63
|
|
|
100
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
If startup fails with missing `libxml2` libraries, install runtime packages and retry:
|
|
124
77
|
|
|
125
78
|
```bash
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
bun install
|
|
192
|
-
```
|
|
86
|
+
## Version pin / stale cache
|
|
193
87
|
|
|
194
|
-
|
|
88
|
+
If your environment keeps resolving an older release, force a specific version:
|
|
195
89
|
|
|
196
90
|
```bash
|
|
197
|
-
|
|
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 {
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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,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
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
+
}
|
package/scripts/pg-dev.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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);
|