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.
Files changed (235) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +105 -1
  3. package/bin/autopg-wrapper.cjs +16 -0
  4. package/bin/pgserve-wrapper.cjs +32 -6
  5. package/bin/postgres-server.js +56 -0
  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 +14 -2
  27. package/scripts/postinstall.cjs +60 -0
  28. package/src/cli-config.cjs +310 -0
  29. package/src/cli-install.cjs +112 -11
  30. package/src/cli-restart.cjs +228 -0
  31. package/src/cli-ui.cjs +580 -0
  32. package/src/cluster.js +43 -38
  33. package/src/postgres.js +141 -19
  34. package/src/settings-loader.cjs +235 -0
  35. package/src/settings-migrate.cjs +212 -0
  36. package/src/settings-pg-args.cjs +146 -0
  37. package/src/settings-schema.cjs +422 -0
  38. package/src/settings-validator.cjs +416 -0
  39. package/src/settings-writer.cjs +288 -0
  40. package/src/upgrade/index.js +65 -0
  41. package/src/upgrade/runner.js +23 -0
  42. package/src/upgrade/steps/binary-cache-flush.js +67 -0
  43. package/src/upgrade/steps/consumer-signal.js +40 -0
  44. package/src/upgrade/steps/env-refresh.js +89 -0
  45. package/src/upgrade/steps/health-validate.js +53 -0
  46. package/src/upgrade/steps/plpgsql-resolve.js +66 -0
  47. package/src/upgrade/steps/port-reconcile.js +52 -0
  48. package/.claude/context/windows-debug.md +0 -119
  49. package/.genie/AGENTS.md +0 -15
  50. package/.genie/agents/README.md +0 -110
  51. package/.genie/agents/analyze.md +0 -176
  52. package/.genie/agents/forge.md +0 -290
  53. package/.genie/agents/garbage-cleaner.md +0 -324
  54. package/.genie/agents/garbage-collector.md +0 -596
  55. package/.genie/agents/github-issue-gc.md +0 -618
  56. package/.genie/agents/review.md +0 -380
  57. package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
  58. package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
  59. package/.genie/agents/semantic-analyzer.md +0 -101
  60. package/.genie/agents/update.md +0 -182
  61. package/.genie/agents/wish.md +0 -357
  62. package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
  63. package/.genie/code/AGENTS.md +0 -694
  64. package/.genie/code/agents/audit/risk.md +0 -173
  65. package/.genie/code/agents/audit/security.md +0 -189
  66. package/.genie/code/agents/audit.md +0 -145
  67. package/.genie/code/agents/challenge.md +0 -230
  68. package/.genie/code/agents/change-reviewer.md +0 -295
  69. package/.genie/code/agents/code-garbage-collector.md +0 -425
  70. package/.genie/code/agents/code-quality.md +0 -410
  71. package/.genie/code/agents/commit-suggester.md +0 -255
  72. package/.genie/code/agents/commit.md +0 -124
  73. package/.genie/code/agents/consensus.md +0 -204
  74. package/.genie/code/agents/daily-standup.md +0 -722
  75. package/.genie/code/agents/docgen.md +0 -48
  76. package/.genie/code/agents/explore.md +0 -79
  77. package/.genie/code/agents/fix.md +0 -100
  78. package/.genie/code/agents/git/commit-advisory.md +0 -219
  79. package/.genie/code/agents/git/workflows/issue.md +0 -244
  80. package/.genie/code/agents/git/workflows/pr.md +0 -179
  81. package/.genie/code/agents/git/workflows/release.md +0 -460
  82. package/.genie/code/agents/git/workflows/report.md +0 -342
  83. package/.genie/code/agents/git.md +0 -432
  84. package/.genie/code/agents/implementor.md +0 -161
  85. package/.genie/code/agents/install.md +0 -515
  86. package/.genie/code/agents/issue-creator.md +0 -344
  87. package/.genie/code/agents/polish.md +0 -116
  88. package/.genie/code/agents/qa.md +0 -653
  89. package/.genie/code/agents/refactor.md +0 -294
  90. package/.genie/code/agents/release.md +0 -1129
  91. package/.genie/code/agents/roadmap.md +0 -885
  92. package/.genie/code/agents/tests.md +0 -557
  93. package/.genie/code/agents/tracer.md +0 -50
  94. package/.genie/code/agents/update/upstream-update.md +0 -85
  95. package/.genie/code/agents/update/versions/generic-update.md +0 -305
  96. package/.genie/code/agents/vibe.md +0 -1317
  97. package/.genie/code/spells/agent-configuration.md +0 -58
  98. package/.genie/code/spells/automated-rc-publishing.md +0 -106
  99. package/.genie/code/spells/branch-tracker-guidance.md +0 -28
  100. package/.genie/code/spells/debug.md +0 -320
  101. package/.genie/code/spells/emoji-naming-convention.md +0 -303
  102. package/.genie/code/spells/evidence-storage.md +0 -26
  103. package/.genie/code/spells/file-naming-rules.md +0 -35
  104. package/.genie/code/spells/forge-code-blueprints.md +0 -195
  105. package/.genie/code/spells/genie-integration.md +0 -153
  106. package/.genie/code/spells/publishing-protocol.md +0 -61
  107. package/.genie/code/spells/team-consultation-protocol.md +0 -284
  108. package/.genie/code/spells/tool-requirements.md +0 -20
  109. package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
  110. package/.genie/code/teams/tech-council/council.md +0 -328
  111. package/.genie/code/teams/tech-council/jt.md +0 -352
  112. package/.genie/code/teams/tech-council/nayr.md +0 -305
  113. package/.genie/code/teams/tech-council/oettam.md +0 -375
  114. package/.genie/neurons/README.md +0 -193
  115. package/.genie/neurons/forge.md +0 -106
  116. package/.genie/neurons/genie.md +0 -63
  117. package/.genie/neurons/review.md +0 -106
  118. package/.genie/neurons/wish.md +0 -104
  119. package/.genie/product/README.md +0 -20
  120. package/.genie/product/cli-automation.md +0 -359
  121. package/.genie/product/environment.md +0 -60
  122. package/.genie/product/mission.md +0 -60
  123. package/.genie/product/roadmap.md +0 -44
  124. package/.genie/product/tech-stack.md +0 -34
  125. package/.genie/product/templates/context-template.md +0 -218
  126. package/.genie/product/templates/qa-done-report-template.md +0 -68
  127. package/.genie/product/templates/review-report-template.md +0 -89
  128. package/.genie/product/templates/wish-template.md +0 -120
  129. package/.genie/scripts/helpers/analyze-commit.js +0 -195
  130. package/.genie/scripts/helpers/bullet-counter.js +0 -194
  131. package/.genie/scripts/helpers/bullet-find.js +0 -289
  132. package/.genie/scripts/helpers/bullet-id.js +0 -244
  133. package/.genie/scripts/helpers/check-secrets.js +0 -237
  134. package/.genie/scripts/helpers/count-tokens.js +0 -200
  135. package/.genie/scripts/helpers/create-frontmatter.js +0 -456
  136. package/.genie/scripts/helpers/detect-markers.js +0 -293
  137. package/.genie/scripts/helpers/detect-todos.js +0 -267
  138. package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
  139. package/.genie/scripts/helpers/embeddings.js +0 -344
  140. package/.genie/scripts/helpers/find-empty-sections.js +0 -158
  141. package/.genie/scripts/helpers/index.js +0 -319
  142. package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
  143. package/.genie/scripts/helpers/validate-links.js +0 -207
  144. package/.genie/scripts/helpers/validate-paths.js +0 -373
  145. package/.genie/spells/README.md +0 -9
  146. package/.genie/spells/ace-protocol.md +0 -118
  147. package/.genie/spells/ask-one-at-a-time.md +0 -175
  148. package/.genie/spells/backup-analyzer.md +0 -542
  149. package/.genie/spells/blocker.md +0 -12
  150. package/.genie/spells/break-things-move-fast.md +0 -56
  151. package/.genie/spells/context-candidates.md +0 -72
  152. package/.genie/spells/context-critic.md +0 -51
  153. package/.genie/spells/defer-to-expertise.md +0 -278
  154. package/.genie/spells/delegate-dont-do.md +0 -292
  155. package/.genie/spells/error-investigation-protocol.md +0 -328
  156. package/.genie/spells/evidence-based-completion.md +0 -273
  157. package/.genie/spells/experiment.md +0 -65
  158. package/.genie/spells/file-creation-protocol.md +0 -229
  159. package/.genie/spells/forge-integration.md +0 -281
  160. package/.genie/spells/forge-orchestration.md +0 -514
  161. package/.genie/spells/gather-context.md +0 -18
  162. package/.genie/spells/global-health-check.md +0 -34
  163. package/.genie/spells/global-noop-roundtrip.md +0 -25
  164. package/.genie/spells/install-genie.md +0 -1232
  165. package/.genie/spells/install.md +0 -82
  166. package/.genie/spells/investigate-before-commit.md +0 -112
  167. package/.genie/spells/know-yourself.md +0 -288
  168. package/.genie/spells/learn.md +0 -828
  169. package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
  170. package/.genie/spells/mcp-first.md +0 -124
  171. package/.genie/spells/multi-step-execution.md +0 -67
  172. package/.genie/spells/orchestration-boundary-protocol.md +0 -256
  173. package/.genie/spells/orchestrator-not-implementor.md +0 -189
  174. package/.genie/spells/prompt.md +0 -746
  175. package/.genie/spells/reflect.md +0 -404
  176. package/.genie/spells/routing-decision-matrix.md +0 -368
  177. package/.genie/spells/run-in-parallel.md +0 -12
  178. package/.genie/spells/session-state-updater-example.md +0 -196
  179. package/.genie/spells/session-state-updater.md +0 -220
  180. package/.genie/spells/track-long-running-tasks.md +0 -133
  181. package/.genie/spells/troubleshoot-infrastructure.md +0 -176
  182. package/.genie/spells/upgrade-genie.md +0 -415
  183. package/.genie/spells/url-presentation-protocol.md +0 -301
  184. package/.genie/spells/wish-initiation.md +0 -158
  185. package/.genie/spells/wish-issue-linkage.md +0 -410
  186. package/.genie/spells/wish-lifecycle.md +0 -100
  187. package/.genie/state/provider-status.json +0 -3
  188. package/.genie/state/version.json +0 -16
  189. package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
  190. package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
  191. package/.genie/wishes/pgserve-v2/WISH.md +0 -442
  192. package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
  193. package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
  194. package/.gitguardian.yaml +0 -29
  195. package/.gitguardianignore +0 -16
  196. package/.github/workflows/ci.yml +0 -122
  197. package/.github/workflows/release.yml +0 -289
  198. package/.github/workflows/version.yml +0 -228
  199. package/.husky/pre-commit +0 -2
  200. package/AGENTS.md +0 -433
  201. package/CLAUDE.md +0 -1
  202. package/Makefile +0 -285
  203. package/assets/icon.ico +0 -0
  204. package/bun.lock +0 -435
  205. package/bunfig.toml +0 -28
  206. package/ecosystem.config.cjs +0 -23
  207. package/eslint.config.js +0 -63
  208. package/examples/multi-tenant-demo.js +0 -104
  209. package/install.sh +0 -123
  210. package/knip.json +0 -9
  211. package/tests/audit.test.js +0 -189
  212. package/tests/backpressure.test.js +0 -167
  213. package/tests/benchmarks/runner.js +0 -1197
  214. package/tests/benchmarks/vector-generator.js +0 -368
  215. package/tests/cli-install.test.js +0 -322
  216. package/tests/control-db.test.js +0 -285
  217. package/tests/daemon-args.test.js +0 -86
  218. package/tests/daemon-control.test.js +0 -171
  219. package/tests/daemon-fingerprint-integration.test.js +0 -111
  220. package/tests/daemon-pr24-regression.test.js +0 -198
  221. package/tests/fingerprint.test.js +0 -263
  222. package/tests/fixtures/240-orphan-seed.sql +0 -30
  223. package/tests/multi-tenant.test.js +0 -374
  224. package/tests/orphan-cleanup.test.js +0 -390
  225. package/tests/pg-version-regex.test.js +0 -129
  226. package/tests/quick-bench.js +0 -135
  227. package/tests/router-handshake-retry.test.js +0 -119
  228. package/tests/router-handshake-watchdog.test.js +0 -110
  229. package/tests/sdk.test.js +0 -71
  230. package/tests/stale-postmaster-pid.test.js +0 -85
  231. package/tests/stress-test.js +0 -439
  232. package/tests/sync-perf-test.js +0 -150
  233. package/tests/tcp-listen.test.js +0 -368
  234. package/tests/tenancy.test.js +0 -403
  235. package/tests/wrapper-supervision.test.js +0 -107
