pgserve 2.1.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +105 -1
- package/bin/autopg-wrapper.cjs +16 -0
- package/bin/pgserve-wrapper.cjs +31 -6
- package/bin/postgres-server.js +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 +11 -1
- package/src/cli-config.cjs +310 -0
- package/src/cli-install.cjs +98 -11
- package/src/cli-restart.cjs +228 -0
- package/src/cli-ui.cjs +580 -0
- package/src/cluster.js +43 -38
- package/src/postgres.js +141 -19
- package/src/settings-loader.cjs +235 -0
- package/src/settings-migrate.cjs +212 -0
- package/src/settings-pg-args.cjs +146 -0
- package/src/settings-schema.cjs +422 -0
- package/src/settings-validator.cjs +416 -0
- package/src/settings-writer.cjs +288 -0
- package/.claude/context/windows-debug.md +0 -119
- package/.genie/AGENTS.md +0 -15
- package/.genie/agents/README.md +0 -110
- package/.genie/agents/analyze.md +0 -176
- package/.genie/agents/forge.md +0 -290
- package/.genie/agents/garbage-cleaner.md +0 -324
- package/.genie/agents/garbage-collector.md +0 -596
- package/.genie/agents/github-issue-gc.md +0 -618
- package/.genie/agents/review.md +0 -380
- package/.genie/agents/semantic-analyzer/find-duplicates.md +0 -90
- package/.genie/agents/semantic-analyzer/find-orphans.md +0 -99
- package/.genie/agents/semantic-analyzer.md +0 -101
- package/.genie/agents/update.md +0 -182
- package/.genie/agents/wish.md +0 -357
- package/.genie/brainstorms/pgserve-v2/DESIGN.md +0 -174
- package/.genie/code/AGENTS.md +0 -694
- package/.genie/code/agents/audit/risk.md +0 -173
- package/.genie/code/agents/audit/security.md +0 -189
- package/.genie/code/agents/audit.md +0 -145
- package/.genie/code/agents/challenge.md +0 -230
- package/.genie/code/agents/change-reviewer.md +0 -295
- package/.genie/code/agents/code-garbage-collector.md +0 -425
- package/.genie/code/agents/code-quality.md +0 -410
- package/.genie/code/agents/commit-suggester.md +0 -255
- package/.genie/code/agents/commit.md +0 -124
- package/.genie/code/agents/consensus.md +0 -204
- package/.genie/code/agents/daily-standup.md +0 -722
- package/.genie/code/agents/docgen.md +0 -48
- package/.genie/code/agents/explore.md +0 -79
- package/.genie/code/agents/fix.md +0 -100
- package/.genie/code/agents/git/commit-advisory.md +0 -219
- package/.genie/code/agents/git/workflows/issue.md +0 -244
- package/.genie/code/agents/git/workflows/pr.md +0 -179
- package/.genie/code/agents/git/workflows/release.md +0 -460
- package/.genie/code/agents/git/workflows/report.md +0 -342
- package/.genie/code/agents/git.md +0 -432
- package/.genie/code/agents/implementor.md +0 -161
- package/.genie/code/agents/install.md +0 -515
- package/.genie/code/agents/issue-creator.md +0 -344
- package/.genie/code/agents/polish.md +0 -116
- package/.genie/code/agents/qa.md +0 -653
- package/.genie/code/agents/refactor.md +0 -294
- package/.genie/code/agents/release.md +0 -1129
- package/.genie/code/agents/roadmap.md +0 -885
- package/.genie/code/agents/tests.md +0 -557
- package/.genie/code/agents/tracer.md +0 -50
- package/.genie/code/agents/update/upstream-update.md +0 -85
- package/.genie/code/agents/update/versions/generic-update.md +0 -305
- package/.genie/code/agents/vibe.md +0 -1317
- package/.genie/code/spells/agent-configuration.md +0 -58
- package/.genie/code/spells/automated-rc-publishing.md +0 -106
- package/.genie/code/spells/branch-tracker-guidance.md +0 -28
- package/.genie/code/spells/debug.md +0 -320
- package/.genie/code/spells/emoji-naming-convention.md +0 -303
- package/.genie/code/spells/evidence-storage.md +0 -26
- package/.genie/code/spells/file-naming-rules.md +0 -35
- package/.genie/code/spells/forge-code-blueprints.md +0 -195
- package/.genie/code/spells/genie-integration.md +0 -153
- package/.genie/code/spells/publishing-protocol.md +0 -61
- package/.genie/code/spells/team-consultation-protocol.md +0 -284
- package/.genie/code/spells/tool-requirements.md +0 -20
- package/.genie/code/spells/triad-maintenance-protocol.md +0 -154
- package/.genie/code/teams/tech-council/council.md +0 -328
- package/.genie/code/teams/tech-council/jt.md +0 -352
- package/.genie/code/teams/tech-council/nayr.md +0 -305
- package/.genie/code/teams/tech-council/oettam.md +0 -375
- package/.genie/neurons/README.md +0 -193
- package/.genie/neurons/forge.md +0 -106
- package/.genie/neurons/genie.md +0 -63
- package/.genie/neurons/review.md +0 -106
- package/.genie/neurons/wish.md +0 -104
- package/.genie/product/README.md +0 -20
- package/.genie/product/cli-automation.md +0 -359
- package/.genie/product/environment.md +0 -60
- package/.genie/product/mission.md +0 -60
- package/.genie/product/roadmap.md +0 -44
- package/.genie/product/tech-stack.md +0 -34
- package/.genie/product/templates/context-template.md +0 -218
- package/.genie/product/templates/qa-done-report-template.md +0 -68
- package/.genie/product/templates/review-report-template.md +0 -89
- package/.genie/product/templates/wish-template.md +0 -120
- package/.genie/scripts/helpers/analyze-commit.js +0 -195
- package/.genie/scripts/helpers/bullet-counter.js +0 -194
- package/.genie/scripts/helpers/bullet-find.js +0 -289
- package/.genie/scripts/helpers/bullet-id.js +0 -244
- package/.genie/scripts/helpers/check-secrets.js +0 -237
- package/.genie/scripts/helpers/count-tokens.js +0 -200
- package/.genie/scripts/helpers/create-frontmatter.js +0 -456
- package/.genie/scripts/helpers/detect-markers.js +0 -293
- package/.genie/scripts/helpers/detect-todos.js +0 -267
- package/.genie/scripts/helpers/detect-unlabeled-blocks.js +0 -135
- package/.genie/scripts/helpers/embeddings.js +0 -344
- package/.genie/scripts/helpers/find-empty-sections.js +0 -158
- package/.genie/scripts/helpers/index.js +0 -319
- package/.genie/scripts/helpers/validate-frontmatter.js +0 -578
- package/.genie/scripts/helpers/validate-links.js +0 -207
- package/.genie/scripts/helpers/validate-paths.js +0 -373
- package/.genie/spells/README.md +0 -9
- package/.genie/spells/ace-protocol.md +0 -118
- package/.genie/spells/ask-one-at-a-time.md +0 -175
- package/.genie/spells/backup-analyzer.md +0 -542
- package/.genie/spells/blocker.md +0 -12
- package/.genie/spells/break-things-move-fast.md +0 -56
- package/.genie/spells/context-candidates.md +0 -72
- package/.genie/spells/context-critic.md +0 -51
- package/.genie/spells/defer-to-expertise.md +0 -278
- package/.genie/spells/delegate-dont-do.md +0 -292
- package/.genie/spells/error-investigation-protocol.md +0 -328
- package/.genie/spells/evidence-based-completion.md +0 -273
- package/.genie/spells/experiment.md +0 -65
- package/.genie/spells/file-creation-protocol.md +0 -229
- package/.genie/spells/forge-integration.md +0 -281
- package/.genie/spells/forge-orchestration.md +0 -514
- package/.genie/spells/gather-context.md +0 -18
- package/.genie/spells/global-health-check.md +0 -34
- package/.genie/spells/global-noop-roundtrip.md +0 -25
- package/.genie/spells/install-genie.md +0 -1232
- package/.genie/spells/install.md +0 -82
- package/.genie/spells/investigate-before-commit.md +0 -112
- package/.genie/spells/know-yourself.md +0 -288
- package/.genie/spells/learn.md +0 -828
- package/.genie/spells/mcp-diagnostic-protocol.md +0 -246
- package/.genie/spells/mcp-first.md +0 -124
- package/.genie/spells/multi-step-execution.md +0 -67
- package/.genie/spells/orchestration-boundary-protocol.md +0 -256
- package/.genie/spells/orchestrator-not-implementor.md +0 -189
- package/.genie/spells/prompt.md +0 -746
- package/.genie/spells/reflect.md +0 -404
- package/.genie/spells/routing-decision-matrix.md +0 -368
- package/.genie/spells/run-in-parallel.md +0 -12
- package/.genie/spells/session-state-updater-example.md +0 -196
- package/.genie/spells/session-state-updater.md +0 -220
- package/.genie/spells/track-long-running-tasks.md +0 -133
- package/.genie/spells/troubleshoot-infrastructure.md +0 -176
- package/.genie/spells/upgrade-genie.md +0 -415
- package/.genie/spells/url-presentation-protocol.md +0 -301
- package/.genie/spells/wish-initiation.md +0 -158
- package/.genie/spells/wish-issue-linkage.md +0 -410
- package/.genie/spells/wish-lifecycle.md +0 -100
- package/.genie/state/provider-status.json +0 -3
- package/.genie/state/version.json +0 -16
- package/.genie/wishes/canonical-pgserve-pm2-supervision/WISH.md +0 -290
- package/.genie/wishes/pgserve-v2/BRIEF-from-genie-pgserve.md +0 -99
- package/.genie/wishes/pgserve-v2/WISH.md +0 -442
- package/.genie/wishes/release-system-genie-pattern/WISH.md +0 -268
- package/.genie/wishes/release-system-genie-pattern/validation.md +0 -205
- package/.gitguardian.yaml +0 -29
- package/.gitguardianignore +0 -16
- package/.github/workflows/ci.yml +0 -122
- package/.github/workflows/release.yml +0 -289
- package/.github/workflows/version.yml +0 -228
- package/.husky/pre-commit +0 -2
- package/AGENTS.md +0 -433
- package/CLAUDE.md +0 -1
- package/Makefile +0 -285
- package/assets/icon.ico +0 -0
- package/bun.lock +0 -435
- package/bunfig.toml +0 -28
- package/ecosystem.config.cjs +0 -23
- package/eslint.config.js +0 -63
- package/examples/multi-tenant-demo.js +0 -104
- package/install.sh +0 -123
- package/knip.json +0 -9
- package/scripts/test-bun-self-heal.sh +0 -163
- package/scripts/test-npx.sh +0 -60
- package/tests/audit.test.js +0 -189
- package/tests/backpressure.test.js +0 -167
- package/tests/benchmarks/runner.js +0 -1197
- package/tests/benchmarks/vector-generator.js +0 -368
- package/tests/cli-install.test.js +0 -322
- package/tests/control-db.test.js +0 -285
- package/tests/daemon-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
package/src/postgres.js
CHANGED
|
@@ -19,6 +19,8 @@ import os from 'os';
|
|
|
19
19
|
import path from 'path';
|
|
20
20
|
import fs from 'fs';
|
|
21
21
|
import crypto from 'crypto';
|
|
22
|
+
import { loadEffectiveConfig } from './settings-loader.cjs';
|
|
23
|
+
import { buildPostgresArgs } from './settings-pg-args.cjs';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Get platform key for binary lookup (e.g., 'windows-x64', 'linux-x64', 'darwin-arm64')
|
|
@@ -38,12 +40,107 @@ function getPlatformKey() {
|
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
*
|
|
43
|
+
* Pinned PostgreSQL major.minor.patch we expect in the cache. Bump alongside
|
|
44
|
+
* `package.json` `optionalDependencies.@embedded-postgres/*`. Used both as
|
|
45
|
+
* the download target AND as the cache-validity check — see `isCachedValid`.
|
|
46
|
+
*/
|
|
47
|
+
const PINNED_PG_VERSION = '18.3.0-beta.17';
|
|
48
|
+
|
|
49
|
+
const VERSION_MARKER_FILENAME = '.version';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the binary cache root, honouring the same env-var precedence
|
|
53
|
+
* as `getConfigDir()` in `src/cli-install.cjs`. Defaults to `~/.autopg/`
|
|
54
|
+
* (post-rename) so config and binary cache live in the same tree.
|
|
55
|
+
*
|
|
56
|
+
* Legacy `~/.pgserve/bin/<platform>` is migrated by `migrateLegacyBinaryCache`
|
|
57
|
+
* on first call; users with a 2.1.x cache still get a one-time move.
|
|
58
|
+
*/
|
|
59
|
+
function getAutopgRoot() {
|
|
60
|
+
return process.env.AUTOPG_CONFIG_DIR
|
|
61
|
+
|| process.env.PGSERVE_CONFIG_DIR
|
|
62
|
+
|| path.join(os.homedir(), '.autopg');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the directory where extracted binaries are cached.
|
|
42
67
|
* @returns {string} Cache directory path
|
|
43
68
|
*/
|
|
44
69
|
function getBinaryCacheDir() {
|
|
45
70
|
const platformKey = getPlatformKey();
|
|
46
|
-
return path.join(
|
|
71
|
+
return path.join(getAutopgRoot(), 'bin', platformKey);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the cached version marker (the `.version` file) written next to
|
|
76
|
+
* the bin/lib trees on a successful extract. Returns the trimmed string
|
|
77
|
+
* or null when missing/unreadable.
|
|
78
|
+
*/
|
|
79
|
+
function readCachedVersion(cacheDir) {
|
|
80
|
+
try {
|
|
81
|
+
return fs.readFileSync(path.join(cacheDir, VERSION_MARKER_FILENAME), 'utf8').trim();
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write the version marker after a successful extract so future cache
|
|
89
|
+
* checks can compare against `PINNED_PG_VERSION` and re-download when
|
|
90
|
+
* the package.json has bumped to a new release.
|
|
91
|
+
*/
|
|
92
|
+
function writeCachedVersion(cacheDir, version) {
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(cacheDir, VERSION_MARKER_FILENAME),
|
|
96
|
+
`${version}\n`,
|
|
97
|
+
{ mode: 0o644 },
|
|
98
|
+
);
|
|
99
|
+
} catch {
|
|
100
|
+
// best-effort — failure here just means next boot re-downloads
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Cache hit when BOTH:
|
|
106
|
+
* - initdb + postgres exist in the bin/ subtree
|
|
107
|
+
* - the `.version` marker matches `PINNED_PG_VERSION`
|
|
108
|
+
*
|
|
109
|
+
* The legacy presence-only check (no version marker) deliberately FAILS
|
|
110
|
+
* here so users carrying a pre-rename cache get a fresh, version-correct
|
|
111
|
+
* tree on the next daemon boot.
|
|
112
|
+
*/
|
|
113
|
+
function isCachedValid(cacheBinDir, expectedVersion) {
|
|
114
|
+
const platform = os.platform();
|
|
115
|
+
const initdbName = platform === 'win32' ? 'initdb.exe' : 'initdb';
|
|
116
|
+
const postgresName = platform === 'win32' ? 'postgres.exe' : 'postgres';
|
|
117
|
+
if (!fs.existsSync(path.join(cacheBinDir, initdbName))) return false;
|
|
118
|
+
if (!fs.existsSync(path.join(cacheBinDir, postgresName))) return false;
|
|
119
|
+
const cached = readCachedVersion(path.dirname(cacheBinDir));
|
|
120
|
+
return cached === expectedVersion;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* One-time best-effort migration of `~/.pgserve/bin/<platform>` into the
|
|
125
|
+
* new `~/.autopg/bin/<platform>` location. We RENAME (atomic on the same
|
|
126
|
+
* fs) rather than copy to keep the operation cheap. On failure we fall
|
|
127
|
+
* back silently — the download path will recreate the cache.
|
|
128
|
+
*/
|
|
129
|
+
function migrateLegacyBinaryCache() {
|
|
130
|
+
const platformKey = getPlatformKey();
|
|
131
|
+
const newDir = getBinaryCacheDir();
|
|
132
|
+
if (fs.existsSync(newDir)) return;
|
|
133
|
+
const oldDir = path.join(os.homedir(), '.pgserve', 'bin', platformKey);
|
|
134
|
+
if (!fs.existsSync(oldDir)) return;
|
|
135
|
+
try {
|
|
136
|
+
fs.mkdirSync(path.dirname(newDir), { recursive: true });
|
|
137
|
+
fs.renameSync(oldDir, newDir);
|
|
138
|
+
console.log(`[pgserve] Migrated binary cache: ${oldDir} → ${newDir}`);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// EXDEV (cross-device) or permission errors fall through; the
|
|
141
|
+
// downloader will repopulate the new location.
|
|
142
|
+
console.log(`[pgserve] Could not migrate legacy binary cache (${err.code || err.message}); will re-download`);
|
|
143
|
+
}
|
|
47
144
|
}
|
|
48
145
|
|
|
49
146
|
/**
|
|
@@ -54,20 +151,32 @@ function getBinaryCacheDir() {
|
|
|
54
151
|
*/
|
|
55
152
|
async function downloadPostgresBinaries() {
|
|
56
153
|
const platform = os.platform();
|
|
154
|
+
|
|
155
|
+
// Carry over a legacy ~/.pgserve/bin cache the first time we run under
|
|
156
|
+
// the autopg path. After this, the new path is canonical.
|
|
157
|
+
migrateLegacyBinaryCache();
|
|
158
|
+
|
|
57
159
|
const cacheDir = getBinaryCacheDir();
|
|
58
160
|
const cacheBinDir = path.join(cacheDir, 'bin');
|
|
59
|
-
const initdbName = platform === 'win32' ? 'initdb.exe' : 'initdb';
|
|
60
|
-
const postgresName = platform === 'win32' ? 'postgres.exe' : 'postgres';
|
|
61
161
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
162
|
+
// Cache hit: bin/initdb + bin/postgres present AND .version matches
|
|
163
|
+
// PINNED_PG_VERSION. Mismatch (e.g. user upgraded the npm package) or
|
|
164
|
+
// absence triggers a fresh download.
|
|
165
|
+
if (isCachedValid(cacheBinDir, PINNED_PG_VERSION)) {
|
|
65
166
|
return cacheDir;
|
|
66
167
|
}
|
|
67
168
|
|
|
169
|
+
const cachedVersion = readCachedVersion(cacheDir);
|
|
170
|
+
if (cachedVersion && cachedVersion !== PINNED_PG_VERSION) {
|
|
171
|
+
console.log(`[pgserve] Cached binaries are version ${cachedVersion}; pinned is ${PINNED_PG_VERSION} — re-downloading`);
|
|
172
|
+
// Wipe the stale tree before re-extracting to avoid mixing files
|
|
173
|
+
// from two different PG majors.
|
|
174
|
+
try { fs.rmSync(cacheDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
68
177
|
const platformKey = getPlatformKey();
|
|
69
178
|
const pkgName = `@embedded-postgres/${platformKey}`;
|
|
70
|
-
const pkgVersion =
|
|
179
|
+
const pkgVersion = PINNED_PG_VERSION;
|
|
71
180
|
|
|
72
181
|
console.log(`[pgserve] PostgreSQL binaries not found.`);
|
|
73
182
|
console.log(`[pgserve] Downloading ${pkgName}@${pkgVersion}...`);
|
|
@@ -145,6 +254,10 @@ async function downloadPostgresBinaries() {
|
|
|
145
254
|
// Ignore cleanup errors
|
|
146
255
|
}
|
|
147
256
|
|
|
257
|
+
// Persist the version marker so the next boot can detect package-version
|
|
258
|
+
// bumps and re-download instead of silently running the old major.
|
|
259
|
+
writeCachedVersion(cacheDir, pkgVersion);
|
|
260
|
+
|
|
148
261
|
console.log(`[pgserve] PostgreSQL binaries installed to ${cacheDir}`);
|
|
149
262
|
return cacheDir;
|
|
150
263
|
}
|
|
@@ -763,12 +876,22 @@ export class PostgresManager extends EventEmitter {
|
|
|
763
876
|
async _startPostgres() {
|
|
764
877
|
await this._ensureNoStalePostmasterLock();
|
|
765
878
|
return new Promise((resolve, reject) => {
|
|
879
|
+
// Resolve effective postgres settings (defaults < ~/.autopg/settings.json
|
|
880
|
+
// < env). Curated GUCs land first; `postgres._extra` is layered in
|
|
881
|
+
// beneath them so curated values win on conflict. Invalid entries are
|
|
882
|
+
// dropped with a logger.warn — postgres still starts.
|
|
883
|
+
const { settings } = loadEffectiveConfig({ logger: this.logger });
|
|
884
|
+
const { args: gucArgs, applied: appliedGucs } = buildPostgresArgs(
|
|
885
|
+
settings.postgres,
|
|
886
|
+
{ logger: this.logger },
|
|
887
|
+
);
|
|
888
|
+
|
|
766
889
|
// Build PostgreSQL arguments
|
|
767
890
|
const pgArgs = [
|
|
768
891
|
this.binaries.postgres,
|
|
769
892
|
'-D', this.databaseDir,
|
|
770
893
|
'-p', this.port.toString(),
|
|
771
|
-
|
|
894
|
+
...gucArgs,
|
|
772
895
|
];
|
|
773
896
|
|
|
774
897
|
// Enable Unix socket for faster local connections (Linux/macOS)
|
|
@@ -779,17 +902,16 @@ export class PostgresManager extends EventEmitter {
|
|
|
779
902
|
pgArgs.push('-k', ''); // Disable Unix socket on Windows
|
|
780
903
|
}
|
|
781
904
|
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
'
|
|
905
|
+
// Surface the WAL block as an info log when sync is enabled, the same
|
|
906
|
+
// signal the previous hardcoded path emitted. The actual GUCs are now
|
|
907
|
+
// schema defaults (wal_level=logical, max_replication_slots=10,
|
|
908
|
+
// max_wal_senders=10, wal_keep_size=512MB) so they ship in `gucArgs`
|
|
909
|
+
// already — this log just preserves the operator-visible breadcrumb.
|
|
910
|
+
if (this.syncEnabled || settings.sync?.enabled) {
|
|
911
|
+
this.logger.info(
|
|
912
|
+
{ walLevel: appliedGucs.wal_level },
|
|
913
|
+
'Logical replication enabled for sync',
|
|
791
914
|
);
|
|
792
|
-
this.logger.info('Logical replication enabled for sync');
|
|
793
915
|
}
|
|
794
916
|
|
|
795
917
|
this.process = Bun.spawn(buildCommand(pgArgs, this.binaries.libDir), {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings loader — reads `~/.autopg/settings.json`, merges defaults < file
|
|
3
|
+
* < env, returns `{ settings, sources, etag }`.
|
|
4
|
+
*
|
|
5
|
+
* Precedence:
|
|
6
|
+
* default (lowest) — from settings-schema.js
|
|
7
|
+
* file — `~/.autopg/settings.json` (or AUTOPG_CONFIG_DIR override)
|
|
8
|
+
* env (highest) — process.env.AUTOPG_<X> beats process.env.PGSERVE_<X>
|
|
9
|
+
*
|
|
10
|
+
* `sources` is a flat map of dotted keys → 'default' | 'file' | 'env:<NAME>'.
|
|
11
|
+
* `etag` is sha256 of the raw file bytes (or 'sha256:empty' when no file
|
|
12
|
+
* exists). Deterministic for unchanged files, used for optimistic
|
|
13
|
+
* concurrency control on the UI helper's PUT path.
|
|
14
|
+
*
|
|
15
|
+
* The PGSERVE_<X>-only fall-through path emits a one-time deprecation
|
|
16
|
+
* note via `logger.warn` (suppressed on subsequent calls within the
|
|
17
|
+
* same process).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const crypto = require('node:crypto');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const { SCHEMA, buildDefaults } = require('./settings-schema.cjs');
|
|
28
|
+
|
|
29
|
+
const SETTINGS_FILENAME = 'settings.json';
|
|
30
|
+
const EMPTY_FILE_ETAG = 'sha256:empty';
|
|
31
|
+
|
|
32
|
+
let _legacyEnvWarningEmitted = false;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Where settings.json lives. AUTOPG_CONFIG_DIR wins over PGSERVE_CONFIG_DIR
|
|
36
|
+
* (the legacy var) which wins over the default `~/.autopg/`.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: when only PGSERVE_CONFIG_DIR is set we fall back to `~/.pgserve/`
|
|
39
|
+
* (the legacy directory), not `~/.autopg/`. This is the migration-bridge
|
|
40
|
+
* path so existing operators keep working until they migrate. The migrate
|
|
41
|
+
* helper is what decouples the two.
|
|
42
|
+
*/
|
|
43
|
+
function getConfigDir() {
|
|
44
|
+
if (process.env.AUTOPG_CONFIG_DIR) return process.env.AUTOPG_CONFIG_DIR;
|
|
45
|
+
if (process.env.PGSERVE_CONFIG_DIR) return process.env.PGSERVE_CONFIG_DIR;
|
|
46
|
+
return path.join(os.homedir(), '.autopg');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getSettingsPath() {
|
|
50
|
+
return path.join(getConfigDir(), SETTINGS_FILENAME);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read raw file bytes and parse JSON. Returns `{ raw, parsed }` where
|
|
55
|
+
* `raw` is the bytes used to compute the etag and `parsed` is the JSON
|
|
56
|
+
* tree. Returns `{ raw: null, parsed: null }` when the file is missing.
|
|
57
|
+
*
|
|
58
|
+
* Throws SyntaxError when the file exists but doesn't parse — callers
|
|
59
|
+
* (CLI dispatch) should surface the path so operators can fix or
|
|
60
|
+
* re-init.
|
|
61
|
+
*/
|
|
62
|
+
function readSettingsFile(settingsPath = getSettingsPath()) {
|
|
63
|
+
if (!fs.existsSync(settingsPath)) return { raw: null, parsed: null };
|
|
64
|
+
const raw = fs.readFileSync(settingsPath);
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(raw.toString('utf8'));
|
|
67
|
+
return { raw, parsed };
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const wrapped = new SyntaxError(
|
|
70
|
+
`Failed to parse ${settingsPath}: ${err.message}`,
|
|
71
|
+
);
|
|
72
|
+
wrapped.cause = err;
|
|
73
|
+
wrapped.path = settingsPath;
|
|
74
|
+
throw wrapped;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Compute sha256 etag of the raw file bytes. Stable for unchanged
|
|
80
|
+
* files. `EMPTY_FILE_ETAG` for the missing-file case so callers can
|
|
81
|
+
* still pass an `If-Match` (and a CLI write that creates the file
|
|
82
|
+
* round-trips deterministically).
|
|
83
|
+
*/
|
|
84
|
+
function computeEtag(rawBytes) {
|
|
85
|
+
if (!rawBytes || rawBytes.length === 0) return EMPTY_FILE_ETAG;
|
|
86
|
+
const hash = crypto.createHash('sha256').update(rawBytes).digest('hex');
|
|
87
|
+
return `sha256:${hash}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cast an env var string into the descriptor's runtime type. Mirrors
|
|
92
|
+
* `coerce` in settings-validator but without throwing — env vars are
|
|
93
|
+
* trusted by definition (the operator set them) and any garbage value
|
|
94
|
+
* surfaces at runtime via the validator on the next write.
|
|
95
|
+
*/
|
|
96
|
+
function castEnv(descriptor, raw) {
|
|
97
|
+
switch (descriptor.type) {
|
|
98
|
+
case 'int': {
|
|
99
|
+
const n = Number.parseInt(raw, 10);
|
|
100
|
+
return Number.isFinite(n) ? n : descriptor.default;
|
|
101
|
+
}
|
|
102
|
+
case 'bool': {
|
|
103
|
+
if (raw === 'true' || raw === '1') return true;
|
|
104
|
+
if (raw === 'false' || raw === '0') return false;
|
|
105
|
+
return descriptor.default;
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return raw;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pick the env var that wins for a leaf, in priority order. Returns
|
|
114
|
+
* `{ envName, raw }` on hit, `null` on miss.
|
|
115
|
+
*
|
|
116
|
+
* The first AUTOPG_<X> that is set wins outright. Falling through to
|
|
117
|
+
* a PGSERVE_<X>-only setting trips the one-time deprecation note.
|
|
118
|
+
*/
|
|
119
|
+
function resolveEnv(descriptor, env, logger) {
|
|
120
|
+
if (!Array.isArray(descriptor.env) || descriptor.env.length === 0) return null;
|
|
121
|
+
for (const name of descriptor.env) {
|
|
122
|
+
if (env[name] !== undefined && env[name] !== '') {
|
|
123
|
+
const isLegacy = name.startsWith('PGSERVE_');
|
|
124
|
+
if (isLegacy && !_legacyEnvWarningEmitted) {
|
|
125
|
+
_legacyEnvWarningEmitted = true;
|
|
126
|
+
logger?.warn?.(
|
|
127
|
+
{ env: name },
|
|
128
|
+
`${name} is deprecated; prefer ${descriptor.env[0]}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return { envName: name, raw: env[name] };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Merge defaults < file < env, building up `sources` in lockstep so
|
|
139
|
+
* each leaf's origin is recorded. `postgres._extra` is treated
|
|
140
|
+
* specially: file value wins as a whole map (no per-key env overrides).
|
|
141
|
+
*/
|
|
142
|
+
function mergeWithSources({ defaults, fileSettings, env, logger, schema = SCHEMA }) {
|
|
143
|
+
const settings = {};
|
|
144
|
+
const sources = {};
|
|
145
|
+
|
|
146
|
+
for (const [section, fields] of Object.entries(schema)) {
|
|
147
|
+
settings[section] = {};
|
|
148
|
+
for (const [field, descriptor] of Object.entries(fields)) {
|
|
149
|
+
const dotted = `${section}.${field}`;
|
|
150
|
+
let value = clone(descriptor.default);
|
|
151
|
+
let source = 'default';
|
|
152
|
+
|
|
153
|
+
const fileSection = fileSettings && fileSettings[section];
|
|
154
|
+
if (fileSection && Object.prototype.hasOwnProperty.call(fileSection, field)) {
|
|
155
|
+
value = clone(fileSection[field]);
|
|
156
|
+
source = 'file';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const envHit = resolveEnv(descriptor, env, logger);
|
|
160
|
+
if (envHit) {
|
|
161
|
+
value = castEnv(descriptor, envHit.raw);
|
|
162
|
+
source = `env:${envHit.envName}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
settings[section][field] = value;
|
|
166
|
+
sources[dotted] = source;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { settings, sources };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function clone(value) {
|
|
173
|
+
if (value === null || value === undefined) return value;
|
|
174
|
+
if (typeof value !== 'object') return value;
|
|
175
|
+
if (Array.isArray(value)) return value.map(clone);
|
|
176
|
+
const out = {};
|
|
177
|
+
for (const [k, v] of Object.entries(value)) out[k] = clone(v);
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Load + merge entry point. Returns `{ settings, sources, etag, path }`.
|
|
183
|
+
*
|
|
184
|
+
* `logger` is optional — when omitted, deprecation notes go to
|
|
185
|
+
* stderr via console.warn so the CLI surface still has visibility.
|
|
186
|
+
*
|
|
187
|
+
* `env` defaults to process.env so callers can inject a frozen snapshot
|
|
188
|
+
* for tests.
|
|
189
|
+
*/
|
|
190
|
+
function loadEffectiveConfig({
|
|
191
|
+
schema = SCHEMA,
|
|
192
|
+
env = process.env,
|
|
193
|
+
logger,
|
|
194
|
+
settingsPath = getSettingsPath(),
|
|
195
|
+
} = {}) {
|
|
196
|
+
const fallbackLogger = logger || {
|
|
197
|
+
warn: (data, msg) => console.warn(`[autopg] ${msg ?? ''} ${JSON.stringify(data ?? {})}`),
|
|
198
|
+
};
|
|
199
|
+
const { raw, parsed } = readSettingsFile(settingsPath);
|
|
200
|
+
const defaults = buildDefaults(schema);
|
|
201
|
+
const { settings, sources } = mergeWithSources({
|
|
202
|
+
defaults,
|
|
203
|
+
fileSettings: parsed,
|
|
204
|
+
env,
|
|
205
|
+
logger: fallbackLogger,
|
|
206
|
+
schema,
|
|
207
|
+
});
|
|
208
|
+
const etag = computeEtag(raw);
|
|
209
|
+
return { settings, sources, etag, path: settingsPath };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Test helper: reset the once-flag so multiple test cases can each
|
|
214
|
+
* observe the deprecation log line.
|
|
215
|
+
*/
|
|
216
|
+
function resetLegacyEnvWarning() {
|
|
217
|
+
_legacyEnvWarningEmitted = false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
loadEffectiveConfig,
|
|
222
|
+
computeEtag,
|
|
223
|
+
readSettingsFile,
|
|
224
|
+
getConfigDir,
|
|
225
|
+
getSettingsPath,
|
|
226
|
+
EMPTY_FILE_ETAG,
|
|
227
|
+
SETTINGS_FILENAME,
|
|
228
|
+
// Test surface
|
|
229
|
+
_internals: {
|
|
230
|
+
castEnv,
|
|
231
|
+
resolveEnv,
|
|
232
|
+
mergeWithSources,
|
|
233
|
+
resetLegacyEnvWarning,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot migration from `~/.pgserve/` to `~/.autopg/`.
|
|
3
|
+
*
|
|
4
|
+
* Trigger: dispatcher pre-flight on every CLI entry point.
|
|
5
|
+
* Behavior:
|
|
6
|
+
* - If `~/.autopg/` already exists OR `~/.pgserve/` does not exist → no-op.
|
|
7
|
+
* - Else: copy the legacy directory contents to `~/.autopg/` preserving
|
|
8
|
+
* mtimes, then drop `MIGRATED-FROM-PGSERVE.md` in the old dir as the
|
|
9
|
+
* idempotency marker.
|
|
10
|
+
* - Skips if the marker already exists, even if the new dir was later
|
|
11
|
+
* deleted (so the migration is permanent — operators who want to redo
|
|
12
|
+
* it must remove the marker manually).
|
|
13
|
+
*
|
|
14
|
+
* The legacy dir is left in place (not removed) so operators can A/B and
|
|
15
|
+
* roll back if anything goes wrong. This is intentional: this code runs
|
|
16
|
+
* unattended on every `autopg <subcommand>` invocation, so we err on the
|
|
17
|
+
* side of preserving the user's data.
|
|
18
|
+
*
|
|
19
|
+
* The migration also normalizes the legacy `config.json` (just port +
|
|
20
|
+
* dataDir + registeredAt) into the new `settings.json` shape: the legacy
|
|
21
|
+
* fields populate `server.port` and `runtime.dataDir`, everything else is
|
|
22
|
+
* filled with defaults.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const os = require('node:os');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
|
|
31
|
+
const { buildDefaults, SCHEMA_VERSION } = require('./settings-schema.cjs');
|
|
32
|
+
const { serializeSettings } = require('./settings-writer.cjs');
|
|
33
|
+
|
|
34
|
+
const MARKER_FILENAME = 'MIGRATED-FROM-PGSERVE.md';
|
|
35
|
+
const MARKER_BODY = `# Migrated to ~/.autopg/
|
|
36
|
+
|
|
37
|
+
This directory has been migrated to \`~/.autopg/\` as part of the
|
|
38
|
+
soft-rename to autopg. Settings, data, and logs were copied verbatim;
|
|
39
|
+
the legacy location is preserved so you can roll back if needed.
|
|
40
|
+
|
|
41
|
+
To complete the cutover (after verifying autopg works):
|
|
42
|
+
|
|
43
|
+
rm -rf ~/.pgserve
|
|
44
|
+
|
|
45
|
+
This file is the migration's idempotency marker — its presence prevents
|
|
46
|
+
re-migration on subsequent autopg invocations.
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the legacy and new config directories.
|
|
51
|
+
*
|
|
52
|
+
* Migration only runs against the *default* `~/.pgserve` → `~/.autopg`
|
|
53
|
+
* pair. When the user has either env override set, they're in
|
|
54
|
+
* custom-config-dir mode (CI, tests, multi-instance setups) and we
|
|
55
|
+
* never auto-migrate — that's a foot-gun. Operators in custom mode
|
|
56
|
+
* who want to migrate can:
|
|
57
|
+
*
|
|
58
|
+
* 1. unset AUTOPG_CONFIG_DIR/PGSERVE_CONFIG_DIR
|
|
59
|
+
* 2. run `autopg config init`
|
|
60
|
+
* 3. copy whatever they need by hand
|
|
61
|
+
*
|
|
62
|
+
* The override-skip path is the reason `legacy`/`fresh` may be null
|
|
63
|
+
* here; `migrateIfNeeded` short-circuits in that case.
|
|
64
|
+
*
|
|
65
|
+
* Tests opt in to the default-path flow by passing an explicit `home`
|
|
66
|
+
* (a tempdir) instead of relying on env vars.
|
|
67
|
+
*/
|
|
68
|
+
function resolveDirs({ home = os.homedir(), env = process.env, allowOverrides = false } = {}) {
|
|
69
|
+
if (!allowOverrides && (env.AUTOPG_CONFIG_DIR || env.PGSERVE_CONFIG_DIR)) {
|
|
70
|
+
return { legacy: null, fresh: null, skipped: true };
|
|
71
|
+
}
|
|
72
|
+
const legacy = path.join(home, '.pgserve');
|
|
73
|
+
const fresh = path.join(home, '.autopg');
|
|
74
|
+
return { legacy, fresh };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Top-level directory names under the legacy `~/.pgserve/` that we DON'T
|
|
79
|
+
* carry over during the soft-rename migration:
|
|
80
|
+
*
|
|
81
|
+
* - `bin/` — embedded-postgres binary cache, ~120 MB. Postgres binaries
|
|
82
|
+
* are version-specific; the new daemon re-fetches the pinned
|
|
83
|
+
* version on first boot via the version-aware cache check
|
|
84
|
+
* in `src/postgres.js`. Copying defeats that AND wastes disk.
|
|
85
|
+
*
|
|
86
|
+
* Persistent `data/` and pm2 `logs/` ARE migrated — users with stateful
|
|
87
|
+
* setups expect them to survive the rename.
|
|
88
|
+
*/
|
|
89
|
+
const LEGACY_SKIP_TOPLEVEL = new Set(['bin']);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Recursively copy `src` to `dest` preserving mtimes. Skips the marker
|
|
93
|
+
* file (so a re-migration after marker removal doesn't carry it over)
|
|
94
|
+
* and skips any path under `dest` that already exists (we never
|
|
95
|
+
* overwrite during migration). At the top level, also skips the
|
|
96
|
+
* heavy/version-specific directories listed in `LEGACY_SKIP_TOPLEVEL`.
|
|
97
|
+
*/
|
|
98
|
+
function copyTree(src, dest, depth = 0) {
|
|
99
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
100
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true, mode: 0o700 });
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.name === MARKER_FILENAME) continue;
|
|
104
|
+
if (depth === 0 && LEGACY_SKIP_TOPLEVEL.has(entry.name)) continue;
|
|
105
|
+
const srcPath = path.join(src, entry.name);
|
|
106
|
+
const destPath = path.join(dest, entry.name);
|
|
107
|
+
if (entry.isDirectory()) {
|
|
108
|
+
copyTree(srcPath, destPath, depth + 1);
|
|
109
|
+
} else if (entry.isFile()) {
|
|
110
|
+
if (fs.existsSync(destPath)) continue;
|
|
111
|
+
fs.copyFileSync(srcPath, destPath);
|
|
112
|
+
const stat = fs.statSync(srcPath);
|
|
113
|
+
try {
|
|
114
|
+
fs.utimesSync(destPath, stat.atime, stat.mtime);
|
|
115
|
+
} catch {
|
|
116
|
+
// best-effort; don't fail migration over an mtime preserve hiccup
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Symlinks / sockets are skipped; the legacy dir never contains them.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Translate the legacy `config.json` shape ({ port, dataDir, registeredAt })
|
|
125
|
+
* into the new `settings.json` shape, filling everything else with defaults.
|
|
126
|
+
*
|
|
127
|
+
* Returns the serialized JSON string ready to write.
|
|
128
|
+
*/
|
|
129
|
+
function buildSettingsFromLegacyConfig(legacyConfig) {
|
|
130
|
+
const tree = buildDefaults();
|
|
131
|
+
if (legacyConfig && typeof legacyConfig === 'object') {
|
|
132
|
+
if (Number.isInteger(legacyConfig.port)) {
|
|
133
|
+
tree.server.port = legacyConfig.port;
|
|
134
|
+
}
|
|
135
|
+
if (typeof legacyConfig.dataDir === 'string' && legacyConfig.dataDir.length) {
|
|
136
|
+
tree.runtime.dataDir = legacyConfig.dataDir;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Marker the migration even within the file payload so a future
|
|
140
|
+
// dump-with-context tool can identify it.
|
|
141
|
+
tree._migratedFrom = '~/.pgserve';
|
|
142
|
+
tree._schemaVersion = SCHEMA_VERSION;
|
|
143
|
+
return serializeSettings(tree);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run the migration. Idempotent — safe to call from the dispatcher
|
|
148
|
+
* pre-flight on every command. Returns `{ migrated: bool, reason }`
|
|
149
|
+
* for callers (and tests) that want to log the outcome.
|
|
150
|
+
*/
|
|
151
|
+
function migrateIfNeeded(opts = {}) {
|
|
152
|
+
const dirs = resolveDirs(opts);
|
|
153
|
+
if (dirs.skipped) {
|
|
154
|
+
return { migrated: false, reason: 'env-override-set', ...dirs };
|
|
155
|
+
}
|
|
156
|
+
const { legacy, fresh } = dirs;
|
|
157
|
+
const markerPath = path.join(legacy, MARKER_FILENAME);
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(legacy)) {
|
|
160
|
+
return { migrated: false, reason: 'no-legacy-dir', legacy, fresh };
|
|
161
|
+
}
|
|
162
|
+
if (fs.existsSync(markerPath)) {
|
|
163
|
+
return { migrated: false, reason: 'already-migrated', legacy, fresh };
|
|
164
|
+
}
|
|
165
|
+
if (fs.existsSync(fresh)) {
|
|
166
|
+
// Both exist, no marker — operator may have created `~/.autopg/`
|
|
167
|
+
// independently. Don't touch either; leave the marker in place so
|
|
168
|
+
// we don't try again.
|
|
169
|
+
fs.writeFileSync(markerPath, MARKER_BODY, { mode: 0o644 });
|
|
170
|
+
return { migrated: false, reason: 'both-exist-marker-set', legacy, fresh };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Copy the directory tree first so any failure leaves the legacy
|
|
174
|
+
// dir untouched.
|
|
175
|
+
copyTree(legacy, fresh);
|
|
176
|
+
|
|
177
|
+
// Translate config.json → settings.json if present and the new file
|
|
178
|
+
// wasn't already created via copyTree (which would have copied any
|
|
179
|
+
// existing settings.json verbatim).
|
|
180
|
+
const legacyConfigPath = path.join(legacy, 'config.json');
|
|
181
|
+
const freshSettingsPath = path.join(fresh, 'settings.json');
|
|
182
|
+
if (fs.existsSync(legacyConfigPath) && !fs.existsSync(freshSettingsPath)) {
|
|
183
|
+
let parsed = null;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(fs.readFileSync(legacyConfigPath, 'utf8'));
|
|
186
|
+
} catch {
|
|
187
|
+
parsed = null;
|
|
188
|
+
}
|
|
189
|
+
const bytes = buildSettingsFromLegacyConfig(parsed);
|
|
190
|
+
fs.writeFileSync(freshSettingsPath, bytes, { mode: 0o600 });
|
|
191
|
+
try {
|
|
192
|
+
fs.chmodSync(freshSettingsPath, 0o600);
|
|
193
|
+
} catch {
|
|
194
|
+
// ignore on platforms without chmod support
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fs.writeFileSync(markerPath, MARKER_BODY, { mode: 0o644 });
|
|
199
|
+
return { migrated: true, reason: 'copied', legacy, fresh };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
migrateIfNeeded,
|
|
204
|
+
resolveDirs,
|
|
205
|
+
buildSettingsFromLegacyConfig,
|
|
206
|
+
MARKER_FILENAME,
|
|
207
|
+
MARKER_BODY,
|
|
208
|
+
// Test surface
|
|
209
|
+
_internals: {
|
|
210
|
+
copyTree,
|
|
211
|
+
},
|
|
212
|
+
};
|