Lines 1-85javascript
3 * Constella self-updater — a STANDALONE script that stops the running server, installs the latest npm
4 * release, and relaunches. It runs as its own process (NOT a child the server can take down with it), so
5 * it can kill every node process the server owns, replace the in-use package files, and bring Constella
6 * back on the new version. Two callers:
8 * • the in-app "Update now" button → spawns this detached + hidden with `--quiet`;
9 * • `constella update` → runs it inline so you can update by hand from any terminal.
11 * Order matters: it INSTALLS FIRST, with the server still running. A live Constella does NOT lock the
12 * global package files (verified on Windows — `npm i -g` succeeds with the server up), so installing first
13 * just works, and if it can't we haven't taken the host down. Only THEN does it restart the server to load
14 * the new code: stop the process tree (the launcher AND its web + worker children — on Windows
15 * `process.kill` is an uncatchable terminate that does NOT cascade, so they're killed explicitly by PID,
16 * launcher first so its supervisor can't resurrect them) and relaunch. Stopping the server BEFORE npm is
17 * what made earlier updates fail ("installs but loops, never lands"); it survives here only as a fallback
18 * for a host where a live process really does hold the files. POSIX restarts via a SIGTERM cascade.
20 * The running server is discovered from ~/.constella/run.json (written by the launcher) or, failing that,
21 * from whoever is LISTENing on the port — so a manual run recovers a stuck instance of ANY version.
24 * node bin/constella-update.mjs # auto-detect the running server, update, relaunch
25 * node bin/constella-update.mjs --mode start --port 3000
27import { spawnSync, spawn, execFileSync } from "node:child_process";
28import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
HighRuntime Package Install
Package source invokes a package manager install command at runtime.
bin/constella-update.mjsView on unpkg · L11 HighChild Process
Package source references child process execution.
bin/constella-update.mjsView on unpkg · L26 29import { join } from "node:path";
30import { homedir, tmpdir } from "node:os";
32const PKG = "constellai";
33const WIN = process.platform === "win32";
35// CRITICAL: run from a directory OUTSIDE the package being replaced. npm's global install is atomic — it
36// RENAMES the existing `…/node_modules/constellai` dir aside before swapping in the new one, and on Windows
37// you cannot rename a directory tree that contains the npm process's own current working directory. This
38// updater is spawned by the web server, whose cwd is the install dir (`next start` runs with cwd=PKG_ROOT),
39// and npm inherits that cwd → `EBUSY: rename …/constellai`. Stepping out to the OS temp dir lets the rename
40// succeed even with the server fully up. (The real fix for the "installs but loops" bug — not killing files.)
41const SAFE_CWD = tmpdir();
42try { process.chdir(SAFE_CWD); } catch { /* keep current cwd if temp is unavailable */ }
43const has = (n) => process.argv.includes(n);
44const arg = (n, d) => { const i = process.argv.indexOf(n); return i >= 0 ? process.argv[i + 1] : d; };
46const HOME = process.env.CONSTELLA_HOME || arg("--home") || join(homedir(), ".constella");
47const RESULT = join(HOME, "backups", "last-update.json");
48const PIDFILE = join(HOME, "run.json");
49const QUIET = has("--quiet"); // in-app run is detached → no stdout
52try { state = JSON.parse(readFileSync(PIDFILE, "utf8")); } catch { /* no pidfile → discover by port */ }
53const MODE = arg("--mode") || process.env.CONSTELLA_RUN_MODE || state.mode || "start";
54const PORT = String(arg("--port") || process.env.PORT || state.port || "3000");
55let LAUNCHER = Number(arg("--pid") || process.env.CONSTELLA_LAUNCHER_PID || state.launcherPid || 0);
57const log = (...a) => { if (!QUIET) console.log(...a); };
58const sleep = (ms) => {
59 try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); return; } catch { /* no SAB → busy-wait */ }
60 // Fallback when SharedArrayBuffer is unavailable (sandbox / hardened runtime): a real blocking wait. The old
61 // no-op return made the graceful-shutdown loops spin instantly → an immediate SIGKILL that skipped run.json
62 // cleanup + the child-kill cascade.
63 const end = Date.now() + ms; while (Date.now() < end) { /* block */ }
65const alive = (p) => { try { process.kill(p, 0); return true; } catch { return false; } };
66const psout = (s) => { try { return execFileSync("powershell", ["-NoProfile", "-Command", s], { timeout: 9000, windowsHide: true }).toString(); } catch { return ""; } };
67const ints = (s) => s.split(/\r?\n/).map((x) => +x.trim()).filter((n) => n > 0);
68function result(o) { try { mkdirSync(join(HOME, "backups"), { recursive: true }); writeFileSync(RESULT, JSON.stringify({ ...o, at: new Date().toISOString() })); } catch { /* best-effort */ } }
70// Windows process helpers (no /T anywhere — we kill by explicit PID so we never terminate ourselves).
71const winKids = (pid) => ints(psout(`Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${pid} } | Select-Object -ExpandProperty ProcessId`));
72const winParent = (pid) => ints(psout(`Get-CimInstance Win32_Process -Filter "ProcessId=${pid}" | Select-Object -ExpandProperty ParentProcessId`))[0] || 0;
73const winPortPid = (port) => ints(psout(`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess`))[0] || 0;
74function kill(pid) { if (!pid) return; try { if (WIN) execFileSync("taskkill", ["/F", "/PID", String(pid)], { stdio: "ignore", windowsHide: true }); else process.kill(pid, "SIGTERM"); } catch { /* already gone */ } }
76/** Stop the running Constella so `npm i -g` can overwrite the package. Returns true if it killed one. */
77function stopServer() {
79 // Resolve the launcher: an explicit/persisted pid, else derive it from the port listener (web → parent).
80 if (!LAUNCHER || !alive(LAUNCHER)) { const web = winPortPid(PORT); if (web) LAUNCHER = winParent(web) || web; }
81 if (!LAUNCHER) { log("• No running server found — updating in place."); return false; }
82 log(`• Stopping Constella (launcher pid ${LAUNCHER})…`);
83 const kids = winKids(LAUNCHER); // web + worker — capture BEFORE the launcher dies
84 kill(LAUNCHER); // launcher first → its supervisor can't auto-restart the children
85 for (const k of kids) kill(k); // then the orphaned web + worker (release the native SQLite lock)