package/CHANGELOG.md CHANGED
@@ -1,9 +1,105 @@
1
+ ## v2.2.x — Transparent Upgrade
2
+
3
+ **Added:** `autopg upgrade` CLI verb — idempotent migration runner that reconciles port back to canonical 8432, flushes the binary cache against the pinned PG version, re-resolves the plpgsql `.so` path per database, refreshes `~/.autopg/<app>.env` files, signals consumers, and validates final health.
4
+
5
+ **Added:** npm `postinstall` hook (`scripts/postinstall.cjs`) auto-runs `autopg upgrade --quiet` when an existing `~/.autopg/data/` is detected on `bun install`. Soft-fails so package install never breaks; manual `autopg upgrade` remains the explicit escape hatch.
6
+
7
+ **Contract:** Users upgrading from pgserve@2.1.3 to autopg@2.2.x get transparent migration via the postinstall hook. Manual `autopg upgrade` remains as the explicit escape hatch for forced re-runs. Patches the upgrade-path hole left by autopg-v22 partial roll-out (binary moved to `~/.autopg/`, default port silently shifted to 9432, plpgsql extensions referenced stale `$libdir`).
8
+
9
+ **Override:** Set `AUTOPG_SKIP_POSTINSTALL=1` to bypass the hook (CI / containers / install-only flows).
10
+
1
11
  # Changelog
