pgserve 2.1.3 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +105 -1
- package/bin/autopg-wrapper.cjs +16 -0
- package/bin/pgserve-wrapper.cjs +32 -6
- package/bin/postgres-server.js +56 -0
- package/console/README.md +131 -0
- package/console/api.js +173 -0
- package/console/app.jsx +483 -0
- package/console/colors_and_type.css +227 -0
- package/console/components.jsx +167 -0
- package/console/console.css +1666 -0
- package/console/data.jsx +350 -0
- package/console/index.html +31 -0
- package/console/screens/databases.jsx +5 -0
- package/console/screens/health.jsx +5 -0
- package/console/screens/ingress.jsx +5 -0
- package/console/screens/optimizer.jsx +5 -0
- package/console/screens/rlm-sim.jsx +5 -0
- package/console/screens/rlm-trace.jsx +5 -0
- package/console/screens/security.jsx +5 -0
- package/console/screens/settings.jsx +611 -0
- package/console/screens/sql.jsx +5 -0
- package/console/screens/sync.jsx +5 -0
- package/console/screens/tables.jsx +5 -0
- package/console/tweaks-panel.jsx +425 -0
- package/package.json +14 -2
- package/scripts/postinstall.cjs +60 -0
- package/src/cli-config.cjs +310 -0
- package/src/cli-install.cjs +112 -11
- package/src/cli-restart.cjs +228 -0
- package/src/cli-ui.cjs +580 -0
- package/src/cluster.js +43 -38
- package/src/postgres.js +141 -19
- package/src/settings-loader.cjs +235 -0
- package/src/settings-migrate.cjs +212 -0
- package/src/settings-pg-args.cjs +146 -0
- package/src/settings-schema.cjs +422 -0
- package/src/settings-validator.cjs +416 -0
- package/src/settings-writer.cjs +288 -0
- package/src/upgrade/index.js +65 -0
- package/src/upgrade/runner.js +23 -0
- package/src/upgrade/steps/binary-cache-flush.js +67 -0
- package/src/upgrade/steps/consumer-signal.js +40 -0
- package/src/upgrade/steps/env-refresh.js +89 -0
- package/src/upgrade/steps/health-validate.js +53 -0
- package/src/upgrade/steps/plpgsql-resolve.js +66 -0
- package/src/upgrade/steps/port-reconcile.js +52 -0
- package/.claude/context/windows-debug.md +0 -119
- package/.genie/AGENTS.md +0 -15
- package/.genie/agents/README.md +0 -110
- package/.genie/agents/analyze.md +0 -176
- package/.genie/agents/forge.md +0 -290
- package/.genie/agents/garbage-cleaner.md +0 -324
- package/.genie/agents/garbage-collector.md +0 -596
- package/.genie/agents/github-issue-gc.md +0 -618
- package/.genie/agents/review.md +0 -380
- package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
- package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
- package/.genie/agents/semantic-analyzer.md +0 -101
- package/.genie/agents/update.md +0 -182
- package/.genie/agents/wish.md +0 -357
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
- package/.genie/code/AGENTS.md +0 -694
- package/.genie/code/agents/audit/risk.md +0 -173
- package/.genie/code/agents/audit/security.md +0 -189
- package/.genie/code/agents/audit.md +0 -145
- package/.genie/code/agents/challenge.md +0 -230
- package/.genie/code/agents/change-reviewer.md +0 -295
- package/.genie/code/agents/code-garbage-collector.md +0 -425
- package/.genie/code/agents/code-quality.md +0 -410
- package/.genie/code/agents/commit-suggester.md +0 -255
- package/.genie/code/agents/commit.md +0 -124
- package/.genie/code/agents/consensus.md +0 -204
- package/.genie/code/agents/daily-standup.md +0 -722
- package/.genie/code/agents/docgen.md +0 -48
- package/.genie/code/agents/explore.md +0 -79
- package/.genie/code/agents/fix.md +0 -100
- package/.genie/code/agents/git/commit-advisory.md +0 -219
- package/.genie/code/agents/git/workflows/issue.md +0 -244
- package/.genie/code/agents/git/workflows/pr.md +0 -179
- package/.genie/code/agents/git/workflows/release.md +0 -460
- package/.genie/code/agents/git/workflows/report.md +0 -342
- package/.genie/code/agents/git.md +0 -432
- package/.genie/code/agents/implementor.md +0 -161
- package/.genie/code/agents/install.md +0 -515
- package/.genie/code/agents/issue-creator.md +0 -344
- package/.genie/code/agents/polish.md +0 -116
- package/.genie/code/agents/qa.md +0 -653
- package/.genie/code/agents/refactor.md +0 -294
- package/.genie/code/agents/release.md +0 -1129
- package/.genie/code/agents/roadmap.md +0 -885
- package/.genie/code/agents/tests.md +0 -557
- package/.genie/code/agents/tracer.md +0 -50
- package/.genie/code/agents/update/upstream-update.md +0 -85
- package/.genie/code/agents/update/versions/generic-update.md +0 -305
- package/.genie/code/agents/vibe.md +0 -1317
- package/.genie/code/spells/agent-configuration.md +0 -58
- package/.genie/code/spells/automated-rc-publishing.md +0 -106
- package/.genie/code/spells/branch-tracker-guidance.md +0 -28
- package/.genie/code/spells/debug.md +0 -320
- package/.genie/code/spells/emoji-naming-convention.md +0 -303
- package/.genie/code/spells/evidence-storage.md +0 -26
- package/.genie/code/spells/file-naming-rules.md +0 -35
- package/.genie/code/spells/forge-code-blueprints.md +0 -195
- package/.genie/code/spells/genie-integration.md +0 -153
- package/.genie/code/spells/publishing-protocol.md +0 -61
- package/.genie/code/spells/team-consultation-protocol.md +0 -284
- package/.genie/code/spells/tool-requirements.md +0 -20
- package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
- package/.genie/code/teams/tech-council/council.md +0 -328
- package/.genie/code/teams/tech-council/jt.md +0 -352
- package/.genie/code/teams/tech-council/nayr.md +0 -305
- package/.genie/code/teams/tech-council/oettam.md +0 -375
- package/.genie/neurons/README.md +0 -193
- package/.genie/neurons/forge.md +0 -106
- package/.genie/neurons/genie.md +0 -63
- package/.genie/neurons/review.md +0 -106
- package/.genie/neurons/wish.md +0 -104
- package/.genie/product/README.md +0 -20
- package/.genie/product/cli-automation.md +0 -359
- package/.genie/product/environment.md +0 -60
- package/.genie/product/mission.md +0 -60
- package/.genie/product/roadmap.md +0 -44
- package/.genie/product/tech-stack.md +0 -34
- package/.genie/product/templates/context-template.md +0 -218
- package/.genie/product/templates/qa-done-report-template.md +0 -68
- package/.genie/product/templates/review-report-template.md +0 -89
- package/.genie/product/templates/wish-template.md +0 -120
- package/.genie/scripts/helpers/analyze-commit.js +0 -195
- package/.genie/scripts/helpers/bullet-counter.js +0 -194
- package/.genie/scripts/helpers/bullet-find.js +0 -289
- package/.genie/scripts/helpers/bullet-id.js +0 -244
- package/.genie/scripts/helpers/check-secrets.js +0 -237
- package/.genie/scripts/helpers/count-tokens.js +0 -200
- package/.genie/scripts/helpers/create-frontmatter.js +0 -456
- package/.genie/scripts/helpers/detect-markers.js +0 -293
- package/.genie/scripts/helpers/detect-todos.js +0 -267
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
- package/.genie/scripts/helpers/embeddings.js +0 -344
- package/.genie/scripts/helpers/find-empty-sections.js +0 -158
- package/.genie/scripts/helpers/index.js +0 -319
- package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
- package/.genie/scripts/helpers/validate-links.js +0 -207
- package/.genie/scripts/helpers/validate-paths.js +0 -373
- package/.genie/spells/README.md +0 -9
- package/.genie/spells/ace-protocol.md +0 -118
- package/.genie/spells/ask-one-at-a-time.md +0 -175
- package/.genie/spells/backup-analyzer.md +0 -542
- package/.genie/spells/blocker.md +0 -12
- package/.genie/spells/break-things-move-fast.md +0 -56
- package/.genie/spells/context-candidates.md +0 -72
- package/.genie/spells/context-critic.md +0 -51
- package/.genie/spells/defer-to-expertise.md +0 -278
- package/.genie/spells/delegate-dont-do.md +0 -292
- package/.genie/spells/error-investigation-protocol.md +0 -328
- package/.genie/spells/evidence-based-completion.md +0 -273
- package/.genie/spells/experiment.md +0 -65
- package/.genie/spells/file-creation-protocol.md +0 -229
- package/.genie/spells/forge-integration.md +0 -281
- package/.genie/spells/forge-orchestration.md +0 -514
- package/.genie/spells/gather-context.md +0 -18
- package/.genie/spells/global-health-check.md +0 -34
- package/.genie/spells/global-noop-roundtrip.md +0 -25
- package/.genie/spells/install-genie.md +0 -1232
- package/.genie/spells/install.md +0 -82
- package/.genie/spells/investigate-before-commit.md +0 -112
- package/.genie/spells/know-yourself.md +0 -288
- package/.genie/spells/learn.md +0 -828
- package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
- package/.genie/spells/mcp-first.md +0 -124
- package/.genie/spells/multi-step-execution.md +0 -67
- package/.genie/spells/orchestration-boundary-protocol.md +0 -256
- package/.genie/spells/orchestrator-not-implementor.md +0 -189
- package/.genie/spells/prompt.md +0 -746
- package/.genie/spells/reflect.md +0 -404
- package/.genie/spells/routing-decision-matrix.md +0 -368
- package/.genie/spells/run-in-parallel.md +0 -12
- package/.genie/spells/session-state-updater-example.md +0 -196
- package/.genie/spells/session-state-updater.md +0 -220
- package/.genie/spells/track-long-running-tasks.md +0 -133
- package/.genie/spells/troubleshoot-infrastructure.md +0 -176
- package/.genie/spells/upgrade-genie.md +0 -415
- package/.genie/spells/url-presentation-protocol.md +0 -301
- package/.genie/spells/wish-initiation.md +0 -158
- package/.genie/spells/wish-issue-linkage.md +0 -410
- package/.genie/spells/wish-lifecycle.md +0 -100
- package/.genie/state/provider-status.json +0 -3
- package/.genie/state/version.json +0 -16
- package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
- package/.genie/wishes/pgserve-v2/WISH.md +0 -442
- package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
- package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
- package/.gitguardian.yaml +0 -29
- package/.gitguardianignore +0 -16
- package/.github/workflows/ci.yml +0 -122
- package/.github/workflows/release.yml +0 -289
- package/.github/workflows/version.yml +0 -228
- package/.husky/pre-commit +0 -2
- package/AGENTS.md +0 -433
- package/CLAUDE.md +0 -1
- package/Makefile +0 -285
- package/assets/icon.ico +0 -0
- package/bun.lock +0 -435
- package/bunfig.toml +0 -28
- package/ecosystem.config.cjs +0 -23
- package/eslint.config.js +0 -63
- package/examples/multi-tenant-demo.js +0 -104
- package/install.sh +0 -123
- package/knip.json +0 -9
- package/tests/audit.test.js +0 -189
- package/tests/backpressure.test.js +0 -167
- package/tests/benchmarks/runner.js +0 -1197
- package/tests/benchmarks/vector-generator.js +0 -368
- package/tests/cli-install.test.js +0 -322
- package/tests/control-db.test.js +0 -285
- package/tests/daemon-args.test.js +0 -86
- package/tests/daemon-control.test.js +0 -171
- package/tests/daemon-fingerprint-integration.test.js +0 -111
- package/tests/daemon-pr24-regression.test.js +0 -198
- package/tests/fingerprint.test.js +0 -263
- package/tests/fixtures/240-orphan-seed.sql +0 -30
- package/tests/multi-tenant.test.js +0 -374
- package/tests/orphan-cleanup.test.js +0 -390
- package/tests/pg-version-regex.test.js +0 -129
- package/tests/quick-bench.js +0 -135
- package/tests/router-handshake-retry.test.js +0 -119
- package/tests/router-handshake-watchdog.test.js +0 -110
- package/tests/sdk.test.js +0 -71
- package/tests/stale-postmaster-pid.test.js +0 -85
- package/tests/stress-test.js +0 -439
- package/tests/sync-perf-test.js +0 -150
- package/tests/tcp-listen.test.js +0 -368
- package/tests/tenancy.test.js +0 -403
- package/tests/wrapper-supervision.test.js +0 -107
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Multi-Tenant Router Demo
|
|
3
|
-
*
|
|
4
|
-
* Shows how to use the new multi-tenant architecture
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { startMultiTenantServer } from '../src/index.js';
|
|
8
|
-
import pg from 'pg';
|
|
9
|
-
|
|
10
|
-
const { Client } = pg;
|
|
11
|
-
|
|
12
|
-
async function demo() {
|
|
13
|
-
console.log('🚀 Starting multi-tenant router demo...\n');
|
|
14
|
-
|
|
15
|
-
// Start multi-tenant router
|
|
16
|
-
const router = await startMultiTenantServer({
|
|
17
|
-
port: 15432,
|
|
18
|
-
baseDir: './demo-data',
|
|
19
|
-
logLevel: 'info'
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
console.log('\n📊 Initial stats:');
|
|
23
|
-
console.log(JSON.stringify(router.getStats(), null, 2));
|
|
24
|
-
|
|
25
|
-
// Connect to database "user123" (auto-created)
|
|
26
|
-
console.log('\n📥 Connecting to database: user123');
|
|
27
|
-
const client1 = new Client({
|
|
28
|
-
host: '127.0.0.1',
|
|
29
|
-
port: 15432,
|
|
30
|
-
database: 'user123'
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
await client1.connect();
|
|
34
|
-
console.log('✅ Connected to user123');
|
|
35
|
-
|
|
36
|
-
// Create table and insert data
|
|
37
|
-
await client1.query('CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)');
|
|
38
|
-
await client1.query("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");
|
|
39
|
-
|
|
40
|
-
const result1 = await client1.query('SELECT * FROM users');
|
|
41
|
-
console.log('📋 user123 data:', result1.rows);
|
|
42
|
-
|
|
43
|
-
await client1.end();
|
|
44
|
-
console.log('🔌 Disconnected from user123');
|
|
45
|
-
|
|
46
|
-
// Connect to database "app456" (auto-created)
|
|
47
|
-
console.log('\n📥 Connecting to database: app456');
|
|
48
|
-
const client2 = new Client({
|
|
49
|
-
host: '127.0.0.1',
|
|
50
|
-
port: 15432,
|
|
51
|
-
database: 'app456'
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await client2.connect();
|
|
55
|
-
console.log('✅ Connected to app456');
|
|
56
|
-
|
|
57
|
-
// Different schema in different database
|
|
58
|
-
await client2.query('CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT)');
|
|
59
|
-
await client2.query("INSERT INTO posts (title) VALUES ('Hello World'), ('Multi-tenant magic')");
|
|
60
|
-
|
|
61
|
-
const result2 = await client2.query('SELECT * FROM posts');
|
|
62
|
-
console.log('📋 app456 data:', result2.rows);
|
|
63
|
-
|
|
64
|
-
await client2.end();
|
|
65
|
-
console.log('🔌 Disconnected from app456');
|
|
66
|
-
|
|
67
|
-
// Show final stats
|
|
68
|
-
console.log('\n📊 Final stats:');
|
|
69
|
-
console.log(JSON.stringify(router.getStats(), null, 2));
|
|
70
|
-
|
|
71
|
-
console.log('\n📋 All databases:');
|
|
72
|
-
console.log(JSON.stringify(router.listDatabases(), null, 2));
|
|
73
|
-
|
|
74
|
-
// Reconnect to user123 to verify data persists
|
|
75
|
-
console.log('\n🔄 Reconnecting to user123 to verify data persists...');
|
|
76
|
-
const client1Again = new Client({
|
|
77
|
-
host: '127.0.0.1',
|
|
78
|
-
port: 15432,
|
|
79
|
-
database: 'user123'
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await client1Again.connect();
|
|
83
|
-
const persistedData = await client1Again.query('SELECT * FROM users');
|
|
84
|
-
console.log('✅ Persisted data in user123:', persistedData.rows);
|
|
85
|
-
|
|
86
|
-
await client1Again.end();
|
|
87
|
-
|
|
88
|
-
// Stop router
|
|
89
|
-
console.log('\n🛑 Stopping router...');
|
|
90
|
-
await router.stop();
|
|
91
|
-
|
|
92
|
-
console.log('\n✅ Demo complete!');
|
|
93
|
-
console.log('\n🎯 Key achievements:');
|
|
94
|
-
console.log(' • Single port (15432) handled multiple databases');
|
|
95
|
-
console.log(' • Auto-provisioned user123 and app456');
|
|
96
|
-
console.log(' • Data isolated between databases');
|
|
97
|
-
console.log(' • Data persisted across reconnections');
|
|
98
|
-
console.log(' • Zero configuration required!');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
demo().catch((error) => {
|
|
102
|
-
console.error('❌ Demo failed:', error);
|
|
103
|
-
process.exit(1);
|
|
104
|
-
});
|
package/install.sh
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# ============================================================================
|
|
3
|
-
# pgserve — Canonical PostgreSQL backbone installer
|
|
4
|
-
#
|
|
5
|
-
# Bootstraps a single shared pgserve instance under pm2 supervision. Used as
|
|
6
|
-
# a prerequisite by `omni/install.sh` and `genie/install.sh` so every
|
|
7
|
-
# automagik service on a host points at the same Postgres.
|
|
8
|
-
#
|
|
9
|
-
# Usage:
|
|
10
|
-
# curl -fsSL https://raw.githubusercontent.com/namastexlabs/pgserve/main/install.sh | bash
|
|
11
|
-
#
|
|
12
|
-
# With pinned version:
|
|
13
|
-
# PGSERVE_VERSION=^2.1.1 curl -fsSL .../install.sh | bash
|
|
14
|
-
#
|
|
15
|
-
# Local checkout:
|
|
16
|
-
# bash install.sh
|
|
17
|
-
#
|
|
18
|
-
# Idempotent — re-running is a no-op success when pgserve is already
|
|
19
|
-
# registered under pm2 with a healthy entry.
|
|
20
|
-
# ============================================================================
|
|
21
|
-
set -euo pipefail
|
|
22
|
-
|
|
23
|
-
PGSERVE_VERSION="${PGSERVE_VERSION:-^2.1.0}"
|
|
24
|
-
|
|
25
|
-
# Colors (no-op when stdout isn't a tty)
|
|
26
|
-
if [[ -t 1 ]]; then
|
|
27
|
-
RED='\033[0;31m'
|
|
28
|
-
GREEN='\033[0;32m'
|
|
29
|
-
YELLOW='\033[1;33m'
|
|
30
|
-
BLUE='\033[0;34m'
|
|
31
|
-
CYAN='\033[0;36m'
|
|
32
|
-
BOLD='\033[1m'
|
|
33
|
-
NC='\033[0m'
|
|
34
|
-
else
|
|
35
|
-
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
info() { printf "${BLUE}ℹ${NC} %s\n" "$*"; }
|
|
39
|
-
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
|
|
40
|
-
warn() { printf "${YELLOW}⚠${NC} %s\n" "$*"; }
|
|
41
|
-
fail() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
|
|
42
|
-
step() { printf "\n${BOLD}${CYAN}▸ %s${NC}\n" "$*"; }
|
|
43
|
-
|
|
44
|
-
has_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
45
|
-
|
|
46
|
-
# ============================================================================
|
|
47
|
-
# Prerequisites: bun + pm2
|
|
48
|
-
# ============================================================================
|
|
49
|
-
|
|
50
|
-
ensure_bun() {
|
|
51
|
-
if has_cmd bun; then
|
|
52
|
-
ok "bun $(bun --version 2>/dev/null || echo '?')"
|
|
53
|
-
return 0
|
|
54
|
-
fi
|
|
55
|
-
info "Installing bun (https://bun.sh)..."
|
|
56
|
-
curl -fsSL https://bun.sh/install | bash >/dev/null 2>&1 || fail "bun install failed — see https://bun.sh"
|
|
57
|
-
# Make bun available to the rest of this script without requiring a re-login.
|
|
58
|
-
export PATH="$HOME/.bun/bin:$PATH"
|
|
59
|
-
has_cmd bun || fail "bun installed but not on PATH — restart your shell and re-run."
|
|
60
|
-
ok "bun $(bun --version)"
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
ensure_pm2() {
|
|
64
|
-
if has_cmd pm2; then
|
|
65
|
-
ok "pm2 $(pm2 --version 2>/dev/null || echo '?')"
|
|
66
|
-
return 0
|
|
67
|
-
fi
|
|
68
|
-
info "Installing pm2 (process supervisor)..."
|
|
69
|
-
bun add -g pm2 >/dev/null 2>&1 || fail "pm2 install failed — try: bun add -g pm2"
|
|
70
|
-
has_cmd pm2 || fail "pm2 installed but not on PATH — restart your shell and re-run."
|
|
71
|
-
ok "pm2 installed"
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# ============================================================================
|
|
75
|
-
# pgserve binary + pm2 registration
|
|
76
|
-
# ============================================================================
|
|
77
|
-
|
|
78
|
-
ensure_pgserve_binary() {
|
|
79
|
-
# Probe via `pgserve port` (real subcommand). `pgserve --version` doesn't
|
|
80
|
-
# exist in 2.1.x — using it would false-negative and trigger a redundant
|
|
81
|
-
# reinstall every time install.sh runs.
|
|
82
|
-
if has_cmd pgserve && pgserve port >/dev/null 2>&1; then
|
|
83
|
-
ok "pgserve binary present (port $(pgserve port 2>/dev/null))"
|
|
84
|
-
return 0
|
|
85
|
-
fi
|
|
86
|
-
info "Installing pgserve@${PGSERVE_VERSION} globally..."
|
|
87
|
-
bun add -g "pgserve@${PGSERVE_VERSION}" >/dev/null 2>&1 \
|
|
88
|
-
|| fail "pgserve install failed — try: bun add -g pgserve@${PGSERVE_VERSION}"
|
|
89
|
-
has_cmd pgserve || fail "pgserve installed but not on PATH — restart your shell and re-run."
|
|
90
|
-
ok "pgserve $(pgserve port 2>/dev/null || echo '?')"
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
register_pgserve_pm2() {
|
|
94
|
-
info "Registering pgserve under pm2 (idempotent)..."
|
|
95
|
-
# `pgserve install` prints its own success/already-installed line and exits
|
|
96
|
-
# 0 in both cases. We pipe stderr through so any pm2 errors surface to the
|
|
97
|
-
# operator (the pm2-6.x --min-uptime breakage we hit on 2026-04-30 was
|
|
98
|
-
# invisible because stderr was being captured).
|
|
99
|
-
pgserve install || fail "pgserve install failed — see ~/.pgserve/logs/pgserve-error.log"
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
# ============================================================================
|
|
103
|
-
# Main
|
|
104
|
-
# ============================================================================
|
|
105
|
-
|
|
106
|
-
main() {
|
|
107
|
-
step "Installing canonical pgserve"
|
|
108
|
-
ensure_bun
|
|
109
|
-
ensure_pm2
|
|
110
|
-
ensure_pgserve_binary
|
|
111
|
-
register_pgserve_pm2
|
|
112
|
-
|
|
113
|
-
echo ""
|
|
114
|
-
ok "Canonical pgserve ready"
|
|
115
|
-
info "URL: $(pgserve url 2>/dev/null || echo '<run: pgserve url>')"
|
|
116
|
-
info "Port: $(pgserve port 2>/dev/null || echo '?')"
|
|
117
|
-
info "Logs: ~/.pgserve/logs/"
|
|
118
|
-
echo ""
|
|
119
|
-
info "Other automagik services on this host (omni, genie, ...) will share this pgserve."
|
|
120
|
-
echo ""
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
main "$@"
|
package/knip.json
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://unpkg.com/knip@5/schema.json",
|
|
3
|
-
"entry": ["src/index.js", "bin/postgres-server.js", "bin/pgserve-wrapper.cjs"],
|
|
4
|
-
"project": ["src/**/*.js", "bin/**/*.js", "bin/**/*.cjs"],
|
|
5
|
-
"ignore": ["tests/**", "helpers/**", "scripts/**"],
|
|
6
|
-
"ignoreBinaries": ["scripts/test-npx.sh", "scripts/test-bun-self-heal.sh", "make"],
|
|
7
|
-
"ignoreDependencies": ["bun"],
|
|
8
|
-
"ignoreExportsUsedInFile": true
|
|
9
|
-
}
|
package/tests/audit.test.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for src/audit.js — JSONL writer with rotation + syslog target.
|
|
3
|
-
*
|
|
4
|
-
* Tests use temp dirs under /tmp; nothing touches the user's real
|
|
5
|
-
* `~/.pgserve/audit.log`. The syslog test stubs `logger` via PATH so we
|
|
6
|
-
* don't depend on (or pollute) the host's syslog daemon.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { test, expect, beforeEach, afterEach } from 'bun:test';
|
|
10
|
-
import fs from 'fs';
|
|
11
|
-
import os from 'os';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import {
|
|
14
|
-
audit,
|
|
15
|
-
configureAudit,
|
|
16
|
-
readAuditTarget,
|
|
17
|
-
AUDIT_EVENTS,
|
|
18
|
-
_internals,
|
|
19
|
-
} from '../src/audit.js';
|
|
20
|
-
|
|
21
|
-
let scratchDir;
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
scratchDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgserve-audit-test-'));
|
|
25
|
-
configureAudit({
|
|
26
|
-
logFile: path.join(scratchDir, 'audit.log'),
|
|
27
|
-
target: 'file',
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
try {
|
|
33
|
-
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
34
|
-
} catch { /* noop */ }
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('audit() appends a JSON line per event', () => {
|
|
38
|
-
audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'abc123def456', db: 'app_demo_abc123def456' });
|
|
39
|
-
audit(AUDIT_EVENTS.CONNECTION_ROUTED, { fingerprint: 'abc123def456', peer_pid: 1234 });
|
|
40
|
-
|
|
41
|
-
const logFile = path.join(scratchDir, 'audit.log');
|
|
42
|
-
const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n');
|
|
43
|
-
expect(lines.length).toBe(2);
|
|
44
|
-
const r1 = JSON.parse(lines[0]);
|
|
45
|
-
expect(r1.event).toBe('db_created');
|
|
46
|
-
expect(r1.fingerprint).toBe('abc123def456');
|
|
47
|
-
expect(typeof r1.ts).toBe('string');
|
|
48
|
-
expect(new Date(r1.ts).toString()).not.toBe('Invalid Date');
|
|
49
|
-
|
|
50
|
-
const r2 = JSON.parse(lines[1]);
|
|
51
|
-
expect(r2.event).toBe('connection_routed');
|
|
52
|
-
expect(r2.peer_pid).toBe(1234);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('audit() refuses unknown events', () => {
|
|
56
|
-
expect(() => audit('definitely_not_a_real_event', {})).toThrow(/unknown event/);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('audit() creates the parent directory if missing', () => {
|
|
60
|
-
const nested = path.join(scratchDir, 'nested', 'sub', 'audit.log');
|
|
61
|
-
audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'a'.repeat(12) }, { logFile: nested });
|
|
62
|
-
expect(fs.existsSync(nested)).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('all v2.0 event names are exported (incl. Group 6 tcp_*)', () => {
|
|
66
|
-
expect(Object.values(AUDIT_EVENTS).sort()).toEqual([
|
|
67
|
-
'connection_denied_fingerprint_mismatch',
|
|
68
|
-
'connection_routed',
|
|
69
|
-
'db_created',
|
|
70
|
-
'db_persist_honored',
|
|
71
|
-
'db_reaped_liveness',
|
|
72
|
-
'db_reaped_ttl',
|
|
73
|
-
'enforcement_kill_switch_used',
|
|
74
|
-
'tcp_token_denied',
|
|
75
|
-
'tcp_token_issued',
|
|
76
|
-
'tcp_token_used',
|
|
77
|
-
]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('rotation kicks in once existing file crosses 50 MB', () => {
|
|
81
|
-
const logFile = path.join(scratchDir, 'audit.log');
|
|
82
|
-
// Use a sparse file to simulate a 50 MB log without writing 50 MB.
|
|
83
|
-
const fd = fs.openSync(logFile, 'w');
|
|
84
|
-
fs.ftruncateSync(fd, _internals.ROTATE_THRESHOLD_BYTES);
|
|
85
|
-
fs.closeSync(fd);
|
|
86
|
-
|
|
87
|
-
audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'r'.repeat(12) });
|
|
88
|
-
|
|
89
|
-
// Original file rotated to .1, fresh file holds the new line.
|
|
90
|
-
expect(fs.existsSync(`${logFile}.1`)).toBe(true);
|
|
91
|
-
const fresh = fs.readFileSync(logFile, 'utf8');
|
|
92
|
-
expect(fresh.trim().split('\n').length).toBe(1);
|
|
93
|
-
expect(JSON.parse(fresh.trim()).event).toBe('db_created');
|
|
94
|
-
|
|
95
|
-
// The rotated file is the original 50 MB sparse file.
|
|
96
|
-
expect(fs.statSync(`${logFile}.1`).size).toBe(_internals.ROTATE_THRESHOLD_BYTES);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('rotation cascades up to KEEP files and drops the eldest', () => {
|
|
100
|
-
const logFile = path.join(scratchDir, 'audit.log');
|
|
101
|
-
// Pre-populate audit.log.1 ... audit.log.5 with distinct markers.
|
|
102
|
-
for (let i = 1; i <= _internals.ROTATE_KEEP; i++) {
|
|
103
|
-
fs.writeFileSync(`${logFile}.${i}`, `slot-${i}\n`);
|
|
104
|
-
}
|
|
105
|
-
// And the live audit.log just under threshold.
|
|
106
|
-
const fd = fs.openSync(logFile, 'w');
|
|
107
|
-
fs.ftruncateSync(fd, _internals.ROTATE_THRESHOLD_BYTES);
|
|
108
|
-
fs.closeSync(fd);
|
|
109
|
-
|
|
110
|
-
audit(AUDIT_EVENTS.DB_CREATED, { fingerprint: 'q'.repeat(12) });
|
|
111
|
-
|
|
112
|
-
// .5 (was "slot-5") dropped; .4 → .5; .3 → .4; .2 → .3; .1 → .2; live → .1.
|
|
113
|
-
expect(fs.readFileSync(`${logFile}.5`, 'utf8').trim()).toBe('slot-4');
|
|
114
|
-
expect(fs.readFileSync(`${logFile}.4`, 'utf8').trim()).toBe('slot-3');
|
|
115
|
-
expect(fs.readFileSync(`${logFile}.3`, 'utf8').trim()).toBe('slot-2');
|
|
116
|
-
expect(fs.readFileSync(`${logFile}.2`, 'utf8').trim()).toBe('slot-1');
|
|
117
|
-
expect(fs.statSync(`${logFile}.1`).size).toBe(_internals.ROTATE_THRESHOLD_BYTES);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('audit({target:"syslog"}) spawns logger -t pgserve-audit', async () => {
|
|
121
|
-
// Stub `logger` by prepending a temp shim to PATH.
|
|
122
|
-
const shimDir = path.join(scratchDir, 'shim');
|
|
123
|
-
fs.mkdirSync(shimDir, { recursive: true });
|
|
124
|
-
const marker = path.join(scratchDir, 'logger-calls.txt');
|
|
125
|
-
const shimPath = path.join(shimDir, 'logger');
|
|
126
|
-
fs.writeFileSync(
|
|
127
|
-
shimPath,
|
|
128
|
-
`#!/usr/bin/env bash
|
|
129
|
-
# Capture argv to a marker file so the test can verify the spawn.
|
|
130
|
-
printf '%s\\n' "$*" >> "${marker}"
|
|
131
|
-
`,
|
|
132
|
-
{ mode: 0o755 },
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const oldPath = process.env.PATH;
|
|
136
|
-
process.env.PATH = `${shimDir}:${oldPath}`;
|
|
137
|
-
try {
|
|
138
|
-
audit(
|
|
139
|
-
AUDIT_EVENTS.CONNECTION_ROUTED,
|
|
140
|
-
{ fingerprint: 's'.repeat(12) },
|
|
141
|
-
{ target: 'syslog' },
|
|
142
|
-
);
|
|
143
|
-
// logger is spawned async; poll briefly for the marker.
|
|
144
|
-
const deadline = Date.now() + 2000;
|
|
145
|
-
while (!fs.existsSync(marker) && Date.now() < deadline) {
|
|
146
|
-
await new Promise(r => setTimeout(r, 25));
|
|
147
|
-
}
|
|
148
|
-
expect(fs.existsSync(marker)).toBe(true);
|
|
149
|
-
const contents = fs.readFileSync(marker, 'utf8');
|
|
150
|
-
expect(contents).toContain('-t pgserve-audit');
|
|
151
|
-
expect(contents).toContain('"event":"connection_routed"');
|
|
152
|
-
} finally {
|
|
153
|
-
process.env.PATH = oldPath;
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('audit({target:"syslog"}) swallows missing logger binary', () => {
|
|
158
|
-
// Point PATH at an empty dir → `logger` cannot be found → no throw.
|
|
159
|
-
const empty = path.join(scratchDir, 'empty');
|
|
160
|
-
fs.mkdirSync(empty);
|
|
161
|
-
const oldPath = process.env.PATH;
|
|
162
|
-
process.env.PATH = empty;
|
|
163
|
-
try {
|
|
164
|
-
expect(() =>
|
|
165
|
-
audit(
|
|
166
|
-
AUDIT_EVENTS.CONNECTION_ROUTED,
|
|
167
|
-
{ fingerprint: 'z'.repeat(12) },
|
|
168
|
-
{ target: 'syslog' },
|
|
169
|
-
),
|
|
170
|
-
).not.toThrow();
|
|
171
|
-
} finally {
|
|
172
|
-
process.env.PATH = oldPath;
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test('readAuditTarget reads pgserve.audit.target from package.json', () => {
|
|
177
|
-
const pkgFile = path.join(scratchDir, 'package.json');
|
|
178
|
-
fs.writeFileSync(
|
|
179
|
-
pkgFile,
|
|
180
|
-
JSON.stringify({ name: 'demo', pgserve: { audit: { target: 'syslog' } } }),
|
|
181
|
-
);
|
|
182
|
-
expect(readAuditTarget(pkgFile)).toBe('syslog');
|
|
183
|
-
|
|
184
|
-
fs.writeFileSync(pkgFile, JSON.stringify({ name: 'demo' }));
|
|
185
|
-
expect(readAuditTarget(pkgFile)).toBe('file');
|
|
186
|
-
|
|
187
|
-
// Missing file → file (default).
|
|
188
|
-
expect(readAuditTarget(path.join(scratchDir, 'missing.json'))).toBe('file');
|
|
189
|
-
});
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backpressure / Large Message Regression Tests
|
|
3
|
-
*
|
|
4
|
-
* Reproduces the deadlock from issue #14: TCP proxy drops bytes when
|
|
5
|
-
* socket buffers are full, causing PostgreSQL to wait forever for the
|
|
6
|
-
* remainder of a truncated wire protocol message.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { startMultiTenantServer } from '../src/index.js';
|
|
10
|
-
import pg from 'pg';
|
|
11
|
-
import { test, expect } from 'bun:test';
|
|
12
|
-
import fs from 'fs';
|
|
13
|
-
|
|
14
|
-
const { Client } = pg;
|
|
15
|
-
|
|
16
|
-
const TEST_PORT = 15433;
|
|
17
|
-
const testDataDir = './test-data-backpressure';
|
|
18
|
-
|
|
19
|
-
function cleanup() {
|
|
20
|
-
if (fs.existsSync(testDataDir)) {
|
|
21
|
-
fs.rmSync(testDataDir, { recursive: true, force: true });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Create a connected pg.Client */
|
|
26
|
-
async function connect(dbName) {
|
|
27
|
-
const client = new Client({
|
|
28
|
-
host: '127.0.0.1',
|
|
29
|
-
port: TEST_PORT,
|
|
30
|
-
database: dbName,
|
|
31
|
-
user: 'postgres',
|
|
32
|
-
password: 'postgres',
|
|
33
|
-
});
|
|
34
|
-
await client.connect();
|
|
35
|
-
return client;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
test('Large INSERT (~360KB payload) does not deadlock', async () => {
|
|
39
|
-
cleanup();
|
|
40
|
-
const router = await startMultiTenantServer({
|
|
41
|
-
port: TEST_PORT,
|
|
42
|
-
baseDir: testDataDir,
|
|
43
|
-
logLevel: 'warn',
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
let client;
|
|
47
|
-
try {
|
|
48
|
-
client = await connect('bp_insert');
|
|
49
|
-
await client.query('CREATE TABLE big (id SERIAL PRIMARY KEY, payload TEXT)');
|
|
50
|
-
|
|
51
|
-
// ~360KB of text — exceeds typical socket buffer size
|
|
52
|
-
const bigPayload = 'x'.repeat(360_000);
|
|
53
|
-
await client.query('INSERT INTO big (payload) VALUES ($1)', [bigPayload]);
|
|
54
|
-
|
|
55
|
-
const res = await client.query('SELECT length(payload) AS len FROM big');
|
|
56
|
-
expect(Number(res.rows[0].len)).toBe(360000);
|
|
57
|
-
} finally {
|
|
58
|
-
if (client) await client.end().catch(() => {});
|
|
59
|
-
await router.stop();
|
|
60
|
-
cleanup();
|
|
61
|
-
}
|
|
62
|
-
}, 30_000);
|
|
63
|
-
|
|
64
|
-
test('Large SELECT result (500KB+) does not deadlock', async () => {
|
|
65
|
-
cleanup();
|
|
66
|
-
const router = await startMultiTenantServer({
|
|
67
|
-
port: TEST_PORT,
|
|
68
|
-
baseDir: testDataDir,
|
|
69
|
-
logLevel: 'warn',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
let client;
|
|
73
|
-
try {
|
|
74
|
-
client = await connect('bp_select');
|
|
75
|
-
await client.query('CREATE TABLE chunks (id SERIAL PRIMARY KEY, data TEXT)');
|
|
76
|
-
|
|
77
|
-
// Insert many rows that sum to >500KB
|
|
78
|
-
const chunkSize = 10_000;
|
|
79
|
-
const numChunks = 60; // 60 * 10KB = 600KB total
|
|
80
|
-
const chunk = 'y'.repeat(chunkSize);
|
|
81
|
-
|
|
82
|
-
for (let i = 0; i < numChunks; i++) {
|
|
83
|
-
await client.query('INSERT INTO chunks (data) VALUES ($1)', [chunk]);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Fetch all rows in a single result set (PG→Client backpressure)
|
|
87
|
-
const res = await client.query('SELECT * FROM chunks');
|
|
88
|
-
expect(res.rows.length).toBe(numChunks);
|
|
89
|
-
expect(res.rows[0].data.length).toBe(chunkSize);
|
|
90
|
-
} finally {
|
|
91
|
-
if (client) await client.end().catch(() => {});
|
|
92
|
-
await router.stop();
|
|
93
|
-
cleanup();
|
|
94
|
-
}
|
|
95
|
-
}, 30_000);
|
|
96
|
-
|
|
97
|
-
test('Large single query with multi-value INSERT (500KB+)', async () => {
|
|
98
|
-
cleanup();
|
|
99
|
-
const router = await startMultiTenantServer({
|
|
100
|
-
port: TEST_PORT,
|
|
101
|
-
baseDir: testDataDir,
|
|
102
|
-
logLevel: 'warn',
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
let client;
|
|
106
|
-
try {
|
|
107
|
-
client = await connect('bp_multivalue');
|
|
108
|
-
await client.query('CREATE TABLE items (id INT, val TEXT)');
|
|
109
|
-
|
|
110
|
-
// Build a single INSERT with many value tuples to produce a large wire message
|
|
111
|
-
const rowCount = 500;
|
|
112
|
-
const rowValue = 'z'.repeat(1_000); // 1KB per row → ~500KB total
|
|
113
|
-
const values = [];
|
|
114
|
-
const params = [];
|
|
115
|
-
for (let i = 0; i < rowCount; i++) {
|
|
116
|
-
values.push(`($${i * 2 + 1}, $${i * 2 + 2})`);
|
|
117
|
-
params.push(i, rowValue);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const sql = `INSERT INTO items (id, val) VALUES ${values.join(', ')}`;
|
|
121
|
-
await client.query(sql, params);
|
|
122
|
-
|
|
123
|
-
const res = await client.query('SELECT count(*)::int AS cnt FROM items');
|
|
124
|
-
expect(res.rows[0].cnt).toBe(rowCount);
|
|
125
|
-
} finally {
|
|
126
|
-
if (client) await client.end().catch(() => {});
|
|
127
|
-
await router.stop();
|
|
128
|
-
cleanup();
|
|
129
|
-
}
|
|
130
|
-
}, 30_000);
|
|
131
|
-
|
|
132
|
-
test('Concurrent large operations (5 clients x 300KB)', async () => {
|
|
133
|
-
cleanup();
|
|
134
|
-
const router = await startMultiTenantServer({
|
|
135
|
-
port: TEST_PORT,
|
|
136
|
-
baseDir: testDataDir,
|
|
137
|
-
logLevel: 'warn',
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const numClients = 5;
|
|
142
|
-
const payloadSize = 300_000;
|
|
143
|
-
const payload = 'c'.repeat(payloadSize);
|
|
144
|
-
|
|
145
|
-
// Run all clients concurrently
|
|
146
|
-
const results = await Promise.all(
|
|
147
|
-
Array.from({ length: numClients }, async (_, i) => {
|
|
148
|
-
const dbName = `bp_concurrent_${i}`;
|
|
149
|
-
const client = await connect(dbName);
|
|
150
|
-
await client.query('CREATE TABLE stress (id SERIAL PRIMARY KEY, data TEXT)');
|
|
151
|
-
await client.query('INSERT INTO stress (data) VALUES ($1)', [payload]);
|
|
152
|
-
|
|
153
|
-
const res = await client.query('SELECT length(data) AS len FROM stress');
|
|
154
|
-
await client.end();
|
|
155
|
-
return parseInt(res.rows[0].len, 10);
|
|
156
|
-
})
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
// All clients should have successfully stored the full payload
|
|
160
|
-
for (const len of results) {
|
|
161
|
-
expect(len).toBe(payloadSize);
|
|
162
|
-
}
|
|
163
|
-
} finally {
|
|
164
|
-
await router.stop();
|
|
165
|
-
cleanup();
|
|
166
|
-
}
|
|
167
|
-
}, 60_000);
|