@join-pantheon/cli 0.31.2-rc.1 → 0.32.0-rc.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/README.md +4 -1
- package/dist/claude-confine.js +12 -2
- package/dist/cli.js +56 -17
- package/dist/doctor.js +64 -6
- package/dist/helpers-build.js +69 -1
- package/dist/install.js +25 -11
- package/dist/installer.js +204 -0
- package/dist/machine.js +15 -1
- package/dist/managed.js +238 -0
- package/dist/mcp-supervisor.js +2 -1
- package/dist/measure.js +2 -1
- package/dist/notifier.js +7 -17
- package/dist/notify.js +7 -1
- package/dist/relay.js +67 -6
- package/dist/service.js +14 -6
- package/dist/tray.js +5 -10
- package/dist/uninstall.js +194 -0
- package/dist/upgrade.js +44 -5
- package/dist/which.js +11 -3
- package/dist/worker-policy.js +2 -0
- package/dist/worker.js +30 -7
- package/helper/macos/prebuilt/manifest.json +29 -0
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/macos/prebuilt/pantheon-tray +0 -0
- package/installer/install.ps1 +547 -0
- package/installer/install.sh +612 -0
- package/package.json +8 -3
package/dist/relay.js
CHANGED
|
@@ -6,6 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
import { WebSocketServer } from "ws";
|
|
7
7
|
import { badRemote, Inbound, isContinuationFor, LIMITS, newId, PROTOCOL_VERSION, RELAY_FEATURES, SENDABLE, shouldDeferToHuman, } from "./protocol.js";
|
|
8
8
|
import { VERSION } from "./version.js";
|
|
9
|
+
import { installerScript, InstallerGate } from "./installer.js";
|
|
10
|
+
import { installerLine } from "./managed.js";
|
|
9
11
|
import { Ledger, SpaceFull } from "./ledger.js";
|
|
10
12
|
import { fingerprint, newNonce, verifyChallenge, verifyActivity, verifyAccount, verifyRead, actPayload, goalPayload, verifyBytes, verifyDelegate, canonicalJson } from "./identity.js";
|
|
11
13
|
import { Accounts, normalizeUsername, validEmail, validUsername } from "./accounts.js";
|
|
@@ -434,6 +436,7 @@ export class Relay {
|
|
|
434
436
|
authEpoch = 0;
|
|
435
437
|
rotateOnRemove;
|
|
436
438
|
backupTimer;
|
|
439
|
+
installerGate;
|
|
437
440
|
get features() { return this.opts.features ?? RELAY_FEATURES; }
|
|
438
441
|
constructor(opts = {}) {
|
|
439
442
|
this.opts = opts;
|
|
@@ -471,6 +474,7 @@ export class Relay {
|
|
|
471
474
|
spaceMinAgeMs: opts.spaceLimits?.spaceMinAgeMs ?? LIMITS.spaceMinAgeMs,
|
|
472
475
|
};
|
|
473
476
|
this.rotateOnRemove = opts.rotateOnRemove ?? process.env.PANTHEON_ROTATE_ON_REMOVE === "1";
|
|
477
|
+
this.installerGate = opts.installerGate ?? new InstallerGate({ log: this.log });
|
|
474
478
|
this.busyTtlMs = opts.activityBusyTtlMs ?? LIMITS.activityBusyTtlMs;
|
|
475
479
|
this.pruneMs = opts.activityPruneMs ?? LIMITS.activityPruneMs;
|
|
476
480
|
const readsEnv = (process.env.PANTHEON_READS ?? "").trim().toLowerCase();
|
|
@@ -510,6 +514,7 @@ export class Relay {
|
|
|
510
514
|
}
|
|
511
515
|
if (this.backupTimer)
|
|
512
516
|
clearInterval(this.backupTimer);
|
|
517
|
+
this.installerGate.close();
|
|
513
518
|
if (this.accounts && !this.opts.accounts)
|
|
514
519
|
this.accounts.close();
|
|
515
520
|
if (this.heartbeat)
|
|
@@ -596,6 +601,12 @@ export class Relay {
|
|
|
596
601
|
: {}),
|
|
597
602
|
});
|
|
598
603
|
}
|
|
604
|
+
if (url.pathname === "/install.sh" || url.pathname === "/install.ps1") {
|
|
605
|
+
if (req.method !== "GET" && req.method !== "HEAD")
|
|
606
|
+
return this.json(res, 405, { error: "GET only" });
|
|
607
|
+
void this.serveInstaller(req, res, url.pathname.slice(1));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
599
610
|
if (url.pathname === "/join") {
|
|
600
611
|
if (!this.authRateOk(ip))
|
|
601
612
|
return this.json(res, 429, { error: "too many lookups from your network — wait a minute" });
|
|
@@ -730,7 +741,7 @@ export class Relay {
|
|
|
730
741
|
}
|
|
731
742
|
onAuthHttp(req, res, url, ip) {
|
|
732
743
|
if (url.pathname === "/")
|
|
733
|
-
return this.landingPage(res);
|
|
744
|
+
return this.landingPage(req, res);
|
|
734
745
|
if (url.pathname === "/admin" || url.pathname === "/admin/space" || url.pathname === "/admin/export" || url.pathname === "/admin/backup" || url.pathname === "/admin/action" || url.pathname === "/admin/login" || url.pathname === "/admin/logout" || url.pathname === "/admin/report") {
|
|
735
746
|
const A0 = this.accounts;
|
|
736
747
|
if (!A0 || !this.adminToken)
|
|
@@ -2318,16 +2329,66 @@ ${url.searchParams.get("done") ? `<div class=done role=status>${esc(url.searchPa
|
|
|
2318
2329
|
}
|
|
2319
2330
|
catch { } });
|
|
2320
2331
|
}
|
|
2321
|
-
landingPage(res) {
|
|
2332
|
+
landingPage(req, res) {
|
|
2333
|
+
const host = String(req.headers.host ?? "").replace(/[^A-Za-z0-9.:\[\]-]/g, "");
|
|
2334
|
+
const proto = String(req.headers["x-forwarded-proto"] ?? "").split(",")[0].trim() === "https" ? "https" : "http";
|
|
2335
|
+
const base = host ? `${proto}://${host}` : null;
|
|
2336
|
+
const sh = installerLine(base, "sh", ["start"]), win = installerLine(base, "windows", ["start"]);
|
|
2337
|
+
const esc = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2322
2338
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2323
2339
|
res.end(`<!doctype html><meta charset=utf-8><title>pantheon</title>
|
|
2324
|
-
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:
|
|
2340
|
+
<style>body{font:16px/1.5 system-ui,sans-serif;max-width:40rem;margin:12vh auto;padding:0 1.2rem;color:#222}code,pre{background:#f4f4f5;border-radius:5px}code{padding:2px 6px}pre{padding:.6rem .8rem;overflow-x:auto}h1{font-size:1.6rem}</style>
|
|
2325
2341
|
<h1>pantheon</h1>
|
|
2326
2342
|
<p>Your coding agent and your teammates' agents, coordinating across machines.</p>
|
|
2327
|
-
|
|
2343
|
+
${sh && win ? `<p>This is a relay endpoint, not a website. To start a team, paste one line into a terminal on your own computer. It brings its own Node, needs no admin rights, and asks for your email and a 6-digit code.</p>
|
|
2344
|
+
<p>Mac or Linux (Terminal):</p>
|
|
2345
|
+
<pre><code>${esc(sh)}</code></pre>
|
|
2346
|
+
<p>Windows (Command Prompt or PowerShell):</p>
|
|
2347
|
+
<pre><code>${esc(win)}</code></pre>
|
|
2348
|
+
<p>Invited to a team? Use the line in your invite instead. Already have Node.js 22.13 or newer: <code>npm install -g @join-pantheon/cli</code>, then <code>pantheon start</code>.</p>` : `<p>This is a relay endpoint, not a website. With Node.js 22.13 or newer, install the CLI and start a team:</p>
|
|
2328
2349
|
<pre><code>npm install -g @join-pantheon/cli
|
|
2329
|
-
pantheon
|
|
2330
|
-
<p>
|
|
2350
|
+
pantheon start</code></pre>
|
|
2351
|
+
<p>It asks for your email and sends you a 6-digit code. Invited to a team? Use the line in your invite.</p>`}`);
|
|
2352
|
+
}
|
|
2353
|
+
startInstallerCheck() { this.installerGate.start(); }
|
|
2354
|
+
async serveInstaller(req, res, name) {
|
|
2355
|
+
const plain = (code, body, extra = {}) => {
|
|
2356
|
+
res.writeHead(code, {
|
|
2357
|
+
"content-type": "text/plain; charset=utf-8",
|
|
2358
|
+
"content-length": Buffer.byteLength(body),
|
|
2359
|
+
"cache-control": "no-store",
|
|
2360
|
+
"x-content-type-options": "nosniff",
|
|
2361
|
+
...extra,
|
|
2362
|
+
});
|
|
2363
|
+
res.end(req.method === "HEAD" ? undefined : body);
|
|
2364
|
+
};
|
|
2365
|
+
try {
|
|
2366
|
+
const choice = await this.installerGate.choose();
|
|
2367
|
+
if (res.writableEnded || res.destroyed)
|
|
2368
|
+
return;
|
|
2369
|
+
if ("unavailable" in choice) {
|
|
2370
|
+
const sentence = `pantheon install failed: ${choice.unavailable}`.replace(/'/g, "");
|
|
2371
|
+
return plain(200, name === "install.sh"
|
|
2372
|
+
? `#!/bin/sh\nprintf '%s\\n' '${sentence}' >&2\nexit 1\n`
|
|
2373
|
+
: `Write-Host '${sentence}' -ForegroundColor Red\nthrow 'Pantheon was not installed.'\n`, { "retry-after": "120", "x-pantheon-installer": "unavailable" });
|
|
2374
|
+
}
|
|
2375
|
+
const body = installerScript(name, undefined, choice.version);
|
|
2376
|
+
if (body === null) {
|
|
2377
|
+
const sentence = "pantheon install failed: this relay cannot serve the installer (its installer files or version are missing). Install with npm instead: npm install -g @join-pantheon/cli";
|
|
2378
|
+
return plain(200, name === "install.sh" ? `#!/bin/sh\nprintf '%s\\n' '${sentence}' >&2\nexit 1\n` : `Write-Host '${sentence}' -ForegroundColor Red\nthrow 'Pantheon was not installed.'\n`, { "x-pantheon-installer": "unavailable" });
|
|
2379
|
+
}
|
|
2380
|
+
plain(200, body, { "x-pantheon-installs": choice.version });
|
|
2381
|
+
}
|
|
2382
|
+
catch (e) {
|
|
2383
|
+
this.log(`! ${name} failed: ${e?.message ?? e}`);
|
|
2384
|
+
try {
|
|
2385
|
+
if (!res.headersSent)
|
|
2386
|
+
this.json(res, 500, { error: "the relay could not handle that request" });
|
|
2387
|
+
else
|
|
2388
|
+
res.end();
|
|
2389
|
+
}
|
|
2390
|
+
catch { }
|
|
2391
|
+
}
|
|
2331
2392
|
}
|
|
2332
2393
|
json(res, code, body) {
|
|
2333
2394
|
const s = JSON.stringify(body);
|
package/dist/service.js
CHANGED
|
@@ -2,9 +2,10 @@ import { writeFileSync, mkdirSync, existsSync, unlinkSync, readFileSync } from "
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { homedir, platform, userInfo } from "node:os";
|
|
4
4
|
import { which } from "./which.js";
|
|
5
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { launcher } from "./install.js";
|
|
8
|
+
import { childNodeFlags, managedLauncher, managedLayout } from "./managed.js";
|
|
8
9
|
import { CONFIG_DIR } from "./config.js";
|
|
9
10
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const LABEL = (kind) => `dev.pantheon.${kind}`;
|
|
@@ -45,8 +46,10 @@ export const serviceLogFile = (kind) => join(serviceLogDir(), `${kind}.log`);
|
|
|
45
46
|
const SERVICE_BASE_PATH = [
|
|
46
47
|
"/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin",
|
|
47
48
|
];
|
|
48
|
-
export function servicePath(envPath = process.env.PATH ?? "") {
|
|
49
|
-
|
|
49
|
+
export function servicePath(envPath = process.env.PATH ?? "", managed = managedLayout()) {
|
|
50
|
+
const first = managed ? [managed.binDir] : [];
|
|
51
|
+
const last = managed ? [posix.dirname(managed.node)] : [];
|
|
52
|
+
return [...new Set([...first, ...envPath.split(":"), ...SERVICE_BASE_PATH, ...last].filter(Boolean))].join(":");
|
|
50
53
|
}
|
|
51
54
|
export function servicesUnavailable(plat = platform()) {
|
|
52
55
|
if (plat !== "win32")
|
|
@@ -99,14 +102,19 @@ export function existingUnitPath(kind, env = systemdUserEnv(), shellEnv = proces
|
|
|
99
102
|
function xml(s) {
|
|
100
103
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
101
104
|
}
|
|
102
|
-
function argvFor(kind, args) {
|
|
105
|
+
function argvFor(kind, args, managed = managedLauncher()) {
|
|
106
|
+
if (managed)
|
|
107
|
+
return [managed, kind, ...args];
|
|
103
108
|
const l = launcher();
|
|
104
109
|
if (!l.portable)
|
|
105
110
|
return [l.command, ...l.args.slice(0, -1), kind, ...args];
|
|
106
111
|
const abs = whichPantheon();
|
|
107
|
-
return abs ? [abs, kind, ...args] : [process.execPath, resolve(HERE, "cli.js"), kind, ...args];
|
|
112
|
+
return abs ? [abs, kind, ...args] : [process.execPath, ...childNodeFlags(), resolve(HERE, "cli.js"), kind, ...args];
|
|
108
113
|
}
|
|
109
|
-
export
|
|
114
|
+
export const serviceArgv = (kind, args, managed) => argvFor(kind, args, managed);
|
|
115
|
+
export function whichPantheon(find = which, managed = managedLauncher()) {
|
|
116
|
+
if (managed)
|
|
117
|
+
return managed;
|
|
110
118
|
const p = find("pantheon");
|
|
111
119
|
if (!p)
|
|
112
120
|
return null;
|
package/dist/tray.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { loadPolicy } from "./worker-policy.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { helperStale, installHelper } from "./helpers-build.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
4
|
import { platform } from "node:os";
|
|
6
5
|
import { dirname, join, resolve } from "node:path";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { CONFIG_DIR, load, loadAll, configFor } from "./config.js";
|
|
9
8
|
import { workerProc, notifierProc } from "./machine.js";
|
|
10
9
|
import { agentHookStates } from "./notify.js";
|
|
11
|
-
import { requireDeveloperTools } from "./notifier.js";
|
|
12
10
|
import { plistPath, serviceInstalled, whichPantheon } from "./service.js";
|
|
13
11
|
import { VERSION } from "./version.js";
|
|
14
12
|
import { LIMITS } from "./protocol.js";
|
|
@@ -18,7 +16,8 @@ import { loadProfile, SURFACE_BUNDLE_IDS } from "./profile.js";
|
|
|
18
16
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
17
|
export const trayBinaryPath = (home = CONFIG_DIR) => join(home, "bin", "pantheon-tray");
|
|
20
18
|
export const traySourcePath = () => resolve(HERE, "..", "helper", "macos", "menubar.swift");
|
|
21
|
-
export
|
|
19
|
+
export const TRAY_PREBUILT = "pantheon-tray";
|
|
20
|
+
export function ensureTrayBinary(home = CONFIG_DIR, deps = {}) {
|
|
22
21
|
if (platform() !== "darwin")
|
|
23
22
|
throw new Error("the pantheon menu bar is currently macOS-only");
|
|
24
23
|
const source = traySourcePath();
|
|
@@ -26,11 +25,7 @@ export function ensureTrayBinary(home = CONFIG_DIR) {
|
|
|
26
25
|
throw new Error(`menu bar source is missing: ${source}`);
|
|
27
26
|
const binary = trayBinaryPath(home);
|
|
28
27
|
mkdirSync(dirname(binary), { recursive: true });
|
|
29
|
-
|
|
30
|
-
execFileSync("/usr/bin/swiftc", ["-O", source, "-o", binary], { stdio: "pipe" });
|
|
31
|
-
chmodSync(binary, 0o755);
|
|
32
|
-
recordHelperBuild(binary, source);
|
|
33
|
-
return binary;
|
|
28
|
+
return installHelper({ name: TRAY_PREBUILT, source, binary, what: "the menu bar", service: "menubar" }, deps).binary;
|
|
34
29
|
}
|
|
35
30
|
export function trayStale(home = CONFIG_DIR) { return helperStale(trayBinaryPath(home), traySourcePath()); }
|
|
36
31
|
export function writeTrayCommands(home = CONFIG_DIR) {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
import { expectedLauncher } from "./managed.js";
|
|
6
|
+
export const BEGIN_MARK = "# >>> pantheon >>>";
|
|
7
|
+
export const END_MARK = "# <<< pantheon <<<";
|
|
8
|
+
export const PROFILES = [".profile", ".bash_profile", ".bashrc", ".zshrc", ".zprofile"];
|
|
9
|
+
export const FISH_CONF = ".config/fish/conf.d/pantheon.fish";
|
|
10
|
+
export function withoutPathBlock(text) {
|
|
11
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
12
|
+
const bare = (l) => l.replace(/\n$/, "").replace(/\r$/, "");
|
|
13
|
+
const out = [];
|
|
14
|
+
let blocks = 0;
|
|
15
|
+
for (let i = 0; i < lines.length; i++) {
|
|
16
|
+
if (bare(lines[i]) !== BEGIN_MARK) {
|
|
17
|
+
out.push(lines[i]);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
let end = i + 1;
|
|
21
|
+
while (end < lines.length && bare(lines[end]) !== END_MARK)
|
|
22
|
+
end++;
|
|
23
|
+
if (end >= lines.length)
|
|
24
|
+
return { error: `a "${BEGIN_MARK}" line with no "${END_MARK}" after it` };
|
|
25
|
+
if (out.length && bare(out[out.length - 1]) === "")
|
|
26
|
+
out.pop();
|
|
27
|
+
blocks++;
|
|
28
|
+
i = end;
|
|
29
|
+
}
|
|
30
|
+
return blocks ? { text: out.join(""), blocks } : null;
|
|
31
|
+
}
|
|
32
|
+
export function expandWindows(s, env) {
|
|
33
|
+
const keys = Object.keys(env);
|
|
34
|
+
return s.replace(/%([^%;]+)%/g, (all, name) => {
|
|
35
|
+
const k = keys.find((x) => x.toLowerCase() === name.toLowerCase());
|
|
36
|
+
return k !== undefined && env[k] !== undefined ? env[k] : all;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function withoutPathEntry(raw, binDir, env) {
|
|
40
|
+
const norm = (p) => expandWindows(p, env).trim().replace(/\\+$/, "").toLowerCase();
|
|
41
|
+
const want = norm(binDir);
|
|
42
|
+
const parts = raw.split(";");
|
|
43
|
+
const kept = parts.filter((p) => p === "" || norm(p) !== want);
|
|
44
|
+
return kept.length === parts.length ? null : kept.join(";");
|
|
45
|
+
}
|
|
46
|
+
export function registryUserPath(run = (script, env) => execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 60_000, windowsHide: true, env })) {
|
|
47
|
+
return {
|
|
48
|
+
read: () => {
|
|
49
|
+
const out = run("$k = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment'); " +
|
|
50
|
+
"if ($null -eq $k) { 'NONE'; exit 0 }; " +
|
|
51
|
+
"try { $v = $k.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) } finally { $k.Close() }; " +
|
|
52
|
+
"if ($null -eq $v) { 'NONE' } else { 'B64:' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes([string]$v)) }", process.env).trim();
|
|
53
|
+
if (out === "NONE")
|
|
54
|
+
return null;
|
|
55
|
+
const m = /B64:([A-Za-z0-9+/=]*)/.exec(out);
|
|
56
|
+
if (!m)
|
|
57
|
+
throw new Error(`the user PATH could not be read (${out.split("\n")[0] || "no answer"})`);
|
|
58
|
+
return Buffer.from(m[1], "base64").toString("utf8");
|
|
59
|
+
},
|
|
60
|
+
write: (value) => {
|
|
61
|
+
run("$v = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($env:PANTHEON_USER_PATH_B64)); " +
|
|
62
|
+
"$k = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment'); " +
|
|
63
|
+
"try { if ($v -eq '') { $k.DeleteValue('Path', $false) } else { $k.SetValue('Path', $v, [Microsoft.Win32.RegistryValueKind]::ExpandString) } } finally { $k.Close() }; " +
|
|
64
|
+
"[Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', '1', 'User'); " +
|
|
65
|
+
"[Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', $null, 'User')", { ...process.env, PANTHEON_USER_PATH_B64: Buffer.from(value, "utf8").toString("base64") });
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export const realUninstallFs = {
|
|
70
|
+
exists: (p) => existsSync(p),
|
|
71
|
+
isSymlink: (p) => { try {
|
|
72
|
+
return lstatSync(p).isSymbolicLink();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
} },
|
|
77
|
+
read: (p) => readFileSync(p, "utf8"),
|
|
78
|
+
write: (p, text) => writeFileSync(p, text),
|
|
79
|
+
remove: (p) => rmSync(p, { recursive: true, force: true }),
|
|
80
|
+
removeIfEmpty: (p) => { try {
|
|
81
|
+
if (!readdirSync(p).length)
|
|
82
|
+
rmdirSync(p);
|
|
83
|
+
}
|
|
84
|
+
catch { } },
|
|
85
|
+
};
|
|
86
|
+
export function managedInstall(home, os = platform(), env = process.env, fs = realUninstallFs) {
|
|
87
|
+
const launcher = expectedLauncher(home, os, env);
|
|
88
|
+
if (!launcher)
|
|
89
|
+
return null;
|
|
90
|
+
const p = os === "win32" ? win32 : posix;
|
|
91
|
+
const binDir = p.dirname(launcher);
|
|
92
|
+
const root = p.dirname(binDir);
|
|
93
|
+
const runtime = p.join(root, "runtime");
|
|
94
|
+
const log = os === "win32" ? p.join(root, "install.log") : p.join(root, "logs", "install.log");
|
|
95
|
+
return { os, root, runtime, binDir, launcher, log, present: fs.exists(runtime) || fs.exists(launcher) };
|
|
96
|
+
}
|
|
97
|
+
export function removeManagedInstall(o) {
|
|
98
|
+
const fs = o.fs ?? realUninstallFs;
|
|
99
|
+
const env = o.env ?? process.env;
|
|
100
|
+
const { install: m } = o;
|
|
101
|
+
const removed = [], left = [];
|
|
102
|
+
const p = m.os === "win32" ? win32 : posix;
|
|
103
|
+
if (m.os === "win32") {
|
|
104
|
+
try {
|
|
105
|
+
const io = o.userPath ?? registryUserPath();
|
|
106
|
+
const raw = io.read();
|
|
107
|
+
const next = raw === null ? null : withoutPathEntry(raw, m.binDir, env);
|
|
108
|
+
if (next !== null) {
|
|
109
|
+
io.write(next);
|
|
110
|
+
removed.push(`${m.binDir} from your user PATH (the other entries as they were)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
left.push(`${m.binDir} in your user PATH — could not change it (${e.message.split("\n")[0]}); remove it in Settings > System > About > Advanced system settings > Environment Variables`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
for (const name of PROFILES) {
|
|
119
|
+
const f = p.join(o.home, name);
|
|
120
|
+
if (!fs.exists(f))
|
|
121
|
+
continue;
|
|
122
|
+
let text;
|
|
123
|
+
try {
|
|
124
|
+
text = fs.read(f);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
left.push(`the PATH block in ${f} — could not read it (${e.message.split("\n")[0]})`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const r = withoutPathBlock(text);
|
|
131
|
+
if (!r)
|
|
132
|
+
continue;
|
|
133
|
+
if ("error" in r) {
|
|
134
|
+
left.push(`${f} — ${r.error}, so it was left as it is; delete the pantheon lines yourself`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (r.text === "" && !fs.isSymlink(f)) {
|
|
139
|
+
fs.remove(f);
|
|
140
|
+
removed.push(`${f} (it held only Pantheon's PATH lines)`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
fs.write(f, r.text);
|
|
144
|
+
removed.push(`the PATH lines in ${f}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
left.push(`the PATH block in ${f} — could not write it (${e.message.split("\n")[0]}); delete the lines from "${BEGIN_MARK}" to "${END_MARK}" yourself`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const fish = p.join(o.home, FISH_CONF);
|
|
152
|
+
if (fs.exists(fish)) {
|
|
153
|
+
let ours = false;
|
|
154
|
+
try {
|
|
155
|
+
ours = fs.read(fish).includes(".pantheon/bin");
|
|
156
|
+
}
|
|
157
|
+
catch { }
|
|
158
|
+
if (ours) {
|
|
159
|
+
try {
|
|
160
|
+
fs.remove(fish);
|
|
161
|
+
removed.push(fish);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
left.push(`${fish} — could not remove (${e.message.split("\n")[0]})`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
left.push(`${fish} — it does not look like the installer's, so it was left`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const lockedHere = m.os === "win32" && !!o.running && o.running.root.toLowerCase() === m.root.toLowerCase();
|
|
172
|
+
if (lockedHere) {
|
|
173
|
+
left.push(`${m.root} — Windows keeps Pantheon's node.exe locked while this command runs. Close your agent windows and this one, then in a new PowerShell: Remove-Item -Recurse -Force "$env:LOCALAPPDATA\\Pantheon"`);
|
|
174
|
+
return { removed, left };
|
|
175
|
+
}
|
|
176
|
+
for (const [path, what] of [[m.launcher, "the pantheon command"], [m.log, "the installer's log"], [m.runtime, "Pantheon's own Node and the CLI in it"]]) {
|
|
177
|
+
if (!fs.exists(path))
|
|
178
|
+
continue;
|
|
179
|
+
try {
|
|
180
|
+
fs.remove(path);
|
|
181
|
+
removed.push(`${what} (${path})`);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
const busy = /EBUSY|EPERM|EACCES/.test(String(e.code ?? e.message));
|
|
185
|
+
left.push(`${what}, ${path} — could not remove (${e.message.split("\n")[0]})` +
|
|
186
|
+
(m.os === "win32" && busy ? `; an agent window is still running it. Close them, then in PowerShell: Remove-Item -Recurse -Force "${path}"` : `; remove it with: rm -rf '${path.replace(/'/g, "'\\''")}'`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const dir of [m.binDir, ...(m.os === "win32" ? [] : [p.dirname(m.log)]), m.root])
|
|
190
|
+
fs.removeIfEmpty(dir);
|
|
191
|
+
return { removed, left };
|
|
192
|
+
}
|
|
193
|
+
export const NPM_UNINSTALL = "npm uninstall -g @join-pantheon/cli";
|
|
194
|
+
//# sourceMappingURL=uninstall.js.map
|
package/dist/upgrade.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, posix, win32 } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { installForUser } from "./install.js";
|
|
6
6
|
import { npmSpawn } from "./which.js";
|
|
7
7
|
import { proxyEnvFor } from "./network.js";
|
|
8
8
|
import { relayProc, workerProc, notifierProc, menubarProc } from "./machine.js";
|
|
9
|
+
import { load } from "./config.js";
|
|
10
|
+
import { childNodeFlags, installerLineHere, managedLayout, nodeSatisfies, nodeTooOldSentence, runtimeNodeVersion } from "./managed.js";
|
|
9
11
|
export function repoRoot() {
|
|
10
12
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
13
|
}
|
|
@@ -18,7 +20,20 @@ function pkgVersion(root) {
|
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
export const PKG_NAME = "@join-pantheon/cli";
|
|
21
|
-
export function
|
|
23
|
+
export function npmInstallArgs(version, managed = managedLayout()) {
|
|
24
|
+
const P = /\.exe$/i.test(managed?.node ?? "") ? win32 : posix;
|
|
25
|
+
const own = managed ? ["--prefix", managed.nodeDir, "--cache", P.join(managed.root, "runtime", "npm-cache"), "--fetch-timeout=60000", "--fetch-retries=2"] : [];
|
|
26
|
+
return ["install", "-g", ...own, `${PKG_NAME}@${version ?? "latest"}`, "--no-audit", "--no-fund"];
|
|
27
|
+
}
|
|
28
|
+
export function npmFailed(spec, e, managed, os = process.platform, relay) {
|
|
29
|
+
const first = String(e?.message ?? e).split("\n")[0];
|
|
30
|
+
if (managed) {
|
|
31
|
+
return `\`${managed.npm} install -g ${PKG_NAME}@${spec}\` failed: ${first}. Run the installer again to repair it: ${installerLineHere(relay, os)}`;
|
|
32
|
+
}
|
|
33
|
+
return `\`npm install -g ${PKG_NAME}@${spec}\` failed: ${first}. `
|
|
34
|
+
+ `You may need to re-run it with the right permissions (or your Node version manager's global scope).`;
|
|
35
|
+
}
|
|
36
|
+
export function rewireVia(root, fallback, run = (file, args) => execFileSync(process.execPath, [...childNodeFlags(), file, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120_000 })) {
|
|
22
37
|
try {
|
|
23
38
|
const out = JSON.parse(run(join(root, "dist", "cli.js"), ["rewire", "--json"]));
|
|
24
39
|
if (Array.isArray(out) && out.every((o) => o && typeof o.tool === "string" && typeof o.status === "string")) {
|
|
@@ -70,7 +85,7 @@ export function defaultDeps() {
|
|
|
70
85
|
catch { }
|
|
71
86
|
return up;
|
|
72
87
|
},
|
|
73
|
-
npmUpgrade: (version) => npm(
|
|
88
|
+
npmUpgrade: (version) => npm(npmInstallArgs(version), { stdio: ["ignore", "pipe", "pipe"] }),
|
|
74
89
|
npmLatest: () => {
|
|
75
90
|
try {
|
|
76
91
|
return npm(["view", `${PKG_NAME}`, "version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 4000 }).trim() || null;
|
|
@@ -79,6 +94,22 @@ export function defaultDeps() {
|
|
|
79
94
|
return null;
|
|
80
95
|
}
|
|
81
96
|
},
|
|
97
|
+
engines: (spec) => {
|
|
98
|
+
try {
|
|
99
|
+
const out = npm(["view", `${PKG_NAME}@${spec}`, "engines.node"], { stdio: ["ignore", "pipe", "ignore"], timeout: 20_000 }).trim();
|
|
100
|
+
return out.split("\n").at(-1).trim().replace(/^['"]|['"]$/g, "") || null;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
runtimeNode: () => runtimeNodeVersion(),
|
|
107
|
+
relay: (() => { try {
|
|
108
|
+
return load()?.relay ?? null;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
} })(),
|
|
82
113
|
};
|
|
83
114
|
}
|
|
84
115
|
export function upgrade(deps = defaultDeps(), opts = {}) {
|
|
@@ -88,6 +119,15 @@ export function upgrade(deps = defaultDeps(), opts = {}) {
|
|
|
88
119
|
const steps = [];
|
|
89
120
|
const stop = (name, detail) => ({ root, from, to: from, steps: [...steps, { name, ok: false, detail }], ok: false, alreadyCurrent: false, restart: [] });
|
|
90
121
|
if (!deps.isGit(root)) {
|
|
122
|
+
const managed = deps.managed === undefined ? managedLayout() : deps.managed;
|
|
123
|
+
if (managed && deps.engines) {
|
|
124
|
+
const range = deps.engines(spec);
|
|
125
|
+
const node = deps.runtimeNode ? deps.runtimeNode() : runtimeNodeVersion(managed);
|
|
126
|
+
if (range && nodeSatisfies(node, range) === false) {
|
|
127
|
+
const why = nodeTooOldSentence(spec === "latest" ? null : spec, range, node, deps.relay);
|
|
128
|
+
return { ...stop("node", why), needsPerson: why };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
91
131
|
try {
|
|
92
132
|
deps.npmUpgrade(spec === "latest" ? undefined : spec);
|
|
93
133
|
steps.push({ name: "npm", ok: true, detail: `installed ${PKG_NAME}@${spec}` });
|
|
@@ -96,8 +136,7 @@ export function upgrade(deps = defaultDeps(), opts = {}) {
|
|
|
96
136
|
if (spec !== "latest" && /ETARGET|No matching version|notarget/i.test(String(e.stderr ?? "") + e.message)) {
|
|
97
137
|
return { root, from, to: from, steps: [...steps, { name: "npm", ok: true, detail: `${PKG_NAME}@${spec} is not on npm yet` }], ok: true, alreadyCurrent: true, restart: [] };
|
|
98
138
|
}
|
|
99
|
-
return stop("npm",
|
|
100
|
-
`You may need to re-run it with the right permissions (or your Node version manager's global scope).`);
|
|
139
|
+
return stop("npm", npmFailed(spec, e, managed, process.platform, deps.relay));
|
|
101
140
|
}
|
|
102
141
|
let wired = [];
|
|
103
142
|
try {
|
package/dist/which.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
5
5
|
import { platform } from "node:os";
|
|
6
|
+
import { managedLayout } from "./managed.js";
|
|
6
7
|
export const DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.PS1";
|
|
7
8
|
const isWindows = (os = platform()) => os === "win32";
|
|
8
9
|
function runnable(file) {
|
|
@@ -124,11 +125,18 @@ export function spawnShape(binary, os = platform()) {
|
|
|
124
125
|
return { command: binary, shell: false };
|
|
125
126
|
return { command: binary, shell: /\.(cmd|bat)$/i.test(binary) };
|
|
126
127
|
}
|
|
127
|
-
export function npmSpawn(args, os = platform(), resolveNpm =
|
|
128
|
-
const
|
|
128
|
+
export function npmSpawn(args, os = platform(), resolveNpm, proxyEnv = {}, managed = managedLayout(process.execPath, os)) {
|
|
129
|
+
const extra = { ...proxyEnv };
|
|
130
|
+
if (managed && !resolveNpm) {
|
|
131
|
+
const dir = isWindows(os) ? win32.dirname(managed.node) : posix.dirname(managed.node);
|
|
132
|
+
const key = isWindows(os) ? Object.keys(process.env).find((k) => k.toUpperCase() === "PATH") ?? "Path" : "PATH";
|
|
133
|
+
extra[key] = [dir, process.env[key] ?? ""].filter(Boolean).join(isWindows(os) ? ";" : ":");
|
|
134
|
+
return { command: managed.node, args: ["--use-system-ca", managed.npmCli, ...args], options: { env: { ...process.env, ...extra } } };
|
|
135
|
+
}
|
|
136
|
+
const env = Object.keys(extra).length ? { env: { ...process.env, ...extra } } : {};
|
|
129
137
|
if (!isWindows(os))
|
|
130
138
|
return { command: "npm", args: [...args], options: { ...env } };
|
|
131
|
-
const s = agentSpawn(resolveNpm() ?? "npm.cmd", args, os);
|
|
139
|
+
const s = agentSpawn((resolveNpm ?? (() => which("npm", os)))() ?? "npm.cmd", args, os);
|
|
132
140
|
return { ...s, options: { ...s.options, ...env } };
|
|
133
141
|
}
|
|
134
142
|
const CMD_UNSAFE = /["%!^&|<>\r\n\0]/;
|
package/dist/worker-policy.js
CHANGED
|
@@ -68,6 +68,8 @@ export function loadPolicy(home) {
|
|
|
68
68
|
...(isRecord(raw.wakeOnce) ? { wakeOnce: Object.fromEntries(Object.entries(raw.wakeOnce).filter(([, v]) => typeof v === "number" && v > 0 && v < 100)) } : {}),
|
|
69
69
|
...(isRecord(raw.wakeNo) ? { wakeNo: Object.fromEntries(Object.entries(raw.wakeNo).filter(([, v]) => typeof v === "number")) } : {}),
|
|
70
70
|
...(isRecord(raw.wakeTeamFlag) ? { wakeTeamFlag: Object.fromEntries(Object.entries(raw.wakeTeamFlag).filter(([, v]) => typeof v === "number")) } : {}),
|
|
71
|
+
...(isRecord(raw.upgradeHeld) && typeof raw.upgradeHeld.version === "string" && typeof raw.upgradeHeld.node === "string"
|
|
72
|
+
? { upgradeHeld: { version: raw.upgradeHeld.version, node: raw.upgradeHeld.node, at: Number(raw.upgradeHeld.at) || 0, why: String(raw.upgradeHeld.why ?? "") } } : {}),
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
catch {
|
package/dist/worker.js
CHANGED
|
@@ -18,7 +18,8 @@ import { headlessSpawn, materializeSpawnFiles, chooseAgent, agentById, describeC
|
|
|
18
18
|
import { activeOutages, detectOutage, earliest, whenAgain } from "./agent-outage.js";
|
|
19
19
|
import { agentSpawn, CmdLineUnsafe } from "./which.js";
|
|
20
20
|
import { VERSION } from "./version.js";
|
|
21
|
-
import { applyAnswer, CAPABILITY_ARGS, CARRY_REASON, carryOverWakers, dropLedgerGrants, hasWakeOnce, isApprovedWaker, mayAskWake, takeWakeOnce, OLD_RELAY_WAKE, relayKnowsAdmitters, loadPolicy, mayAsk, needsSelfUpgrade, parseGrantAnswer, recordAsk, savePolicy, useGrant, WOKEN_RUN_ENV, takeTeamFlagOnYes, unsandboxedAllowed, policyPath, } from "./worker-policy.js";
|
|
21
|
+
import { applyAnswer, CAPABILITY_ARGS, CARRY_REASON, carryOverWakers, dropLedgerGrants, hasWakeOnce, isApprovedWaker, mayAskWake, takeWakeOnce, OLD_RELAY_WAKE, relayKnowsAdmitters, loadPolicy, mayAsk, needsSelfUpgrade, parseGrantAnswer, recordAsk, savePolicy, useGrant, WOKEN_RUN_ENV, takeTeamFlagOnYes, unsandboxedAllowed, policyPath, isNewer, } from "./worker-policy.js";
|
|
22
|
+
import { runtimeNodeVersion } from "./managed.js";
|
|
22
23
|
import { agentHookStates, repairHook } from "./notify.js";
|
|
23
24
|
import { fence, assess } from "./untrusted.js";
|
|
24
25
|
import { allowed, closeStartedRun, finishRun, recordRun, startRun } from "./spend.js";
|
|
@@ -3143,13 +3144,19 @@ export class Worker {
|
|
|
3143
3144
|
const lastAttempt = policy.attempts.self_upgrade ?? null;
|
|
3144
3145
|
if (!needsSelfUpgrade(VERSION, relayPkg, lastAttempt))
|
|
3145
3146
|
return;
|
|
3147
|
+
const nodeNow = this.opts.selfUpgrade?.node ?? (() => runtimeNodeVersion());
|
|
3148
|
+
const held = policy.upgradeHeld;
|
|
3149
|
+
if (held && held.version === relayPkg && held.node === nodeNow())
|
|
3150
|
+
return;
|
|
3146
3151
|
if (policy.grants.self_upgrade) {
|
|
3147
3152
|
const slot = this.takeSlot();
|
|
3148
3153
|
if ("held" in slot)
|
|
3149
3154
|
return;
|
|
3150
3155
|
try {
|
|
3156
|
+
const grantBefore = policy.grants.self_upgrade;
|
|
3151
3157
|
useGrant(policy, "self_upgrade");
|
|
3152
3158
|
policy.attempts.self_upgrade = Date.now();
|
|
3159
|
+
delete policy.upgradeHeld;
|
|
3153
3160
|
savePolicy(policy, home);
|
|
3154
3161
|
this.writeState("upgrading", `pantheon ${VERSION} -> ${relayPkg} (relay is ahead)`);
|
|
3155
3162
|
this.opts.log(`* self-upgrade: relay runs ${relayPkg}, this machine runs ${VERSION} — upgrading (granted by this machine's human)`);
|
|
@@ -3161,20 +3168,36 @@ export class Worker {
|
|
|
3161
3168
|
result = { ok: false, alreadyCurrent: false, from: VERSION, to: relayPkg ?? "?" };
|
|
3162
3169
|
this.opts.log(`! self-upgrade failed: ${e.message}`);
|
|
3163
3170
|
}
|
|
3164
|
-
if (result.
|
|
3171
|
+
if (result.needsPerson) {
|
|
3172
|
+
if (grantBefore)
|
|
3173
|
+
policy.grants.self_upgrade = grantBefore;
|
|
3174
|
+
delete policy.attempts.self_upgrade;
|
|
3175
|
+
policy.upgradeHeld = { version: relayPkg ?? result.to, node: nodeNow(), at: Date.now(), why: result.needsPerson };
|
|
3176
|
+
savePolicy(policy, home);
|
|
3177
|
+
this.writeState("needs you", result.needsPerson);
|
|
3178
|
+
this.opts.log(`! self-upgrade needs you: ${result.needsPerson}`);
|
|
3179
|
+
try {
|
|
3180
|
+
this.opts.notify?.({ kind: "worker", who: "Your worker", heading: "Pantheon needs a newer Node", context: this.cfg.space, body: result.needsPerson, ttl: 0, sound: false });
|
|
3181
|
+
}
|
|
3182
|
+
catch { }
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
const behindDisk = result.ok && result.to === result.from && isNewer(result.to, VERSION);
|
|
3186
|
+
if (result.ok && result.to === result.from && !behindDisk) {
|
|
3165
3187
|
delete policy.attempts.self_upgrade;
|
|
3166
3188
|
savePolicy(policy, home);
|
|
3167
3189
|
this.opts.log(`* self-upgrade: nothing newer than ${result.to} to install yet (the relay runs ${relayPkg}); will check again`);
|
|
3168
3190
|
return;
|
|
3169
3191
|
}
|
|
3170
|
-
if (result.ok && !result.alreadyCurrent) {
|
|
3192
|
+
if (result.ok && (!result.alreadyCurrent || behindDisk)) {
|
|
3193
|
+
const from = behindDisk ? VERSION : result.from;
|
|
3171
3194
|
await client.send({
|
|
3172
3195
|
act: "NOTE", goal: null, after_seq: Math.max(client.readSeq, client.seq),
|
|
3173
|
-
summary: `self-upgraded pantheon ${
|
|
3174
|
-
body: `This machine's worker upgraded pantheon ${
|
|
3196
|
+
summary: `self-upgraded pantheon ${from} -> ${result.to}; worker restarting on the new build`,
|
|
3197
|
+
body: `This machine's worker upgraded pantheon ${from} -> ${result.to} (fast-forward pull + rebuild, granted by its human) and is restarting on the new build. Agent windows on this machine opened on 0.24 or later switch to the new version by themselves; a window opened on an older pantheon needs one reconnect (/mcp → pantheon → Reconnect, or reopen it).`,
|
|
3175
3198
|
}).catch((e) => this.opts.log(`! could not report the upgrade on the ledger: ${e.message}`));
|
|
3176
3199
|
await this.bringMachineAlong();
|
|
3177
|
-
this.opts.log(`* self-upgrade done: ${
|
|
3200
|
+
this.opts.log(`* self-upgrade done: ${from} -> ${result.to}; restarting worker to load it`);
|
|
3178
3201
|
(this.opts.selfUpgrade?.restart ?? (() => process.exit(0)))();
|
|
3179
3202
|
}
|
|
3180
3203
|
else if (!result.ok) {
|
|
@@ -3512,7 +3535,7 @@ export class Worker {
|
|
|
3512
3535
|
async runRealUpgrade(version) {
|
|
3513
3536
|
const { upgrade } = await import("./upgrade.js");
|
|
3514
3537
|
const r = upgrade(undefined, version ? { version } : {});
|
|
3515
|
-
return { ok: r.ok, alreadyCurrent: r.alreadyCurrent, from: r.from, to: r.to };
|
|
3538
|
+
return { ok: r.ok, alreadyCurrent: r.alreadyCurrent, from: r.from, to: r.to, ...(r.needsPerson ? { needsPerson: r.needsPerson } : {}) };
|
|
3516
3539
|
}
|
|
3517
3540
|
writeState(state, detail) {
|
|
3518
3541
|
const line = `${state}|${detail}`;
|