2
12
 
3
13
  All notable changes to `pgserve` are documented here. The format follows
4
14
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres
5
15
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
16
 
17
+ ## Unreleased — autopg console settings
18
+
19
+ ### Added
20
+
21
+ - **Soft rename to `autopg`.** The npm package stays `pgserve` (no
22
+ `npm deprecate`); the package now also ships an `autopg` bin that
23
+ routes through the same dispatcher. Use either name interchangeably:
24
+ `autopg config list` and `pgserve config list` are byte-equivalent.
25
+ pm2 process name stays `pgserve` so existing supervised installs
26
+ upgrade cleanly with no migration step.
27
+ - **`~/.autopg/settings.json` (schema version 1).** Six sections —
28
+ `server`, `runtime`, `sync`, `supervision`, `postgres`, `ui` —
29
+ with a curated set of 15 PostgreSQL GUCs plus a `postgres._extra`
30
+ raw passthrough map. Every write is atomic (`tmp + rename`),
31
+ chmod 0600, and tagged with a sha256 etag for optimistic
32
+ concurrency control on the UI helper. Override the directory with
33
+ `AUTOPG_CONFIG_DIR`. See [`docs/settings-schema.md`](./docs/settings-schema.md)
34
+ for the full key reference.
35
+ - **`autopg config (list / get / set / edit / path / init)`** — manage
36
+ settings from the shell. `list` prints a `KEY VALUE SOURCE` table
37
+ showing where each leaf was resolved from (default / file / env).
38
+ `set` validates with a stable error format (`error: <field> — <CODE>:
39
+ <detail>`, exit code 2). Seven error codes: `INVALID_KEY`,
40
+ `INVALID_GUC_NAME`, `INVALID_GUC_VALUE`, `INVALID_TYPE`,
41
+ `OUT_OF_RANGE`, `READONLY`, `ETAG_MISMATCH`.
42
+ - **`autopg restart`** — pm2-aware. If the `pgserve` process appears
43
+ in `pm2 jlist`, calls `pm2 restart pgserve` (single-fire, respects
44
+ the hardened defaults registered at install time). Otherwise reads
45
+ the pidfile, sends SIGTERM, waits, and respawns the daemon
46
+ detached.
47
+ - **`autopg ui [--port N] [--no-open]`** — boots a local web console
48
+ on 127.0.0.1 (default port walk: 8433–8533). Single-user dev tool,
49
+ no auth, no TLS. Mounts four endpoints: `GET /api/settings` (returns
50
+ `{ settings, sources, etag }`), `PUT /api/settings` (requires
51
+ `If-Match`, returns 409 on stale etag), `POST /api/restart`,
52
+ `GET /api/status`. All handlers shell out to the CLI — the daemon
53
+ stays untouched, so the console works even with no daemon running.
54
+ - **Console scaffolding (`console/`).** React + Babel via CDN, no
55
+ build step. All 11 routes are registered; the **Settings** screen
56
+ is the first stateful one and renders the full 6-section schema
57
+ with type-aware controls, inline validation, an `OVERRIDDEN BY ENV`
58
+ chip on env-overridden rows, and an etag-mismatch reload banner.
59
+ The remaining 10 screens (Databases, Tables, SQL, Optimizer,
60
+ Security, Ingress, Health, Sync, RLM-trace, RLM-sim) render
61
+ `[ coming soon ]` placeholders — Health ships next.
62
+ - **Daemon now reads from settings.** `cluster.js` calls
63
+ `loadEffectiveConfig()` (env > file > defaults). `postgres.js`
64
+ emits `-c key=value` for every entry in `settings.postgres` and
65
+ `settings.postgres._extra`, with name regex (`^[a-z][a-z0-9_]*$`)
66
+ and scalar value validation enforced at boot — invalid GUCs are
67
+ dropped with a `logger.warn` so a typo in `_extra` doesn't crash
68
+ the daemon. Hardcoded `max_connections=1000` and the WAL
69
+ replication block (`wal_level=logical`,
70
+ `max_replication_slots=10`, `max_wal_senders=10`,
71
+ `wal_keep_size=512MB`) are now schema defaults — overridable
72
+ per-install via `autopg config set`.
73
+ - **`AUTOPG_*` env vars** as the new primary form. `PGSERVE_*` is
74
+ still honored at the daemon (one-time deprecation log per process
75
+ when `PGSERVE_*` is the only one set); `AUTOPG_*` wins on
76
+ conflict.
77
+
78
+ ### Migrated
79
+
80
+ - **`~/.pgserve/` → `~/.autopg/` (one-shot, idempotent).** On first
81
+ run, if `~/.pgserve/` exists and `~/.autopg/` does not, the contents
82
+ are copied (preserving mtimes). A `MIGRATED-FROM-PGSERVE.md` marker
83
+ is dropped in the old directory so subsequent runs skip the copy
84
+ cleanly. If both directories exist, neither is touched and
85
+ `~/.autopg/` wins. No automatic merge.
86
+
87
+ ### Notes for operators
88
+
89
+ - pm2 process name stays `pgserve`. Running `autopg install` on a
90
+ host that already has the legacy install is a no-op — pm2 sees the
91
+ same process name. Re-issue `pm2 save` if you want pm2 to persist
92
+ any settings changes through reboots.
93
+ - Local dev loop:
94
+ ```bash
95
+ bun install && npm link && autopg install && autopg ui
96
+ ```
97
+ Then edit `postgres.shared_buffers` in the UI, click Save & Restart,
98
+ and `psql -c "SHOW shared_buffers;"` reflects the new value.
99
+ - The npm package name is **not changing** — keep installing with
100
+ `npm install pgserve` (or `npx pgserve`); both `autopg` and
101
+ `pgserve` bins ship in the same tarball.
102
+
7
103
  ## 2.0.8
