@agenticmail/enterprise 0.5.614 → 0.5.615
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/dist/cli.js +15 -0
- package/package.json +4 -2
- package/scripts/ensure-pm2-startup.cjs +216 -0
- package/scripts/migrate-from-supabase.sh +153 -0
package/dist/cli.js
CHANGED
|
@@ -70,6 +70,21 @@ Skill Development:
|
|
|
70
70
|
case "agent":
|
|
71
71
|
import("./cli-agent-DOLO7OCU.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
|
|
72
72
|
break;
|
|
73
|
+
case "startup":
|
|
74
|
+
case "autostart":
|
|
75
|
+
Promise.all([
|
|
76
|
+
import("child_process"),
|
|
77
|
+
import("path"),
|
|
78
|
+
import("url")
|
|
79
|
+
]).then(([cp, p, u]) => {
|
|
80
|
+
const here = p.dirname(u.fileURLToPath(import.meta.url));
|
|
81
|
+
const helper = p.join(here, "..", "scripts", "ensure-pm2-startup.cjs");
|
|
82
|
+
const child = cp.spawn(process.execPath, [helper, ...args.slice(1)], {
|
|
83
|
+
stdio: "inherit"
|
|
84
|
+
});
|
|
85
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
86
|
+
}).catch(fatal);
|
|
87
|
+
break;
|
|
73
88
|
case "setup":
|
|
74
89
|
default:
|
|
75
90
|
import("./setup-SLLV37YT.js").then((m) => m.runSetupWizard()).catch(fatal);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/enterprise",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.615",
|
|
4
4
|
"description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"build": "tsup src/index.ts src/cli.ts src/registry/cli.ts src/watchdog.ts --format esm --external better-sqlite3 --external mongodb --external mysql2 --external @libsql/client --external @aws-sdk/client-dynamodb --external @aws-sdk/lib-dynamodb --external @aws-sdk/client-s3 --external @aws-sdk/s3-request-presigner --external @google-cloud/storage --external @azure/storage-blob --external @mozilla/readability --external imapflow --external nodemailer --external linkedom --external postgres --external playwright-core --external ws --external express && mkdir -p dist/dashboard/components dist/dashboard/pages dist/dashboard/vendor dist/dashboard/assets dist/registry && cp src/dashboard/index.html dist/dashboard/ && cp src/dashboard/app.js dist/dashboard/ && cp src/dashboard/components/*.js dist/dashboard/components/ && cp src/dashboard/pages/*.js dist/dashboard/pages/ && rm -rf dist/dashboard/pages/agent-detail && cp -r src/dashboard/pages/agent-detail dist/dashboard/pages/agent-detail && cp src/dashboard/vendor/*.js dist/dashboard/vendor/ && cp -r src/dashboard/assets/* dist/dashboard/assets/ && mkdir -p dist/dashboard/data && cp src/dashboard/data/*.js dist/dashboard/data/ && mkdir -p dist/dashboard/docs && cp src/dashboard/docs/*.html dist/dashboard/docs/ && cp src/dashboard/docs/*.css dist/dashboard/docs/ && mkdir -p dist/assets && cp src/engine/assets/* dist/assets/ && cp src/engine/soul-templates.json dist/",
|
|
21
21
|
"dev": "npm run build && node --watch start-live.mjs",
|
|
22
22
|
"rebuild": "npm run build && pm2 restart enterprise",
|
|
23
|
-
"preuninstall": "node scripts/preuninstall.js"
|
|
23
|
+
"preuninstall": "node scripts/preuninstall.js",
|
|
24
|
+
"postinstall": "node scripts/ensure-pm2-startup.cjs --quiet || true",
|
|
25
|
+
"startup": "node scripts/ensure-pm2-startup.cjs"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
26
28
|
"ai",
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ensure-pm2-startup.cjs — make sure PM2 resurrects on reboot.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ────────────────────────────────────────────────────────────────
|
|
7
|
+
* `pm2 startup` ships a launchd plist on macOS that has
|
|
8
|
+
* historically had two bugs:
|
|
9
|
+
*
|
|
10
|
+
* 1. LaunchOnlyOnce=true — a deprecated key launchd interprets
|
|
11
|
+
* as "only ever run this ONCE in the lifetime of the plist."
|
|
12
|
+
* After the first execution, it never fires on subsequent
|
|
13
|
+
* boots. Reboots stop restoring processes.
|
|
14
|
+
*
|
|
15
|
+
* 2. The agent isn't always re-bootstrapped after a plist
|
|
16
|
+
* change. The user has the file but launchctl never loaded
|
|
17
|
+
* it (launchctl list shows nothing).
|
|
18
|
+
*
|
|
19
|
+
* This script writes a correct plist + bootstraps it cleanly +
|
|
20
|
+
* runs `pm2 save` so the dump that resurrect reads is current.
|
|
21
|
+
*
|
|
22
|
+
* Idempotent — safe to run on every install / upgrade / manual
|
|
23
|
+
* invocation. Best-effort — failures log a WARN and exit 0 so
|
|
24
|
+
* npm install never aborts on a launchd hiccup.
|
|
25
|
+
*
|
|
26
|
+
* Platforms
|
|
27
|
+
* ────────────────────────────────────────────────────────────────
|
|
28
|
+
* macOS — full support (plist + launchctl)
|
|
29
|
+
* linux — currently no-op with a hint to run `pm2 startup`
|
|
30
|
+
* manually. systemd unit generation is a TODO.
|
|
31
|
+
* win32 — no-op.
|
|
32
|
+
*
|
|
33
|
+
* CLI
|
|
34
|
+
* ────────────────────────────────────────────────────────────────
|
|
35
|
+
* node scripts/ensure-pm2-startup.cjs # apply + save
|
|
36
|
+
* node scripts/ensure-pm2-startup.cjs --check # verify only
|
|
37
|
+
* node scripts/ensure-pm2-startup.cjs --quiet # no output on OK
|
|
38
|
+
*/
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const os = require('os');
|
|
43
|
+
const path = require('path');
|
|
44
|
+
const { execFileSync, execSync, spawnSync } = require('child_process');
|
|
45
|
+
|
|
46
|
+
const QUIET = process.argv.includes('--quiet');
|
|
47
|
+
const CHECK_ONLY = process.argv.includes('--check');
|
|
48
|
+
|
|
49
|
+
function log(msg) { if (!QUIET) console.log('[pm2-startup] ' + msg); }
|
|
50
|
+
function warn(msg) { console.warn('[pm2-startup] ' + msg); }
|
|
51
|
+
|
|
52
|
+
function which(bin) {
|
|
53
|
+
try {
|
|
54
|
+
return execSync('command -v ' + bin, { encoding: 'utf8' }).trim();
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findPm2() {
|
|
59
|
+
// Prefer the global homebrew install path because the launchd
|
|
60
|
+
// plist needs an absolute path (no $PATH lookup at boot).
|
|
61
|
+
const candidates = [
|
|
62
|
+
'/opt/homebrew/lib/node_modules/pm2/bin/pm2',
|
|
63
|
+
'/usr/local/lib/node_modules/pm2/bin/pm2',
|
|
64
|
+
which('pm2'),
|
|
65
|
+
].filter(Boolean);
|
|
66
|
+
for (const c of candidates) {
|
|
67
|
+
if (fs.existsSync(c)) return c;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Build the plist string. The PATH and PM2_HOME we capture from
|
|
73
|
+
* the current process so the agent's resurrect run sees the same
|
|
74
|
+
* toolchain the user does in their interactive shell. */
|
|
75
|
+
function buildPlist(user, pm2Bin) {
|
|
76
|
+
const pm2Home = process.env.PM2_HOME || path.join(os.homedir(), '.pm2');
|
|
77
|
+
const pathEnv = process.env.PATH || '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin';
|
|
78
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
79
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
80
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
81
|
+
<plist version="1.0">
|
|
82
|
+
<dict>
|
|
83
|
+
<key>Label</key>
|
|
84
|
+
<string>com.PM2</string>
|
|
85
|
+
<key>UserName</key>
|
|
86
|
+
<string>${xml(user)}</string>
|
|
87
|
+
<!-- Fire once at boot/login. KeepAlive=false because pm2
|
|
88
|
+
resurrect is a one-shot — PM2 itself manages the children
|
|
89
|
+
after that. NO LaunchOnlyOnce (deprecated, breaks reboots). -->
|
|
90
|
+
<key>RunAtLoad</key>
|
|
91
|
+
<true/>
|
|
92
|
+
<key>KeepAlive</key>
|
|
93
|
+
<false/>
|
|
94
|
+
<key>ProgramArguments</key>
|
|
95
|
+
<array>
|
|
96
|
+
<string>/bin/sh</string>
|
|
97
|
+
<string>-c</string>
|
|
98
|
+
<string>${xml(pm2Bin)} resurrect</string>
|
|
99
|
+
</array>
|
|
100
|
+
<key>EnvironmentVariables</key>
|
|
101
|
+
<dict>
|
|
102
|
+
<key>PATH</key>
|
|
103
|
+
<string>${xml(pathEnv)}</string>
|
|
104
|
+
<key>PM2_HOME</key>
|
|
105
|
+
<string>${xml(pm2Home)}</string>
|
|
106
|
+
</dict>
|
|
107
|
+
<key>StandardErrorPath</key>
|
|
108
|
+
<string>/tmp/com.PM2.err</string>
|
|
109
|
+
<key>StandardOutPath</key>
|
|
110
|
+
<string>/tmp/com.PM2.out</string>
|
|
111
|
+
</dict>
|
|
112
|
+
</plist>
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Return true if the plist on disk matches what we'd write today.
|
|
117
|
+
* Used by --check to bail without rewriting when nothing changed. */
|
|
118
|
+
function plistIsCurrent(plistPath, expected) {
|
|
119
|
+
try {
|
|
120
|
+
const actual = fs.readFileSync(plistPath, 'utf8');
|
|
121
|
+
if (actual === expected) return true;
|
|
122
|
+
// Tolerate trivial whitespace differences.
|
|
123
|
+
return actual.replace(/\s+/g, '') === expected.replace(/\s+/g, '');
|
|
124
|
+
} catch { return false; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True when launchctl reports the com.PM2 agent loaded under
|
|
128
|
+
* this user's GUI session. */
|
|
129
|
+
function pm2AgentLoaded() {
|
|
130
|
+
try {
|
|
131
|
+
const out = execSync('launchctl list', { encoding: 'utf8' });
|
|
132
|
+
return /\bcom\.PM2\b/.test(out);
|
|
133
|
+
} catch { return false; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function ensureMac() {
|
|
137
|
+
const user = process.env.USER || os.userInfo().username;
|
|
138
|
+
if (!user) {
|
|
139
|
+
warn('Could not determine $USER — skipping launchd setup.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const pm2Bin = findPm2();
|
|
143
|
+
if (!pm2Bin) {
|
|
144
|
+
warn('pm2 not found. Install with: npm i -g pm2');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `pm2.${user}.plist`);
|
|
148
|
+
const desired = buildPlist(user, pm2Bin);
|
|
149
|
+
|
|
150
|
+
const upToDate = plistIsCurrent(plistPath, desired);
|
|
151
|
+
const loaded = pm2AgentLoaded();
|
|
152
|
+
|
|
153
|
+
if (CHECK_ONLY) {
|
|
154
|
+
if (upToDate && loaded) {
|
|
155
|
+
log('OK — plist current, launchd agent loaded.');
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
warn(`Needs fixup — plist current=${upToDate}, agent loaded=${loaded}.`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!upToDate) {
|
|
163
|
+
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
164
|
+
fs.writeFileSync(plistPath, desired, 'utf8');
|
|
165
|
+
log(`Wrote ${plistPath}`);
|
|
166
|
+
} else {
|
|
167
|
+
log(`Plist already current at ${plistPath}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Re-bootstrap regardless — covers the case where the file is
|
|
171
|
+
// right but launchd never loaded it. bootout is allowed to fail
|
|
172
|
+
// (agent may not currently be loaded) — that's fine.
|
|
173
|
+
const uid = os.userInfo().uid;
|
|
174
|
+
const tryBootout = spawnSync('launchctl',
|
|
175
|
+
['bootout', `gui/${uid}`, plistPath],
|
|
176
|
+
{ stdio: 'ignore' });
|
|
177
|
+
const tryBootstrap = spawnSync('launchctl',
|
|
178
|
+
['bootstrap', `gui/${uid}`, plistPath],
|
|
179
|
+
{ stdio: 'inherit' });
|
|
180
|
+
if (tryBootstrap.status !== 0) {
|
|
181
|
+
warn('launchctl bootstrap returned non-zero — agent may already '
|
|
182
|
+
+ 'be loaded under a different session. Verify with `launchctl list | grep PM2`.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Save current process list so resurrect has a fresh dump.
|
|
186
|
+
try {
|
|
187
|
+
execFileSync(pm2Bin, ['save'], { stdio: 'inherit' });
|
|
188
|
+
} catch (ex) {
|
|
189
|
+
warn(`pm2 save failed: ${ex.message}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
log('Done. Verify: `launchctl list | grep PM2` should show com.PM2');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function ensureLinux() {
|
|
196
|
+
if (CHECK_ONLY) return process.exit(0);
|
|
197
|
+
warn('Linux: this script only knows macOS. Run `pm2 startup` and '
|
|
198
|
+
+ 'follow its instructions, then `pm2 save`.');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function main() {
|
|
202
|
+
switch (process.platform) {
|
|
203
|
+
case 'darwin': return ensureMac();
|
|
204
|
+
case 'linux': return ensureLinux();
|
|
205
|
+
default:
|
|
206
|
+
if (CHECK_ONLY) return process.exit(0);
|
|
207
|
+
warn(`Platform ${process.platform} not supported by this helper.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try { main(); }
|
|
212
|
+
catch (ex) {
|
|
213
|
+
warn(`Unexpected error: ${ex.message}`);
|
|
214
|
+
// Postinstall context: never fail the install.
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Background migration runner.
|
|
3
|
+
#
|
|
4
|
+
# Polls Supabase every 120s. The moment a `SELECT 1` succeeds against the
|
|
5
|
+
# pooler, runs pg_dump → restore into the local agenticmail_enterprise DB,
|
|
6
|
+
# rewrites enterprise/.env so DATABASE_URL points at localhost, and
|
|
7
|
+
# restarts the enterprise PM2 process so it picks up the new DB.
|
|
8
|
+
#
|
|
9
|
+
# Idempotent: on success it writes a sentinel file and exits 0. If
|
|
10
|
+
# something fails partway (dump corrupted, restore errored), it logs the
|
|
11
|
+
# detail and exits non-zero — the user can re-run by deleting the
|
|
12
|
+
# sentinel and starting the script again.
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
ENV_FILE="/Users/ope/Desktop/projects/agenticmail/enterprise/.env"
|
|
17
|
+
ENV_BACKUP="${ENV_FILE}.pre-localpg"
|
|
18
|
+
LOG_FILE="/Users/ope/Desktop/projects/agenticmail/enterprise/logs/migrate-from-supabase.log"
|
|
19
|
+
DUMP_FILE="/tmp/agenticmail-enterprise-supabase.sql"
|
|
20
|
+
SENTINEL="/Users/ope/Desktop/projects/agenticmail/enterprise/logs/migrate-from-supabase.done"
|
|
21
|
+
|
|
22
|
+
LOCAL_DB="agenticmail_enterprise"
|
|
23
|
+
LOCAL_URL="postgresql://ope@localhost:5432/${LOCAL_DB}"
|
|
24
|
+
|
|
25
|
+
# Try both pooler hostnames the Supabase project may be reachable on.
|
|
26
|
+
# Session pooler (port 5432) is preferred for pg_dump — pgbouncer in
|
|
27
|
+
# transaction mode (6543) chokes on prepared statements.
|
|
28
|
+
SUPABASE_TXN="postgresql://postgres.ziurzgoffaexxgjmfjph:MK6PHWIpjDO0cPwU@aws-1-us-east-2.pooler.supabase.com:6543/postgres"
|
|
29
|
+
SUPABASE_SESSION="postgresql://postgres.ziurzgoffaexxgjmfjph:MK6PHWIpjDO0cPwU@aws-1-us-east-2.pooler.supabase.com:5432/postgres"
|
|
30
|
+
SUPABASE_DIRECT="postgresql://postgres:MK6PHWIpjDO0cPwU@db.ziurzgoffaexxgjmfjph.supabase.co:5432/postgres"
|
|
31
|
+
|
|
32
|
+
RETRY_INTERVAL_SECS=120
|
|
33
|
+
MAX_RETRIES=720 # 720 * 2 min = ~24 h budget
|
|
34
|
+
|
|
35
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
36
|
+
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" >&2; }
|
|
37
|
+
|
|
38
|
+
if [[ -f "$SENTINEL" ]]; then
|
|
39
|
+
log "Sentinel exists at $SENTINEL — migration already completed. Exiting."
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
log "Starting migration runner. Will retry every ${RETRY_INTERVAL_SECS}s for up to ${MAX_RETRIES} attempts."
|
|
44
|
+
|
|
45
|
+
# Pick the first URL that returns a successful SELECT 1. Echo the URL,
|
|
46
|
+
# or empty string if all fail.
|
|
47
|
+
pick_reachable_url() {
|
|
48
|
+
for url in "$SUPABASE_SESSION" "$SUPABASE_DIRECT" "$SUPABASE_TXN"; do
|
|
49
|
+
if PGCONNECT_TIMEOUT=8 psql "$url" -tAc "SELECT 1;" >/dev/null 2>&1; then
|
|
50
|
+
echo "$url"
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
return 1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
attempt=0
|
|
58
|
+
while (( attempt < MAX_RETRIES )); do
|
|
59
|
+
attempt=$(( attempt + 1 ))
|
|
60
|
+
|
|
61
|
+
if reachable_url=$(pick_reachable_url); then
|
|
62
|
+
log "Attempt $attempt: Supabase reachable via $(echo "$reachable_url" | sed 's/:[^@]*@/:****@/')"
|
|
63
|
+
break
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
log "Attempt $attempt: Supabase still unreachable — sleeping ${RETRY_INTERVAL_SECS}s"
|
|
67
|
+
sleep "$RETRY_INTERVAL_SECS"
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
if [[ -z "${reachable_url:-}" ]]; then
|
|
71
|
+
log "ERROR: exhausted ${MAX_RETRIES} attempts without ever reaching Supabase. Giving up."
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# pg_dump preference: session pooler (5432) or direct, since pgbouncer
|
|
76
|
+
# transaction-mode (6543) breaks pg_dump. If we landed on the txn pooler,
|
|
77
|
+
# switch to session pooler for the actual dump.
|
|
78
|
+
dump_url="$reachable_url"
|
|
79
|
+
if [[ "$dump_url" == *":6543/postgres" ]]; then
|
|
80
|
+
log "Reachable URL is txn pooler — switching to session pooler for the dump"
|
|
81
|
+
dump_url="$SUPABASE_SESSION"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
log "Running pg_dump → $DUMP_FILE"
|
|
85
|
+
if ! pg_dump \
|
|
86
|
+
--no-owner --no-acl \
|
|
87
|
+
--no-publications --no-subscriptions \
|
|
88
|
+
--schema=public \
|
|
89
|
+
--quote-all-identifiers \
|
|
90
|
+
--verbose \
|
|
91
|
+
--file="$DUMP_FILE" \
|
|
92
|
+
"$dump_url" 2>>"$LOG_FILE"; then
|
|
93
|
+
log "ERROR: pg_dump failed. Inspect $LOG_FILE for the tail of pg_dump stderr."
|
|
94
|
+
exit 3
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
dump_bytes=$(stat -f%z "$DUMP_FILE" 2>/dev/null || stat -c%s "$DUMP_FILE")
|
|
98
|
+
log "pg_dump complete — $(printf '%d\n' "$dump_bytes") bytes"
|
|
99
|
+
|
|
100
|
+
# Wipe + restore. The local DB was created empty just before this
|
|
101
|
+
# script ran, so a clean restore is safe.
|
|
102
|
+
log "Resetting public schema on local DB and restoring"
|
|
103
|
+
psql "$LOCAL_URL" -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;" >>"$LOG_FILE" 2>&1 || {
|
|
104
|
+
log "ERROR: could not reset public schema on local DB"
|
|
105
|
+
exit 4
|
|
106
|
+
}
|
|
107
|
+
if ! psql --quiet --single-transaction \
|
|
108
|
+
--set ON_ERROR_STOP=on \
|
|
109
|
+
"$LOCAL_URL" -f "$DUMP_FILE" >>"$LOG_FILE" 2>&1; then
|
|
110
|
+
log "ERROR: psql restore failed. Inspect $LOG_FILE for details."
|
|
111
|
+
exit 5
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Quick sanity check: count tables + rows in each (top 5 by row count).
|
|
115
|
+
log "Restore complete. Verifying:"
|
|
116
|
+
psql "$LOCAL_URL" -c "
|
|
117
|
+
SELECT schemaname, relname AS table, n_live_tup AS rows
|
|
118
|
+
FROM pg_stat_user_tables
|
|
119
|
+
ORDER BY n_live_tup DESC NULLS LAST
|
|
120
|
+
LIMIT 20;
|
|
121
|
+
" >>"$LOG_FILE" 2>&1 || true
|
|
122
|
+
|
|
123
|
+
# Rewrite .env atomically. Keep a one-shot backup so the original
|
|
124
|
+
# Supabase URL is recoverable if anything explodes.
|
|
125
|
+
log "Backing up .env → $ENV_BACKUP and rewriting DATABASE_URL → local"
|
|
126
|
+
cp -p "$ENV_FILE" "$ENV_BACKUP"
|
|
127
|
+
# Use a sentinel marker line in the replacement so a re-run can be
|
|
128
|
+
# detected; perl in-place edit because BSD sed is annoying on macOS.
|
|
129
|
+
perl -i -pe 'BEGIN{$u=$ENV{LOCAL_URL}} s|^DATABASE_URL=.*$|DATABASE_URL=$u|' \
|
|
130
|
+
LOCAL_URL="$LOCAL_URL" "$ENV_FILE"
|
|
131
|
+
if ! grep -q "^DATABASE_URL=${LOCAL_URL}$" "$ENV_FILE"; then
|
|
132
|
+
log "ERROR: .env rewrite did not take effect. Restoring backup."
|
|
133
|
+
cp -p "$ENV_BACKUP" "$ENV_FILE"
|
|
134
|
+
exit 6
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
log "Restarting enterprise PM2 process"
|
|
138
|
+
if ! pm2 restart enterprise >>"$LOG_FILE" 2>&1; then
|
|
139
|
+
log "ERROR: pm2 restart failed. Inspect $LOG_FILE."
|
|
140
|
+
exit 7
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Give it a few seconds to start and verify health.
|
|
144
|
+
sleep 8
|
|
145
|
+
if curl -sS -o /dev/null -w "%{http_code}" http://127.0.0.1:3100/ | grep -qE "^(2|3)"; then
|
|
146
|
+
log "Enterprise responding on http://127.0.0.1:3100 — migration successful."
|
|
147
|
+
else
|
|
148
|
+
log "WARN: enterprise restarted but http://127.0.0.1:3100 not yet returning 2xx/3xx. Tail logs/enterprise-error.log."
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
date > "$SENTINEL"
|
|
152
|
+
log "Wrote sentinel $SENTINEL. Done."
|
|
153
|
+
exit 0
|