pgserve 2.1.1 → 2.1.3
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/postgres-server.js +24 -7
- package/install.sh +123 -0
- package/package.json +1 -1
- package/src/cli-install.cjs +18 -1
- package/tests/cli-install.test.js +16 -0
- package/tests/daemon-args.test.js +86 -0
package/bin/postgres-server.js
CHANGED
|
@@ -132,6 +132,7 @@ function parseDaemonArgs(daemonArgs) {
|
|
|
132
132
|
autoProvision: true,
|
|
133
133
|
tcpListens: [],
|
|
134
134
|
enablePgvector: false,
|
|
135
|
+
maxConnections: null,
|
|
135
136
|
};
|
|
136
137
|
for (let i = 0; i < daemonArgs.length; i++) {
|
|
137
138
|
const arg = daemonArgs[i];
|
|
@@ -156,6 +157,21 @@ function parseDaemonArgs(daemonArgs) {
|
|
|
156
157
|
case '--pgvector':
|
|
157
158
|
opts.enablePgvector = true;
|
|
158
159
|
break;
|
|
160
|
+
case '--max-connections': {
|
|
161
|
+
// Accept the same flag the foreground/router mode takes so callers
|
|
162
|
+
// (genie's `getOrStartDaemon`, anything that spawns `pgserve daemon`
|
|
163
|
+
// with a tuned cap) can override the postmaster's `max_connections`.
|
|
164
|
+
// The `PgserveDaemon` constructor already honors `options.maxConnections`
|
|
165
|
+
// (see src/daemon.js — defaults to 1000); we just plumb it through.
|
|
166
|
+
const raw = daemonArgs[++i];
|
|
167
|
+
const parsed = Number.parseInt(raw, 10);
|
|
168
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
169
|
+
console.error(`--max-connections: expected a positive integer, got "${raw}"`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
opts.maxConnections = parsed;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
159
175
|
case '--help':
|
|
160
176
|
console.log(`
|
|
161
177
|
pgserve daemon — singleton control-socket mode
|
|
@@ -167,13 +183,14 @@ USAGE:
|
|
|
167
183
|
pgserve daemon revoke-token <id>
|
|
168
184
|
|
|
169
185
|
OPTIONS:
|
|
170
|
-
--data <path>
|
|
171
|
-
--ram
|
|
172
|
-
--log <level>
|
|
173
|
-
--no-provision
|
|
174
|
-
--listen [host:]port
|
|
175
|
-
--pgvector
|
|
176
|
-
--
|
|
186
|
+
--data <path> Persistent data directory (default: in-memory)
|
|
187
|
+
--ram Use /dev/shm storage (Linux only)
|
|
188
|
+
--log <level> Log level: error|warn|info|debug (default: info)
|
|
189
|
+
--no-provision Disable auto-provisioning of databases
|
|
190
|
+
--listen [host:]port Bind opt-in TCP listener (repeatable)
|
|
191
|
+
--pgvector Auto-enable pgvector extension on new databases
|
|
192
|
+
--max-connections <n> Override the postmaster's max_connections (default: 1000)
|
|
193
|
+
--help Show this help
|
|
177
194
|
|
|
178
195
|
The daemon binds $XDG_RUNTIME_DIR/pgserve/control.sock (fallback /tmp/pgserve/control.sock).
|
|
179
196
|
A second invocation while the first is running exits with "already running".
|
package/install.sh
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ============================================================================
|
|
3
|
+
# pgserve — Canonical PostgreSQL backbone installer
|
|
4
|
+
#
|
|
5
|
+
# Bootstraps a single shared pgserve instance under pm2 supervision. Used as
|
|
6
|
+
# a prerequisite by `omni/install.sh` and `genie/install.sh` so every
|
|
7
|
+
# automagik service on a host points at the same Postgres.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash
|
|
11
|
+
#
|
|
12
|
+
# With pinned version:
|
|
13
|
+
# PGSERVE_VERSION=^2.1.1 curl -fsSL .../install.sh | bash
|
|
14
|
+
#
|
|
15
|
+
# Local checkout:
|
|
16
|
+
# bash install.sh
|
|
17
|
+
#
|
|
18
|
+
# Idempotent — re-running is a no-op success when pgserve is already
|
|
19
|
+
# registered under pm2 with a healthy entry.
|
|
20
|
+
# ============================================================================
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
PGSERVE_VERSION="${PGSERVE_VERSION:-^2.1.0}"
|
|
24
|
+
|
|
25
|
+
# Colors (no-op when stdout isn't a tty)
|
|
26
|
+
if [[ -t 1 ]]; then
|
|
27
|
+
RED='\033[0;31m'
|
|
28
|
+
GREEN='\033[0;32m'
|
|
29
|
+
YELLOW='\033[1;33m'
|
|
30
|
+
BLUE='\033[0;34m'
|
|
31
|
+
CYAN='\033[0;36m'
|
|
32
|
+
BOLD='\033[1m'
|
|
33
|
+
NC='\033[0m'
|
|
34
|
+
else
|
|
35
|
+
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
info() { printf "${BLUE}ℹ${NC} %s\n" "$*"; }
|
|
39
|
+
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
|
40
|
+
warn() { printf "${YELLOW}⚠${NC} %s\n" "$*"; }
|
|
41
|
+
fail() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
|
|
42
|
+
step() { printf "\n${BOLD}${CYAN}▸ %s${NC}\n" "$*"; }
|
|
43
|
+
|
|
44
|
+
has_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# Prerequisites: bun + pm2
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
ensure_bun() {
|
|
51
|
+
if has_cmd bun; then
|
|
52
|
+
ok "bun $(bun --version 2>/dev/null || echo '?')"
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
info "Installing bun (https://bun.sh)..."
|
|
56
|
+
curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || fail "bun install failed — see https://bun.sh"
|
|
57
|
+
# Make bun available to the rest of this script without requiring a re-login.
|
|
58
|
+
export PATH="$HOME/.bun/bin:$PATH"
|
|
59
|
+
has_cmd bun || fail "bun installed but not on PATH — restart your shell and re-run."
|
|
60
|
+
ok "bun $(bun --version)"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ensure_pm2() {
|
|
64
|
+
if has_cmd pm2; then
|
|
65
|
+
ok "pm2 $(pm2 --version 2>/dev/null || echo '?')"
|
|
66
|
+
return 0
|
|
67
|
+
fi
|
|
68
|
+
info "Installing pm2 (process supervisor)..."
|
|
69
|
+
bun add -g pm2 >/dev/null 2>&1 || fail "pm2 install failed — try: bun add -g pm2"
|
|
70
|
+
has_cmd pm2 || fail "pm2 installed but not on PATH — restart your shell and re-run."
|
|
71
|
+
ok "pm2 installed"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ============================================================================
|
|
75
|
+
# pgserve binary + pm2 registration
|
|
76
|
+
# ============================================================================
|
|
77
|
+
|
|
78
|
+
ensure_pgserve_binary() {
|
|
79
|
+
# Probe via `pgserve port` (real subcommand). `pgserve --version` doesn't
|
|
80
|
+
# exist in 2.1.x — using it would false-negative and trigger a redundant
|
|
81
|
+
# reinstall every time install.sh runs.
|
|
82
|
+
if has_cmd pgserve && pgserve port >/dev/null 2>&1; then
|
|
83
|
+
ok "pgserve binary present (port $(pgserve port 2>/dev/null))"
|
|
84
|
+
return 0
|
|
85
|
+
fi
|
|
86
|
+
info "Installing pgserve@${PGSERVE_VERSION} globally..."
|
|
87
|
+
bun add -g "pgserve@${PGSERVE_VERSION}" >/dev/null 2>&1 \
|
|
88
|
+
|| fail "pgserve install failed — try: bun add -g pgserve@${PGSERVE_VERSION}"
|
|
89
|
+
has_cmd pgserve || fail "pgserve installed but not on PATH — restart your shell and re-run."
|
|
90
|
+
ok "pgserve $(pgserve port 2>/dev/null || echo '?')"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
register_pgserve_pm2() {
|
|
94
|
+
info "Registering pgserve under pm2 (idempotent)..."
|
|
95
|
+
# `pgserve install` prints its own success/already-installed line and exits
|
|
96
|
+
# 0 in both cases. We pipe stderr through so any pm2 errors surface to the
|
|
97
|
+
# operator (the pm2-6.x --min-uptime breakage we hit on 2026-04-30 was
|
|
98
|
+
# invisible because stderr was being captured).
|
|
99
|
+
pgserve install || fail "pgserve install failed — see ~/.pgserve/logs/pgserve-error.log"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Main
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
main() {
|
|
107
|
+
step "Installing canonical pgserve"
|
|
108
|
+
ensure_bun
|
|
109
|
+
ensure_pm2
|
|
110
|
+
ensure_pgserve_binary
|
|
111
|
+
register_pgserve_pm2
|
|
112
|
+
|
|
113
|
+
echo ""
|
|
114
|
+
ok "Canonical pgserve ready"
|
|
115
|
+
info "URL: $(pgserve url 2>/dev/null || echo '<run: pgserve url>')"
|
|
116
|
+
info "Port: $(pgserve port 2>/dev/null || echo '?')"
|
|
117
|
+
info "Logs: ~/.pgserve/logs/"
|
|
118
|
+
echo ""
|
|
119
|
+
info "Other automagik services on this host (omni, genie, ...) will share this pgserve."
|
|
120
|
+
echo ""
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main "$@"
|
package/package.json
CHANGED
package/src/cli-install.cjs
CHANGED
|
@@ -179,7 +179,24 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
179
179
|
'--error',
|
|
180
180
|
logs.error,
|
|
181
181
|
'--',
|
|
182
|
-
|
|
182
|
+
// Foreground multi-tenant mode (`pgserve [options]`), NOT daemon mode.
|
|
183
|
+
//
|
|
184
|
+
// Daemon mode binds a unix control socket and requires libpq peers to
|
|
185
|
+
// authenticate via a fingerprint+token handshake (`pgserve daemon
|
|
186
|
+
// issue-token`). Downstream services that connect with a plain
|
|
187
|
+
// `postgres://` URL (omni, genie, anything that doesn't speak the
|
|
188
|
+
// fingerprint protocol) cannot reach a daemon-mode listener. We also
|
|
189
|
+
// observed live: `pgserve install` was passing `--port` to the daemon
|
|
190
|
+
// parser, which only accepts `--data | --ram | --log | --no-provision
|
|
191
|
+
// | --listen | --pgvector` — every install attempt crashed with
|
|
192
|
+
// `Unknown daemon option: --port` and pm2 burned its restart budget.
|
|
193
|
+
//
|
|
194
|
+
// Foreground mode (the default `pgserve [options]` invocation in
|
|
195
|
+
// postgres-server.js) accepts `--port`, auto-provisions databases on
|
|
196
|
+
// first connect, runs the cluster on multi-core hosts, and binds TCP
|
|
197
|
+
// on `127.0.0.1:<port>` with no auth dance — exactly what canonical
|
|
198
|
+
// pgserve consumers expect. Pass the same flags pgserve already
|
|
199
|
+
// documents in its own `pgserve --help` output.
|
|
183
200
|
'--port',
|
|
184
201
|
String(port),
|
|
185
202
|
'--data',
|
|
@@ -137,6 +137,22 @@ describe('pgserve install', () => {
|
|
|
137
137
|
expect(startCall).toContain('60000');
|
|
138
138
|
expect(startCall).toContain('--interpreter');
|
|
139
139
|
expect(startCall).toContain('none');
|
|
140
|
+
|
|
141
|
+
// pgserve install must launch the foreground multi-tenant server, NOT
|
|
142
|
+
// the daemon. Daemon mode rejects `--port` (it only accepts --data,
|
|
143
|
+
// --ram, --log, --no-provision, --listen, --pgvector) and its TCP
|
|
144
|
+
// listeners require fingerprint+token auth which downstream services
|
|
145
|
+
// (omni, genie) don't speak. Foreground mode binds plain TCP on
|
|
146
|
+
// 127.0.0.1:<port> with auto-provisioning. Lock that out:
|
|
147
|
+
expect(startCall).not.toContain('daemon');
|
|
148
|
+
// The script-arg handover (after `--`) must include `--port` so the
|
|
149
|
+
// foreground parser binds the right TCP port.
|
|
150
|
+
const dashDashIdx = startCall.indexOf('--');
|
|
151
|
+
const scriptArgs = startCall.slice(dashDashIdx + 1);
|
|
152
|
+
expect(scriptArgs).toContain('--port');
|
|
153
|
+
expect(scriptArgs).toContain('--data');
|
|
154
|
+
expect(scriptArgs).toContain('--log');
|
|
155
|
+
expect(scriptArgs).not.toContain('daemon');
|
|
140
156
|
});
|
|
141
157
|
|
|
142
158
|
test('second install is idempotent (no second pm2 start)', () => {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for `pgserve daemon` argv parsing.
|
|
3
|
+
*
|
|
4
|
+
* `parseDaemonArgs` lives inside `bin/postgres-server.js` (the script
|
|
5
|
+
* entry point) and isn't exported, so we exercise it via subprocess
|
|
6
|
+
* invocations of the wrapper. Each test runs in <100ms — they only ask
|
|
7
|
+
* the daemon to print help or reject an invalid argument; no real
|
|
8
|
+
* postgres backend is started.
|
|
9
|
+
*
|
|
10
|
+
* Background: every recent CLI-flag mismatch between callers and
|
|
11
|
+
* `pgserve daemon` exited the daemon child with code 1 immediately,
|
|
12
|
+
* surfacing upstream as the unhelpful "pgserve v2 daemon exited before
|
|
13
|
+
* binding …" error. These tests pin the daemon's accepted flag set
|
|
14
|
+
* explicitly so the next mismatch fails CI here, not at runtime.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { describe, expect, test } from 'bun:test';
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = join(fileURLToPath(import.meta.url), '..', '..');
|
|
23
|
+
const PGSERVE_BIN = join(REPO_ROOT, 'bin', 'pgserve-wrapper.cjs');
|
|
24
|
+
|
|
25
|
+
function runDaemon(args, timeoutMs = 3000) {
|
|
26
|
+
return spawnSync('node', [PGSERVE_BIN, 'daemon', ...args], {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: timeoutMs,
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('pgserve daemon — argv parser', () => {
|
|
34
|
+
test('--help lists every flag the daemon accepts', () => {
|
|
35
|
+
const result = runDaemon(['--help']);
|
|
36
|
+
expect(result.status).toBe(0);
|
|
37
|
+
const help = result.stdout;
|
|
38
|
+
// Every flag the parser accepts must appear in --help so callers
|
|
39
|
+
// (and the next operator running `pgserve daemon --help`) discover them.
|
|
40
|
+
expect(help).toContain('--data');
|
|
41
|
+
expect(help).toContain('--ram');
|
|
42
|
+
expect(help).toContain('--log');
|
|
43
|
+
expect(help).toContain('--no-provision');
|
|
44
|
+
expect(help).toContain('--listen');
|
|
45
|
+
expect(help).toContain('--pgvector');
|
|
46
|
+
expect(help).toContain('--max-connections');
|
|
47
|
+
expect(help).toContain('--help');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('--max-connections accepts a positive integer (no "Unknown option" error)', () => {
|
|
51
|
+
// Use a bogus --data path so the daemon never actually starts postgres
|
|
52
|
+
// — the parser runs, accepts --max-connections, then PgserveDaemon
|
|
53
|
+
// tries to start and fails on the missing/invalid data dir. We only
|
|
54
|
+
// care that the parser doesn't reject the flag.
|
|
55
|
+
const result = runDaemon(['--data', '/nonexistent/pgserve-test-dir', '--max-connections', '5000', '--log', 'error']);
|
|
56
|
+
// The daemon may exit non-zero because the data dir is invalid, but
|
|
57
|
+
// it MUST NOT exit with "Unknown daemon option" — that's the
|
|
58
|
+
// pre-fix behavior we're guarding against.
|
|
59
|
+
const stderr = result.stderr ?? '';
|
|
60
|
+
expect(stderr).not.toContain('Unknown daemon option: --max-connections');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('--max-connections rejects non-numeric values with a clear error', () => {
|
|
64
|
+
const result = runDaemon(['--max-connections', 'abc']);
|
|
65
|
+
expect(result.status).toBe(1);
|
|
66
|
+
expect(result.stderr).toContain('--max-connections: expected a positive integer');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('--max-connections rejects zero / negative values', () => {
|
|
70
|
+
const zero = runDaemon(['--max-connections', '0']);
|
|
71
|
+
expect(zero.status).toBe(1);
|
|
72
|
+
expect(zero.stderr).toContain('--max-connections: expected a positive integer');
|
|
73
|
+
|
|
74
|
+
const negative = runDaemon(['--max-connections', '-50']);
|
|
75
|
+
expect(negative.status).toBe(1);
|
|
76
|
+
expect(negative.stderr).toContain('--max-connections: expected a positive integer');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('unknown flags still exit 1 with the documented "Unknown daemon option" error', () => {
|
|
80
|
+
// Sanity: the parser hasn't become permissive. Genuinely unknown
|
|
81
|
+
// flags must still error out so callers learn about the mismatch.
|
|
82
|
+
const result = runDaemon(['--definitely-not-a-flag']);
|
|
83
|
+
expect(result.status).toBe(1);
|
|
84
|
+
expect(result.stderr).toContain('Unknown daemon option: --definitely-not-a-flag');
|
|
85
|
+
});
|
|
86
|
+
});
|