8
104
 
9
105
  ### Changed
package/README.md CHANGED
@@ -37,6 +37,14 @@ psql postgresql://localhost:8432/myapp
37
37
 
38
38
  > Note: v2 default is the Unix socket — see [Daemon mode](#daemon-mode). The TCP form above is the v1 compat path.
39
39
 
40
+ > **Naming.** The npm package stays `pgserve`. The CLI now also ships as
41
+ > `autopg` — both bins route to the same dispatcher. Use `autopg` for the
42
+ > new console (`autopg ui`) and configuration surface (`autopg config`,
43
+ > `autopg restart`); `pgserve <subcommand>` keeps working as a forever
44
+ > alias. Settings live at `~/.autopg/settings.json` and are migrated
45
+ > from `~/.pgserve/` automatically on first run. See
46
+ > [Console](#console-autopg-ui) and [Configuration](#configuration).
47
+
40
48
  <br>
41
49
 
42
50
  ## Features
@@ -120,9 +128,25 @@ pgserve-windows-x64.exe --data C:\pgserve-data
120
128
 
121
129
  ## CLI Reference
122
130
 
131
+ `autopg` and `pgserve` are interchangeable — every subcommand routes
132
+ through the same dispatcher. Use whichever you prefer; new examples in
133
+ this README and in `console/` use `autopg`.
134
+
123
135
  ```
124
- pgserve [options]
136
+ autopg [options] # foreground server (alias: pgserve)
137
+ autopg daemon # long-lived background daemon
138
+ autopg install [--port N] [--data P] # register pgserve under pm2
139
+ autopg uninstall # remove from pm2 (data dir kept)
140
+ autopg status # pm2 + on-disk config snapshot
141
+ autopg url | autopg port # canonical connection string / port
142
+ autopg config <list|get|set|edit|path|init> # manage ~/.autopg/settings.json
143
+ autopg restart # pm2-aware: pm2 restart pgserve, else SIGTERM+respawn
144
+ autopg ui [--port N] [--no-open] # local web console on 127.0.0.1
145
+ ```
146
+
147
+ Foreground options accepted by `autopg` / `pgserve` (no subcommand):
125
148
 
149
+ ```
126
150
  Options:
127
151
  --port <number> PostgreSQL port (default: 8432)
128
152
  --data <path> Data directory for persistence (default: in-memory)
@@ -351,6 +375,86 @@ agent state), not just for convenience.
351
375
 
352
376
  <br>
353
377
 
378
+ ## Console (`autopg ui`)
379
+
380
+ A local web console for inspecting and editing the running cluster.
381
+ Runs in-process via `node:http`, binds 127.0.0.1 only, single-user dev
382
+ tool — no auth, no TLS, never expose it.
383
+
384
+ ```bash
385
+ autopg ui # walk 8433–8533 picking the first free port
386
+ autopg ui --port 8500 # bind exactly 8500
387
+ autopg ui --no-open # skip browser launch (CI / headless)
388
+ ```
389
+
390
+ The first stateful screen — **Settings** — is functional today: it
391
+ renders the 6-section schema (server / runtime / sync / supervision /
392
+ postgres / ui), validates inline, and round-trips through
393
+ `~/.autopg/settings.json` with optimistic concurrency (sha256 etag +
394
+ `If-Match`). The other 10 screens (Databases, Tables, SQL, Optimizer,
395
+ Security, Ingress, Health, Sync, RLM-trace, RLM-sim) are scaffolded
396
+ as `[ coming soon ]` placeholders — Health ships next.
397
+
398
+ The UI shells out to the CLI for every mutation (`autopg config set`
399
+ under PUT, `autopg restart` under POST). The daemon stays untouched
400
+ — no HTTP API, no signal-based reload — so the console works even
401
+ when no daemon is running.
402
+
403
+ See [`console/README.md`](./console/README.md) for the local dev loop
404
+ and design-system source.
405
+
406
+ <br>
407
+
408
+ ## Configuration
409
+
410
+ The CLI is the source of truth. Settings live at
411
+ `~/.autopg/settings.json` (override the directory with
412
+ `AUTOPG_CONFIG_DIR`; the legacy `PGSERVE_CONFIG_DIR` is still honored
413
+ and falls back to `~/.pgserve/`). Every write is atomic, chmod 0600,
414
+ and tagged with a sha256 etag for optimistic concurrency on the UI
415
+ helper's PUT path.
416
+
417
+ Schema sections (one per `~/.autopg/settings.json` top-level key):
418
+
419
+ | Section | Purpose |
420
+ |---------|---------|
421
+ | `server` | Router port/host, backend socket, superuser credentials |
422
+ | `runtime` | Log level, auto-provision, pgvector, data dir |
423
+ | `sync` | WAL-based logical replication toggle |
424
+ | `supervision` | pm2 hardening defaults (memory, restart, kill timeout) |
425
+ | `postgres` | 15 curated GUCs (`shared_buffers`, `wal_level`, …) + `_extra` raw passthrough |
426
+ | `ui` | Console theme / phosphor / density / CRT toggle |
427
+
428
+ ```bash
429
+ autopg config init # write defaults
430
+ autopg config list # KEY VALUE SOURCE table
431
+ autopg config get postgres.shared_buffers # machine-friendly value
432
+ autopg config set postgres.shared_buffers 256MB # validates + atomic write
433
+ autopg config edit # opens $EDITOR on settings.json
434
+ autopg config path # absolute path (honors AUTOPG_CONFIG_DIR)
435
+ ```
436
+
437
+ **Precedence:** `default < file < env`. `AUTOPG_*` env vars beat
438
+ `PGSERVE_*` (the legacy form is still honored with a one-time
439
+ deprecation log per process, so existing operators keep working).
440
+ The console shows a yellow `OVERRIDDEN BY ENV` chip on rows whose
441
+ env var is currently set.
442
+
443
+ **GUC passthrough:** `postgres._extra` is a free-form `{ gucName: scalar }`
444
+ map for any PostgreSQL setting outside the curated 15. Names must match
445
+ `^[a-z][a-z0-9_]*$`; values must be string / number / boolean (no
446
+ newlines, no leading `-`). Both layers are revalidated at boot, so a
447
+ typo logs a `logger.warn` and is dropped — postgres still starts.
448
+
449
+ **One-shot migration:** on first run, if `~/.pgserve/` exists and
450
+ `~/.autopg/` does not, the contents are copied (preserving mtimes)
451
+ and a `MIGRATED-FROM-PGSERVE.md` marker is dropped in the old dir.
452
+ Idempotent — second run is a no-op.
453
+
454
+ Full schema reference: [`docs/settings-schema.md`](./docs/settings-schema.md).
455
+
456
+ <br>
457
+
354
458
  ## Compat TCP via `--listen`
355
459
 
356
460
  TCP is **off by default** in v2. Bring it back only when you need it
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * autopg wrapper — primary CLI bin name post soft-rename.
4
+ *
5
+ * autopg and pgserve route through the same dispatcher. The package
6
+ * stays published as `pgserve` on npm; this wrapper is the new
7
+ * preferred command, with `pgserve` retained as a forever alias.
8
+ *
9
+ * Implementation: delegate to pgserve-wrapper.cjs so dispatch logic
10
+ * stays single-sourced. argv[0]/argv[1] preservation is what matters
11
+ * for the inner module — node already wires argv correctly when
12
+ * require()'d at module load time, and the wrapper inspects
13
+ * process.argv directly.
14
+ */
15
+
16
+ require('./pgserve-wrapper.cjs');
@@ -29,14 +29,40 @@ const fs = require('fs');
29
29
  // sees the original `daemon` token.
30
30
  // ────────────────────────────────────────────────────────────────────────
31
31
  const __subcommand = process.argv[2];
32
- const __installSubcommands = new Set(['install', 'uninstall', 'status', 'url', 'port']);
32
+ const __installSubcommands = new Set([
33
+ 'install',
34
+ 'uninstall',
35
+ 'status',
36
+ 'url',
37
+ 'port',
38
+ // autopg-console-settings (Group 2): config / restart / ui are pure node
39
+ // wrappers that read/write `~/.autopg/settings.json` (and shell out to
40
+ // pm2 for restart). They don't need bun, so route them BEFORE the bun
41
+ // probe — same rationale as the wave-1 install commands.
42
+ 'config',
43
+ 'upgrade',
44
+ 'restart',
45
+ 'ui',
46
+ ]);
33
47
  if (__subcommand && __installSubcommands.has(__subcommand)) {
34
48
  const cli = require(path.join(__dirname, '..', 'src', 'cli-install.cjs'));
35
- process.exit(
36
- cli.dispatch(__subcommand, process.argv.slice(3), {
37
- scriptPath: path.join(__dirname, 'postgres-server.js'),
38
- }),
39
- );
49
+ const result = cli.dispatch(__subcommand, process.argv.slice(3), {
50
+ scriptPath: path.join(__dirname, 'postgres-server.js'),
51
+ wrapperPath: __filename,
52
+ });
53
+ // `ui` returns a Promise that never resolves (the server parks on
54
+ // signals). Other subcommands return a number directly. Handle both.
55
+ if (result && typeof result.then === 'function') {
56
+ result.then(
57
+ (code) => process.exit(typeof code === 'number' ? code : 0),
58
+ (err) => {
59
+ process.stderr.write(`pgserve: ${err?.message ?? err}\n`);
60
+ process.exit(1);
61
+ },
62
+ );
63
+ return;
64
+ }
65
+ process.exit(typeof result === 'number' ? result : 0);
40
66
  }
41
67
  if (__subcommand === 'serve') {
42
68
  // Alias `serve` → `daemon` so the wish's canonical command name maps
@@ -12,6 +12,7 @@ import path from 'path';
12
12
  import os from 'os';
13
13
  import { startMultiTenantServer } from '../src/index.js';
14
14
  import { startClusterServer } from '../src/cluster.js';
15
+ import { loadEffectiveConfig as loadAutopgConfig } from '../src/settings-loader.cjs';
15
16
  import {
16
17
  PgserveDaemon,
17
18
  stopDaemon,
@@ -387,8 +388,60 @@ FEATURES:
387
388
  `);
388
389
  }
