pgserve 2.1.3 → 2.2.1
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/CHANGELOG.md +96 -0
- package/README.md +105 -1
- package/bin/autopg-wrapper.cjs +16 -0
- package/bin/pgserve-wrapper.cjs +32 -6
- package/bin/postgres-server.js +56 -0
- package/console/README.md +131 -0
- package/console/api.js +173 -0
- package/console/app.jsx +483 -0
- package/console/colors_and_type.css +227 -0
- package/console/components.jsx +167 -0
- package/console/console.css +1666 -0
- package/console/data.jsx +350 -0
- package/console/index.html +31 -0
- package/console/screens/databases.jsx +5 -0
- package/console/screens/health.jsx +5 -0
- package/console/screens/ingress.jsx +5 -0
- package/console/screens/optimizer.jsx +5 -0
- package/console/screens/rlm-sim.jsx +5 -0
- package/console/screens/rlm-trace.jsx +5 -0
- package/console/screens/security.jsx +5 -0
- package/console/screens/settings.jsx +611 -0
- package/console/screens/sql.jsx +5 -0
- package/console/screens/sync.jsx +5 -0
- package/console/screens/tables.jsx +5 -0
- package/console/tweaks-panel.jsx +425 -0
- package/package.json +14 -2
- package/scripts/postinstall.cjs +60 -0
- package/src/cli-config.cjs +310 -0
- package/src/cli-install.cjs +112 -11
- package/src/cli-restart.cjs +228 -0
- package/src/cli-ui.cjs +580 -0
- package/src/cluster.js +43 -38
- package/src/postgres.js +141 -19
- package/src/settings-loader.cjs +235 -0
- package/src/settings-migrate.cjs +212 -0
- package/src/settings-pg-args.cjs +146 -0
- package/src/settings-schema.cjs +422 -0
- package/src/settings-validator.cjs +416 -0
- package/src/settings-writer.cjs +288 -0
- package/src/upgrade/index.js +65 -0
- package/src/upgrade/runner.js +23 -0
- package/src/upgrade/steps/binary-cache-flush.js +67 -0
- package/src/upgrade/steps/consumer-signal.js +40 -0
- package/src/upgrade/steps/env-refresh.js +89 -0
- package/src/upgrade/steps/health-validate.js +53 -0
- package/src/upgrade/steps/plpgsql-resolve.js +66 -0
- package/src/upgrade/steps/port-reconcile.js +52 -0
- package/.claude/context/windows-debug.md +0 -119
- package/.genie/AGENTS.md +0 -15
- package/.genie/agents/README.md +0 -110
- package/.genie/agents/analyze.md +0 -176
- package/.genie/agents/forge.md +0 -290
- package/.genie/agents/garbage-cleaner.md +0 -324
- package/.genie/agents/garbage-collector.md +0 -596
- package/.genie/agents/github-issue-gc.md +0 -618
- package/.genie/agents/review.md +0 -380
- package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
- package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
- package/.genie/agents/semantic-analyzer.md +0 -101
- package/.genie/agents/update.md +0 -182
- package/.genie/agents/wish.md +0 -357
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
- package/.genie/code/AGENTS.md +0 -694
- package/.genie/code/agents/audit/risk.md +0 -173
- package/.genie/code/agents/audit/security.md +0 -189
- package/.genie/code/agents/audit.md +0 -145
- package/.genie/code/agents/challenge.md +0 -230
- package/.genie/code/agents/change-reviewer.md +0 -295
- package/.genie/code/agents/code-garbage-collector.md +0 -425
- package/.genie/code/agents/code-quality.md +0 -410
- package/.genie/code/agents/commit-suggester.md +0 -255
- package/.genie/code/agents/commit.md +0 -124
- package/.genie/code/agents/consensus.md +0 -204
- package/.genie/code/agents/daily-standup.md +0 -722
- package/.genie/code/agents/docgen.md +0 -48
- package/.genie/code/agents/explore.md +0 -79
- package/.genie/code/agents/fix.md +0 -100
- package/.genie/code/agents/git/commit-advisory.md +0 -219
- package/.genie/code/agents/git/workflows/issue.md +0 -244
- package/.genie/code/agents/git/workflows/pr.md +0 -179
- package/.genie/code/agents/git/workflows/release.md +0 -460
- package/.genie/code/agents/git/workflows/report.md +0 -342
- package/.genie/code/agents/git.md +0 -432
- package/.genie/code/agents/implementor.md +0 -161
- package/.genie/code/agents/install.md +0 -515
- package/.genie/code/agents/issue-creator.md +0 -344
- package/.genie/code/agents/polish.md +0 -116
- package/.genie/code/agents/qa.md +0 -653
- package/.genie/code/agents/refactor.md +0 -294
- package/.genie/code/agents/release.md +0 -1129
- package/.genie/code/agents/roadmap.md +0 -885
- package/.genie/code/agents/tests.md +0 -557
- package/.genie/code/agents/tracer.md +0 -50
- package/.genie/code/agents/update/upstream-update.md +0 -85
- package/.genie/code/agents/update/versions/generic-update.md +0 -305
- package/.genie/code/agents/vibe.md +0 -1317
- package/.genie/code/spells/agent-configuration.md +0 -58
- package/.genie/code/spells/automated-rc-publishing.md +0 -106
- package/.genie/code/spells/branch-tracker-guidance.md +0 -28
- package/.genie/code/spells/debug.md +0 -320
- package/.genie/code/spells/emoji-naming-convention.md +0 -303
- package/.genie/code/spells/evidence-storage.md +0 -26
- package/.genie/code/spells/file-naming-rules.md +0 -35
- package/.genie/code/spells/forge-code-blueprints.md +0 -195
- package/.genie/code/spells/genie-integration.md +0 -153
- package/.genie/code/spells/publishing-protocol.md +0 -61
- package/.genie/code/spells/team-consultation-protocol.md +0 -284
- package/.genie/code/spells/tool-requirements.md +0 -20
- package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
- package/.genie/code/teams/tech-council/council.md +0 -328
- package/.genie/code/teams/tech-council/jt.md +0 -352
- package/.genie/code/teams/tech-council/nayr.md +0 -305
- package/.genie/code/teams/tech-council/oettam.md +0 -375
- package/.genie/neurons/README.md +0 -193
- package/.genie/neurons/forge.md +0 -106
- package/.genie/neurons/genie.md +0 -63
- package/.genie/neurons/review.md +0 -106
- package/.genie/neurons/wish.md +0 -104
- package/.genie/product/README.md +0 -20
- package/.genie/product/cli-automation.md +0 -359
- package/.genie/product/environment.md +0 -60
- package/.genie/product/mission.md +0 -60
- package/.genie/product/roadmap.md +0 -44
- package/.genie/product/tech-stack.md +0 -34
- package/.genie/product/templates/context-template.md +0 -218
- package/.genie/product/templates/qa-done-report-template.md +0 -68
- package/.genie/product/templates/review-report-template.md +0 -89
- package/.genie/product/templates/wish-template.md +0 -120
- package/.genie/scripts/helpers/analyze-commit.js +0 -195
- package/.genie/scripts/helpers/bullet-counter.js +0 -194
- package/.genie/scripts/helpers/bullet-find.js +0 -289
- package/.genie/scripts/helpers/bullet-id.js +0 -244
- package/.genie/scripts/helpers/check-secrets.js +0 -237
- package/.genie/scripts/helpers/count-tokens.js +0 -200
- package/.genie/scripts/helpers/create-frontmatter.js +0 -456
- package/.genie/scripts/helpers/detect-markers.js +0 -293
- package/.genie/scripts/helpers/detect-todos.js +0 -267
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
- package/.genie/scripts/helpers/embeddings.js +0 -344
- package/.genie/scripts/helpers/find-empty-sections.js +0 -158
- package/.genie/scripts/helpers/index.js +0 -319
- package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
- package/.genie/scripts/helpers/validate-links.js +0 -207
- package/.genie/scripts/helpers/validate-paths.js +0 -373
- package/.genie/spells/README.md +0 -9
- package/.genie/spells/ace-protocol.md +0 -118
- package/.genie/spells/ask-one-at-a-time.md +0 -175
- package/.genie/spells/backup-analyzer.md +0 -542
- package/.genie/spells/blocker.md +0 -12
- package/.genie/spells/break-things-move-fast.md +0 -56
- package/.genie/spells/context-candidates.md +0 -72
- package/.genie/spells/context-critic.md +0 -51
- package/.genie/spells/defer-to-expertise.md +0 -278
- package/.genie/spells/delegate-dont-do.md +0 -292
- package/.genie/spells/error-investigation-protocol.md +0 -328
- package/.genie/spells/evidence-based-completion.md +0 -273
- package/.genie/spells/experiment.md +0 -65
- package/.genie/spells/file-creation-protocol.md +0 -229
- package/.genie/spells/forge-integration.md +0 -281
- package/.genie/spells/forge-orchestration.md +0 -514
- package/.genie/spells/gather-context.md +0 -18
- package/.genie/spells/global-health-check.md +0 -34
- package/.genie/spells/global-noop-roundtrip.md +0 -25
- package/.genie/spells/install-genie.md +0 -1232
- package/.genie/spells/install.md +0 -82
- package/.genie/spells/investigate-before-commit.md +0 -112
- package/.genie/spells/know-yourself.md +0 -288
- package/.genie/spells/learn.md +0 -828
- package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
- package/.genie/spells/mcp-first.md +0 -124
- package/.genie/spells/multi-step-execution.md +0 -67
- package/.genie/spells/orchestration-boundary-protocol.md +0 -256
- package/.genie/spells/orchestrator-not-implementor.md +0 -189
- package/.genie/spells/prompt.md +0 -746
- package/.genie/spells/reflect.md +0 -404
- package/.genie/spells/routing-decision-matrix.md +0 -368
- package/.genie/spells/run-in-parallel.md +0 -12
- package/.genie/spells/session-state-updater-example.md +0 -196
- package/.genie/spells/session-state-updater.md +0 -220
- package/.genie/spells/track-long-running-tasks.md +0 -133
- package/.genie/spells/troubleshoot-infrastructure.md +0 -176
- package/.genie/spells/upgrade-genie.md +0 -415
- package/.genie/spells/url-presentation-protocol.md +0 -301
- package/.genie/spells/wish-initiation.md +0 -158
- package/.genie/spells/wish-issue-linkage.md +0 -410
- package/.genie/spells/wish-lifecycle.md +0 -100
- package/.genie/state/provider-status.json +0 -3
- package/.genie/state/version.json +0 -16
- package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
- package/.genie/wishes/pgserve-v2/WISH.md +0 -442
- package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
- package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
- package/.gitguardian.yaml +0 -29
- package/.gitguardianignore +0 -16
- package/.github/workflows/ci.yml +0 -122
- package/.github/workflows/release.yml +0 -289
- package/.github/workflows/version.yml +0 -228
- package/.husky/pre-commit +0 -2
- package/AGENTS.md +0 -433
- package/CLAUDE.md +0 -1
- package/Makefile +0 -285
- package/assets/icon.ico +0 -0
- package/bun.lock +0 -435
- package/bunfig.toml +0 -28
- package/ecosystem.config.cjs +0 -23
- package/eslint.config.js +0 -63
- package/examples/multi-tenant-demo.js +0 -104
- package/install.sh +0 -123
- package/knip.json +0 -9
- package/tests/audit.test.js +0 -189
- package/tests/backpressure.test.js +0 -167
- package/tests/benchmarks/runner.js +0 -1197
- package/tests/benchmarks/vector-generator.js +0 -368
- package/tests/cli-install.test.js +0 -322
- package/tests/control-db.test.js +0 -285
- package/tests/daemon-args.test.js +0 -86
- package/tests/daemon-control.test.js +0 -171
- package/tests/daemon-fingerprint-integration.test.js +0 -111
- package/tests/daemon-pr24-regression.test.js +0 -198
- package/tests/fingerprint.test.js +0 -263
- package/tests/fixtures/240-orphan-seed.sql +0 -30
- package/tests/multi-tenant.test.js +0 -374
- package/tests/orphan-cleanup.test.js +0 -390
- package/tests/pg-version-regex.test.js +0 -129
- package/tests/quick-bench.js +0 -135
- package/tests/router-handshake-retry.test.js +0 -119
- package/tests/router-handshake-watchdog.test.js +0 -110
- package/tests/sdk.test.js +0 -71
- package/tests/stale-postmaster-pid.test.js +0 -85
- package/tests/stress-test.js +0 -439
- package/tests/sync-perf-test.js +0 -150
- package/tests/tcp-listen.test.js +0 -368
- package/tests/tenancy.test.js +0 -403
- package/tests/wrapper-supervision.test.js +0 -107
package/src/cli-ui.cjs
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `autopg ui [--port N] [--no-open]` (also reachable via `pgserve ui`).
|
|
3
|
+
*
|
|
4
|
+
* Boots a tiny http server bound to 127.0.0.1 that:
|
|
5
|
+
* - serves the static console at `console/` (React + Babel CDN, no build).
|
|
6
|
+
* - exposes 4 helper endpoints used by the SPA:
|
|
7
|
+
* GET /api/settings → { settings, sources, etag }
|
|
8
|
+
* PUT /api/settings → writeSettings + If-Match etag check
|
|
9
|
+
* POST /api/restart → invokes cli-restart.dispatch
|
|
10
|
+
* GET /api/status → shells out to the existing wave-1 status
|
|
11
|
+
*
|
|
12
|
+
* Single-user dev tool: 127.0.0.1 only, no auth, no TLS. Designed to ride
|
|
13
|
+
* inside an operator's localhost session — not to be exposed.
|
|
14
|
+
*
|
|
15
|
+
* Port selection:
|
|
16
|
+
* --port N → bind exactly N or fail.
|
|
17
|
+
* (no flag) → walk 8433..8533 picking the first free port.
|
|
18
|
+
*
|
|
19
|
+
* Browser opening:
|
|
20
|
+
* --no-open → skip browser launch (CI/headless paths).
|
|
21
|
+
* default → `open` (macOS) / `xdg-open` (Linux) / `start` (Windows).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const http = require('node:http');
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const { spawn, execFileSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
const { loadEffectiveConfig, getSettingsPath } = require('./settings-loader.cjs');
|
|
32
|
+
const { writeSettings } = require('./settings-writer.cjs');
|
|
33
|
+
const cliRestart = require('./cli-restart.cjs');
|
|
34
|
+
|
|
35
|
+
// `pg` is a devDependency today — graceful degradation if not installed.
|
|
36
|
+
// /api/stats returns { ok: false, reason: 'pg-not-installed' } in that case.
|
|
37
|
+
let PgClient = null;
|
|
38
|
+
try { PgClient = require('pg').Client; } catch { /* optional */ }
|
|
39
|
+
const {
|
|
40
|
+
ValidationError,
|
|
41
|
+
EtagMismatchError,
|
|
42
|
+
ERROR_CODES,
|
|
43
|
+
} = require('./settings-validator.cjs');
|
|
44
|
+
|
|
45
|
+
const PORT_RANGE_START = 8433;
|
|
46
|
+
const PORT_RANGE_END = 8533;
|
|
47
|
+
const HOST = '127.0.0.1';
|
|
48
|
+
|
|
49
|
+
const MIME_TYPES = {
|
|
50
|
+
'.html': 'text/html; charset=utf-8',
|
|
51
|
+
'.htm': 'text/html; charset=utf-8',
|
|
52
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
53
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
54
|
+
'.jsx': 'application/javascript; charset=utf-8',
|
|
55
|
+
'.cjs': 'application/javascript; charset=utf-8',
|
|
56
|
+
'.css': 'text/css; charset=utf-8',
|
|
57
|
+
'.json': 'application/json; charset=utf-8',
|
|
58
|
+
'.svg': 'image/svg+xml',
|
|
59
|
+
'.png': 'image/png',
|
|
60
|
+
'.jpg': 'image/jpeg',
|
|
61
|
+
'.jpeg': 'image/jpeg',
|
|
62
|
+
'.gif': 'image/gif',
|
|
63
|
+
'.ico': 'image/x-icon',
|
|
64
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
65
|
+
'.woff': 'font/woff',
|
|
66
|
+
'.woff2': 'font/woff2',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function parseArgs(args) {
|
|
70
|
+
const out = { port: null, noOpen: false, host: HOST };
|
|
71
|
+
for (let i = 0; i < args.length; i++) {
|
|
72
|
+
const a = args[i];
|
|
73
|
+
if (a === '--port') {
|
|
74
|
+
const v = args[++i];
|
|
75
|
+
const n = Number.parseInt(v, 10);
|
|
76
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
77
|
+
throw new Error(`invalid --port "${v}"`);
|
|
78
|
+
}
|
|
79
|
+
out.port = n;
|
|
80
|
+
} else if (a === '--no-open') {
|
|
81
|
+
out.noOpen = true;
|
|
82
|
+
} else if (a === '--host') {
|
|
83
|
+
// Defense: still bind 127.0.0.1 unless explicitly opted out via env.
|
|
84
|
+
// We accept --host for parity but ignore non-loopback values.
|
|
85
|
+
const v = args[++i];
|
|
86
|
+
if (v === '127.0.0.1' || v === 'localhost') {
|
|
87
|
+
out.host = v;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Try to bind a server on each candidate port until one succeeds.
|
|
96
|
+
* Returns a Promise<{server, port}>. Rejects if no port in the range works.
|
|
97
|
+
*/
|
|
98
|
+
function listenWithFallback(server, host, preferredPort) {
|
|
99
|
+
const candidates = preferredPort
|
|
100
|
+
? [preferredPort]
|
|
101
|
+
: (() => {
|
|
102
|
+
const list = [];
|
|
103
|
+
for (let p = PORT_RANGE_START; p <= PORT_RANGE_END; p++) list.push(p);
|
|
104
|
+
return list;
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
let i = 0;
|
|
109
|
+
function attempt() {
|
|
110
|
+
if (i >= candidates.length) {
|
|
111
|
+
reject(
|
|
112
|
+
new Error(
|
|
113
|
+
preferredPort
|
|
114
|
+
? `port ${preferredPort} is not available`
|
|
115
|
+
: `no free port in ${PORT_RANGE_START}-${PORT_RANGE_END}`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const port = candidates[i++];
|
|
121
|
+
const onErr = (err) => {
|
|
122
|
+
if (err.code === 'EADDRINUSE' && !preferredPort) {
|
|
123
|
+
server.removeListener('error', onErr);
|
|
124
|
+
attempt();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
reject(err);
|
|
128
|
+
};
|
|
129
|
+
server.once('error', onErr);
|
|
130
|
+
server.listen(port, host, () => {
|
|
131
|
+
server.removeListener('error', onErr);
|
|
132
|
+
resolve({ server, port });
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
attempt();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the static document root. The console directory lives at the
|
|
141
|
+
* repo root (alongside `bin/` and `src/`). When the package is installed
|
|
142
|
+
* via npm the `files` allowlist preserves the layout.
|
|
143
|
+
*/
|
|
144
|
+
function resolveConsoleRoot() {
|
|
145
|
+
// src/ → repo root → console/
|
|
146
|
+
return path.resolve(__dirname, '..', 'console');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sanitize a request path against directory traversal, return the absolute
|
|
151
|
+
* file path on disk or null if the request escapes the document root.
|
|
152
|
+
*/
|
|
153
|
+
function safeJoin(root, urlPath) {
|
|
154
|
+
// Strip query string defensively even though the caller already removed it.
|
|
155
|
+
const clean = urlPath.split('?')[0];
|
|
156
|
+
// Normalize then refuse anything starting with `..` or absolute outside
|
|
157
|
+
// the root.
|
|
158
|
+
const decoded = decodeURIComponent(clean);
|
|
159
|
+
const normalized = path.posix.normalize(decoded).replace(/^\/+/, '');
|
|
160
|
+
const candidate = path.resolve(root, normalized);
|
|
161
|
+
if (!candidate.startsWith(`${root}${path.sep}`) && candidate !== root) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sendJson(res, status, payload) {
|
|
168
|
+
const body = JSON.stringify(payload);
|
|
169
|
+
res.writeHead(status, {
|
|
170
|
+
'content-type': 'application/json; charset=utf-8',
|
|
171
|
+
'content-length': Buffer.byteLength(body),
|
|
172
|
+
'cache-control': 'no-store',
|
|
173
|
+
});
|
|
174
|
+
res.end(body);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function sendError(res, status, code, message, extra = {}) {
|
|
178
|
+
sendJson(res, status, { error: { code, message, ...extra } });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readBody(req, { limitBytes = 1_048_576 } = {}) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const chunks = [];
|
|
184
|
+
let total = 0;
|
|
185
|
+
req.on('data', (chunk) => {
|
|
186
|
+
total += chunk.length;
|
|
187
|
+
if (total > limitBytes) {
|
|
188
|
+
reject(new Error('request body too large'));
|
|
189
|
+
req.destroy();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
chunks.push(chunk);
|
|
193
|
+
});
|
|
194
|
+
req.on('end', () => {
|
|
195
|
+
try {
|
|
196
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
197
|
+
if (!raw) return resolve({});
|
|
198
|
+
resolve(JSON.parse(raw));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
reject(err);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
req.on('error', reject);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── handlers ────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function handleGetSettings(req, res) {
|
|
210
|
+
try {
|
|
211
|
+
const { settings, sources, etag, path: settingsPath } = loadEffectiveConfig();
|
|
212
|
+
sendJson(res, 200, { settings, sources, etag, path: settingsPath });
|
|
213
|
+
} catch (err) {
|
|
214
|
+
sendError(res, 500, 'LOAD_FAILED', err.message ?? String(err));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handlePutSettings(req, res) {
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = await readBody(req);
|
|
222
|
+
} catch (err) {
|
|
223
|
+
sendError(res, 400, 'BAD_BODY', err.message ?? 'invalid JSON');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const ifMatch = req.headers['if-match'];
|
|
227
|
+
if (!ifMatch) {
|
|
228
|
+
sendError(res, 428, 'PRECONDITION_REQUIRED', 'If-Match header required');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
// Merge the patch onto the current effective tree before writing so
|
|
233
|
+
// partial PUTs only touch the supplied keys. The writer re-validates.
|
|
234
|
+
const { settings: current } = loadEffectiveConfig();
|
|
235
|
+
const merged = deepMergePlain(current, body);
|
|
236
|
+
const { etag } = writeSettings(merged, { ifMatch });
|
|
237
|
+
sendJson(res, 200, { ok: true, etag });
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err instanceof EtagMismatchError) {
|
|
240
|
+
sendJson(res, 409, {
|
|
241
|
+
error: {
|
|
242
|
+
code: ERROR_CODES.ETAG_MISMATCH,
|
|
243
|
+
message: 'settings changed on disk; reload before retry',
|
|
244
|
+
},
|
|
245
|
+
currentEtag: err.currentEtag,
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (err instanceof ValidationError) {
|
|
250
|
+
sendError(res, 400, err.code, err.detail ?? err.message, { field: err.field });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
sendError(res, 500, 'WRITE_FAILED', err.message ?? String(err));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function deepMergePlain(base, patch) {
|
|
258
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return base;
|
|
259
|
+
const out = base && typeof base === 'object' && !Array.isArray(base) ? { ...base } : {};
|
|
260
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
261
|
+
if (
|
|
262
|
+
value &&
|
|
263
|
+
typeof value === 'object' &&
|
|
264
|
+
!Array.isArray(value) &&
|
|
265
|
+
out[key] &&
|
|
266
|
+
typeof out[key] === 'object' &&
|
|
267
|
+
!Array.isArray(out[key])
|
|
268
|
+
) {
|
|
269
|
+
out[key] = deepMergePlain(out[key], value);
|
|
270
|
+
} else {
|
|
271
|
+
out[key] = value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function handlePostRestart(req, res, ctx) {
|
|
278
|
+
try {
|
|
279
|
+
const code = cliRestart.dispatch([], { scriptPath: ctx.scriptPath });
|
|
280
|
+
if (code === 0) {
|
|
281
|
+
sendJson(res, 200, { ok: true });
|
|
282
|
+
} else {
|
|
283
|
+
sendError(res, 500, 'RESTART_FAILED', `restart exited with code ${code}`);
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
sendError(res, 500, 'RESTART_FAILED', err.message ?? String(err));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function handleGetStatus(req, res, ctx) {
|
|
291
|
+
// The existing wave-1 `status --json` flow returns the canonical shape.
|
|
292
|
+
// Shell out via the wrapper so the response mirrors what an operator
|
|
293
|
+
// would see at the CLI.
|
|
294
|
+
try {
|
|
295
|
+
if (ctx.statusOverride) {
|
|
296
|
+
sendJson(res, 200, ctx.statusOverride());
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const out = execFileSync(process.execPath, [ctx.scriptPath, 'status', '--json'], {
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
timeout: 5000,
|
|
302
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
303
|
+
});
|
|
304
|
+
const trimmed = out.trim();
|
|
305
|
+
sendJson(res, 200, trimmed ? JSON.parse(trimmed) : {});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
// `pgserve status` exits 1 when not installed but still prints JSON.
|
|
308
|
+
// Surface the parsed payload when present; otherwise wrap the error.
|
|
309
|
+
const stdout = err?.stdout ? err.stdout.toString().trim() : '';
|
|
310
|
+
if (stdout) {
|
|
311
|
+
try {
|
|
312
|
+
sendJson(res, 200, JSON.parse(stdout));
|
|
313
|
+
return;
|
|
314
|
+
} catch {
|
|
315
|
+
// fall through
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
sendError(res, 500, 'STATUS_FAILED', err.message ?? String(err));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Cached package.json — read once at module load so we never block on disk.
|
|
323
|
+
const PKG_VERSION = (() => {
|
|
324
|
+
try { return require('../package.json').version; } catch { return 'unknown'; }
|
|
325
|
+
})();
|
|
326
|
+
|
|
327
|
+
async function handleGetStats(req, res) {
|
|
328
|
+
if (!PgClient) {
|
|
329
|
+
sendJson(res, 200, {
|
|
330
|
+
ok: false,
|
|
331
|
+
reason: 'pg-not-installed',
|
|
332
|
+
autopg: { version: PKG_VERSION },
|
|
333
|
+
});
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
let client;
|
|
337
|
+
try {
|
|
338
|
+
const { settings } = loadEffectiveConfig();
|
|
339
|
+
const server = settings.server || {};
|
|
340
|
+
client = new PgClient({
|
|
341
|
+
host: server.host || '127.0.0.1',
|
|
342
|
+
port: server.port || 8432,
|
|
343
|
+
database: 'postgres',
|
|
344
|
+
user: server.pgUser || 'postgres',
|
|
345
|
+
password: server.pgPassword || 'postgres',
|
|
346
|
+
connectionTimeoutMillis: 1500,
|
|
347
|
+
query_timeout: 1500,
|
|
348
|
+
});
|
|
349
|
+
client.on('error', () => {}); // never crash the helper on PG hiccup
|
|
350
|
+
await client.connect();
|
|
351
|
+
// Single round-trip query covering everything the footer needs.
|
|
352
|
+
// pg_stat_activity gives client connections; pg_database the user-db
|
|
353
|
+
// count + total size; pg_stat_database the cache + xact aggregates;
|
|
354
|
+
// pg_settings for the short server_version (no parsing); and
|
|
355
|
+
// pg_postmaster_start_time for uptime.
|
|
356
|
+
const { rows: [row] } = await client.query(`
|
|
357
|
+
SELECT
|
|
358
|
+
(SELECT count(*)::int FROM pg_stat_activity
|
|
359
|
+
WHERE backend_type = 'client backend' AND pid <> pg_backend_pid()) AS connections,
|
|
360
|
+
(SELECT count(*)::int FROM pg_database
|
|
361
|
+
WHERE NOT datistemplate AND datname <> 'postgres') AS databases,
|
|
362
|
+
(SELECT setting FROM pg_settings WHERE name = 'server_version') AS pg_version,
|
|
363
|
+
EXTRACT(epoch FROM (now() - pg_postmaster_start_time()))::int AS uptime_sec,
|
|
364
|
+
(SELECT round(
|
|
365
|
+
100.0 * sum(blks_hit)::numeric
|
|
366
|
+
/ nullif(sum(blks_hit) + sum(blks_read), 0),
|
|
367
|
+
2
|
|
368
|
+
)::float FROM pg_stat_database) AS cache_hit_pct,
|
|
369
|
+
(SELECT sum(xact_commit + xact_rollback)::bigint
|
|
370
|
+
FROM pg_stat_database) AS tx_total,
|
|
371
|
+
(SELECT sum(pg_database_size(datname))::bigint
|
|
372
|
+
FROM pg_database WHERE NOT datistemplate) AS size_bytes
|
|
373
|
+
`);
|
|
374
|
+
sendJson(res, 200, {
|
|
375
|
+
ok: true,
|
|
376
|
+
connections: row.connections,
|
|
377
|
+
databases: row.databases,
|
|
378
|
+
port: server.port || 8432,
|
|
379
|
+
pg: {
|
|
380
|
+
version: row.pg_version,
|
|
381
|
+
uptimeSec: row.uptime_sec,
|
|
382
|
+
cacheHitPct: row.cache_hit_pct,
|
|
383
|
+
txTotal: Number(row.tx_total ?? 0),
|
|
384
|
+
sizeBytes: Number(row.size_bytes ?? 0),
|
|
385
|
+
},
|
|
386
|
+
autopg: { version: PKG_VERSION },
|
|
387
|
+
ts: Date.now(),
|
|
388
|
+
});
|
|
389
|
+
} catch (err) {
|
|
390
|
+
sendJson(res, 200, {
|
|
391
|
+
ok: false,
|
|
392
|
+
reason: err.code || 'disconnected',
|
|
393
|
+
message: err.message,
|
|
394
|
+
autopg: { version: PKG_VERSION },
|
|
395
|
+
});
|
|
396
|
+
} finally {
|
|
397
|
+
if (client) {
|
|
398
|
+
try { await client.end(); } catch { /* already closed */ }
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function handleStatic(req, res, root) {
|
|
404
|
+
let url = req.url.split('?')[0];
|
|
405
|
+
if (url === '/' || url === '') url = '/index.html';
|
|
406
|
+
const target = safeJoin(root, url);
|
|
407
|
+
if (!target) {
|
|
408
|
+
sendError(res, 400, 'BAD_PATH', 'invalid path');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
fs.stat(target, (statErr, stat) => {
|
|
412
|
+
if (statErr || !stat.isFile()) {
|
|
413
|
+
// SPA fallback: serve index.html on a miss so client routing works.
|
|
414
|
+
const fallback = path.join(root, 'index.html');
|
|
415
|
+
if (fs.existsSync(fallback)) {
|
|
416
|
+
serveFile(res, fallback);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
sendError(res, 404, 'NOT_FOUND', `no file at ${url}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
serveFile(res, target);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function serveFile(res, filePath) {
|
|
427
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
428
|
+
const mime = MIME_TYPES[ext] || 'application/octet-stream';
|
|
429
|
+
fs.readFile(filePath, (err, data) => {
|
|
430
|
+
if (err) {
|
|
431
|
+
sendError(res, 500, 'READ_FAILED', err.message);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
res.writeHead(200, {
|
|
435
|
+
'content-type': mime,
|
|
436
|
+
'content-length': data.length,
|
|
437
|
+
'cache-control': 'no-cache',
|
|
438
|
+
});
|
|
439
|
+
res.end(data);
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Build the request handler. `ctx.scriptPath` is the absolute path to
|
|
445
|
+
* `bin/pgserve-wrapper.cjs` (used for shell-outs). `ctx.consoleRoot`
|
|
446
|
+
* defaults to the repo's `console/` directory.
|
|
447
|
+
*/
|
|
448
|
+
function createHandler(ctx = {}) {
|
|
449
|
+
const consoleRoot = ctx.consoleRoot || resolveConsoleRoot();
|
|
450
|
+
return function handler(req, res) {
|
|
451
|
+
const url = req.url || '/';
|
|
452
|
+
const method = req.method || 'GET';
|
|
453
|
+
|
|
454
|
+
if (url.startsWith('/api/')) {
|
|
455
|
+
if (url === '/api/settings' && method === 'GET') return handleGetSettings(req, res);
|
|
456
|
+
if (url === '/api/settings' && method === 'PUT') return handlePutSettings(req, res);
|
|
457
|
+
if (url === '/api/restart' && method === 'POST') return handlePostRestart(req, res, ctx);
|
|
458
|
+
if (url === '/api/status' && method === 'GET') return handleGetStatus(req, res, ctx);
|
|
459
|
+
if (url === '/api/stats' && method === 'GET') return handleGetStats(req, res);
|
|
460
|
+
sendError(res, 404, 'NOT_FOUND', `${method} ${url}`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Non-API → static file, GET/HEAD only.
|
|
465
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
466
|
+
res.writeHead(405, { allow: 'GET, HEAD' });
|
|
467
|
+
res.end();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
handleStatic(req, res, consoleRoot);
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Open a URL in the user's default browser. Best-effort: a failure is
|
|
476
|
+
* logged and the server keeps running. Operators can always copy the
|
|
477
|
+
* URL out of the boot banner.
|
|
478
|
+
*/
|
|
479
|
+
function openBrowser(url) {
|
|
480
|
+
let cmd;
|
|
481
|
+
let args;
|
|
482
|
+
if (process.platform === 'darwin') {
|
|
483
|
+
cmd = 'open';
|
|
484
|
+
args = [url];
|
|
485
|
+
} else if (process.platform === 'win32') {
|
|
486
|
+
cmd = 'cmd';
|
|
487
|
+
args = ['/c', 'start', '""', url];
|
|
488
|
+
} else {
|
|
489
|
+
cmd = 'xdg-open';
|
|
490
|
+
args = [url];
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
494
|
+
child.on('error', () => {
|
|
495
|
+
process.stderr.write(`autopg: could not auto-open browser; visit ${url}\n`);
|
|
496
|
+
});
|
|
497
|
+
child.unref();
|
|
498
|
+
} catch {
|
|
499
|
+
process.stderr.write(`autopg: could not auto-open browser; visit ${url}\n`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Boot the UI server. Resolves to `{ server, port, close }` so callers
|
|
505
|
+
* (and tests) can shut it down deterministically.
|
|
506
|
+
*
|
|
507
|
+
* In CLI mode, callers should pass `wireSignals: true` so SIGINT/SIGTERM
|
|
508
|
+
* stop the server cleanly and the process exits 0.
|
|
509
|
+
*/
|
|
510
|
+
async function startServer({ args = [], scriptPath, consoleRoot, wireSignals = false, openInBrowser = openBrowser } = {}) {
|
|
511
|
+
const opts = parseArgs(args);
|
|
512
|
+
const handler = createHandler({ scriptPath, consoleRoot });
|
|
513
|
+
const server = http.createServer(handler);
|
|
514
|
+
const { port } = await listenWithFallback(server, opts.host, opts.port);
|
|
515
|
+
|
|
516
|
+
const url = `http://${opts.host}:${port}`;
|
|
517
|
+
process.stdout.write(`autopg ui: listening on ${url}\n`);
|
|
518
|
+
process.stdout.write(`autopg ui: settings file is ${getSettingsPath()}\n`);
|
|
519
|
+
|
|
520
|
+
if (!opts.noOpen) {
|
|
521
|
+
openInBrowser(url);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function close() {
|
|
525
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (wireSignals) {
|
|
529
|
+
const stop = async (sig) => {
|
|
530
|
+
process.stdout.write(`\nautopg ui: ${sig} received, shutting down\n`);
|
|
531
|
+
await close();
|
|
532
|
+
process.exit(0);
|
|
533
|
+
};
|
|
534
|
+
process.once('SIGINT', () => stop('SIGINT'));
|
|
535
|
+
process.once('SIGTERM', () => stop('SIGTERM'));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return { server, port, url, close };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* CLI dispatch entry. Boots the server and parks until SIGINT/SIGTERM.
|
|
543
|
+
* Always returns 0 — the signal handlers exit the process directly.
|
|
544
|
+
*/
|
|
545
|
+
async function dispatch(args = [], ctx = {}) {
|
|
546
|
+
try {
|
|
547
|
+
await startServer({
|
|
548
|
+
args,
|
|
549
|
+
scriptPath: ctx.scriptPath,
|
|
550
|
+
consoleRoot: ctx.consoleRoot,
|
|
551
|
+
wireSignals: true,
|
|
552
|
+
});
|
|
553
|
+
} catch (err) {
|
|
554
|
+
process.stderr.write(`autopg ui: ${err.message ?? err}\n`);
|
|
555
|
+
return 1;
|
|
556
|
+
}
|
|
557
|
+
// Park forever — signal handlers terminate the process.
|
|
558
|
+
return new Promise(() => {});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
module.exports = {
|
|
562
|
+
dispatch,
|
|
563
|
+
startServer,
|
|
564
|
+
createHandler,
|
|
565
|
+
parseArgs,
|
|
566
|
+
resolveConsoleRoot,
|
|
567
|
+
// Test surface
|
|
568
|
+
_internals: {
|
|
569
|
+
listenWithFallback,
|
|
570
|
+
safeJoin,
|
|
571
|
+
deepMergePlain,
|
|
572
|
+
handleGetSettings,
|
|
573
|
+
handlePutSettings,
|
|
574
|
+
handlePostRestart,
|
|
575
|
+
handleGetStatus,
|
|
576
|
+
openBrowser,
|
|
577
|
+
PORT_RANGE_START,
|
|
578
|
+
PORT_RANGE_END,
|
|
579
|
+
},
|
|
580
|
+
};
|
package/src/cluster.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createLogger } from './logger.js';
|
|
|
16
16
|
import { PostgresManager } from './postgres.js';
|
|
17
17
|
import { extractDatabaseName } from './protocol.js';
|
|
18
18
|
import { EventEmitter } from 'events';
|
|
19
|
+
import { loadEffectiveConfig } from './settings-loader.cjs';
|
|
19
20
|
|
|
20
21
|
// PostgreSQL protocol constants
|
|
21
22
|
const PROTOCOL_VERSION_3 = 196608;
|
|
@@ -427,21 +428,31 @@ export async function startClusterServer(options = {}) {
|
|
|
427
428
|
const workers = new Map();
|
|
428
429
|
const workerStats = new Map(); // Track stats from each worker
|
|
429
430
|
|
|
430
|
-
// Fork workers with PostgreSQL connection info
|
|
431
|
+
// Fork workers with PostgreSQL connection info.
|
|
432
|
+
//
|
|
433
|
+
// Pass through using AUTOPG_<X> (the primary names) so workers don't
|
|
434
|
+
// emit the legacy-PGSERVE deprecation log for our own internal IPC.
|
|
435
|
+
// PGSERVE_WORKER stays as-is — it's an internal flag, not part of the
|
|
436
|
+
// settings precedence chain.
|
|
437
|
+
const workerEnv = () => ({
|
|
438
|
+
PGSERVE_WORKER: 'true',
|
|
439
|
+
AUTOPG_PORT: String(port),
|
|
440
|
+
AUTOPG_HOST: host,
|
|
441
|
+
AUTOPG_PG_SOCKET: pgSocketPath || '',
|
|
442
|
+
AUTOPG_PG_PORT: String(pgPort),
|
|
443
|
+
AUTOPG_PG_USER: 'postgres',
|
|
444
|
+
AUTOPG_PG_PASSWORD: 'postgres',
|
|
445
|
+
AUTOPG_LOG_LEVEL: options.logLevel || 'info',
|
|
446
|
+
AUTOPG_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
447
|
+
// max_connections is a postgres GUC, not a server-level env. Workers
|
|
448
|
+
// read it from settings.postgres via loadEffectiveConfig; we still
|
|
449
|
+
// ship the value here so a CLI override (e.g. `--max-connections`)
|
|
450
|
+
// reaches the worker before the daemon writes settings.json.
|
|
451
|
+
AUTOPG_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
452
|
+
AUTOPG_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false',
|
|
453
|
+
});
|
|
431
454
|
for (let i = 0; i < numWorkers; i++) {
|
|
432
|
-
const worker = cluster.fork(
|
|
433
|
-
PGSERVE_WORKER: 'true',
|
|
434
|
-
PGSERVE_PORT: String(port),
|
|
435
|
-
PGSERVE_HOST: host,
|
|
436
|
-
PGSERVE_PG_SOCKET: pgSocketPath || '',
|
|
437
|
-
PGSERVE_PG_PORT: String(pgPort),
|
|
438
|
-
PGSERVE_PG_USER: 'postgres',
|
|
439
|
-
PGSERVE_PG_PASSWORD: 'postgres',
|
|
440
|
-
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
441
|
-
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
442
|
-
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
443
|
-
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
444
|
-
});
|
|
455
|
+
const worker = cluster.fork(workerEnv());
|
|
445
456
|
workers.set(worker.id, worker);
|
|
446
457
|
}
|
|
447
458
|
|
|
@@ -457,19 +468,7 @@ export async function startClusterServer(options = {}) {
|
|
|
457
468
|
}
|
|
458
469
|
|
|
459
470
|
console.log(`[pgserve] Worker ${worker.id} died (${signal || code}), restarting...`);
|
|
460
|
-
const newWorker = cluster.fork(
|
|
461
|
-
PGSERVE_WORKER: 'true',
|
|
462
|
-
PGSERVE_PORT: String(port),
|
|
463
|
-
PGSERVE_HOST: host,
|
|
464
|
-
PGSERVE_PG_SOCKET: pgSocketPath || '',
|
|
465
|
-
PGSERVE_PG_PORT: String(pgPort),
|
|
466
|
-
PGSERVE_PG_USER: 'postgres',
|
|
467
|
-
PGSERVE_PG_PASSWORD: 'postgres',
|
|
468
|
-
PGSERVE_LOG_LEVEL: options.logLevel || 'info',
|
|
469
|
-
PGSERVE_AUTO_PROVISION: options.autoProvision !== false ? 'true' : 'false',
|
|
470
|
-
PGSERVE_MAX_CONNECTIONS: String(options.maxConnections || 1000),
|
|
471
|
-
PGSERVE_ENABLE_PGVECTOR: options.enablePgvector ? 'true' : 'false'
|
|
472
|
-
});
|
|
471
|
+
const newWorker = cluster.fork(workerEnv());
|
|
473
472
|
workers.set(newWorker.id, newWorker);
|
|
474
473
|
});
|
|
475
474
|
|
|
@@ -545,18 +544,24 @@ export async function startClusterServer(options = {}) {
|
|
|
545
544
|
pgManager
|
|
546
545
|
};
|
|
547
546
|
} else {
|
|
548
|
-
// WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL
|
|
547
|
+
// WORKER: Only run TCP routing, connect to PRIMARY's PostgreSQL.
|
|
548
|
+
//
|
|
549
|
+
// Worker config comes from loadEffectiveConfig() (defaults < file < env).
|
|
550
|
+
// The primary fork() above sets PGSERVE_* env vars so existing supervised
|
|
551
|
+
// installs keep working; AUTOPG_* env vars take precedence when set, and
|
|
552
|
+
// a one-time deprecation note is emitted for legacy-only PGSERVE_* hits.
|
|
553
|
+
const { settings } = loadEffectiveConfig();
|
|
549
554
|
const router = new ClusterRouter({
|
|
550
|
-
port:
|
|
551
|
-
host:
|
|
552
|
-
pgSocketPath:
|
|
553
|
-
pgPort:
|
|
554
|
-
pgUser:
|
|
555
|
-
pgPassword:
|
|
556
|
-
logLevel:
|
|
557
|
-
autoProvision:
|
|
558
|
-
maxConnections:
|
|
559
|
-
enablePgvector:
|
|
555
|
+
port: settings.server.port,
|
|
556
|
+
host: settings.server.host,
|
|
557
|
+
pgSocketPath: settings.server.pgSocketPath || null,
|
|
558
|
+
pgPort: settings.server.pgPort,
|
|
559
|
+
pgUser: settings.server.pgUser,
|
|
560
|
+
pgPassword: settings.server.pgPassword,
|
|
561
|
+
logLevel: settings.runtime.logLevel,
|
|
562
|
+
autoProvision: settings.runtime.autoProvision,
|
|
563
|
+
maxConnections: settings.postgres.max_connections,
|
|
564
|
+
enablePgvector: settings.runtime.enablePgvector,
|
|
560
565
|
});
|
|
561
566
|
|
|
562
567
|
await router.start();
|