pgserve 2.3.0 → 2.5.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 +9 -4
- package/bin/postgres-server.js +170 -631
- package/config/logrotate.d/pgserve +47 -0
- package/config/pgaudit.conf +31 -0
- package/package.json +3 -2
- package/scripts/audit-redaction-lint.js +349 -0
- package/scripts/test-npx.sh +32 -10
- package/src/audit/audit.js +134 -0
- package/src/cli-install.cjs +340 -100
- package/src/commands/uninstall.js +241 -0
- package/src/commands/verify.js +360 -0
- package/src/cosign/cache-token.js +328 -0
- package/src/cosign/schema.js +97 -0
- package/src/cosign/trust-list.js +81 -0
- package/src/cosign/verify-binary.js +277 -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/runtime-json.js +181 -0
- package/src/lib/socket-dir.js +69 -0
- package/src/postgres.js +64 -5
- package/src/upgrade/index.js +5 -0
- package/src/upgrade/steps/cosign-meta-migration.js +123 -0
- 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
package/src/cli-install.cjs
CHANGED
|
@@ -27,8 +27,42 @@ 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
|
+
const _runtimeJsonModuleP = import('./lib/runtime-json.js');
|
|
38
|
+
|
|
39
|
+
async function loadCohortModules() {
|
|
40
|
+
const [adminJson, socketDirMod, runtimeJson] = await Promise.all([
|
|
41
|
+
_adminJsonModuleP,
|
|
42
|
+
_socketDirModuleP,
|
|
43
|
+
_runtimeJsonModuleP,
|
|
44
|
+
]);
|
|
45
|
+
return { adminJson, socketDirMod, runtimeJson };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 1.
|
|
49
|
+
//
|
|
50
|
+
// The pm2 entry name moves from `pgserve` to `autopg-server` so the canonical
|
|
51
|
+
// two-process layout matches the cohort design (`autopg-server` + `autopg-ui`).
|
|
52
|
+
// Tier B operators install a systemd-user unit that ALSO claims the
|
|
53
|
+
// `autopg-server` lifecycle role — `~/.autopg/admin.json` records which
|
|
54
|
+
// supervisor owns the host and `pgserve install` refuses to register pm2 over
|
|
55
|
+
// an existing Tier B record.
|
|
56
|
+
//
|
|
57
|
+
// Default postgres port moves from 8432 (the old bun-proxy listener) to 5432
|
|
58
|
+
// (the postgres standard, since the postmaster now binds TCP directly).
|
|
59
|
+
const PM2_PROCESS_NAME = 'autopg-server';
|
|
60
|
+
// Legacy entry name (pre-v2.4). Self-healing migration in Group 6 will
|
|
61
|
+
// `pm2 delete pgserve` after registering `autopg-server`; we surface the
|
|
62
|
+
// constant here so cleanup tooling and tests can reference it without
|
|
63
|
+
// hardcoding strings.
|
|
64
|
+
const LEGACY_PM2_PROCESS_NAME = 'pgserve';
|
|
65
|
+
const DEFAULT_PORT = 5432;
|
|
32
66
|
|
|
33
67
|
// Console UI is auto-supervised under pm2 alongside the daemon since v2.2.3.
|
|
34
68
|
// The bundled SPA (console/dist/) is served on this port; operator-facing
|
|
@@ -136,6 +170,131 @@ function readConfig() {
|
|
|
136
170
|
}
|
|
137
171
|
}
|
|
138
172
|
|
|
173
|
+
// cutover G19: discovery layer used by `autopg port / url / status`.
|
|
174
|
+
//
|
|
175
|
+
// Order of precedence (most-authoritative first):
|
|
176
|
+
// 1. `<socketDir>/runtime.json` — written by the live postmaster at greet
|
|
177
|
+
// time, removed on graceful shutdown. Carries the *current* port + pid
|
|
178
|
+
// for an actually-running daemon.
|
|
179
|
+
// 2. `~/.autopg/admin.json` — supervisor record written at install time.
|
|
180
|
+
// Survives postmaster restarts; doesn't reflect runtime state.
|
|
181
|
+
// 3. `~/.autopg/config.json` — legacy pre-G19 install record. Final
|
|
182
|
+
// fallback so older installs that haven't been re-installed under v2.4
|
|
183
|
+
// still discover cleanly.
|
|
184
|
+
//
|
|
185
|
+
// All readers swallow errors — discovery must never throw on a missing or
|
|
186
|
+
// truncated file. Synchronous on purpose: `dispatch()` for status/url/port
|
|
187
|
+
// is sync and the wrapper handles only `Promise OR number` return types.
|
|
188
|
+
function readRuntimeJsonSync(socketDir) {
|
|
189
|
+
if (typeof socketDir !== 'string' || socketDir.length === 0) return null;
|
|
190
|
+
const file = path.join(socketDir, 'runtime.json');
|
|
191
|
+
let raw;
|
|
192
|
+
try {
|
|
193
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(raw);
|
|
199
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readAdminJsonSync() {
|
|
206
|
+
const file = path.join(getConfigDir(), ADMIN_FILE_NAME);
|
|
207
|
+
let raw;
|
|
208
|
+
try {
|
|
209
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(raw);
|
|
215
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function resolveCanonicalSocketDir() {
|
|
222
|
+
// Mirror src/lib/socket-dir.js#resolveSocketDir — pure function, no fs
|
|
223
|
+
// touch. Inlined here so the sync discovery layer doesn't need a top-
|
|
224
|
+
// level await on the ESM module.
|
|
225
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
226
|
+
const base = xdg && xdg.length > 0 ? xdg : '/tmp';
|
|
227
|
+
return path.join(base, 'pgserve');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isLivePid(pid) {
|
|
231
|
+
if (!Number.isInteger(pid) || pid < 1) return false;
|
|
232
|
+
try {
|
|
233
|
+
process.kill(pid, 0);
|
|
234
|
+
return true;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return err && err.code === 'EPERM';
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compose a discovery view from runtime.json (preferred), admin.json
|
|
242
|
+
* (fallback), and config.json (legacy fallback). Returns:
|
|
243
|
+
* {
|
|
244
|
+
* runtime: { socketDir, port, pid, autopgPid, schemaVersion } | null,
|
|
245
|
+
* admin: { supervisor, socketDir, port, installedAt, ... } | null,
|
|
246
|
+
* config: { port, dataDir, registeredAt } | null,
|
|
247
|
+
* // composed view — best effort merge for callers that just want
|
|
248
|
+
* // "where do I connect right now?":
|
|
249
|
+
* socketDir: <string|null>,
|
|
250
|
+
* port: <number|null>,
|
|
251
|
+
* liveAutopg: <boolean> // true when runtime.json names a live pid
|
|
252
|
+
* }
|
|
253
|
+
*/
|
|
254
|
+
function readDiscovery() {
|
|
255
|
+
const config = readConfig();
|
|
256
|
+
const admin = readAdminJsonSync();
|
|
257
|
+
// Prefer the socket dir the supervisor recorded at install time — that's
|
|
258
|
+
// the path operators configured. Only fall back to the canonical resolver
|
|
259
|
+
// when the install record is missing (fresh-host case).
|
|
260
|
+
const socketDir = (admin && typeof admin.socketDir === 'string' && admin.socketDir.length > 0)
|
|
261
|
+
? admin.socketDir
|
|
262
|
+
: resolveCanonicalSocketDir();
|
|
263
|
+
const runtime = readRuntimeJsonSync(socketDir);
|
|
264
|
+
|
|
265
|
+
// PR #80 P2 fix: previous logic treated ANY parsed runtime.json as
|
|
266
|
+
// authoritative — a malformed-but-JSON file (no port, no socketDir) would
|
|
267
|
+
// hide later admin.json / config fallbacks because composedPort stayed
|
|
268
|
+
// null while the precedence chain stopped early. Validate that runtime
|
|
269
|
+
// actually carries a usable port + socketDir before treating it as live.
|
|
270
|
+
// Mirrors the admin / config branches' Number.isInteger guard.
|
|
271
|
+
let composedSocketDir = null;
|
|
272
|
+
let composedPort = null;
|
|
273
|
+
const runtimeUsable = runtime
|
|
274
|
+
&& Number.isInteger(runtime.port)
|
|
275
|
+
&& typeof runtime.socketDir === 'string'
|
|
276
|
+
&& runtime.socketDir.length > 0;
|
|
277
|
+
if (runtimeUsable) {
|
|
278
|
+
composedSocketDir = runtime.socketDir;
|
|
279
|
+
composedPort = runtime.port;
|
|
280
|
+
} else if (admin && Number.isInteger(admin.port)) {
|
|
281
|
+
composedSocketDir = admin.socketDir ?? socketDir;
|
|
282
|
+
composedPort = admin.port;
|
|
283
|
+
} else if (config && Number.isInteger(config.port)) {
|
|
284
|
+
composedPort = config.port;
|
|
285
|
+
composedSocketDir = socketDir;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
runtime,
|
|
290
|
+
admin,
|
|
291
|
+
config,
|
|
292
|
+
socketDir: composedSocketDir,
|
|
293
|
+
port: composedPort,
|
|
294
|
+
liveAutopg: !!(runtime && isLivePid(runtime.autopgPid)),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
139
298
|
function writeConfig(config) {
|
|
140
299
|
const dir = getConfigDir();
|
|
141
300
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
|
|
@@ -200,7 +359,7 @@ function getEffectiveSupervision() {
|
|
|
200
359
|
}
|
|
201
360
|
}
|
|
202
361
|
|
|
203
|
-
function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
362
|
+
function buildPm2StartArgs({ scriptPath, port, dataDir, socketDir }) {
|
|
204
363
|
const logs = {
|
|
205
364
|
out: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-out.log`),
|
|
206
365
|
error: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-error.log`),
|
|
@@ -215,19 +374,11 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
215
374
|
'none',
|
|
216
375
|
'--max-restarts',
|
|
217
376
|
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.
|
|
377
|
+
// pm2 ≥ 6.0 dropped `--min-uptime` from the CLI surface — passing it
|
|
378
|
+
// aborts the install. Restart budget (50) is sized to absorb a few
|
|
379
|
+
// long-uptime crashes without burning through.
|
|
226
380
|
'--restart-delay',
|
|
227
381
|
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
382
|
'--exp-backoff-restart-delay',
|
|
232
383
|
String(supervision.expBackoffRestartDelayMs),
|
|
233
384
|
'--max-memory-restart',
|
|
@@ -241,28 +392,19 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
241
392
|
'--error',
|
|
242
393
|
logs.error,
|
|
243
394
|
'--',
|
|
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.
|
|
395
|
+
// pgserve singleton (v2.4): pm2 supervises the postmaster directly via
|
|
396
|
+
// the `pgserve postmaster` subcommand — no router, no bun proxy, no
|
|
397
|
+
// daemon control socket. Postgres binds the canonical Unix socket
|
|
398
|
+
// under <socketDir> AND TCP <port> natively. Operators connect via
|
|
399
|
+
// psql -h $XDG_RUNTIME_DIR/pgserve (Unix socket, no -p)
|
|
400
|
+
// psql -h 127.0.0.1 -p 5432 (canonical TCP)
|
|
401
|
+
'postmaster',
|
|
262
402
|
'--port',
|
|
263
403
|
String(port),
|
|
264
404
|
'--data',
|
|
265
405
|
dataDir,
|
|
406
|
+
'--socket-dir',
|
|
407
|
+
socketDir,
|
|
266
408
|
'--log',
|
|
267
409
|
'warn',
|
|
268
410
|
];
|
|
@@ -384,21 +526,9 @@ function cmdInstallUi(ctx, options = {}) {
|
|
|
384
526
|
return 0;
|
|
385
527
|
}
|
|
386
528
|
|
|
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
|
-
}
|
|
529
|
+
// `cmdUninstall` + `cmdUninstallUi` migrated to `src/commands/uninstall.js`
|
|
530
|
+
// as part of canonical-pgserve-pm2-supervision Group 1. The dispatch
|
|
531
|
+
// `case 'uninstall'` above dynamically imports the new module.
|
|
402
532
|
|
|
403
533
|
// ─── Admin password (Basic Auth for `autopg ui`) ─────────────────────────
|
|
404
534
|
|
|
@@ -429,12 +559,19 @@ function writeAdminFile({ password, rotated = false }) {
|
|
|
429
559
|
const hash = hashAdminPassword(password, salt);
|
|
430
560
|
const file = getAdminFilePath();
|
|
431
561
|
const now = new Date().toISOString();
|
|
562
|
+
// pgserve singleton (v2.4): admin.json is shared with the supervisor
|
|
563
|
+
// record (`{ supervisor, socketDir, port, installedAt }`) written by
|
|
564
|
+
// `src/lib/admin-json.js`. Merge with any existing fields so the scrypt
|
|
565
|
+
// Basic-Auth scheme can never wipe the supervisor metadata (and vice
|
|
566
|
+
// versa).
|
|
567
|
+
const existing = readAdminFile() || {};
|
|
432
568
|
const payload = {
|
|
569
|
+
...existing,
|
|
433
570
|
scheme: 'scrypt',
|
|
434
571
|
params: SCRYPT_PARAMS,
|
|
435
572
|
salt: salt.toString('base64'),
|
|
436
573
|
hash: hash.toString('base64'),
|
|
437
|
-
createdAt: rotated ?
|
|
574
|
+
createdAt: rotated ? existing.createdAt ?? now : now,
|
|
438
575
|
rotatedAt: rotated ? now : null,
|
|
439
576
|
};
|
|
440
577
|
ensureConfigDir();
|
|
@@ -487,9 +624,14 @@ function verifyAdminPassword(candidate) {
|
|
|
487
624
|
|
|
488
625
|
function ensureAdminPassword({ rotate = false } = {}) {
|
|
489
626
|
const existing = readAdminFile();
|
|
490
|
-
|
|
627
|
+
// pgserve singleton (v2.4): admin.json may exist with only supervisor
|
|
628
|
+
// fields (`{ supervisor, socketDir, port, installedAt }`) and no scrypt
|
|
629
|
+
// scheme yet. Treat "no scrypt scheme" as "no password" so the install
|
|
630
|
+
// path still mints one on first run.
|
|
631
|
+
const hasScryptScheme = !!(existing && existing.scheme === 'scrypt');
|
|
632
|
+
if (hasScryptScheme && !rotate) return null;
|
|
491
633
|
const password = generateAdminPassword();
|
|
492
|
-
writeAdminFile({ password, rotated: !!
|
|
634
|
+
writeAdminFile({ password, rotated: !!hasScryptScheme });
|
|
493
635
|
return password;
|
|
494
636
|
}
|
|
495
637
|
|
|
@@ -532,14 +674,41 @@ function cmdAuthDispatch(args) {
|
|
|
532
674
|
* `scriptPath` is the path to `bin/postgres-server.js` resolved by the
|
|
533
675
|
* wrapper before this module is required (avoids re-resolving here).
|
|
534
676
|
*/
|
|
535
|
-
function cmdInstall(args, ctx) {
|
|
536
|
-
|
|
537
|
-
|
|
677
|
+
async function cmdInstall(args, ctx) {
|
|
678
|
+
const { adminJson, socketDirMod } = await loadCohortModules();
|
|
679
|
+
|
|
680
|
+
// pgserve singleton (v2.4): refuse to install if a different supervisor
|
|
681
|
+
// (Tier B systemd-user / launchd) already owns the host. The cohort
|
|
682
|
+
// contract guarantees one and only one supervisor records itself in
|
|
683
|
+
// `~/.autopg/admin.json`. `pgserve install` is the Tier A (pm2) entry —
|
|
684
|
+
// it asserts that nothing else has already claimed the host before
|
|
685
|
+
// touching pm2.
|
|
686
|
+
const noPm2 = args.includes('--no-pm2');
|
|
687
|
+
const expectedSupervisor = noPm2 ? 'external' : 'pm2';
|
|
688
|
+
try {
|
|
689
|
+
adminJson.assertSupervisor(expectedSupervisor, { configDir: getConfigDir() });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
fail(err.message);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (!noPm2 && !pm2IsAvailable()) {
|
|
695
|
+
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
696
|
}
|
|
539
697
|
|
|
540
698
|
const port = parsePort(args) ?? readConfig()?.port ?? DEFAULT_PORT;
|
|
541
699
|
const dataDir = parseDataDir(args) ?? readConfig()?.dataDir ?? getDataDir();
|
|
542
700
|
|
|
701
|
+
// Set up the canonical socket directory before pm2 launches the
|
|
702
|
+
// postmaster. `ensureSocketDir` enforces mode 0700 and probes writability
|
|
703
|
+
// so failure surfaces here — not as a libpq bind error a few seconds
|
|
704
|
+
// into pm2 backoff.
|
|
705
|
+
const socketDir = parseSocketDir(args) ?? socketDirMod.resolveSocketDir();
|
|
706
|
+
try {
|
|
707
|
+
socketDirMod.ensureSocketDir(socketDir);
|
|
708
|
+
} catch (err) {
|
|
709
|
+
fail(err.message);
|
|
710
|
+
}
|
|
711
|
+
|
|
543
712
|
const noUi = args.includes('--no-ui');
|
|
544
713
|
const withUi = args.includes('--with-ui');
|
|
545
714
|
const redeploy = args.includes('--redeploy');
|
|
@@ -559,6 +728,22 @@ function cmdInstall(args, ctx) {
|
|
|
559
728
|
return 0;
|
|
560
729
|
}
|
|
561
730
|
|
|
731
|
+
// --no-pm2: skip pm2 register entirely. Used by CI fixture provisioning
|
|
732
|
+
// (no sudo, no pm2 ambient state) and by Tier B-bound hosts that will
|
|
733
|
+
// immediately hand off to `autopg service install`. Still record the
|
|
734
|
+
// supervisor + canonical socket dir + port in admin.json so downstream
|
|
735
|
+
// tooling (pgserve doctor, omni install, genie install) can discover
|
|
736
|
+
// where to connect without an active pm2 entry.
|
|
737
|
+
if (noPm2) {
|
|
738
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
739
|
+
writeConfig({ port, dataDir, registeredAt: readConfig()?.registeredAt ?? new Date().toISOString() });
|
|
740
|
+
writeSupervisorRecord(adminJson, { supervisor: 'external', socketDir, port });
|
|
741
|
+
ok(`installed (--no-pm2): supervisor=external, socketDir=${socketDir}, port=${port}`);
|
|
742
|
+
ok(`url: postgres://localhost:${port}/postgres`);
|
|
743
|
+
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.`);
|
|
744
|
+
return 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
562
747
|
// --redeploy: full reset. Tear down both processes, then proceed with
|
|
563
748
|
// a fresh install. Equivalent to `autopg uninstall && autopg install`
|
|
564
749
|
// but in one verb.
|
|
@@ -581,6 +766,7 @@ function cmdInstall(args, ctx) {
|
|
|
581
766
|
// don't tear down the live process. Operators wanting a port change
|
|
582
767
|
// should `uninstall` then `install` (or pass --redeploy).
|
|
583
768
|
writeConfig({ port, dataDir, registeredAt: readConfig()?.registeredAt ?? new Date().toISOString() });
|
|
769
|
+
writeSupervisorRecord(adminJson, { supervisor: 'pm2', socketDir, port });
|
|
584
770
|
if (!noUi) cmdInstallUi(ctx, { uiPort, uiHost });
|
|
585
771
|
return 0;
|
|
586
772
|
}
|
|
@@ -588,14 +774,15 @@ function cmdInstall(args, ctx) {
|
|
|
588
774
|
ensureLogsDir();
|
|
589
775
|
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true, mode: 0o700 });
|
|
590
776
|
|
|
591
|
-
const pm2Args = buildPm2StartArgs({ scriptPath: ctx.scriptPath, port, dataDir });
|
|
777
|
+
const pm2Args = buildPm2StartArgs({ scriptPath: ctx.scriptPath, port, dataDir, socketDir });
|
|
592
778
|
const result = spawnSync('pm2', pm2Args, { stdio: 'inherit' });
|
|
593
779
|
if (result.status !== 0) {
|
|
594
780
|
fail(`pm2 start failed (exit ${result.status}). Logs: ${getLogsDir()}/${PM2_PROCESS_NAME}-error.log`);
|
|
595
781
|
}
|
|
596
782
|
|
|
597
783
|
writeConfig({ port, dataDir, registeredAt: new Date().toISOString() });
|
|
598
|
-
|
|
784
|
+
writeSupervisorRecord(adminJson, { supervisor: 'pm2', socketDir, port });
|
|
785
|
+
ok(`installed: pm2 process "${PM2_PROCESS_NAME}" on port ${port} (socket: ${socketDir}, data: ${dataDir})`);
|
|
599
786
|
ok(`url: postgres://localhost:${port}/postgres`);
|
|
600
787
|
|
|
601
788
|
if (noUi) {
|
|
@@ -620,43 +807,47 @@ function cmdInstall(args, ctx) {
|
|
|
620
807
|
}
|
|
621
808
|
|
|
622
809
|
/**
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* downstream service still depends on the data.
|
|
810
|
+
* Persist the supervisor record into `~/.autopg/admin.json`. Wraps
|
|
811
|
+
* `writeAdminJson` from the cohort-shared module so the install path can
|
|
812
|
+
* pin its own ISO timestamp + log a friendly diagnostic on the
|
|
813
|
+
* supervisor-lock refusal.
|
|
628
814
|
*/
|
|
629
|
-
function
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
815
|
+
function writeSupervisorRecord(adminJson, { supervisor, socketDir, port }) {
|
|
816
|
+
try {
|
|
817
|
+
adminJson.writeAdminJson(
|
|
818
|
+
{
|
|
819
|
+
supervisor,
|
|
820
|
+
socketDir,
|
|
821
|
+
port,
|
|
822
|
+
installedAt: new Date().toISOString(),
|
|
823
|
+
},
|
|
824
|
+
{ configDir: getConfigDir() },
|
|
825
|
+
);
|
|
826
|
+
} catch (err) {
|
|
827
|
+
// The "Tier B already owns this host" error is the contract failure
|
|
828
|
+
// we surface to operators. Other errors (EACCES, ENOSPC, etc.) just
|
|
829
|
+
// pass through with their stock message.
|
|
830
|
+
fail(err.message);
|
|
642
831
|
}
|
|
643
|
-
ok(`uninstalled (pm2 process removed; data dir preserved at ${getDataDir()})`);
|
|
644
|
-
cmdUninstallUi();
|
|
645
|
-
return 0;
|
|
646
832
|
}
|
|
647
833
|
|
|
648
834
|
/**
|
|
649
835
|
* `pgserve status [--json]`
|
|
650
836
|
*
|
|
651
|
-
* Reports both pm2 state and on-disk
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
837
|
+
* Reports both pm2 state and on-disk discovery (runtime.json → admin.json
|
|
838
|
+
* → config.json fallback chain). Exits 0 with status info regardless of
|
|
839
|
+
* running/stopped — operators script around the JSON output. Non-zero
|
|
840
|
+
* only when nothing was ever installed (no admin.json AND no config.json).
|
|
841
|
+
*
|
|
842
|
+
* Cutover G19: surfaces `runtime` (live socket discovery) and `socketDir`
|
|
843
|
+
* top-level so consumers can pick UDS vs TCP without parsing pm2 jlist.
|
|
655
844
|
*/
|
|
656
845
|
function cmdStatus(args) {
|
|
657
846
|
const json = args.includes('--json');
|
|
658
|
-
const
|
|
659
|
-
|
|
847
|
+
const discovery = readDiscovery();
|
|
848
|
+
const { config, admin, runtime } = discovery;
|
|
849
|
+
|
|
850
|
+
if (!config && !admin) {
|
|
660
851
|
if (json) {
|
|
661
852
|
process.stdout.write(`${JSON.stringify({ installed: false })}\n`);
|
|
662
853
|
} else {
|
|
@@ -664,24 +855,41 @@ function cmdStatus(args) {
|
|
|
664
855
|
}
|
|
665
856
|
return 1;
|
|
666
857
|
}
|
|
858
|
+
|
|
667
859
|
const proc = pm2GetProcess(PM2_PROCESS_NAME);
|
|
668
860
|
const status = proc?.pm2_env?.status ?? 'stopped';
|
|
669
861
|
const pid = proc?.pid ?? null;
|
|
670
862
|
const uptimeMs = proc?.pm2_env?.pm_uptime ? Date.now() - proc.pm2_env.pm_uptime : null;
|
|
671
863
|
const restarts = proc?.pm2_env?.restart_time ?? 0;
|
|
672
864
|
|
|
865
|
+
const port = discovery.port;
|
|
866
|
+
const socketDir = discovery.socketDir;
|
|
867
|
+
const dataDir = config?.dataDir ?? null;
|
|
868
|
+
|
|
673
869
|
const payload = {
|
|
674
870
|
installed: true,
|
|
675
871
|
name: PM2_PROCESS_NAME,
|
|
676
872
|
status,
|
|
677
873
|
pid,
|
|
678
|
-
port
|
|
679
|
-
|
|
874
|
+
port,
|
|
875
|
+
socketDir,
|
|
876
|
+
dataDir,
|
|
680
877
|
logsDir: getLogsDir(),
|
|
681
|
-
url: `postgres://localhost:${
|
|
878
|
+
url: port ? `postgres://localhost:${port}/postgres` : null,
|
|
682
879
|
uptimeMs,
|
|
683
880
|
restarts,
|
|
684
|
-
registeredAt: config
|
|
881
|
+
registeredAt: config?.registeredAt ?? null,
|
|
882
|
+
supervisor: admin?.supervisor ?? null,
|
|
883
|
+
runtime: runtime
|
|
884
|
+
? {
|
|
885
|
+
socketDir: runtime.socketDir,
|
|
886
|
+
port: runtime.port,
|
|
887
|
+
pid: runtime.pid,
|
|
888
|
+
autopgPid: runtime.autopgPid,
|
|
889
|
+
schemaVersion: runtime.schemaVersion,
|
|
890
|
+
live: discovery.liveAutopg,
|
|
891
|
+
}
|
|
892
|
+
: null,
|
|
685
893
|
};
|
|
686
894
|
|
|
687
895
|
if (json) {
|
|
@@ -690,42 +898,53 @@ function cmdStatus(args) {
|
|
|
690
898
|
}
|
|
691
899
|
process.stdout.write(`name ${payload.name}\n`);
|
|
692
900
|
process.stdout.write(`status ${payload.status}${payload.pid ? ` (pid ${payload.pid})` : ''}\n`);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
901
|
+
if (payload.supervisor) {
|
|
902
|
+
process.stdout.write(`supervisor ${payload.supervisor}\n`);
|
|
903
|
+
}
|
|
904
|
+
if (payload.port != null) process.stdout.write(`port ${payload.port}\n`);
|
|
905
|
+
if (payload.url) process.stdout.write(`url ${payload.url}\n`);
|
|
906
|
+
if (payload.socketDir) process.stdout.write(`socketDir ${payload.socketDir}\n`);
|
|
907
|
+
if (payload.dataDir) process.stdout.write(`dataDir ${payload.dataDir}\n`);
|
|
696
908
|
process.stdout.write(`logsDir ${payload.logsDir}\n`);
|
|
909
|
+
if (payload.runtime) {
|
|
910
|
+
process.stdout.write(`runtime pid=${payload.runtime.pid} autopgPid=${payload.runtime.autopgPid} live=${payload.runtime.live}\n`);
|
|
911
|
+
} else {
|
|
912
|
+
process.stdout.write(`runtime (no runtime.json — postmaster down or never started)\n`);
|
|
913
|
+
}
|
|
697
914
|
if (payload.uptimeMs != null) {
|
|
698
915
|
const sec = Math.floor(payload.uptimeMs / 1000);
|
|
699
916
|
process.stdout.write(`uptime ${sec}s\n`);
|
|
700
917
|
}
|
|
701
918
|
process.stdout.write(`restarts ${payload.restarts}\n`);
|
|
702
|
-
process.stdout.write(`registered ${payload.registeredAt}\n`);
|
|
919
|
+
if (payload.registeredAt) process.stdout.write(`registered ${payload.registeredAt}\n`);
|
|
703
920
|
return 0;
|
|
704
921
|
}
|
|
705
922
|
|
|
706
923
|
/**
|
|
707
924
|
* `pgserve url`
|
|
708
925
|
*
|
|
709
|
-
* Discovery API. Prints the canonical connection string. Downstream
|
|
926
|
+
* Discovery API. Prints the canonical TCP connection string. Downstream
|
|
710
927
|
* installers (genie install, omni install) call this to learn where to
|
|
711
|
-
* connect, instead of hardcoding a port.
|
|
928
|
+
* connect, instead of hardcoding a port. The TCP form is stable across
|
|
929
|
+
* Tier A / Tier B / fingerprint-disabled hosts; UDS callers should
|
|
930
|
+
* resolve `<socketDir>/.s.PGSQL.<port>` from `autopg status --json`.
|
|
712
931
|
*/
|
|
713
932
|
function cmdUrl() {
|
|
714
|
-
const
|
|
715
|
-
if (
|
|
933
|
+
const discovery = readDiscovery();
|
|
934
|
+
if (discovery.port == null) {
|
|
716
935
|
fail('not installed (run: pgserve install)');
|
|
717
936
|
}
|
|
718
|
-
process.stdout.write(`postgres://localhost:${
|
|
937
|
+
process.stdout.write(`postgres://localhost:${discovery.port}/postgres\n`);
|
|
719
938
|
return 0;
|
|
720
939
|
}
|
|
721
940
|
|
|
722
|
-
/** `pgserve port` — print the canonical port. */
|
|
941
|
+
/** `pgserve port` — print the canonical port from runtime.json → admin.json → config.json. */
|
|
723
942
|
function cmdPort() {
|
|
724
|
-
const
|
|
725
|
-
if (
|
|
943
|
+
const discovery = readDiscovery();
|
|
944
|
+
if (discovery.port == null) {
|
|
726
945
|
fail('not installed (run: pgserve install)');
|
|
727
946
|
}
|
|
728
|
-
process.stdout.write(`${
|
|
947
|
+
process.stdout.write(`${discovery.port}\n`);
|
|
729
948
|
return 0;
|
|
730
949
|
}
|
|
731
950
|
|
|
@@ -747,6 +966,14 @@ function parseDataDir(args) {
|
|
|
747
966
|
return path.resolve(v);
|
|
748
967
|
}
|
|
749
968
|
|
|
969
|
+
function parseSocketDir(args) {
|
|
970
|
+
const i = args.indexOf('--socket-dir');
|
|
971
|
+
if (i < 0) return null;
|
|
972
|
+
const v = args[i + 1];
|
|
973
|
+
if (!v) fail('--socket-dir requires a value');
|
|
974
|
+
return path.resolve(v);
|
|
975
|
+
}
|
|
976
|
+
|
|
750
977
|
function parseUiPort(args) {
|
|
751
978
|
const i = args.indexOf('--ui-port');
|
|
752
979
|
if (i < 0) return null;
|
|
@@ -807,7 +1034,13 @@ function dispatch(subcommand, args, ctx) {
|
|
|
807
1034
|
case 'install':
|
|
808
1035
|
return cmdInstall(args, ctx);
|
|
809
1036
|
case 'uninstall':
|
|
810
|
-
|
|
1037
|
+
// canonical-pgserve-pm2-supervision Group 1: the uninstall surface
|
|
1038
|
+
// moved to `src/commands/uninstall.js` so the cohort baseline (pm2
|
|
1039
|
+
// teardown + admin.json supervisor clear + audit-log entry) lives
|
|
1040
|
+
// alongside `src/lib/pm2-args.js` instead of inside this legacy
|
|
1041
|
+
// dispatcher. dispatch() returns a Promise here; the wrapper
|
|
1042
|
+
// already handles both numeric and Promise returns.
|
|
1043
|
+
return import('./commands/uninstall.js').then((mod) => mod.runUninstall());
|
|
811
1044
|
case 'status':
|
|
812
1045
|
return cmdStatus(args);
|
|
813
1046
|
case 'url':
|
|
@@ -843,6 +1076,13 @@ function dispatch(subcommand, args, ctx) {
|
|
|
843
1076
|
}
|
|
844
1077
|
case 'auth':
|
|
845
1078
|
return cmdAuthDispatch(args);
|
|
1079
|
+
case 'verify': {
|
|
1080
|
+
// pgserve singleton (v2.4) — `pgserve-singleton-no-proxy` wish, Group 4.
|
|
1081
|
+
// `pgserve verify` is a pure-node command (cosign shellout + HMAC
|
|
1082
|
+
// cache token write); routes through the same async-import pattern
|
|
1083
|
+
// as `uninstall` so the ESM module isn't eagerly loaded.
|
|
1084
|
+
return import('./commands/verify.js').then((mod) => mod.runVerify(args));
|
|
1085
|
+
}
|
|
846
1086
|
default:
|
|
847
1087
|
throw new Error(`pgserve: dispatch called with unknown subcommand "${subcommand}"`);
|
|
848
1088
|
}
|