389
390
 
391
+ /**
392
+ * Pull daemon options from ~/.autopg/settings.json (with env overlay).
393
+ * Returns a partial options patch — only keys that are present in the
394
+ * settings file or env override the hardcoded defaults. CLI flags layer
395
+ * on top of this in parseArgs().
396
+ *
397
+ * Failures (missing file, bad JSON) fall through to defaults silently —
398
+ * the daemon must remain runnable on a brand-new install before
399
+ * `autopg config init` has been called.
400
+ */
401
+ function loadSettingsOverlay() {
402
+ try {
403
+ const cpuCount = os.cpus().length;
404
+ const isWindows = os.platform() === 'win32';
405
+ const { settings } = loadAutopgConfig();
406
+ const s = settings.server || {};
407
+ const r = settings.runtime || {};
408
+ const sy = settings.sync || {};
409
+ const pg = settings.postgres || {};
410
+ const overlay = {};
411
+ if (typeof s.port === 'number') overlay.port = s.port;
412
+ if (typeof s.host === 'string' && s.host) overlay.host = s.host;
413
+ if (typeof r.dataDir === 'string' && r.dataDir) overlay.dataDir = r.dataDir;
414
+ if (typeof r.ramMode === 'boolean') overlay.useRam = r.ramMode;
415
+ if (typeof r.logLevel === 'string' && r.logLevel) overlay.logLevel = r.logLevel;
416
+ if (typeof r.autoProvision === 'boolean') overlay.autoProvision = r.autoProvision;
417
+ if (typeof r.cluster === 'string') {
418
+ overlay.cluster = r.cluster === 'auto'
419
+ ? (cpuCount > 1 && !isWindows)
420
+ : r.cluster === 'on';
421
+ }
422
+ if (typeof r.workers === 'number' && r.workers > 0) overlay.workers = r.workers;
423
+ if (typeof r.statsDashboard === 'boolean') overlay.showStats = r.statsDashboard;
424
+ if (typeof r.enablePgvector === 'boolean') overlay.enablePgvector = r.enablePgvector;
425
+ if (sy.enabled && typeof sy.url === 'string' && sy.url) overlay.syncTo = sy.url;
426
+ if (sy.enabled && typeof sy.databases === 'string' && sy.databases) overlay.syncDatabases = sy.databases;
427
+ // pgserve-side connection cap mirrors the postgres GUC unless the user
428
+ // has explicitly diverged via CLI flag (handled in parseArgs).
429
+ if (typeof pg.max_connections === 'number') overlay.maxConnections = pg.max_connections;
430
+ return overlay;
431
+ } catch {
432
+ // First run, no settings.json yet, or file parse error. Hardcoded
433
+ // defaults still produce a working daemon — nothing to do here.
434
+ return {};
435
+ }
436
+ }
437
+
390
438
  /**
391
439
  * Parse command line arguments
440
+ *
441
+ * Precedence (lowest → highest):
442
+ * 1. hardcoded defaults
443
+ * 2. ~/.autopg/settings.json (with env overlay via loadEffectiveConfig)
444
+ * 3. CLI flags ← explicit user intent always wins
392
445
  */
