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.
Files changed (227) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +31 -6
  5. package/bin/postgres-server.js +80 -7
  6. package/console/README.md +131 -0
  7. package/console/api.js +173 -0
  8. package/console/app.jsx +483 -0
  9. package/console/colors_and_type.css +227 -0
  10. package/console/components.jsx +167 -0
  11. package/console/console.css +1666 -0
  12. package/console/data.jsx +350 -0
  13. package/console/index.html +31 -0
  14. package/console/screens/databases.jsx +5 -0
  15. package/console/screens/health.jsx +5 -0
  16. package/console/screens/ingress.jsx +5 -0
  17. package/console/screens/optimizer.jsx +5 -0
  18. package/console/screens/rlm-sim.jsx +5 -0
  19. package/console/screens/rlm-trace.jsx +5 -0
  20. package/console/screens/security.jsx +5 -0
  21. package/console/screens/settings.jsx +611 -0
  22. package/console/screens/sql.jsx +5 -0
  23. package/console/screens/sync.jsx +5 -0
  24. package/console/screens/tables.jsx +5 -0
  25. package/console/tweaks-panel.jsx +425 -0
  26. package/package.json +11 -1
  27. package/src/cli-config.cjs +310 -0
  28. package/src/cli-install.cjs +98 -11
  29. package/src/cli-restart.cjs +228 -0
  30. package/src/cli-ui.cjs +580 -0
  31. package/src/cluster.js +43 -38
  32. package/src/postgres.js +141 -19
  33. package/src/settings-loader.cjs +235 -0
  34. package/src/settings-migrate.cjs +212 -0
  35. package/src/settings-pg-args.cjs +146 -0
  36. package/src/settings-schema.cjs +422 -0
  37. package/src/settings-validator.cjs +416 -0
  38. package/src/settings-writer.cjs +288 -0
  39. package/.claude/context/windows-debug.md +0 -119
  40. package/.genie/AGENTS.md +0 -15
  41. package/.genie/agents/README.md +0 -110
  42. package/.genie/agents/analyze.md +0 -176
  43. package/.genie/agents/forge.md +0 -290
  44. package/.genie/agents/garbage-cleaner.md +0 -324
  45. package/.genie/agents/garbage-collector.md +0 -596
  46. package/.genie/agents/github-issue-gc.md +0 -618
  47. package/.genie/agents/review.md +0 -380
  48. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  49. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  50. package/.genie/agents/semantic-analyzer.md +0 -101
  51. package/.genie/agents/update.md +0 -182
  52. package/.genie/agents/wish.md +0 -357
  53. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  54. package/.genie/code/AGENTS.md +0 -694
  55. package/.genie/code/agents/audit/risk.md +0 -173
  56. package/.genie/code/agents/audit/security.md +0 -189
  57. package/.genie/code/agents/audit.md +0 -145
  58. package/.genie/code/agents/challenge.md +0 -230
  59. package/.genie/code/agents/change-reviewer.md +0 -295
  60. package/.genie/code/agents/code-garbage-collector.md +0 -425
  61. package/.genie/code/agents/code-quality.md +0 -410
  62. package/.genie/code/agents/commit-suggester.md +0 -255
  63. package/.genie/code/agents/commit.md +0 -124
  64. package/.genie/code/agents/consensus.md +0 -204
  65. package/.genie/code/agents/daily-standup.md +0 -722
  66. package/.genie/code/agents/docgen.md +0 -48
  67. package/.genie/code/agents/explore.md +0 -79
  68. package/.genie/code/agents/fix.md +0 -100
  69. package/.genie/code/agents/git/commit-advisory.md +0 -219
  70. package/.genie/code/agents/git/workflows/issue.md +0 -244
  71. package/.genie/code/agents/git/workflows/pr.md +0 -179
  72. package/.genie/code/agents/git/workflows/release.md +0 -460
  73. package/.genie/code/agents/git/workflows/report.md +0 -342
  74. package/.genie/code/agents/git.md +0 -432
  75. package/.genie/code/agents/implementor.md +0 -161
  76. package/.genie/code/agents/install.md +0 -515
  77. package/.genie/code/agents/issue-creator.md +0 -344
  78. package/.genie/code/agents/polish.md +0 -116
  79. package/.genie/code/agents/qa.md +0 -653
  80. package/.genie/code/agents/refactor.md +0 -294
  81. package/.genie/code/agents/release.md +0 -1129
  82. package/.genie/code/agents/roadmap.md +0 -885
  83. package/.genie/code/agents/tests.md +0 -557
  84. package/.genie/code/agents/tracer.md +0 -50
  85. package/.genie/code/agents/update/upstream-update.md +0 -85
  86. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  87. package/.genie/code/agents/vibe.md +0 -1317
  88. package/.genie/code/spells/agent-configuration.md +0 -58
  89. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  90. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  91. package/.genie/code/spells/debug.md +0 -320
  92. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  93. package/.genie/code/spells/evidence-storage.md +0 -26
  94. package/.genie/code/spells/file-naming-rules.md +0 -35
  95. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  96. package/.genie/code/spells/genie-integration.md +0 -153
  97. package/.genie/code/spells/publishing-protocol.md +0 -61
  98. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  99. package/.genie/code/spells/tool-requirements.md +0 -20
  100. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  101. package/.genie/code/teams/tech-council/council.md +0 -328
  102. package/.genie/code/teams/tech-council/jt.md +0 -352
  103. package/.genie/code/teams/tech-council/nayr.md +0 -305
  104. package/.genie/code/teams/tech-council/oettam.md +0 -375
  105. package/.genie/neurons/README.md +0 -193
  106. package/.genie/neurons/forge.md +0 -106
  107. package/.genie/neurons/genie.md +0 -63
  108. package/.genie/neurons/review.md +0 -106
  109. package/.genie/neurons/wish.md +0 -104
  110. package/.genie/product/README.md +0 -20
  111. package/.genie/product/cli-automation.md +0 -359
  112. package/.genie/product/environment.md +0 -60
  113. package/.genie/product/mission.md +0 -60
  114. package/.genie/product/roadmap.md +0 -44
  115. package/.genie/product/tech-stack.md +0 -34
  116. package/.genie/product/templates/context-template.md +0 -218
  117. package/.genie/product/templates/qa-done-report-template.md +0 -68
  118. package/.genie/product/templates/review-report-template.md +0 -89
  119. package/.genie/product/templates/wish-template.md +0 -120
  120. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  121. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  122. package/.genie/scripts/helpers/bullet-find.js +0 -289
  123. package/.genie/scripts/helpers/bullet-id.js +0 -244
  124. package/.genie/scripts/helpers/check-secrets.js +0 -237
  125. package/.genie/scripts/helpers/count-tokens.js +0 -200
  126. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  127. package/.genie/scripts/helpers/detect-markers.js +0 -293
  128. package/.genie/scripts/helpers/detect-todos.js +0 -267
  129. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  130. package/.genie/scripts/helpers/embeddings.js +0 -344
  131. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  132. package/.genie/scripts/helpers/index.js +0 -319
  133. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  134. package/.genie/scripts/helpers/validate-links.js +0 -207
  135. package/.genie/scripts/helpers/validate-paths.js +0 -373
  136. package/.genie/spells/README.md +0 -9
  137. package/.genie/spells/ace-protocol.md +0 -118
  138. package/.genie/spells/ask-one-at-a-time.md +0 -175
  139. package/.genie/spells/backup-analyzer.md +0 -542
  140. package/.genie/spells/blocker.md +0 -12
  141. package/.genie/spells/break-things-move-fast.md +0 -56
  142. package/.genie/spells/context-candidates.md +0 -72
  143. package/.genie/spells/context-critic.md +0 -51
  144. package/.genie/spells/defer-to-expertise.md +0 -278
  145. package/.genie/spells/delegate-dont-do.md +0 -292
  146. package/.genie/spells/error-investigation-protocol.md +0 -328
  147. package/.genie/spells/evidence-based-completion.md +0 -273
  148. package/.genie/spells/experiment.md +0 -65
  149. package/.genie/spells/file-creation-protocol.md +0 -229
  150. package/.genie/spells/forge-integration.md +0 -281
  151. package/.genie/spells/forge-orchestration.md +0 -514
  152. package/.genie/spells/gather-context.md +0 -18
  153. package/.genie/spells/global-health-check.md +0 -34
  154. package/.genie/spells/global-noop-roundtrip.md +0 -25
  155. package/.genie/spells/install-genie.md +0 -1232
  156. package/.genie/spells/install.md +0 -82
  157. package/.genie/spells/investigate-before-commit.md +0 -112
  158. package/.genie/spells/know-yourself.md +0 -288
  159. package/.genie/spells/learn.md +0 -828
  160. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  161. package/.genie/spells/mcp-first.md +0 -124
  162. package/.genie/spells/multi-step-execution.md +0 -67
  163. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  164. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  165. package/.genie/spells/prompt.md +0 -746
  166. package/.genie/spells/reflect.md +0 -404
  167. package/.genie/spells/routing-decision-matrix.md +0 -368
  168. package/.genie/spells/run-in-parallel.md +0 -12
  169. package/.genie/spells/session-state-updater-example.md +0 -196
  170. package/.genie/spells/session-state-updater.md +0 -220
  171. package/.genie/spells/track-long-running-tasks.md +0 -133
  172. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  173. package/.genie/spells/upgrade-genie.md +0 -415
  174. package/.genie/spells/url-presentation-protocol.md +0 -301
  175. package/.genie/spells/wish-initiation.md +0 -158
  176. package/.genie/spells/wish-issue-linkage.md +0 -410
  177. package/.genie/spells/wish-lifecycle.md +0 -100
  178. package/.genie/state/provider-status.json +0 -3
  179. package/.genie/state/version.json +0 -16
  180. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  181. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  182. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  183. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  184. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  185. package/.gitguardian.yaml +0 -29
  186. package/.gitguardianignore +0 -16
  187. package/.github/workflows/ci.yml +0 -122
  188. package/.github/workflows/release.yml +0 -289
  189. package/.github/workflows/version.yml +0 -228
  190. package/.husky/pre-commit +0 -2
  191. package/AGENTS.md +0 -433
  192. package/CLAUDE.md +0 -1
  193. package/Makefile +0 -285
  194. package/assets/icon.ico +0 -0
  195. package/bun.lock +0 -435
  196. package/bunfig.toml +0 -28
  197. package/ecosystem.config.cjs +0 -23
  198. package/eslint.config.js +0 -63
  199. package/examples/multi-tenant-demo.js +0 -104
  200. package/install.sh +0 -123
  201. package/knip.json +0 -9
  202. package/scripts/test-bun-self-heal.sh +0 -163
  203. package/scripts/test-npx.sh +0 -60
  204. package/tests/audit.test.js +0 -189
  205. package/tests/backpressure.test.js +0 -167
  206. package/tests/benchmarks/runner.js +0 -1197
  207. package/tests/benchmarks/vector-generator.js +0 -368
  208. package/tests/cli-install.test.js +0 -322
  209. package/tests/control-db.test.js +0 -285
  210. package/tests/daemon-control.test.js +0 -171
  211. package/tests/daemon-fingerprint-integration.test.js +0 -111
  212. package/tests/daemon-pr24-regression.test.js +0 -198
  213. package/tests/fingerprint.test.js +0 -263
  214. package/tests/fixtures/240-orphan-seed.sql +0 -30
  215. package/tests/multi-tenant.test.js +0 -374
  216. package/tests/orphan-cleanup.test.js +0 -390
  217. package/tests/pg-version-regex.test.js +0 -129
  218. package/tests/quick-bench.js +0 -135
  219. package/tests/router-handshake-retry.test.js +0 -119
  220. package/tests/router-handshake-watchdog.test.js +0 -110
  221. package/tests/sdk.test.js +0 -71
  222. package/tests/stale-postmaster-pid.test.js +0 -85
  223. package/tests/stress-test.js +0 -439
  224. package/tests/sync-perf-test.js +0 -150
  225. package/tests/tcp-listen.test.js +0 -368
  226. package/tests/tenancy.test.js +0 -403
  227. 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
+ };
@@ -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 process.env.PGSERVE_CONFIG_DIR || path.join(os.homedir(), '.pgserve');
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(HARDENED_DEFAULTS.maxRestarts),
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(HARDENED_DEFAULTS.restartDelayMs),
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(HARDENED_DEFAULTS.expBackoffRestartDelayMs),
213
+ String(supervision.expBackoffRestartDelayMs),
171
214
  '--max-memory-restart',
172
- HARDENED_DEFAULTS.maxMemory,
215
+ supervision.maxMemory,
173
216
  '--kill-timeout',
174
- String(HARDENED_DEFAULTS.killTimeoutMs),
217
+ String(supervision.killTimeoutMs),
175
218
  '--log-date-format',
176
- HARDENED_DEFAULTS.logDateFormat,
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
- * Entry point invoked by the wrapper. Returns the exit code. Throws on
397
- * unknown subcommand so the wrapper's normal flow can take over (the
398
- * router treats any non-recognized subcommand as "pass through to the
399
- * postgres-server.js dispatcher").
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
+ };