pgserve 2.1.2 → 2.2.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/CHANGELOG.md +86 -0
- package/README.md +105 -1
- package/bin/autopg-wrapper.cjs +16 -0
- package/bin/pgserve-wrapper.cjs +31 -6
- package/bin/postgres-server.js +80 -7
- 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 +11 -1
- package/src/cli-config.cjs +310 -0
- package/src/cli-install.cjs +98 -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/.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/scripts/test-bun-self-heal.sh +0 -163
- package/scripts/test-npx.sh +0 -60
- 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-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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `autopg config` subcommand router (also reachable via `pgserve config`).
|
|
3
|
+
*
|
|
4
|
+
* Surface:
|
|
5
|
+
* autopg config list - print every leaf as key|value|source
|
|
6
|
+
* autopg config get <key> - print the resolved value (machine-friendly)
|
|
7
|
+
* autopg config set <key> <value> - validate + atomic write, round-trips through get
|
|
8
|
+
* autopg config edit - open $EDITOR on settings.json
|
|
9
|
+
* autopg config path - print absolute path to settings.json
|
|
10
|
+
* autopg config init [--force] - write schema defaults; refuses to clobber
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 - success
|
|
14
|
+
* 1 - unknown subcommand / IO error / EDITOR not set / settings file unreadable
|
|
15
|
+
* 2 - validation error (stable shape: `error: <field> — <CODE>: <detail>`)
|
|
16
|
+
*
|
|
17
|
+
* The CLI is single-process and skips the etag round-trip — each `set` is its
|
|
18
|
+
* own transaction. Concurrency control is the UI helper's responsibility.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { spawnSync } = require('node:child_process');
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
|
|
26
|
+
const { loadEffectiveConfig, getSettingsPath } = require('./settings-loader.cjs');
|
|
27
|
+
const {
|
|
28
|
+
setLeaf,
|
|
29
|
+
initSettings,
|
|
30
|
+
ensureConfigDir,
|
|
31
|
+
} = require('./settings-writer.cjs');
|
|
32
|
+
const {
|
|
33
|
+
ValidationError,
|
|
34
|
+
validateSetting,
|
|
35
|
+
resolveKey,
|
|
36
|
+
} = require('./settings-validator.cjs');
|
|
37
|
+
const { SCHEMA, flattenSchema } = require('./settings-schema.cjs');
|
|
38
|
+
|
|
39
|
+
const EXIT_OK = 0;
|
|
40
|
+
const EXIT_UNKNOWN = 1;
|
|
41
|
+
const EXIT_VALIDATION = 2;
|
|
42
|
+
|
|
43
|
+
function emitError(field, code, detail) {
|
|
44
|
+
process.stderr.write(`error: ${field} — ${code}: ${detail}\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function emitErrorFromValidation(err) {
|
|
48
|
+
emitError(err.field ?? '_root', err.code ?? 'INVALID', err.detail ?? err.message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the current value of `key` from the merged effective config tree.
|
|
53
|
+
* Supports curated leaves (`section.field`) and `_extra` entries
|
|
54
|
+
* (`postgres._extra.<gucName>`). Returns `{ value }` or `null` when missing.
|
|
55
|
+
*/
|
|
56
|
+
function readValue(tree, key) {
|
|
57
|
+
if (key.startsWith('postgres._extra.')) {
|
|
58
|
+
const guc = key.slice('postgres._extra.'.length);
|
|
59
|
+
const map = tree?.postgres?._extra;
|
|
60
|
+
if (map && Object.prototype.hasOwnProperty.call(map, guc)) {
|
|
61
|
+
return { value: map[guc] };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const [section, field] = key.split('.');
|
|
66
|
+
if (!section || !field) return null;
|
|
67
|
+
const node = tree?.[section];
|
|
68
|
+
if (!node || !Object.prototype.hasOwnProperty.call(node, field)) return null;
|
|
69
|
+
return { value: node[field] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Serialize a leaf value for human consumption. Objects (the `_extra` map
|
|
74
|
+
* descriptor) round-trip through JSON; primitives stringify directly.
|
|
75
|
+
* `null` / `undefined` render as the empty string so `autopg config get`
|
|
76
|
+
* stays scriptable.
|
|
77
|
+
*/
|
|
78
|
+
function formatValue(value) {
|
|
79
|
+
if (value === null || value === undefined) return '';
|
|
80
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Assemble the full set of keys to display in `config list`. Curated
|
|
86
|
+
* leaves come from the schema; `_extra` entries are expanded from the
|
|
87
|
+
* effective tree so user-added GUCs surface.
|
|
88
|
+
*/
|
|
89
|
+
function enumerateKeys(tree) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const [section, fields] of Object.entries(SCHEMA)) {
|
|
92
|
+
for (const field of Object.keys(fields)) {
|
|
93
|
+
out.push(`${section}.${field}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const extras = tree?.postgres?._extra;
|
|
97
|
+
if (extras && typeof extras === 'object') {
|
|
98
|
+
for (const guc of Object.keys(extras)) {
|
|
99
|
+
out.push(`postgres._extra.${guc}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function pad(s, n) {
|
|
106
|
+
s = String(s);
|
|
107
|
+
if (s.length >= n) return s;
|
|
108
|
+
return s + ' '.repeat(n - s.length);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cmdList() {
|
|
112
|
+
const { settings, sources } = loadEffectiveConfig();
|
|
113
|
+
const keys = enumerateKeys(settings);
|
|
114
|
+
|
|
115
|
+
// Source for `_extra` entries inherits the parent map's source. The
|
|
116
|
+
// loader doesn't break the map per-entry because env precedence
|
|
117
|
+
// applies wholesale, so we surface each row's source as the parent.
|
|
118
|
+
const rows = keys.map((key) => {
|
|
119
|
+
const valueResolved = readValue(settings, key);
|
|
120
|
+
const value = valueResolved ? formatValue(valueResolved.value) : '';
|
|
121
|
+
let source;
|
|
122
|
+
if (key.startsWith('postgres._extra.')) {
|
|
123
|
+
source = sources['postgres._extra'] || 'default';
|
|
124
|
+
} else {
|
|
125
|
+
source = sources[key] || 'default';
|
|
126
|
+
}
|
|
127
|
+
return { key, value, source };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const widths = {
|
|
131
|
+
key: Math.max(3, ...rows.map((r) => r.key.length)),
|
|
132
|
+
value: Math.max(5, ...rows.map((r) => r.value.length)),
|
|
133
|
+
source: Math.max(6, ...rows.map((r) => r.source.length)),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
process.stdout.write(
|
|
137
|
+
`${pad('KEY', widths.key)} ${pad('VALUE', widths.value)} ${pad('SOURCE', widths.source)}\n`,
|
|
138
|
+
);
|
|
139
|
+
for (const row of rows) {
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
`${pad(row.key, widths.key)} ${pad(row.value, widths.value)} ${pad(row.source, widths.source)}\n`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return EXIT_OK;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function cmdGet(args) {
|
|
148
|
+
const key = args[0];
|
|
149
|
+
if (!key) {
|
|
150
|
+
emitError('_args', 'INVALID_KEY', 'config get requires a key');
|
|
151
|
+
return EXIT_VALIDATION;
|
|
152
|
+
}
|
|
153
|
+
// Validate key shape early so typos surface as INVALID_KEY rather than
|
|
154
|
+
// an empty value print.
|
|
155
|
+
try {
|
|
156
|
+
resolveKey(key);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof ValidationError) {
|
|
159
|
+
emitErrorFromValidation(err);
|
|
160
|
+
return EXIT_VALIDATION;
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { settings } = loadEffectiveConfig();
|
|
166
|
+
const resolved = readValue(settings, key);
|
|
167
|
+
if (!resolved) {
|
|
168
|
+
process.stdout.write('\n');
|
|
169
|
+
return EXIT_OK;
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write(`${formatValue(resolved.value)}\n`);
|
|
172
|
+
return EXIT_OK;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function cmdSet(args) {
|
|
176
|
+
if (args.length < 2) {
|
|
177
|
+
emitError('_args', 'INVALID_KEY', 'config set requires <key> <value>');
|
|
178
|
+
return EXIT_VALIDATION;
|
|
179
|
+
}
|
|
180
|
+
const [key, ...rest] = args;
|
|
181
|
+
// Allow values that contain spaces by joining the remainder. Operators
|
|
182
|
+
// can still quote the value as a single argv slot; this is the safe
|
|
183
|
+
// fallback.
|
|
184
|
+
const value = rest.join(' ');
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
setLeaf(key, value);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (err instanceof ValidationError) {
|
|
190
|
+
emitErrorFromValidation(err);
|
|
191
|
+
return EXIT_VALIDATION;
|
|
192
|
+
}
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
return EXIT_OK;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function cmdPath() {
|
|
199
|
+
process.stdout.write(`${getSettingsPath()}\n`);
|
|
200
|
+
return EXIT_OK;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdInit(args) {
|
|
204
|
+
const force = args.includes('--force');
|
|
205
|
+
ensureConfigDir();
|
|
206
|
+
try {
|
|
207
|
+
initSettings({ force });
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err.code === 'EEXIST') {
|
|
210
|
+
emitError(
|
|
211
|
+
getSettingsPath(),
|
|
212
|
+
'EEXIST',
|
|
213
|
+
'settings.json already exists; pass --force to overwrite',
|
|
214
|
+
);
|
|
215
|
+
return EXIT_VALIDATION;
|
|
216
|
+
}
|
|
217
|
+
if (err instanceof ValidationError) {
|
|
218
|
+
emitErrorFromValidation(err);
|
|
219
|
+
return EXIT_VALIDATION;
|
|
220
|
+
}
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
process.stdout.write(`autopg: wrote defaults to ${getSettingsPath()}\n`);
|
|
224
|
+
return EXIT_OK;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* `autopg config edit` — open the configured editor on `settings.json`,
|
|
229
|
+
* creating the file with defaults if it doesn't exist yet (so the
|
|
230
|
+
* operator gets a useful template instead of an empty buffer).
|
|
231
|
+
*
|
|
232
|
+
* Editor resolution: $VISUAL, $EDITOR, then `vi` (POSIX) / `notepad` (Windows).
|
|
233
|
+
*/
|
|
234
|
+
function cmdEdit() {
|
|
235
|
+
const settingsPath = getSettingsPath();
|
|
236
|
+
if (!fs.existsSync(settingsPath)) {
|
|
237
|
+
ensureConfigDir();
|
|
238
|
+
initSettings({});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const editor =
|
|
242
|
+
process.env.VISUAL ||
|
|
243
|
+
process.env.EDITOR ||
|
|
244
|
+
(process.platform === 'win32' ? 'notepad' : 'vi');
|
|
245
|
+
|
|
246
|
+
// Editors are interactive — inherit stdio so the operator gets the TUI.
|
|
247
|
+
const result = spawnSync(editor, [settingsPath], { stdio: 'inherit' });
|
|
248
|
+
if (result.error) {
|
|
249
|
+
emitError(
|
|
250
|
+
'editor',
|
|
251
|
+
'EEDITOR',
|
|
252
|
+
`failed to launch editor "${editor}": ${result.error.message}`,
|
|
253
|
+
);
|
|
254
|
+
return EXIT_UNKNOWN;
|
|
255
|
+
}
|
|
256
|
+
return result.status ?? EXIT_OK;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Subcommand dispatch. Returns the exit code; the parent dispatcher
|
|
261
|
+
* uses the return value as `process.exit(code)` directly.
|
|
262
|
+
*/
|
|
263
|
+
function dispatch(subcommand, args = []) {
|
|
264
|
+
switch (subcommand) {
|
|
265
|
+
case 'list':
|
|
266
|
+
return cmdList();
|
|
267
|
+
case 'get':
|
|
268
|
+
return cmdGet(args);
|
|
269
|
+
case 'set':
|
|
270
|
+
return cmdSet(args);
|
|
271
|
+
case 'path':
|
|
272
|
+
return cmdPath();
|
|
273
|
+
case 'init':
|
|
274
|
+
return cmdInit(args);
|
|
275
|
+
case 'edit':
|
|
276
|
+
return cmdEdit();
|
|
277
|
+
case undefined:
|
|
278
|
+
case '': {
|
|
279
|
+
// Bare `autopg config` → list (mirrors `git config --list` ergonomics).
|
|
280
|
+
return cmdList();
|
|
281
|
+
}
|
|
282
|
+
default:
|
|
283
|
+
emitError(subcommand, 'INVALID_KEY', `unknown config subcommand "${subcommand}"`);
|
|
284
|
+
process.stderr.write(
|
|
285
|
+
'usage: autopg config <list|get|set|edit|path|init> [args]\n',
|
|
286
|
+
);
|
|
287
|
+
return EXIT_UNKNOWN;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = {
|
|
292
|
+
dispatch,
|
|
293
|
+
EXIT_OK,
|
|
294
|
+
EXIT_UNKNOWN,
|
|
295
|
+
EXIT_VALIDATION,
|
|
296
|
+
// Test surface
|
|
297
|
+
_internals: {
|
|
298
|
+
cmdList,
|
|
299
|
+
cmdGet,
|
|
300
|
+
cmdSet,
|
|
301
|
+
cmdPath,
|
|
302
|
+
cmdInit,
|
|
303
|
+
cmdEdit,
|
|
304
|
+
enumerateKeys,
|
|
305
|
+
formatValue,
|
|
306
|
+
readValue,
|
|
307
|
+
flattenSchema,
|
|
308
|
+
validateSetting,
|
|
309
|
+
},
|
|
310
|
+
};
|
package/src/cli-install.cjs
CHANGED
|
@@ -77,8 +77,22 @@ const HARDENED_DEFAULTS = {
|
|
|
77
77
|
logDateFormat: 'YYYY-MM-DD HH:mm:ss.SSS',
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the config directory. AUTOPG_CONFIG_DIR (the new var) wins,
|
|
82
|
+
* PGSERVE_CONFIG_DIR (the legacy var) is honored as a fall-through, and
|
|
83
|
+
* `~/.autopg/` is the new default. The legacy default `~/.pgserve/` is
|
|
84
|
+
* NOT consulted here — `settings-migrate.js` handles the one-shot copy.
|
|
85
|
+
*
|
|
86
|
+
* Soft-rename rule: AUTOPG_<X> beats PGSERVE_<X>. When only the legacy
|
|
87
|
+
* env is set we still honor it but the loader emits a one-time
|
|
88
|
+
* deprecation log via logger.warn (see settings-loader.js).
|
|
89
|
+
*/
|
|
80
90
|
function getConfigDir() {
|
|
81
|
-
return
|
|
91
|
+
return (
|
|
92
|
+
process.env.AUTOPG_CONFIG_DIR ||
|
|
93
|
+
process.env.PGSERVE_CONFIG_DIR ||
|
|
94
|
+
path.join(os.homedir(), '.autopg')
|
|
95
|
+
);
|
|
82
96
|
}
|
|
83
97
|
|
|
84
98
|
function getConfigPath() {
|
|
@@ -139,11 +153,40 @@ function pm2IsAvailable() {
|
|
|
139
153
|
}
|
|
140
154
|
}
|
|
141
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Resolve the effective supervision config — start from HARDENED_DEFAULTS,
|
|
158
|
+
* overlay any values found in `~/.autopg/settings.json` `supervision`
|
|
159
|
+
* section. Failures fall through to defaults silently so `pgserve install`
|
|
160
|
+
* still works on a fresh machine before `autopg config init` has run.
|
|
161
|
+
*
|
|
162
|
+
* Precedence: defaults < settings.json < env (env wins via loadEffectiveConfig).
|
|
163
|
+
*/
|
|
164
|
+
function getEffectiveSupervision() {
|
|
165
|
+
try {
|
|
166
|
+
const { loadEffectiveConfig } = require('./settings-loader.cjs');
|
|
167
|
+
const { settings } = loadEffectiveConfig();
|
|
168
|
+
const sup = settings?.supervision || {};
|
|
169
|
+
return {
|
|
170
|
+
maxRestarts: sup.maxRestarts ?? HARDENED_DEFAULTS.maxRestarts,
|
|
171
|
+
minUptimeMs: sup.minUptimeMs ?? HARDENED_DEFAULTS.minUptimeMs,
|
|
172
|
+
restartDelayMs: sup.restartDelayMs ?? HARDENED_DEFAULTS.restartDelayMs,
|
|
173
|
+
expBackoffRestartDelayMs: sup.expBackoffRestartDelayMs ?? HARDENED_DEFAULTS.expBackoffRestartDelayMs,
|
|
174
|
+
expBackoffMaxMs: sup.expBackoffMaxMs ?? HARDENED_DEFAULTS.expBackoffMaxMs,
|
|
175
|
+
maxMemory: sup.maxMemory ?? HARDENED_DEFAULTS.maxMemory,
|
|
176
|
+
killTimeoutMs: sup.killTimeoutMs ?? HARDENED_DEFAULTS.killTimeoutMs,
|
|
177
|
+
logDateFormat: sup.logDateFormat ?? HARDENED_DEFAULTS.logDateFormat,
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
return { ...HARDENED_DEFAULTS };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
142
184
|
function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
143
185
|
const logs = {
|
|
144
186
|
out: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-out.log`),
|
|
145
187
|
error: path.join(getLogsDir(), `${PM2_PROCESS_NAME}-error.log`),
|
|
146
188
|
};
|
|
189
|
+
const supervision = getEffectiveSupervision();
|
|
147
190
|
return [
|
|
148
191
|
'start',
|
|
149
192
|
scriptPath,
|
|
@@ -152,7 +195,7 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
152
195
|
'--interpreter',
|
|
153
196
|
'none',
|
|
154
197
|
'--max-restarts',
|
|
155
|
-
String(
|
|
198
|
+
String(supervision.maxRestarts),
|
|
156
199
|
// NOTE: pm2 ≥ 6.0 dropped `--min-uptime` from the CLI surface — passing
|
|
157
200
|
// it produces `error: unknown option --min-uptime` and aborts the
|
|
158
201
|
// install. The flag still works inside an ecosystem file, but per the
|
|
@@ -162,18 +205,18 @@ function buildPm2StartArgs({ scriptPath, port, dataDir }) {
|
|
|
162
205
|
// than only sub-`min_uptime` ones; the budget of 50 above is sized
|
|
163
206
|
// accordingly.
|
|
164
207
|
'--restart-delay',
|
|
165
|
-
String(
|
|
208
|
+
String(supervision.restartDelayMs),
|
|
166
209
|
// Exponential backoff between successive failures: starts at 100ms,
|
|
167
210
|
// doubles each crash, ramps to ~60s. Avoids hammering pm2 + the host
|
|
168
211
|
// when the underlying issue is persistent.
|
|
169
212
|
'--exp-backoff-restart-delay',
|
|
170
|
-
String(
|
|
213
|
+
String(supervision.expBackoffRestartDelayMs),
|
|
171
214
|
'--max-memory-restart',
|
|
172
|
-
|
|
215
|
+
supervision.maxMemory,
|
|
173
216
|
'--kill-timeout',
|
|
174
|
-
String(
|
|
217
|
+
String(supervision.killTimeoutMs),
|
|
175
218
|
'--log-date-format',
|
|
176
|
-
|
|
219
|
+
supervision.logDateFormat,
|
|
177
220
|
'--output',
|
|
178
221
|
logs.out,
|
|
179
222
|
'--error',
|
|
@@ -393,12 +436,42 @@ function parseDataDir(args) {
|
|
|
393
436
|
}
|
|
394
437
|
|
|
395
438
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
439
|
+
* One-shot migration check from `~/.pgserve/` → `~/.autopg/`. Runs once
|
|
440
|
+
* per process at the top of dispatch() so every CLI entry point gets
|
|
441
|
+
* the cutover. Fully best-effort: any failure is swallowed (we never
|
|
442
|
+
* want migration to block an `autopg status` invocation).
|
|
443
|
+
*/
|
|
444
|
+
let _migrationChecked = false;
|
|
445
|
+
function ensureMigrationOnce() {
|
|
446
|
+
if (_migrationChecked) return;
|
|
447
|
+
_migrationChecked = true;
|
|
448
|
+
try {
|
|
449
|
+
const { migrateIfNeeded } = require('./settings-migrate.cjs');
|
|
450
|
+
const result = migrateIfNeeded();
|
|
451
|
+
if (result.migrated) {
|
|
452
|
+
process.stderr.write(
|
|
453
|
+
`autopg: migrated ${result.legacy} → ${result.fresh} (one-time)\n`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// Swallow — operator can re-run migration manually if needed.
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Entry point invoked by the wrapper. Returns the exit code (or a Promise
|
|
463
|
+
* for async subcommands such as `ui`). Throws on unknown subcommand so
|
|
464
|
+
* the wrapper's normal flow can take over (the router treats any
|
|
465
|
+
* non-recognized subcommand as "pass through to the postgres-server.js
|
|
466
|
+
* dispatcher").
|
|
467
|
+
*
|
|
468
|
+
* `ctx.scriptPath` is the path to `bin/postgres-server.js` (used by
|
|
469
|
+
* install for the pm2 entry point). For `restart` and `ui` we need the
|
|
470
|
+
* wrapper script path instead — `ctx.wrapperPath`. The wrapper provides
|
|
471
|
+
* both before calling dispatch.
|
|
400
472
|
*/
|
|
401
473
|
function dispatch(subcommand, args, ctx) {
|
|
474
|
+
ensureMigrationOnce();
|
|
402
475
|
switch (subcommand) {
|
|
403
476
|
case 'install':
|
|
404
477
|
return cmdInstall(args, ctx);
|
|
@@ -410,6 +483,19 @@ function dispatch(subcommand, args, ctx) {
|
|
|
410
483
|
return cmdUrl();
|
|
411
484
|
case 'port':
|
|
412
485
|
return cmdPort();
|
|
486
|
+
case 'config': {
|
|
487
|
+
const cfg = require('./cli-config.cjs');
|
|
488
|
+
const [sub, ...rest] = args;
|
|
489
|
+
return cfg.dispatch(sub, rest);
|
|
490
|
+
}
|
|
491
|
+
case 'restart': {
|
|
492
|
+
const restart = require('./cli-restart.cjs');
|
|
493
|
+
return restart.dispatch(args, { scriptPath: ctx.wrapperPath });
|
|
494
|
+
}
|
|
495
|
+
case 'ui': {
|
|
496
|
+
const ui = require('./cli-ui.cjs');
|
|
497
|
+
return ui.dispatch(args, { scriptPath: ctx.wrapperPath });
|
|
498
|
+
}
|
|
413
499
|
default:
|
|
414
500
|
throw new Error(`pgserve: dispatch called with unknown subcommand "${subcommand}"`);
|
|
415
501
|
}
|
|
@@ -430,6 +516,7 @@ module.exports = {
|
|
|
430
516
|
readConfig,
|
|
431
517
|
writeConfig,
|
|
432
518
|
buildPm2StartArgs,
|
|
519
|
+
getEffectiveSupervision,
|
|
433
520
|
parsePort,
|
|
434
521
|
parseDataDir,
|
|
435
522
|
},
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `autopg restart` (also reachable via `pgserve restart`).
|
|
3
|
+
*
|
|
4
|
+
* Behavior:
|
|
5
|
+
* - If pm2 is supervising the `pgserve` process → `pm2 restart pgserve`.
|
|
6
|
+
* This is the production path: pm2 owns the lifecycle, sending it a
|
|
7
|
+
* restart bumps the supervised counter and respects the hardened
|
|
8
|
+
* defaults registered at install time.
|
|
9
|
+
* - Otherwise → read the daemon's pidfile, SIGTERM, wait for exit, then
|
|
10
|
+
* respawn via `bin/pgserve-wrapper.cjs daemon`. Detached so the
|
|
11
|
+
* respawn outlives this CLI process.
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - restart issued (pm2 path) or respawn started (local path)
|
|
15
|
+
* 1 - pm2 restart failed, or respawn could not start, or the daemon
|
|
16
|
+
* didn't honor SIGTERM within the timeout
|
|
17
|
+
*
|
|
18
|
+
* Why pm2 wins when present: a supervised process restarted via the local
|
|
19
|
+
* SIGTERM path would race pm2's own restart loop and double-fire (pm2
|
|
20
|
+
* relaunches as soon as it sees the exit, then we relaunch again). The
|
|
21
|
+
* pm2 jlist probe is the authoritative gate.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const { spawnSync, spawn, execFileSync } = require('node:child_process');
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const PM2_PROCESS_NAME = 'pgserve';
|
|
31
|
+
const SIGTERM_TIMEOUT_MS = 10_000;
|
|
32
|
+
const POLL_INTERVAL_MS = 100;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Mirror of `resolvePidLockPath` from src/daemon.js (ESM). Inlined here
|
|
36
|
+
* because cli-restart.cjs is CJS and we don't want to pull in dynamic
|
|
37
|
+
* import for a 3-line path resolver. The two MUST stay in sync.
|
|
38
|
+
*/
|
|
39
|
+
function resolveControlSocketDir() {
|
|
40
|
+
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
41
|
+
const base = xdg && xdg.length > 0 ? xdg : '/tmp';
|
|
42
|
+
return path.join(base, 'pgserve');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolvePidLockPath() {
|
|
46
|
+
return path.join(resolveControlSocketDir(), 'pgserve.pid');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `pm2 jlist` probe. Returns the registered process object or null.
|
|
51
|
+
* Mirrors cli-install's helper but runs without the install ctx —
|
|
52
|
+
* we don't need anything other than the process name.
|
|
53
|
+
*/
|
|
54
|
+
function pm2GetProcess(name = PM2_PROCESS_NAME) {
|
|
55
|
+
try {
|
|
56
|
+
const out = execFileSync('pm2', ['jlist'], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
timeout: 5000,
|
|
59
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
60
|
+
});
|
|
61
|
+
const list = JSON.parse(out);
|
|
62
|
+
return list.find((p) => p && p.name === name) || null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function pm2IsAvailable() {
|
|
69
|
+
try {
|
|
70
|
+
execFileSync('pm2', ['--version'], {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
timeout: 3000,
|
|
73
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readPid(pidPath) {
|
|
82
|
+
if (!fs.existsSync(pidPath)) return null;
|
|
83
|
+
try {
|
|
84
|
+
const raw = fs.readFileSync(pidPath, 'utf8').trim();
|
|
85
|
+
const pid = Number.parseInt(raw, 10);
|
|
86
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isAlive(pid) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 0);
|
|
95
|
+
return true;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return err.code === 'EPERM';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Synchronously wait until the pidfile is gone (the daemon's graceful
|
|
103
|
+
* shutdown path removes it). We don't also require !isAlive because the
|
|
104
|
+
* pid may briefly be a zombie until the parent reaps it — the pidfile
|
|
105
|
+
* being absent is the daemon's "I'm clean" signal, matching the existing
|
|
106
|
+
* `pgserve daemon stop` flow in src/daemon.js.
|
|
107
|
+
*/
|
|
108
|
+
function waitForExit(pid, pidPath, timeoutMs = SIGTERM_TIMEOUT_MS) {
|
|
109
|
+
const deadline = Date.now() + timeoutMs;
|
|
110
|
+
while (Date.now() < deadline) {
|
|
111
|
+
if (!fs.existsSync(pidPath)) return true;
|
|
112
|
+
sleepBlocking(POLL_INTERVAL_MS);
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sleepBlocking(ms) {
|
|
118
|
+
// Atomics.wait is a portable blocking sleep — node 16+ supports it on
|
|
119
|
+
// a SharedArrayBuffer-backed Int32Array. No Bun dependency.
|
|
120
|
+
try {
|
|
121
|
+
const sab = new SharedArrayBuffer(4);
|
|
122
|
+
const ia = new Int32Array(sab);
|
|
123
|
+
Atomics.wait(ia, 0, 0, ms);
|
|
124
|
+
} catch {
|
|
125
|
+
// Fall back to a busy spin on platforms that don't allow Atomics.wait
|
|
126
|
+
// on the main thread (rare). Acceptable here — only invoked at most
|
|
127
|
+
// once per ~100ms inside a CLI command.
|
|
128
|
+
const end = Date.now() + ms;
|
|
129
|
+
while (Date.now() < end) { /* spin */ }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function fail(message) {
|
|
134
|
+
process.stderr.write(`autopg: ${message}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ok(message) {
|
|
139
|
+
process.stdout.write(`autopg: ${message}\n`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Pm2-supervised path. `pm2 restart pgserve` is the canonical operator
|
|
145
|
+
* action — pm2 increments its own restart counter and respects all the
|
|
146
|
+
* hardening flags registered at install time.
|
|
147
|
+
*/
|
|
148
|
+
function restartViaPm2() {
|
|
149
|
+
const result = spawnSync('pm2', ['restart', PM2_PROCESS_NAME], {
|
|
150
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
151
|
+
});
|
|
152
|
+
if (result.status !== 0) {
|
|
153
|
+
return fail(`pm2 restart failed (exit ${result.status})`);
|
|
154
|
+
}
|
|
155
|
+
return ok(`restarted via pm2 (process "${PM2_PROCESS_NAME}")`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Local-respawn path. Reads the daemon pidfile, SIGTERMs, waits, then
|
|
160
|
+
* respawns the daemon detached so it survives this CLI process exiting.
|
|
161
|
+
*
|
|
162
|
+
* `scriptPath` is the path to bin/pgserve-wrapper.cjs (resolved by the
|
|
163
|
+
* dispatcher's ctx so the test surface can inject a stub binary).
|
|
164
|
+
*/
|
|
165
|
+
function restartLocally({ scriptPath, env = process.env } = {}) {
|
|
166
|
+
const pidPath = resolvePidLockPath();
|
|
167
|
+
const pid = readPid(pidPath);
|
|
168
|
+
|
|
169
|
+
if (pid && isAlive(pid)) {
|
|
170
|
+
try {
|
|
171
|
+
process.kill(pid, 'SIGTERM');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return fail(`failed to signal pid ${pid}: ${err.message}`);
|
|
174
|
+
}
|
|
175
|
+
if (!waitForExit(pid, pidPath)) {
|
|
176
|
+
return fail(`pid ${pid} did not exit within ${SIGTERM_TIMEOUT_MS}ms`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!scriptPath || !fs.existsSync(scriptPath)) {
|
|
181
|
+
return fail(`cannot respawn: wrapper script not found at ${scriptPath}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Spawn detached so the daemon outlives this CLI.
|
|
185
|
+
const child = spawn(process.execPath, [scriptPath, 'daemon'], {
|
|
186
|
+
detached: true,
|
|
187
|
+
stdio: 'ignore',
|
|
188
|
+
env,
|
|
189
|
+
});
|
|
190
|
+
child.unref();
|
|
191
|
+
|
|
192
|
+
return ok(`respawned daemon (pid ${child.pid})`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Entry point. `ctx.scriptPath` is the path to `bin/pgserve-wrapper.cjs`
|
|
197
|
+
* (so the local respawn can re-enter the wrapper to start the daemon).
|
|
198
|
+
*
|
|
199
|
+
* `ctx.pm2IsAvailable` and `ctx.pm2GetProcess` are dependency-injection
|
|
200
|
+
* hooks for tests — production callers omit them and the module-level
|
|
201
|
+
* helpers (which shell out to the real `pm2` binary) are used.
|
|
202
|
+
*/
|
|
203
|
+
function dispatch(_args = [], ctx = {}) {
|
|
204
|
+
const isAvailable = ctx.pm2IsAvailable || pm2IsAvailable;
|
|
205
|
+
const getProcess = ctx.pm2GetProcess || pm2GetProcess;
|
|
206
|
+
const restartFn = ctx.restartViaPm2 || restartViaPm2;
|
|
207
|
+
if (isAvailable() && getProcess(PM2_PROCESS_NAME)) {
|
|
208
|
+
return restartFn();
|
|
209
|
+
}
|
|
210
|
+
return restartLocally({ scriptPath: ctx.scriptPath });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
dispatch,
|
|
215
|
+
// Test surface
|
|
216
|
+
_internals: {
|
|
217
|
+
pm2GetProcess,
|
|
218
|
+
pm2IsAvailable,
|
|
219
|
+
readPid,
|
|
220
|
+
isAlive,
|
|
221
|
+
waitForExit,
|
|
222
|
+
resolvePidLockPath,
|
|
223
|
+
restartViaPm2,
|
|
224
|
+
restartLocally,
|
|
225
|
+
PM2_PROCESS_NAME,
|
|
226
|
+
SIGTERM_TIMEOUT_MS,
|
|
227
|
+
},
|
|
228
|
+
};
|