pgserve 2.2.4 → 2.4.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/bin/pgserve-wrapper.cjs +5 -4
- package/bin/postgres-server.js +142 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +2 -2
- package/scripts/test-npx.sh +32 -10
- package/src/cli-install.cjs +147 -77
- package/src/commands/uninstall.js +241 -0
- package/src/index.js +11 -44
- package/src/lib/admin-json.js +202 -0
- package/src/lib/pm2-args.js +119 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/admin-client.js +0 -223
- package/src/audit.js +0 -168
- package/src/cluster.js +0 -654
- package/src/control-db.js +0 -330
- package/src/daemon-control.js +0 -468
- package/src/daemon-shared.js +0 -18
- package/src/daemon-tcp.js +0 -297
- package/src/daemon.js +0 -709
- package/src/dashboard.js +0 -217
- package/src/fingerprint.js +0 -479
- package/src/gc.js +0 -351
- package/src/pg-wire.js +0 -869
- package/src/protocol.js +0 -389
- package/src/restore.js +0 -574
- package/src/router.js +0 -546
- package/src/sdk.js +0 -137
- package/src/stats-collector.js +0 -453
- package/src/stats-dashboard.js +0 -401
- package/src/sync.js +0 -335
- package/src/tenancy.js +0 -75
- package/src/tokens.js +0 -102
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# pgserve singleton (v2.4) — logrotate config for the pm2 / systemd-user
|
|
2
|
+
# / launchd-supervised postmaster.
|
|
3
|
+
#
|
|
4
|
+
# Postgres' stderr (where pgaudit's audit log lands when the .so is
|
|
5
|
+
# present, and `log_statement=all` lands in the fallback path) is captured
|
|
6
|
+
# by the supervisor and written to:
|
|
7
|
+
#
|
|
8
|
+
# ~/.autopg/logs/autopg-server-out.log
|
|
9
|
+
# ~/.autopg/logs/autopg-server-error.log
|
|
10
|
+
#
|
|
11
|
+
# Symbolic ~/ resolves under each operator's HOME — logrotate runs the
|
|
12
|
+
# rules per-user when invoked with `--state ~/.config/logrotate.state`
|
|
13
|
+
# (see the autopg installer hint). The `su` directive is intentionally
|
|
14
|
+
# absent: this config ships as a USER-context drop-in, not a system one.
|
|
15
|
+
# System-wide rotation under a dedicated `autopg` UNIX user is delivered
|
|
16
|
+
# by the separate `autopg-service-install-system` wish (out of scope for
|
|
17
|
+
# v2.4).
|
|
18
|
+
#
|
|
19
|
+
# Copy or symlink to: /etc/logrotate.d/pgserve (system-wide)
|
|
20
|
+
# Or use per-user: ~/.config/logrotate.d/pgserve
|
|
21
|
+
|
|
22
|
+
~/.autopg/logs/autopg-server-out.log
|
|
23
|
+
~/.autopg/logs/autopg-server-error.log
|
|
24
|
+
~/.pgserve/audit.log
|
|
25
|
+
{
|
|
26
|
+
# Daily rotation keeps audit/forensic events queryable for ~2 weeks
|
|
27
|
+
# without burning disk on a long-running dev host.
|
|
28
|
+
daily
|
|
29
|
+
rotate 14
|
|
30
|
+
# Compress yesterday's log on tomorrow's rotation so today's log stays
|
|
31
|
+
# uncompressed for tail/grep.
|
|
32
|
+
compress
|
|
33
|
+
delaycompress
|
|
34
|
+
# Don't fail when a log file has not been created yet (e.g. fresh
|
|
35
|
+
# `autopg install` before the postmaster has emitted any output).
|
|
36
|
+
missingok
|
|
37
|
+
# Don't fail when a log file is empty.
|
|
38
|
+
notifempty
|
|
39
|
+
# Truncate-in-place so pm2 / systemd / launchd's open file handles
|
|
40
|
+
# keep writing to the rotated file. Avoids the "send SIGHUP and
|
|
41
|
+
# postgres re-opens" dance which we don't want to trigger from a
|
|
42
|
+
# supervisor-agnostic rule.
|
|
43
|
+
copytruncate
|
|
44
|
+
# File mode 0600: audit lines may include literal SQL (parameter
|
|
45
|
+
# values, error contexts) — operator-only on a multi-user host.
|
|
46
|
+
create 0600
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# pgserve singleton (v2.4) — pgaudit GUC defaults.
|
|
2
|
+
#
|
|
3
|
+
# Loaded by `src/postgres.js::_startPostgres` when the embedded postgres
|
|
4
|
+
# bundle ships pgaudit.so. The postmaster passes these as `-c key=value`
|
|
5
|
+
# pairs at boot; settings.json `_extra` overrides apply on top via the
|
|
6
|
+
# normal curated < extra precedence.
|
|
7
|
+
#
|
|
8
|
+
# When pgaudit.so is NOT present in the bundle (the current state of the
|
|
9
|
+
# `@embedded-postgres/<plat>-<arch>` packages), postgres.js falls back to
|
|
10
|
+
# `log_statement=all`. The cohort sibling `autopg-distribution-cutover`
|
|
11
|
+
# owns shipping the pgaudit binary; this file documents the GUCs that
|
|
12
|
+
# light up the moment the .so lands.
|
|
13
|
+
|
|
14
|
+
shared_preload_libraries = 'pgaudit'
|
|
15
|
+
|
|
16
|
+
# `pgaudit.log = 'all'` is the audit-log breadth contract from the wish.
|
|
17
|
+
# Operators who need a tighter classification (e.g. drop READ to cut log
|
|
18
|
+
# volume on hot read paths) override via:
|
|
19
|
+
# ~/.autopg/settings.json -> postgres._extra.pgaudit.log = 'write,role,ddl'
|
|
20
|
+
pgaudit.log = 'all'
|
|
21
|
+
|
|
22
|
+
# Skip pg_catalog reads — they are constant per session and their
|
|
23
|
+
# inclusion floods the audit log without forensic value.
|
|
24
|
+
pgaudit.log_catalog = off
|
|
25
|
+
|
|
26
|
+
# Audit lines emit on the postmaster's stderr, captured by the supervisor
|
|
27
|
+
# (pm2 -> ~/.autopg/logs/autopg-server-error.log; systemd-user -> journal;
|
|
28
|
+
# launchd -> launchd-managed plist log path). The legacy
|
|
29
|
+
# ~/.pgserve/audit.log path stays rotated by `config/logrotate.d/pgserve`
|
|
30
|
+
# so existing tooling parsing the file keeps working through the
|
|
31
|
+
# migration window.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgserve",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
|
13
13
|
"src/",
|
|
14
|
+
"config/",
|
|
14
15
|
"console/dist/",
|
|
15
16
|
"README.md",
|
|
16
17
|
"CHANGELOG.md",
|
|
@@ -19,7 +20,6 @@
|
|
|
19
20
|
"scripts/"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
|
-
"bench": "bun tests/benchmarks/runner.js",
|
|
23
23
|
"test": "bun test tests/**/*.test.js",
|
|
24
24
|
"test:watch": "bun test --watch tests/**/*.test.js",
|
|
25
25
|
"dev": "bun --watch bin/postgres-server.js",
|
package/scripts/test-npx.sh
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Test that the package works with npx (simulates fresh user install)
|
|
3
|
-
# This catches path resolution issues that static analysis can't detect
|
|
3
|
+
# This catches path resolution issues that static analysis can't detect.
|
|
4
|
+
#
|
|
5
|
+
# v2.4 contract change (2026-05-08): `pgserve` (no args) now prints help and
|
|
6
|
+
# exits cleanly. The long-running entry is `pgserve postmaster` (or its
|
|
7
|
+
# `pgserve serve` alias). This test invokes the postmaster directly to verify
|
|
8
|
+
# npx-installed bits boot a real PG.
|
|
4
9
|
|
|
5
10
|
set -e
|
|
6
11
|
|
|
7
|
-
echo "=== Testing npx compatibility ==="
|
|
12
|
+
echo "=== Testing npx compatibility (v2.4 postmaster entry) ==="
|
|
8
13
|
|
|
9
14
|
# Create temp directory
|
|
10
15
|
TEST_DIR=$(mktemp -d)
|
|
@@ -29,22 +34,39 @@ cd "$TEST_DIR"
|
|
|
29
34
|
echo '{"name":"test-npx-install","private":true}' > package.json
|
|
30
35
|
npm install "./$PACK_FILE" > /dev/null 2>&1
|
|
31
36
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
# Verify the bare invocation prints v2.4 help and exits 0 (regression guard
|
|
38
|
+
# for the breaking-cut: pre-v2.4 auto-started a server here).
|
|
39
|
+
echo "Verifying bare 'npx pgserve' prints v2.4 help and exits cleanly..."
|
|
40
|
+
HELP_OUT=$(npx pgserve 2>&1)
|
|
41
|
+
if ! echo "$HELP_OUT" | grep -q "pgserve postmaster"; then
|
|
42
|
+
echo "✗ Bare 'npx pgserve' output does not match v2.4 help (missing 'pgserve postmaster')"
|
|
43
|
+
echo "Output:"
|
|
44
|
+
echo "$HELP_OUT"
|
|
45
|
+
echo "=== npx test FAILED ==="
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
echo "✓ Bare invocation prints v2.4 help"
|
|
49
|
+
|
|
50
|
+
# Test that the postmaster entry starts (with timeout)
|
|
51
|
+
DATA_DIR="$TEST_DIR/data"
|
|
52
|
+
SOCKET_DIR="$TEST_DIR/sock"
|
|
53
|
+
mkdir -p "$DATA_DIR" "$SOCKET_DIR"
|
|
54
|
+
echo "Testing postmaster startup via npx (port 15432)..."
|
|
55
|
+
timeout 30 npx pgserve postmaster --port 15432 --data "$DATA_DIR" --socket-dir "$SOCKET_DIR" > output.log 2>&1 &
|
|
35
56
|
PID=$!
|
|
36
57
|
|
|
37
|
-
# Wait for ready signal
|
|
58
|
+
# Wait for ready signal — bin/postgres-server.js logs
|
|
59
|
+
# 'pgserve postmaster: ready (Unix socket + TCP)' once both transports are bound.
|
|
38
60
|
for i in {1..60}; do
|
|
39
|
-
if grep -q "
|
|
40
|
-
echo "✓
|
|
61
|
+
if grep -q "pgserve postmaster: ready" output.log 2>/dev/null; then
|
|
62
|
+
echo "✓ Postmaster started successfully via npx"
|
|
41
63
|
kill $PID 2>/dev/null || true
|
|
42
64
|
wait $PID 2>/dev/null || true
|
|
43
65
|
echo "=== npx test PASSED ==="
|
|
44
66
|
exit 0
|
|
45
67
|
fi
|
|
46
68
|
if ! kill -0 $PID 2>/dev/null; then
|
|
47
|
-
echo "✗
|
|
69
|
+
echo "✗ Postmaster exited unexpectedly"
|
|
48
70
|
cat output.log
|
|
49
71
|
echo "=== npx test FAILED ==="
|
|
50
72
|
exit 1
|
|
@@ -54,7 +76,7 @@ done
|
|
|
54
76
|
|
|
55
77
|
# Timeout
|
|
56
78
|
kill $PID 2>/dev/null || true
|
|
57
|
-
echo "✗
|
|
79
|
+
echo "✗ Postmaster did not start within timeout"
|
|
58
80
|
cat output.log
|
|
59
81
|
echo "=== npx test FAILED ==="
|
|
60
82
|
exit 1
|
package/src/cli-install.cjs
CHANGED
|
@@ -27,8 +27,37 @@ const fs = require('node:fs');
|
|
|
27
27
|
const os = require('node:os');
|
|
28
28
|
const path = require('node:path');
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// pgserve singleton (v2.4): the cohort-shared admin-json + socket-dir
|
|
31
|
+
// helpers live in `src/lib/*.js` as ESM modules (project convention: new
|
|
32
|
+
// modules ship as .js / ESM). cli-install.cjs runs under node — which
|
|
33
|
+
// cannot synchronously `require()` ESM — so we cache the dynamic-import
|
|
34
|
+
// promise once at module load and await it from async install paths.
|
|
35
|
+
const _adminJsonModuleP = import('./lib/admin-json.js');
|
|
36
|
+
const _socketDirModuleP = import('./lib/socket-dir.js');
|
|
37
|
+
|
|
38
|
+
async function loadCohortModules() {
|
|
39
|
+
const [adminJson, socketDirMod] = await Promise.all([_adminJsonModuleP, _socketDirModuleP]);
|
|
40
|
+
return { adminJson, socketDirMod };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 1.
|
|
44
|
+
//
|
|
45
|
+
// The pm2 entry name moves from `pgserve` to `autopg-server` so the canonical
|
|
46
|
+
// two-process layout matches the cohort design (`autopg-server` + `autopg-ui`).
|
|
47
|
+
// Tier B operators install a systemd-user unit that ALSO claims the
|
|
48
|
+
// `autopg-server` lifecycle role — `~/.autopg/admin.json` records which
|
|
49
|
+
// supervisor owns the host and `pgserve install` refuses to register pm2 over
|
|
50
|
+
// an existing Tier B record.
|
|
51
|
+
//
|
|
52
|
+
// Default postgres port moves from 8432 (the old bun-proxy listener) to 5432
|
|
53
|
+
// (the postgres standard, since the postmaster now binds TCP directly).
|
|
54
|
+
const PM2_PROCESS_NAME = 'autopg-server';
|
|
55
|
+
// Legacy entry name (pre-v2.4). Self-healing migration in Group 6 will
|
|
56
|
+
// `pm2 delete pgserve` after registering `autopg-server`; we surface the
|
|
57
|
+
// constant here so cleanup tooling and tests can reference it without
|
|
58
|
+
// hardcoding strings.
|
|
59
|
+
const LEGACY_PM2_PROCESS_NAME = 'pgserve';
|
|
60
|
+
const DEFAULT_PORT = 5432;
|
|
32
61
|
|
|
33
62
|
// Console UI is auto-supervised under pm2 alongside the daemon since v2.2.3.
|
|
34
63
|
// The bundled SPA (console/dist/) is served on this port; operator-facing
|
|
@@ -200,7 +229,7 @@ function getEffectiveSupervision() {
|
|
|
200
229
|
}
|
|
201
230
|
}
|
|
202
231
|
|
|
203
|
-
function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
232
|
+
function buildPm2StartArgs({ scriptPath, port, dataDir, socketDir }) {
|
|
204
233
|
const logs = {
|
|
205
234
|
out: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-out.log`),
|
|
206
235
|
error: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-error.log`),
|
|
@@ -215,19 +244,11 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
215
244
|
'none',
|
|
216
245
|
'--max-restarts',
|
|
217
246
|
String(supervision.maxRestarts),
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// canonical-pm2-supervision wish we keep `pgserve install` as a pure
|
|
222
|
-
// CLI flow (no extra files for operators to manage). The trade-off is
|
|
223
|
-
// that `--max-restarts` now counts every restart (rapid or not) rather
|
|
224
|
-
// than only sub-`min_uptime` ones; the budget of 50 above is sized
|
|
225
|
-
// accordingly.
|
|
247
|
+
// pm2 ≥ 6.0 dropped `--min-uptime` from the CLI surface — passing it
|
|
248
|
+
// aborts the install. Restart budget (50) is sized to absorb a few
|
|
249
|
+
// long-uptime crashes without burning through.
|
|
226
250
|
'--restart-delay',
|
|
227
251
|
String(supervision.restartDelayMs),
|
|
228
|
-
// Exponential backoff between successive failures: starts at 100ms,
|
|
229
|
-
// doubles each crash, ramps to ~60s. Avoids hammering pm2 + the host
|
|
230
|
-
// when the underlying issue is persistent.
|
|
231
252
|
'--exp-backoff-restart-delay',
|
|
232
253
|
String(supervision.expBackoffRestartDelayMs),
|
|
233
254
|
'--max-memory-restart',
|
|
@@ -241,28 +262,19 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
241
262
|
'--error',
|
|
242
263
|
logs.error,
|
|
243
264
|
'--',
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
// observed live: `pgserve install` was passing `--port` to the daemon
|
|
252
|
-
// parser, which only accepts `--data | --ram | --log | --no-provision
|
|
253
|
-
// | --listen | --pgvector` — every install attempt crashed with
|
|
254
|
-
// `Unknown daemon option: --port` and pm2 burned its restart budget.
|
|
255
|
-
//
|
|
256
|
-
// Foreground mode (the default `pgserve [options]` invocation in
|
|
257
|
-
// postgres-server.js) accepts `--port`, auto-provisions databases on
|
|
258
|
-
// first connect, runs the cluster on multi-core hosts, and binds TCP
|
|
259
|
-
// on `127.0.0.1:<port>` with no auth dance — exactly what canonical
|
|
260
|
-
// pgserve consumers expect. Pass the same flags pgserve already
|
|
261
|
-
// documents in its own `pgserve --help` output.
|
|
265
|
+
// pgserve singleton (v2.4): pm2 supervises the postmaster directly via
|
|
266
|
+
// the `pgserve postmaster` subcommand — no router, no bun proxy, no
|
|
267
|
+
// daemon control socket. Postgres binds the canonical Unix socket
|
|
268
|
+
// under <socketDir> AND TCP <port> natively. Operators connect via
|
|
269
|
+
// psql -h $XDG_RUNTIME_DIR/pgserve (Unix socket, no -p)
|
|
270
|
+
// psql -h 127.0.0.1 -p 5432 (canonical TCP)
|
|
271
|
+
'postmaster',
|
|
262
272
|
'--port',
|
|
263
273
|
String(port),
|
|
264
274
|
'--data',
|
|
265
275
|
dataDir,
|
|
276
|
+
'--socket-dir',
|
|
277
|
+
socketDir,
|
|
266
278
|
'--log',
|
|
267
279
|
'warn',
|
|
268
280
|
];
|
|
@@ -384,21 +396,9 @@ function cmdInstallUi(ctx, options = {}) {
|
|
|
384
396
|
return 0;
|
|
385
397
|
}
|
|
386
398
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// exits non-zero on a missing process, which is fine to swallow. This
|
|
391
|
-
// is more robust than a pre-existence check against `pm2 jlist`, which
|
|
392
|
-
// can lag the actual process state (or, in test stubs, return a
|
|
393
|
-
// partial registry).
|
|
394
|
-
const result = spawnSync('pm2', ['delete', UI_PM2_PROCESS_NAME], {
|
|
395
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
396
|
-
});
|
|
397
|
-
if (result.status === 0) {
|
|
398
|
-
ok(`UI uninstalled (pm2 process "${UI_PM2_PROCESS_NAME}" removed)`);
|
|
399
|
-
}
|
|
400
|
-
return 0;
|
|
401
|
-
}
|
|
399
|
+
// `cmdUninstall` + `cmdUninstallUi` migrated to `src/commands/uninstall.js`
|
|
400
|
+
// as part of canonical-pgserve-pm2-supervision Group 1. The dispatch
|
|
401
|
+
// `case 'uninstall'` above dynamically imports the new module.
|
|
402
402
|
|
|
403
403
|
// ─── Admin password (Basic Auth for `autopg ui`) ─────────────────────────
|
|
404
404
|
|
|
@@ -429,12 +429,19 @@ function writeAdminFile({ password, rotated = false }) {
|
|
|
429
429
|
const hash = hashAdminPassword(password, salt);
|
|
430
430
|
const file = getAdminFilePath();
|
|
431
431
|
const now = new Date().toISOString();
|
|
432
|
+
// pgserve singleton (v2.4): admin.json is shared with the supervisor
|
|
433
|
+
// record (`{ supervisor, socketDir, port, installedAt }`) written by
|
|
434
|
+
// `src/lib/admin-json.js`. Merge with any existing fields so the scrypt
|
|
435
|
+
// Basic-Auth scheme can never wipe the supervisor metadata (and vice
|
|
436
|
+
// versa).
|
|
437
|
+
const existing = readAdminFile() || {};
|
|
432
438
|
const payload = {
|
|
439
|
+
...existing,
|
|
433
440
|
scheme: 'scrypt',
|
|
434
441
|
params: SCRYPT_PARAMS,
|
|
435
442
|
salt: salt.toString('base64'),
|
|
436
443
|
hash: hash.toString('base64'),
|
|
437
|
-
createdAt: rotated ?
|
|
444
|
+
createdAt: rotated ? existing.createdAt ?? now : now,
|
|
438
445
|
rotatedAt: rotated ? now : null,
|
|
439
446
|
};
|
|
440
447
|
ensureConfigDir();
|
|
@@ -487,9 +494,14 @@ function verifyAdminPassword(candidate) {
|
|
|
487
494
|
|
|
488
495
|
function ensureAdminPassword({ rotate = false } = {}) {
|
|
489
496
|
const existing = readAdminFile();
|
|
490
|
-
|
|
497
|
+
// pgserve singleton (v2.4): admin.json may exist with only supervisor
|
|
498
|
+
// fields (`{ supervisor, socketDir, port, installedAt }`) and no scrypt
|
|
499
|
+
// scheme yet. Treat "no scrypt scheme" as "no password" so the install
|
|
500
|
+
// path still mints one on first run.
|
|
501
|
+
const hasScryptScheme = !!(existing && existing.scheme === 'scrypt');
|
|
502
|
+
if (hasScryptScheme && !rotate) return null;
|
|
491
503
|
const password = generateAdminPassword();
|
|
492
|
-
writeAdminFile({ password, rotated: !!
|
|
504
|
+
writeAdminFile({ password, rotated: !!hasScryptScheme });
|
|
493
505
|
return password;
|
|
494
506
|
}
|
|
495
507
|
|
|
@@ -532,14 +544,41 @@ function cmdAuthDispatch(args) {
|
|
|
532
544
|
* `scriptPath` is the path to `bin/postgres-server.js` resolved by the
|
|
533
545
|
* wrapper before this module is required (avoids re-resolving here).
|
|
534
546
|
*/
|
|
535
|
-
function cmdInstall(args, ctx) {
|
|
536
|
-
|
|
537
|
-
|
|
547
|
+
async function cmdInstall(args, ctx) {
|
|
548
|
+
const { adminJson, socketDirMod } = await loadCohortModules();
|
|
549
|
+
|
|
550
|
+
// pgserve singleton (v2.4): refuse to install if a different supervisor
|
|
551
|
+
// (Tier B systemd-user / launchd) already owns the host. The cohort
|
|
552
|
+
// contract guarantees one and only one supervisor records itself in
|
|
553
|
+
// `~/.autopg/admin.json`. `pgserve install` is the Tier A (pm2) entry —
|
|
554
|
+
// it asserts that nothing else has already claimed the host before
|
|
555
|
+
// touching pm2.
|
|
556
|
+
const noPm2 = args.includes('--no-pm2');
|
|
557
|
+
const expectedSupervisor = noPm2 ? 'external' : 'pm2';
|
|
558
|
+
try {
|
|
559
|
+
adminJson.assertSupervisor(expectedSupervisor, { configDir: getConfigDir() });
|
|
560
|
+
} catch (err) {
|
|
561
|
+
fail(err.message);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!noPm2 && !pm2IsAvailable()) {
|
|
565
|
+
fail('pm2 not found in PATH. Install with: bun add -g pm2 (or npm i -g pm2). Pass --no-pm2 for CI / Tier B-bound hosts.');
|
|
538
566
|
}
|
|
539
567
|
|
|
540
568
|
const port = parsePort(args) ?? readConfig()?.port ?? DEFAULT_PORT;
|
|
541
569
|
const dataDir = parseDataDir(args) ?? readConfig()?.dataDir ?? getDataDir();
|
|
542
570
|
|
|
571
|
+
// Set up the canonical socket directory before pm2 launches the
|
|
572
|
+
// postmaster. `ensureSocketDir` enforces mode 0700 and probes writability
|
|
573
|
+
// so failure surfaces here — not as a libpq bind error a few seconds
|
|
574
|
+
// into pm2 backoff.
|
|
575
|
+
const socketDir = parseSocketDir(args) ?? socketDirMod.resolveSocketDir();
|
|
576
|
+
try {
|
|
577
|
+
socketDirMod.ensureSocketDir(socketDir);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
fail(err.message);
|
|
580
|
+
}
|
|
581
|
+
|
|
543
582
|
const noUi = args.includes('--no-ui');
|
|
544
583
|
const withUi = args.includes('--with-ui');
|
|
545
584
|
const redeploy = args.includes('--redeploy');
|
|
@@ -559,6 +598,22 @@ function cmdInstall(args, ctx) {
|
|
|
559
598
|
return 0;
|
|
560
599
|
}
|
|
561
600
|
|
|
601
|
+
// --no-pm2: skip pm2 register entirely. Used by CI fixture provisioning
|
|
602
|
+
// (no sudo, no pm2 ambient state) and by Tier B-bound hosts that will
|
|
603
|
+
// immediately hand off to `autopg service install`. Still record the
|
|
604
|
+
// supervisor + canonical socket dir + port in admin.json so downstream
|
|
605
|
+
// tooling (pgserve doctor, omni install, genie install) can discover
|
|
606
|
+
// where to connect without an active pm2 entry.
|
|
607
|
+
if (noPm2) {
|
|
608
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
609
|
+
writeConfig({ port, dataDir, registeredAt: readConfig()?.registeredAt ?? new Date().toISOString() });
|
|
610
|
+
writeSupervisorRecord(adminJson, { supervisor: 'external', socketDir, port });
|
|
611
|
+
ok(`installed (--no-pm2): supervisor=external, socketDir=${socketDir}, port=${port}`);
|
|
612
|
+
ok(`url: postgres://localhost:${port}/postgres`);
|
|
613
|
+
note(`--no-pm2: pm2 register skipped. Start the postmaster manually with \`pgserve postmaster --port ${port} --data ${dataDir} --socket-dir ${socketDir}\` or hand off to systemd-user / launchd.`);
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
562
617
|
// --redeploy: full reset. Tear down both processes, then proceed with
|
|
563
618
|
// a fresh install. Equivalent to `autopg uninstall && autopg install`
|
|
564
619
|
// but in one verb.
|
|
@@ -581,6 +636,7 @@ function cmdInstall(args, ctx) {
|
|
|
581
636
|
// don't tear down the live process. Operators wanting a port change
|
|
582
637
|
// should `uninstall` then `install` (or pass --redeploy).
|
|
583
638
|
writeConfig({ port, dataDir, registeredAt: readConfig()?.registeredAt ?? new Date().toISOString() });
|
|
639
|
+
writeSupervisorRecord(adminJson, { supervisor: 'pm2', socketDir, port });
|
|
584
640
|
if (!noUi) cmdInstallUi(ctx, { uiPort, uiHost });
|
|
585
641
|
return 0;
|
|
586
642
|
}
|
|
@@ -588,14 +644,15 @@ function cmdInstall(args, ctx) {
|
|
|
588
644
|
ensureLogsDir();
|
|
589
645
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
590
646
|
|
|
591
|
-
const pm2Args = buildPm2StartArgs({ scriptPath: ctx.scriptPath, port, dataDir });
|
|
647
|
+
const pm2Args = buildPm2StartArgs({ scriptPath: ctx.scriptPath, port, dataDir, socketDir });
|
|
592
648
|
const result = spawnSync('pm2', pm2Args, { stdio: 'inherit' });
|
|
593
649
|
if (result.status !== 0) {
|
|
594
650
|
fail(`pm2 start failed (exit ${result.status}). Logs: ${getLogsDir()}/${PM2_PROCESS_NAME}-error.log`);
|
|
595
651
|
}
|
|
596
652
|
|
|
597
653
|
writeConfig({ port, dataDir, registeredAt: new Date().toISOString() });
|
|
598
|
-
|
|
654
|
+
writeSupervisorRecord(adminJson, { supervisor: 'pm2', socketDir, port });
|
|
655
|
+
ok(`installed: pm2 process "${PM2_PROCESS_NAME}" on port ${port} (socket: ${socketDir}, data: ${dataDir})`);
|
|
599
656
|
ok(`url: postgres://localhost:${port}/postgres`);
|
|
600
657
|
|
|
601
658
|
if (noUi) {
|
|
@@ -620,29 +677,28 @@ function cmdInstall(args, ctx) {
|
|
|
620
677
|
}
|
|
621
678
|
|
|
622
679
|
/**
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* downstream service still depends on the data.
|
|
680
|
+
* Persist the supervisor record into `~/.autopg/admin.json`. Wraps
|
|
681
|
+
* `writeAdminJson` from the cohort-shared module so the install path can
|
|
682
|
+
* pin its own ISO timestamp + log a friendly diagnostic on the
|
|
683
|
+
* supervisor-lock refusal.
|
|
628
684
|
*/
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
685
|
+
function writeSupervisorRecord(adminJson, { supervisor, socketDir, port }) {
|
|
686
|
+
try {
|
|
687
|
+
adminJson.writeAdminJson(
|
|
688
|
+
{
|
|
689
|
+
supervisor,
|
|
690
|
+
socketDir,
|
|
691
|
+
port,
|
|
692
|
+
installedAt: new Date().toISOString(),
|
|
693
|
+
},
|
|
694
|
+
{ configDir: getConfigDir() },
|
|
695
|
+
);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
// The "Tier B already owns this host" error is the contract failure
|
|
698
|
+
// we surface to operators. Other errors (EACCES, ENOSPC, etc.) just
|
|
699
|
+
// pass through with their stock message.
|
|
700
|
+
fail(err.message);
|
|
642
701
|
}
|
|
643
|
-
ok(`uninstalled (pm2 process removed; data dir preserved at ${getDataDir()})`);
|
|
644
|
-
cmdUninstallUi();
|
|
645
|
-
return 0;
|
|
646
702
|
}
|
|
647
703
|
|
|
648
704
|
/**
|
|
@@ -747,6 +803,14 @@ function parseDataDir(args) {
|
|
|
747
803
|
return path.resolve(v);
|
|
748
804
|
}
|
|
749
805
|
|
|
806
|
+
function parseSocketDir(args) {
|
|
807
|
+
const i = args.indexOf('--socket-dir');
|
|
808
|
+
if (i < 0) return null;
|
|
809
|
+
const v = args[i + 1];
|
|
810
|
+
if (!v) fail('--socket-dir requires a value');
|
|
811
|
+
return path.resolve(v);
|
|
812
|
+
}
|
|
813
|
+
|
|
750
814
|
function parseUiPort(args) {
|
|
751
815
|
const i = args.indexOf('--ui-port');
|
|
752
816
|
if (i < 0) return null;
|
|
@@ -807,7 +871,13 @@ function dispatch(subcommand, args, ctx) {
|
|
|
807
871
|
case 'install':
|
|
808
872
|
return cmdInstall(args, ctx);
|
|
809
873
|
case 'uninstall':
|
|
810
|
-
|
|
874
|
+
// canonical-pgserve-pm2-supervision Group 1: the uninstall surface
|
|
875
|
+
// moved to `src/commands/uninstall.js` so the cohort baseline (pm2
|
|
876
|
+
// teardown + admin.json supervisor clear + audit-log entry) lives
|
|
877
|
+
// alongside `src/lib/pm2-args.js` instead of inside this legacy
|
|
878
|
+
// dispatcher. dispatch() returns a Promise here; the wrapper
|
|
879
|
+
// already handles both numeric and Promise returns.
|
|
880
|
+
return import('./commands/uninstall.js').then((mod) => mod.runUninstall());
|
|
811
881
|
case 'status':
|
|
812
882
|
return cmdStatus(args);
|
|
813
883
|
case 'url':
|