pgserve 2.2.0 → 2.2.2

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 (38) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/bin/pgserve-wrapper.cjs +1 -0
  3. package/console/dist/app.js +121 -0
  4. package/console/dist/index.html +13 -0
  5. package/package.json +12 -6
  6. package/scripts/postinstall.cjs +60 -0
  7. package/scripts/test-bun-self-heal.sh +163 -0
  8. package/scripts/test-npx.sh +60 -0
  9. package/src/cli-install.cjs +14 -0
  10. package/src/cli-ui.cjs +19 -2
  11. package/src/upgrade/index.js +65 -0
  12. package/src/upgrade/runner.js +23 -0
  13. package/src/upgrade/steps/binary-cache-flush.js +67 -0
  14. package/src/upgrade/steps/consumer-signal.js +40 -0
  15. package/src/upgrade/steps/env-refresh.js +89 -0
  16. package/src/upgrade/steps/health-validate.js +53 -0
  17. package/src/upgrade/steps/plpgsql-resolve.js +66 -0
  18. package/src/upgrade/steps/port-reconcile.js +52 -0
  19. package/console/README.md +0 -131
  20. package/console/api.js +0 -173
  21. package/console/app.jsx +0 -483
  22. package/console/components.jsx +0 -167
  23. package/console/data.jsx +0 -350
  24. package/console/index.html +0 -31
  25. package/console/screens/databases.jsx +0 -5
  26. package/console/screens/health.jsx +0 -5
  27. package/console/screens/ingress.jsx +0 -5
  28. package/console/screens/optimizer.jsx +0 -5
  29. package/console/screens/rlm-sim.jsx +0 -5
  30. package/console/screens/rlm-trace.jsx +0 -5
  31. package/console/screens/security.jsx +0 -5
  32. package/console/screens/settings.jsx +0 -611
  33. package/console/screens/sql.jsx +0 -5
  34. package/console/screens/sync.jsx +0 -5
  35. package/console/screens/tables.jsx +0 -5
  36. package/console/tweaks-panel.jsx +0 -425
  37. /package/console/{colors_and_type.css → dist/colors_and_type.css} +0 -0
  38. /package/console/{console.css → dist/console.css} +0 -0
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="mdr" data-screen-label="autopg console">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>autopg · console</title>
6
+ <link rel="stylesheet" href="colors_and_type.css" />
7
+ <link rel="stylesheet" href="console.css?v=settings1" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="./app.js"></script>
12
+ </body>
13
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgserve",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Embedded PostgreSQL server with true concurrent connections - zero config, auto-provision databases",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -11,11 +11,12 @@
11
11
  "files": [
12
12
  "bin/",
13
13
  "src/",
14
- "console/",
14
+ "console/dist/",
15
15
  "README.md",
16
16
  "CHANGELOG.md",
17
17
  "LICENSE",
18
- "SECURITY.md"
18
+ "SECURITY.md",
19
+ "scripts/"
19
20
  ],
20
21
  "scripts": {
21
22
  "bench": "bun tests/benchmarks/runner.js",
@@ -25,13 +26,16 @@
25
26
  "start": "bun bin/postgres-server.js",
26
27
  "build": "bun build --compile bin/postgres-server.js --outfile dist/pgserve",
27
28
  "build:all": "make build-all",
29
+ "console:build": "bun build console/src/main.jsx --target browser --minify --define 'process.env.NODE_ENV=\"production\"' --outfile console/dist/app.js && cp console/src/index.html console/dist/index.html && cp console/src/*.css console/dist/",
30
+ "console:dev": "bun build console/src/main.jsx --target browser --define 'process.env.NODE_ENV=\"development\"' --watch --outfile console/dist/app.js",
28
31
  "lint": "eslint src/ bin/",
29
32
  "lint:fix": "eslint src/ bin/ --fix",
30
33
  "deadcode": "knip",
31
34
  "test:npx": "scripts/test-npx.sh",
32
35
  "test:bun-self-heal": "scripts/test-bun-self-heal.sh",
33
- "prepublishOnly": "npm run lint && npm run deadcode && npm run test:npx && npm run test:bun-self-heal",
34
- "prepare": "husky"
36
+ "prepublishOnly": "npm run console:build && npm run lint && npm run deadcode && npm run test:npx && npm run test:bun-self-heal",
37
+ "prepare": "husky",
38
+ "postinstall": "node scripts/postinstall.cjs"
35
39
  },