393
446
  function parseArgs() {
394
447
  // Auto-enable cluster mode on multi-core systems for best performance
@@ -412,6 +465,9 @@ function parseArgs() {
412
465
  enablePgvector: false // Auto-enable pgvector extension on new databases
413
466
  };
414
467
 
468
+ // Layer settings.json + env on top of defaults. CLI flags below win.
469
+ Object.assign(options, loadSettingsOverlay());
470
+
415
471
  for (let i = 0; i < args.length; i++) {
416
472
  const arg = args[i];
417
473
 
@@ -0,0 +1,131 @@
1
+ # `console/` — autopg console
2
+
3
+ Local web console served by `autopg ui`. React + Babel via CDN, no build
4
+ step. Single-user dev tool — binds 127.0.0.1 only, no auth, no TLS.
5
+
6
+ ## Run
7
+
8
+ ```bash
9
+ autopg ui # walks 8433–8533 picking the first free port
10
+ autopg ui --port 8500 # bind exactly 8500 or fail
11
+ autopg ui --no-open # skip browser launch (CI / headless)
12
+ ```
13
+
14
+ `pgserve ui …` is a forever alias of the same command.
15
+
16
+ The server boots in-process via `node:http` and serves this directory as
17
+ its document root. Four helper endpoints are mounted alongside the static
18
+ assets — every mutation shells out to the CLI rather than calling the
19
+ daemon directly, so the console works with or without a running daemon.
20
+
21
+ | Endpoint | Backed by |
22
+ |----------|-----------|
23
+ | `GET /api/settings` | `loadEffectiveConfig()` → `{ settings, sources, etag }` |
24
+ | `PUT /api/settings` | `writeSettings(body, { ifMatch })` (409 on stale `If-Match`) |
25
+ | `POST /api/restart` | `cli-restart.cjs` (pm2-aware) |
26
+ | `GET /api/status` | shells out to `autopg status --json` |
27
+
28
+ ## Layout
29
+
30
+ ```
31
+ console/
32
+ ├── README.md # this file
33
+ ├── index.html # entry — pinned React + Babel CDN scripts
34
+ ├── app.jsx # shell + sidebar router (11 routes)
35
+ ├── api.js # fetch wrapper, holds latest etag, surfaces ETAG_MISMATCH
36
+ ├── components.jsx # shared widgets (Seg, Toggle, Field, …)
37
+ ├── data.jsx # demo data fixtures (used by placeholder screens)
38
+ ├── tweaks-panel.jsx # theme/phosphor/density/CRT toggles (persists to settings.ui)
39
+ ├── colors_and_type.css # design tokens
40
+ ├── console.css # layout + screen styles
41
+ └── screens/
42
+ ├── settings.jsx # ✅ functional — 6-section schema editor
43
+ ├── databases.jsx # [ coming soon ]
44
+ ├── tables.jsx # [ coming soon ]
45
+ ├── sql.jsx # [ coming soon ]
46
+ ├── optimizer.jsx # [ coming soon ]
47
+ ├── security.jsx # [ coming soon ]
48
+ ├── ingress.jsx # [ coming soon ]
49
+ ├── health.jsx # [ coming soon ] — next wish
50
+ ├── sync.jsx # [ coming soon ]
51
+ ├── rlm-trace.jsx # [ coming soon ]
52
+ └── rlm-sim.jsx # [ coming soon ]
53
+ ```
54
+
55
+ ## Screen rollout
56
+
57
+ | Screen | Status | Notes |
58
+ |--------|--------|-------|
59
+ | Settings | ✅ functional | 6 sections, type-aware controls, raw GUC passthrough, etag concurrency, env-override chip |
60
+ | Health | 🟡 next | Live cluster health metrics — next wish |
61
+ | Databases | ⚪ placeholder | List + create + drop |
62
+ | Tables | ⚪ placeholder | Per-DB table inspector |
63
+ | SQL | ⚪ placeholder | Ad-hoc query runner |
64
+ | Optimizer | ⚪ placeholder | Plan inspector / GUC tuner suggestions |
65
+ | Security | ⚪ placeholder | Roles, RLS, audit log |
66
+ | Ingress | ⚪ placeholder | Listener / TLS / token surface |
67
+ | Sync | ⚪ placeholder | Replication-slot status |
68
+ | RLM-trace | ⚪ placeholder | RLM agent trace viewer (depends on rlmx) |
69
+ | RLM-sim | ⚪ placeholder | RLM scenario simulator (depends on rlmx) |
70
+
71
+ ## Local dev loop
72
+
73
+ The console is shipped as static files — no build, no bundler. Edit the
74
+ `.jsx` files in place; refresh the browser tab to pick up the change
75
+ (Babel transpiles in the browser at load time). The CDN scripts are
76
+ pinned with SRI integrity hashes — bumping React or Babel requires
77
+ re-pinning the matching `integrity="sha384-…"` attribute in
78
+ [`index.html`](./index.html).
79
+
80
+ ```bash
81
+ autopg ui --no-open --port 8500 &
82
+ open http://127.0.0.1:8500
83
+ # … edit screens/settings.jsx, refresh browser …
84
+ kill %1
85
+ ```
86
+
87
+ The Settings screen reads live state from `~/.autopg/settings.json`
88
+ through the helper endpoints, so changes survive reload and round-trip
89
+ through `autopg config get` from another shell.
90
+
91
+ ### Concurrency model
92
+
93
+ `api.js` stores the etag returned by every successful GET and sends it
94
+ back as `If-Match` on the next PUT. If a parallel `autopg config set` (or
95
+ another browser tab) drifts the file, the PUT comes back as
96
+ `409 ETAG_MISMATCH` with a fresh `currentEtag`. The Settings screen
97
+ catches this and shows a "settings changed, reload?" banner instead of
98
+ overwriting the operator's other changes.
99
+
100
+ ### Env-override chip
101
+
102
+ `GET /api/settings` returns a `sources` map (one entry per leaf:
103
+ `'default' | 'file' | 'env:<NAME>'`). Rows whose source starts with
104
+ `env:` render a yellow `OVERRIDDEN BY ENV` chip — Save still writes the
105
+ file, but `loadEffectiveConfig()` will keep returning the env value
106
+ until the env var is unset or the daemon is restarted with a clean
107
+ environment.
108
+
109
+ ## Design system
110
+
111
+ The console UI is derived from the `pgserve-console` design kit at
112
+ `namastex-design-system/ui_kits/pgserve-console`. The CSS files
113
+ (`colors_and_type.css`, `console.css`) and the shared widgets
114
+ (`components.jsx`, `tweaks-panel.jsx`) are copied verbatim — the
115
+ soft rename only touches the topbar identity (`pgserve` → `autopg`)
116
+ and the Settings screen, which was rewritten to match the
117
+ 6-section schema documented in
118
+ [`docs/settings-schema.md`](../docs/settings-schema.md).
119
+
120
+ ## What's deliberately not here
121
+
122
+ - **No build step.** Pre-bundling is a future optimization. The CDN +
123
+ Babel-in-browser path is intentional for v1 — zero infrastructure.
124
+ - **No daemon HTTP API.** The CLI is the source of truth; every UI
125
+ mutation shells out. This means the UI works ahead of `autopg
126
+ install` (you can configure before the daemon ever runs) and
127
+ cannot leak privileges through a long-lived listening socket.
128
+ - **No multi-user / multi-machine access.** 127.0.0.1 only, by
129
+ design.
130
+ - **No telemetry, no analytics.** Static page + four endpoints, all
131
+ local.
package/console/api.js ADDED
@@ -0,0 +1,173 @@
1
+ /* autopg console · API client.
2
+ *
3
+ * Wraps the four helper endpoints exposed by `autopg ui`:
4
+ * GET /api/settings → { settings, sources, etag, path }
5
+ * PUT /api/settings → { ok, etag } | { error: { code, message, field? } }
6
+ * POST /api/restart → { ok } | { error }
7
+ * GET /api/status → whatever `pgserve status --json` returns
8
+ *
9
+ * The latest etag from a successful GET is cached on the module so PUTs can
10
+ * send `If-Match` without the caller threading it through manually. PUT
11
+ * replies update the cached etag too so successive saves chain cleanly.
12
+ *
13
+ * Errors from the server come back as `{ error: { code, message, field? } }`.
14
+ * The wrapper raises a structured `ApiError` (with `.code`, `.field`,
15
+ * `.message`, `.status`, `.currentEtag?`) so screens can branch on the code
16
+ * without parsing strings. ETAG_MISMATCH is surfaced as a normal rejection
17
+ * with `error.code === 'ETAG_MISMATCH'` plus `error.currentEtag` so the
18
+ * Settings screen can show a "settings changed, reload?" banner.
19
+ */
20
+ (function (root) {
21
+ 'use strict';
22
+
23
+ const STATE = { etag: null };
24
+
25
+ class ApiError extends Error {
26
+ constructor({ code, message, field, status, currentEtag }) {
27
+ super(message || code || 'api error');
28
+ this.name = 'ApiError';
29
+ this.code = code || 'UNKNOWN';
30
+ if (field) this.field = field;
31
+ if (typeof status === 'number') this.status = status;
32
+ if (currentEtag) this.currentEtag = currentEtag;
33
+ }
34
+ }
35
+
36
+ async function parseJson(res) {
37
+ const text = await res.text();
38
+ if (!text) return {};
39
+ try {
40
+ return JSON.parse(text);
41
+ } catch {
42
+ return { _raw: text };
43
+ }
44
+ }
45
+
46
+ async function getSettings() {
47
+ const res = await fetch('/api/settings', {
48
+ method: 'GET',
49
+ headers: { accept: 'application/json' },
50
+ cache: 'no-store',
51
+ });
52
+ const body = await parseJson(res);
53
+ if (!res.ok) {
54
+ throw new ApiError({
55
+ code: body?.error?.code,
56
+ message: body?.error?.message,
57
+ field: body?.error?.field,
58
+ status: res.status,
59
+ });
60
+ }
61
+ if (body && body.etag) STATE.etag = body.etag;
62
+ return body;
63
+ }
64
+
65
+ async function putSettings(patch, { ifMatch } = {}) {
66
+ const etag = ifMatch ?? STATE.etag;
67
+ if (!etag) {
68
+ throw new ApiError({
69
+ code: 'PRECONDITION_REQUIRED',
70
+ message: 'no etag cached — call getSettings() before putSettings()',
71
+ status: 428,
72
+ });
73
+ }
74
+ const res = await fetch('/api/settings', {
75
+ method: 'PUT',
76
+ headers: {
77
+ 'content-type': 'application/json',
78
+ 'if-match': etag,
79
+ accept: 'application/json',
80
+ },
81
+ body: JSON.stringify(patch ?? {}),
82
+ });
83
+ const body = await parseJson(res);
84
+ if (res.status === 409) {
85
+ // Update the cached etag so the next reload has the latest.
86
+ if (body && body.currentEtag) STATE.etag = body.currentEtag;
87
+ throw new ApiError({
88
+ code: body?.error?.code || 'ETAG_MISMATCH',
89
+ message: body?.error?.message || 'settings changed on disk',
90
+ status: 409,
91
+ currentEtag: body?.currentEtag,
92
+ });
93
+ }
94
+ if (!res.ok) {
95
+ throw new ApiError({
96
+ code: body?.error?.code,
97
+ message: body?.error?.message,
98
+ field: body?.error?.field,
99
+ status: res.status,
100
+ });
101
+ }
102
+ if (body && body.etag) STATE.etag = body.etag;
103
+ return body;
104
+ }
105
+
106
+ async function restart() {
107
+ const res = await fetch('/api/restart', {
108
+ method: 'POST',
109
+ headers: { accept: 'application/json' },
110
+ });
111
+ const body = await parseJson(res);
112
+ if (!res.ok) {
113
+ throw new ApiError({
114
+ code: body?.error?.code,
115
+ message: body?.error?.message,
116
+ status: res.status,
117
+ });
118
+ }
119
+ return body;
120
+ }
121
+
122
+ async function getStatus() {
123
+ const res = await fetch('/api/status', {
124
+ method: 'GET',
125
+ headers: { accept: 'application/json' },
126
+ cache: 'no-store',
127
+ });
128
+ const body = await parseJson(res);
129
+ if (!res.ok) {
130
+ throw new ApiError({
131
+ code: body?.error?.code,
132
+ message: body?.error?.message,
133
+ status: res.status,
134
+ });
135
+ }
136
+ return body;
137
+ }
138
+
139
+ // Live pgserve stats — connections + databases. Always returns a body
140
+ // (status 200) even when the daemon is unreachable; check `body.ok`.
141
+ // Safe to poll from the topbar without try/catch error handling.
142
+ async function getStats() {
143
+ try {
144
+ const res = await fetch('/api/stats', {
145
+ method: 'GET',
146
+ headers: { accept: 'application/json' },
147
+ cache: 'no-store',
148
+ });
149
+ return await parseJson(res);
150
+ } catch (err) {
151
+ return { ok: false, reason: 'fetch-failed', message: err.message };
152
+ }
153
+ }
154
+
155
+ function getCachedEtag() {
156
+ return STATE.etag;
157
+ }
158
+
159
+ function setCachedEtag(etag) {
160
+ STATE.etag = etag || null;
161
+ }
162
+
163
+ root.AutopgApi = {
164
+ getSettings,
165
+ putSettings,
166
+ restart,
167
+ getStatus,
168
+ getStats,
169
+ getCachedEtag,
170
+ setCachedEtag,
171
+ ApiError,
172
+ };
173
+ })(typeof window !== 'undefined' ? window : globalThis);