36
40
  "keywords": [
37
41
  "postgresql",
@@ -74,6 +78,8 @@
74
78
  ]
75
79
  },
76
80
  "dependencies": {
77
- "bun": "^1.3.4"
81
+ "bun": "^1.3.4",
82
+ "react": "^18.3.1",
83
+ "react-dom": "^18.3.1"
78
84
  }
79
85
  }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * autopg postinstall — auto-runs `autopg upgrade` on detected upgrade.
4
+ *
5
+ * Behavior:
6
+ * - Fresh install (no ~/.autopg/data/) → exit 0 silently (no upgrade needed)
7
+ * - Upgrade install (data dir exists) → invoke `autopg upgrade --quiet`
8
+ * - Soft-fail: any error logs warning, exits 0 (never breaks `bun install`)
9
+ * - Skip override: AUTOPG_SKIP_POSTINSTALL=1 → exit 0 immediately
10
+ *
11
+ * The escape hatch for forced re-runs is `autopg upgrade` (manual).
12
+ *
13
+ * See: .genie/wishes/autopg-upgrade-command/WISH.md
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { spawnSync } = require('node:child_process');
19
+
20
+ function getAutopgRoot() {
21
+ return process.env.AUTOPG_CONFIG_DIR || process.env.PGSERVE_CONFIG_DIR || `${process.env.HOME}/.autopg`;
22
+ }
23
+
24
+ function main() {
25
+ if (process.env.AUTOPG_SKIP_POSTINSTALL === '1') {
26
+ return;
27
+ }
28
+ const dataDir = path.join(getAutopgRoot(), 'data');
29
+ if (!fs.existsSync(dataDir)) {
30
+ // Fresh install — nothing to upgrade
31
+ return;
32
+ }
33
+ // Locate own CLI entry — script is run from the package dir at install time
34
+ const cliEntry = path.join(__dirname, '..', 'bin', 'pgserve-wrapper.cjs');
35
+ if (!fs.existsSync(cliEntry)) {
36
+ process.stderr.write(`[autopg-postinstall] wrapper not found at ${cliEntry}, skipping\n`);
37
+ return;
38
+ }
39
+ const result = spawnSync(process.execPath, [cliEntry, 'upgrade', '--quiet'], {
40
+ stdio: ['ignore', 'inherit', 'inherit'],
41
+ timeout: 60_000,
42
+ });
43
+ if (result.error) {
44
+ process.stderr.write(`[autopg-postinstall] WARNING: upgrade invocation failed: ${result.error.message}\n`);
45
+ process.stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to retry.\n');
46
+ return;
47
+ }
48
+ if (result.status !== 0) {
49
+ process.stderr.write(`[autopg-postinstall] WARNING: \`autopg upgrade\` exited ${result.status}\n`);
50
+ process.stderr.write('[autopg-postinstall] Run `autopg upgrade` manually to investigate.\n');
51
+ }
52
+ }
53
+
54
+ try {
55
+ main();
56
+ } catch (err) {
57
+ process.stderr.write(`[autopg-postinstall] WARNING: unexpected error: ${err.message}\n`);
58
+ }
59
+
60
+ process.exit(0);
@@ -0,0 +1,163 @@
1
+ #!/bin/bash
2
+ # Regression test for https://github.com/namastexlabs/pgserve/issues/22
3
+ #
4
+ # When pgserve is installed via `bun install`, the nested `bun` npm package's
5
+ # postinstall can be skipped, leaving @oven/bun-<platform>/bin/bun empty.
6
+ # The bun stub then refuses to run with "Bun's postinstall script was not run".
7
+ # pgserve-wrapper.cjs must detect this and self-heal via `node install.js`.
8
+ #
9
+ # This test stages a synthetic broken install tree, runs the wrapper, and
10
+ # asserts that it recovers and spawns postgres-server.
11
+
12
+ set -e
13
+
14
+ REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
15
+ WRAPPER="$REPO_ROOT/bin/pgserve-wrapper.cjs"
16
+
17
+ if [ ! -f "$WRAPPER" ]; then
18
+ echo "✗ wrapper not found: $WRAPPER"
19
+ exit 1
20
+ fi
21
+
22
+ # Use a real bun binary as the "recovered" payload so the healthy-path
23
+ # assertion is meaningful. Falls back to any bun on PATH.
24
+ REAL_BUN="${BUN_BIN:-$(command -v bun || true)}"
25
+ if [ -z "$REAL_BUN" ] || [ ! -x "$REAL_BUN" ]; then
26
+ echo "✗ bun runtime not found on PATH (set BUN_BIN to override)"
27
+ exit 1
28
+ fi
29
+
30
+ FIXTURE=$(mktemp -d)
31
+ trap "rm -rf $FIXTURE" EXIT
32
+
33
+ mkdir -p "$FIXTURE/node_modules/bun/bin"
34
+ mkdir -p "$FIXTURE/node_modules/@oven/bun-linux-x64/bin" # empty, simulating the bug
35
+ mkdir -p "$FIXTURE/node_modules/.bin"
36
+ mkdir -p "$FIXTURE/node_modules/pgserve/bin"
37
+
38
+ cp "$WRAPPER" "$FIXTURE/node_modules/pgserve/bin/pgserve-wrapper.cjs"
39
+
40
+ # Stub postgres-server so we can detect a successful spawn without needing
41
+ # postgres binaries in the fixture.
42
+ cat > "$FIXTURE/node_modules/pgserve/bin/postgres-server.js" <<'EOF'
43
+ console.log("postgres-server-spawned");
44
+ process.exit(0);
45
+ EOF
46
+
47
+ # Fake bun install.js: copies the real bun into the expected @oven location,
48
+ # mirroring what the real postinstall does.
49
+ cat > "$FIXTURE/node_modules/bun/install.js" <<EOF
50
+ const fs = require('fs');
51
+ const path = require('path');
52
+ const dst = path.resolve(__dirname, '..', '@oven', 'bun-linux-x64', 'bin', 'bun');
53
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
54
+ fs.copyFileSync('$REAL_BUN', dst);
55
+ fs.chmodSync(dst, 0o755);
56
+ console.log('[test] install.js populated', dst);
57
+ EOF
58
+ echo '{"name":"bun","version":"1.3.12"}' > "$FIXTURE/node_modules/bun/package.json"
59
+
60
+ # Broken bun stub: prints the postinstall error unless the @oven binary exists.
61
+ cat > "$FIXTURE/node_modules/bun/bin/bun" <<'EOF'
62
+ #!/bin/sh
63
+ SELF=$(readlink -f "$0")
64
+ TARGET="$(dirname "$SELF")/../../@oven/bun-linux-x64/bin/bun"
65
+ if [ ! -x "$TARGET" ]; then
66
+ echo "Error: Bun's postinstall script was not run." >&2
67
+ echo "" >&2
68
+ echo "To fix this, run the postinstall script manually:" >&2
69
+ echo " cd node_modules/bun && node install.js" >&2
70
+ exit 1
71
+ fi
72
+ exec "$TARGET" "$@"
73
+ EOF
74
+ chmod +x "$FIXTURE/node_modules/bun/bin/bun"
75
+
76
+ ln -s ../bun/bin/bun "$FIXTURE/node_modules/.bin/bun"
77
+
78
+ echo "=== Testing self-heal on broken install ==="
79
+ OUTPUT=$(node "$FIXTURE/node_modules/pgserve/bin/pgserve-wrapper.cjs" 2>&1)
80
+ EXIT=$?
81
+
82
+ if [ $EXIT -ne 0 ]; then
83
+ echo "✗ wrapper exited non-zero: $EXIT"
84
+ echo "$OUTPUT"
85
+ exit 1
86
+ fi
87
+
88
+ if ! echo "$OUTPUT" | grep -q "attempting self-heal"; then
89
+ echo "✗ wrapper did not attempt self-heal"
90
+ echo "$OUTPUT"
91
+ exit 1
92
+ fi
93
+
94
+ if ! echo "$OUTPUT" | grep -q "bun runtime recovered"; then
95
+ echo "✗ wrapper did not report recovery"
96
+ echo "$OUTPUT"
97
+ exit 1
98
+ fi
99
+
100
+ if ! echo "$OUTPUT" | grep -q "postgres-server-spawned"; then
101
+ echo "✗ postgres-server was not spawned after self-heal"
102
+ echo "$OUTPUT"
103
+ exit 1
104
+ fi
105
+
106
+ echo "✓ self-heal path: wrapper detected, repaired, and spawned postgres-server"
107
+
108
+ echo ""
109
+ echo "=== Testing healthy path is unaffected ==="
110
+ OUTPUT=$(node "$FIXTURE/node_modules/pgserve/bin/pgserve-wrapper.cjs" 2>&1)
111
+ EXIT=$?
112
+
113
+ if [ $EXIT -ne 0 ]; then
114
+ echo "✗ wrapper exited non-zero on healthy path: $EXIT"
115
+ echo "$OUTPUT"
116
+ exit 1
117
+ fi
118
+
119
+ if echo "$OUTPUT" | grep -q "self-heal\|recovered"; then
120
+ echo "✗ wrapper logged self-heal messages on a healthy install"
121
+ echo "$OUTPUT"
122
+ exit 1
123
+ fi
124
+
125
+ if ! echo "$OUTPUT" | grep -q "postgres-server-spawned"; then
126
+ echo "✗ postgres-server was not spawned on healthy path"
127
+ echo "$OUTPUT"
128
+ exit 1
129
+ fi
130
+
131
+ echo "✓ healthy path: wrapper was silent and spawned postgres-server directly"
132
+
133
+ echo ""
134
+ echo "=== Testing non-postinstall errors surface raw ==="
135
+ # Replace stub with one that emits an unrelated error.
136
+ cat > "$FIXTURE/node_modules/bun/bin/bun" <<'EOF'
137
+ #!/bin/sh
138
+ echo "Error: GLIBC_2.99 not found (libc mismatch)" >&2
139
+ exit 127
140
+ EOF
141
+ chmod +x "$FIXTURE/node_modules/bun/bin/bun"
142
+
143
+ # Clear the @oven healed binary so the stub is what runs.
144
+ rm -f "$FIXTURE/node_modules/@oven/bun-linux-x64/bin/bun"
145
+
146
+ OUTPUT=$(node "$FIXTURE/node_modules/pgserve/bin/pgserve-wrapper.cjs" 2>&1 || true)
147
+
148
+ if echo "$OUTPUT" | grep -q "self-heal"; then
149
+ echo "✗ wrapper tried self-heal for a non-postinstall error"
150
+ echo "$OUTPUT"
151
+ exit 1
152
+ fi
153
+
154
+ if ! echo "$OUTPUT" | grep -q "GLIBC_2.99"; then
155
+ echo "✗ wrapper did not surface the real error message"
156
+ echo "$OUTPUT"
157
+ exit 1
158
+ fi
159
+
160
+ echo "✓ unrelated-error path: wrapper surfaced the raw error without self-heal"
161
+
162
+ echo ""
163
+ echo "=== bun self-heal test PASSED ==="
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # Test that the package works with npx (simulates fresh user install)
3
+ # This catches path resolution issues that static analysis can't detect
4
+
5
+ set -e
6
+
7
+ echo "=== Testing npx compatibility ==="
8
+
9
+ # Create temp directory
10
+ TEST_DIR=$(mktemp -d)
11
+ trap "rm -rf $TEST_DIR" EXIT
12
+
13
+ # Pack the current package
14
+ echo "Packing package..."
15
+ PACK_OUTPUT=$(npm pack --pack-destination "$TEST_DIR" 2>&1)
16
+ PACK_FILE=$(echo "$PACK_OUTPUT" | grep -E '\.tgz$' | tail -1)
17
+
18
+ # If npm pack fails, exit with an error
19
+ if [ -z "$PACK_FILE" ] || [ ! -f "$TEST_DIR/$PACK_FILE" ]; then
20
+ echo "✗ Failed to pack package with npm"
21
+ echo "Pack output: $PACK_OUTPUT"
22
+ exit 1
23
+ fi
24
+ echo "Packed: $PACK_FILE"
25
+
26
+ # Install in isolated environment using npm
27
+ echo "Installing in isolated environment..."
28
+ cd "$TEST_DIR"
29
+ echo '{"name":"test-npx-install","private":true}' > package.json
30
+ npm install "./$PACK_FILE" > /dev/null 2>&1
31
+
32
+ # Test that it starts (with timeout)
33
+ echo "Testing server startup via npx..."
34
+ timeout 30 npx pgserve --no-cluster --port 15432 > output.log 2>&1 &
35
+ PID=$!
36
+
37
+ # Wait for ready signal (Server started successfully!)
38
+ for i in {1..60}; do
39
+ if grep -q "Server started successfully" output.log 2>/dev/null; then
40
+ echo "✓ Server started successfully via npx"
41
+ kill $PID 2>/dev/null || true
42
+ wait $PID 2>/dev/null || true
43
+ echo "=== npx test PASSED ==="
44
+ exit 0
45
+ fi
46
+ if ! kill -0 $PID 2>/dev/null; then
47
+ echo "✗ Server exited unexpectedly"
48
+ cat output.log
49
+ echo "=== npx test FAILED ==="
50
+ exit 1
51
+ fi
52
+ sleep 0.5
53
+ done
54
+
55
+ # Timeout
56
+ kill $PID 2>/dev/null || true
57
+ echo "✗ Server did not start within timeout"
58
+ cat output.log
59
+ echo "=== npx test FAILED ==="
60
+ exit 1
@@ -483,6 +483,20 @@ function dispatch(subcommand, args, ctx) {
483
483
  return cmdUrl();
484
484
  case 'port':
485
485
  return cmdPort();
486
+ case 'upgrade': {
487
+ const opts = {
488
+ quiet: args.includes('--quiet'),
489
+ dryRun: args.includes('--dry-run'),
490
+ skipSteps: (() => {
491
+ const idx = args.indexOf('--skip-steps');
492
+ if (idx === -1) return [];
493
+ return (args[idx + 1] || '').split(',').filter(Boolean);
494
+ })(),
495
+ };
496
+ return import(require('node:path').join(__dirname, 'upgrade', 'index.js'))
497
+ .then((mod) => mod.upgrade(opts))
498
+ .then((r) => process.exit(r.ok ? 0 : 1));
499
+ }
486
500
  case 'config': {
487
501
  const cfg = require('./cli-config.cjs');
488
502
  const [sub, ...rest] = args;
package/src/cli-ui.cjs CHANGED
@@ -142,8 +142,25 @@ function listenWithFallback(server, host, preferredPort) {
142
142
  * via npm the `files` allowlist preserves the layout.
143
143
  */
144
144
  function resolveConsoleRoot() {
145
- // src/ repo root console/
146
- return path.resolve(__dirname, '..', 'console');
145
+ // After autopg-console-dist (v2.2.2): the SPA ships pre-bundled in
146
+ // console/dist/ instead of as flat .jsx files at console/. Prefer dist/;
147
+ // fall back to console/src/ for repo-checkout dev mode (where dist/ is
148
+ // gitignored and only built on demand).
149
+ const consoleParent = path.resolve(__dirname, '..', 'console');
150
+ const distRoot = path.join(consoleParent, 'dist');
151
+ if (fs.existsSync(distRoot)) return distRoot;
152
+
153
+ const srcRoot = path.join(consoleParent, 'src');
154
+ if (fs.existsSync(srcRoot)) {
155
+ process.stderr.write(
156
+ 'autopg ui: running unbuilt sources from console/src/ — run `bun run console:build` for production behavior\n',
157
+ );
158
+ return srcRoot;
159
+ }
160
+
161
+ throw new Error(
162
+ 'console assets not found: expected console/dist/ (run `bun run console:build`) or console/src/ (repo checkout)',
163
+ );
147
164
  }
148
165
 
149
166
  /**
@@ -0,0 +1,65 @@
1
+ /**
2
+ * autopg upgrade — idempotent migration orchestrator.
3
+ *
4
+ * Runs 6 steps in order, each safe to re-run any number of times:
5
+ * 1. port-reconcile — ensure pgserve listens on canonical port (8432)
6
+ * 2. binary-cache-flush — verify binary version matches PINNED_PG_VERSION
7
+ * 3. plpgsql-resolve — DROP+CREATE plpgsql per DB to refresh .so path
8
+ * 4. env-refresh — regenerate ~/.autopg/<app>.env URLs
9
+ * 5. consumer-signal — touch ~/.autopg/state/upgrade.signal
10
+ * 6. health-validate — pg_isready + per-DB plpgsql smoke test
11
+ *
12
+ * Patches the upgrade-path hole left by autopg-v22 partial roll-out.
13
+ * See: .genie/wishes/autopg-upgrade-command/WISH.md
14
+ */
15
+
16
+ import { runStep } from './runner.js';
17
+ import * as portReconcile from './steps/port-reconcile.js';
18
+ import * as binaryCacheFlush from './steps/binary-cache-flush.js';
19
+ import * as plpgsqlResolve from './steps/plpgsql-resolve.js';
20
+ import * as envRefresh from './steps/env-refresh.js';
21
+ import * as consumerSignal from './steps/consumer-signal.js';
22
+ import * as healthValidate from './steps/health-validate.js';
23
+
24
+ export const STEPS = [
25
+ { name: 'port-reconcile', impl: portReconcile },
26
+ { name: 'binary-cache-flush', impl: binaryCacheFlush },
27
+ { name: 'plpgsql-resolve', impl: plpgsqlResolve },
28
+ { name: 'env-refresh', impl: envRefresh },
29
+ { name: 'consumer-signal', impl: consumerSignal },
30
+ { name: 'health-validate', impl: healthValidate },
31
+ ];
32
+
33
+ export async function upgrade(options = {}) {
34
+ const { quiet = false, dryRun = false, skipSteps = [] } = options;
35
+ const log = (msg) => { if (!quiet) process.stderr.write(`${msg}\n`); };
36
+ const warn = (msg) => process.stderr.write(`${msg}\n`);
37
+
38
+ log(`autopg upgrade starting (dryRun=${dryRun}, quiet=${quiet})`);
39
+
40
+ const results = [];
41
+ for (const step of STEPS) {
42
+ if (skipSteps.includes(step.name)) {
43
+ log(`[${step.name}] SKIP (excluded by --skip-steps)`);
44
+ results.push({ name: step.name, status: 'SKIP', detail: 'excluded' });
45
+ continue;
46
+ }
47
+ try {
48
+ const result = await runStep(step.name, step.impl, { dryRun, log, warn });
49
+ results.push(result);
50
+ } catch (err) {
51
+ warn(`[${step.name}] FAIL: ${err.message}`);
52
+ results.push({ name: step.name, status: 'FAIL', detail: err.message });
53
+ }
54
+ }
55
+
56
+ const failed = results.filter((r) => r.status === 'FAIL');
57
+ const summary = `autopg upgrade complete: ${results.length - failed.length}/${results.length} steps OK`;
58
+ log(summary);
59
+ if (failed.length > 0) {
60
+ warn(`Failed steps: ${failed.map((r) => r.name).join(', ')}`);
61
+ warn('Re-run `autopg upgrade` after addressing the above.');
62
+ return { ok: false, results, summary };
63
+ }
64
+ return { ok: true, results, summary };
65
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Step runner — wraps each step with consistent logging + error capture.
3
+ *
4
+ * A step module exports `{ name, plan(ctx), execute(ctx) }`:
5
+ * - plan(ctx) → string describing what would happen (dry-run output)
6
+ * - execute(ctx) → { status: 'OK'|'SKIP'|'FAIL', detail: string }
7
+ */
8
+
9
+ export async function runStep(name, stepImpl, { dryRun, log, warn }) {
10
+ if (typeof stepImpl.plan !== 'function' || typeof stepImpl.execute !== 'function') {
11
+ throw new Error(`step ${name} missing plan() or execute()`);
12
+ }
13
+ if (dryRun) {
14
+ const planned = await stepImpl.plan({ log, warn });
15
+ log(`[${name}] DRY-RUN: ${planned}`);
16
+ return { name, status: 'DRY-RUN', detail: planned };
17
+ }
18
+ const result = await stepImpl.execute({ log, warn });
19
+ const status = result.status || 'OK';
20
+ const detail = result.detail || '';
21
+ log(`[${name}] ${status}: ${detail}`);
22
+ return { name, status, detail };
23
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Step 2 — Binary cache flush against PINNED_PG_VERSION.
3
+ * Re-downloads if version marker missing or mismatch. Idempotent.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ export const name = 'binary-cache-flush';
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ function getAutopgRoot() {
14
+ return process.env.AUTOPG_CONFIG_DIR || process.env.PGSERVE_CONFIG_DIR || `${process.env.HOME}/.autopg`;
15
+ }
16
+
17
+ function getBinaryCacheDir() {
18
+ return path.join(getAutopgRoot(), 'bin', `${process.platform}-${process.arch}`);
19
+ }
20
+
21
+ function getPinnedVersion() {
22
+ try {
23
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf8'));
24
+ if (pkg.autopg && pkg.autopg.pinnedPgVersion) return pkg.autopg.pinnedPgVersion;
25
+ } catch { /* fall through */ }
26
+ return process.env.AUTOPG_PINNED_PG_VERSION || '18.3';
27
+ }
28
+
29
+ function readVersionMarker(cacheDir) {
30
+ try { return fs.readFileSync(path.join(cacheDir, '.version'), 'utf8').trim(); } catch { return null; }
31
+ }
32
+
33
+ export async function plan() {
34
+ const cacheDir = getBinaryCacheDir();
35
+ const pinned = getPinnedVersion();
36
+ const marker = readVersionMarker(cacheDir);
37
+ if (!fs.existsSync(path.join(cacheDir, 'bin', 'postgres'))) {
38
+ return `binary missing at ${cacheDir} — would trigger download for PG ${pinned}`;
39
+ }
40
+ if (marker !== pinned) return `version drift (cached=${marker || 'unknown'}, pinned=${pinned}) — would re-download`;
41
+ return `binary present and matches pinned ${pinned}, no action needed`;
42
+ }
43
+
44
+ export async function execute({ log, warn }) {
45
+ const cacheDir = getBinaryCacheDir();
46
+ const pinned = getPinnedVersion();
47
+ const marker = readVersionMarker(cacheDir);
48
+ const binaryExists = fs.existsSync(path.join(cacheDir, 'bin', 'postgres'));
49
+
50
+ if (binaryExists && marker === pinned) return { status: 'SKIP', detail: `binary OK (PG ${pinned})` };
51
+
52
+ let downloadFn;
53
+ try {
54
+ const postgres = await import('../../postgres.js');
55
+ downloadFn = postgres.ensureBinary || postgres.downloadBinary || postgres.installBinary;
56
+ } catch { /* postgres module not loadable here */ }
57
+
58
+ if (!downloadFn) {
59
+ warn(`binary needs refresh (pinned=${pinned}, cached=${marker || 'missing'}) but autopg postgres module not exposing download API`);
60
+ warn(`operator action: rerun \`bun install -g @automagik/autopg@latest\``);
61
+ return { status: 'FAIL', detail: 'binary refresh needs autopg npm reinstall' };
62
+ }
63
+ log(`re-downloading PG ${pinned} into ${cacheDir}`);
64
+ await downloadFn({ version: pinned, targetDir: cacheDir });
65
+ fs.writeFileSync(path.join(cacheDir, '.version'), pinned, { mode: 0o644 });
66
+ return { status: 'OK', detail: `binary refreshed to PG ${pinned}` };
67
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Step 5 — Consumer reconnect signal. Touches ~/.autopg/state/upgrade.signal
3
+ * with epoch + autopg version. Consumers (omni-api, genie-serve) opt-in via fs.watch.
4
+ */
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ export const name = 'consumer-signal';
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ function getAutopgRoot() {
14
+ return process.env.AUTOPG_CONFIG_DIR || process.env.PGSERVE_CONFIG_DIR || `${process.env.HOME}/.autopg`;
15
+ }
16
+
17
+ function getAutopgVersion() {
18
+ try {
19
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'package.json'), 'utf8'));
20
+ return pkg.version || 'unknown';
21
+ } catch { return 'unknown'; }
22
+ }
23
+
24
+ export async function plan() {
25
+ return `would write upgrade signal at ${path.join(getAutopgRoot(), 'state', 'upgrade.signal')}`;
26
+ }
27
+
28
+ export async function execute() {
29
+ const signalDir = path.join(getAutopgRoot(), 'state');
30
+ fs.mkdirSync(signalDir, { recursive: true });
31
+ const payload = {
32
+ timestamp: new Date().toISOString(),
33
+ epoch_ms: Date.now(),
34
+ autopg_version: getAutopgVersion(),
35
+ canonical_port: 8432,
36
+ };
37
+ const signalPath = path.join(signalDir, 'upgrade.signal');
38
+ fs.writeFileSync(signalPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o644 });
39
+ return { status: 'OK', detail: `signal written at ${signalPath}` };
40
+ }