@join-pantheon/cli 0.33.0-rc.1 → 0.33.0-rc.3
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/board-peer.js +136 -0
- package/dist/board.js +30 -46
- package/dist/cli.js +89 -50
- package/dist/client.js +16 -0
- package/dist/config.js +7 -1
- package/dist/home-safety.js +195 -0
- package/dist/machine.js +111 -6
- package/dist/service.js +5 -1
- package/dist/uninstall.js +1 -1
- package/dist/win-host.js +59 -7
- package/helper/macos/prebuilt/manifest.json +1 -1
- package/helper/windows/pantheon-notice.ps1 +44 -16
- package/installer/install.ps1 +130 -56
- package/installer/install.sh +15 -1
- package/package.json +1 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { win32 } from "node:path";
|
|
4
|
+
const isPort = (n) => Number.isInteger(n) && n > 0 && n <= 65535;
|
|
5
|
+
export const lsofArgs = (clientPort) => ["-nP", "-w", `-iTCP@127.0.0.1:${clientPort}`, "-sTCP:ESTABLISHED", "-a", "-Fpun"];
|
|
6
|
+
export function parseLsofPeers(out, clientPort, boardPort) {
|
|
7
|
+
const want = `127.0.0.1:${clientPort}->127.0.0.1:${boardPort}`;
|
|
8
|
+
const found = [];
|
|
9
|
+
let pid = NaN, uid = NaN;
|
|
10
|
+
for (const line of out.split("\n")) {
|
|
11
|
+
const k = line[0], v = line.slice(1).trim();
|
|
12
|
+
if (k === "p") {
|
|
13
|
+
pid = Number(v);
|
|
14
|
+
uid = NaN;
|
|
15
|
+
}
|
|
16
|
+
else if (k === "u")
|
|
17
|
+
uid = Number(v);
|
|
18
|
+
else if (k === "n" && v === want && Number.isInteger(pid) && Number.isInteger(uid))
|
|
19
|
+
found.push({ pid, uid });
|
|
20
|
+
}
|
|
21
|
+
return found;
|
|
22
|
+
}
|
|
23
|
+
function runLsof(args) {
|
|
24
|
+
const exe = existsSync("/usr/sbin/lsof") ? "/usr/sbin/lsof" : "lsof";
|
|
25
|
+
return new Promise((done) => {
|
|
26
|
+
execFile(exe, args, { encoding: "utf8", timeout: 5_000, maxBuffer: 1024 * 1024 }, (err, out) => {
|
|
27
|
+
if (err && typeof err.code !== "number") {
|
|
28
|
+
done(null);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
done(out ?? "");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const LSOF_AT_ONCE = 4;
|
|
36
|
+
let lsofRunning = 0;
|
|
37
|
+
const lsofWaiting = [];
|
|
38
|
+
async function lsofSlot(fn) {
|
|
39
|
+
while (lsofRunning >= LSOF_AT_ONCE)
|
|
40
|
+
await new Promise((r) => lsofWaiting.push(r));
|
|
41
|
+
lsofRunning++;
|
|
42
|
+
try {
|
|
43
|
+
return await fn();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
lsofRunning--;
|
|
47
|
+
lsofWaiting.shift()?.();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function macPeer(clientPort, boardPort, me, run = (a) => lsofSlot(() => runLsof(a))) {
|
|
51
|
+
if (me === null || !isPort(clientPort) || !isPort(boardPort))
|
|
52
|
+
return "unknown";
|
|
53
|
+
const out = await run(lsofArgs(clientPort)).catch(() => null);
|
|
54
|
+
if (out === null)
|
|
55
|
+
return "unknown";
|
|
56
|
+
const owners = parseLsofPeers(out, clientPort, boardPort);
|
|
57
|
+
if (!owners.length)
|
|
58
|
+
return "unknown";
|
|
59
|
+
return owners.every((o) => o.uid === me) ? "same" : "other";
|
|
60
|
+
}
|
|
61
|
+
export function windowsPeerScript(boardPort) {
|
|
62
|
+
if (!isPort(boardPort))
|
|
63
|
+
throw new Error("not a port");
|
|
64
|
+
return [
|
|
65
|
+
"[Console]::OutputEncoding = [Text.Encoding]::UTF8",
|
|
66
|
+
"$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
67
|
+
"'ME ' + $me",
|
|
68
|
+
`foreach ($c in @(Get-NetTCPConnection -RemotePort ${boardPort} -State Established -ErrorAction SilentlyContinue)) {`,
|
|
69
|
+
" if (@('127.0.0.1', '::ffff:127.0.0.1') -notcontains $c.LocalAddress -or @('127.0.0.1', '::ffff:127.0.0.1') -notcontains $c.RemoteAddress) { continue }",
|
|
70
|
+
" $sid = ''",
|
|
71
|
+
" try { $p = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $c.OwningProcess); if ($p) { $o = Invoke-CimMethod -InputObject $p -MethodName GetOwnerSid; if ($o.ReturnValue -eq 0) { $sid = [string]$o.Sid } } } catch { $sid = '' }",
|
|
72
|
+
" 'C ' + $c.LocalPort + ' ' + $c.OwningProcess + ' ' + $sid",
|
|
73
|
+
"}",
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
export function parseWindowsPeers(out) {
|
|
77
|
+
let me = null;
|
|
78
|
+
const ports = new Map();
|
|
79
|
+
for (const raw of out.split(/\r?\n/)) {
|
|
80
|
+
const line = raw.trim();
|
|
81
|
+
const m = /^ME (S-1-[0-9-]+)$/.exec(line);
|
|
82
|
+
if (m) {
|
|
83
|
+
me = m[1];
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const c = /^C (\d{1,5}) (\d+)(?: (\S*))?$/.exec(line);
|
|
87
|
+
if (!c)
|
|
88
|
+
continue;
|
|
89
|
+
const sid = /^S-1-[0-9-]+$/.test(c[3] ?? "") ? c[3] : "";
|
|
90
|
+
ports.set(Number(c[1]), [...(ports.get(Number(c[1])) ?? []), sid]);
|
|
91
|
+
}
|
|
92
|
+
return { me, ports };
|
|
93
|
+
}
|
|
94
|
+
export function windowsVerdict(parsed, clientPort) {
|
|
95
|
+
const owners = parsed.ports.get(clientPort);
|
|
96
|
+
if (!parsed.me || !owners?.length)
|
|
97
|
+
return "unknown";
|
|
98
|
+
if (owners.every((s) => s === parsed.me))
|
|
99
|
+
return "same";
|
|
100
|
+
return owners.some((s) => s && s !== parsed.me) ? "other" : "unknown";
|
|
101
|
+
}
|
|
102
|
+
function runWindowsPowerShell(script) {
|
|
103
|
+
const exe = win32.join(process.env.SystemRoot || process.env.windir || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
104
|
+
return new Promise((done) => {
|
|
105
|
+
execFile(exe, ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", timeout: 15_000, windowsHide: true, maxBuffer: 1024 * 1024 }, (err, out) => {
|
|
106
|
+
done(err && !out ? null : (out ?? null));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function windowsPeerCheck(run = runWindowsPowerShell) {
|
|
111
|
+
const waiting = new Map();
|
|
112
|
+
const busy = new Set();
|
|
113
|
+
const go = (boardPort) => {
|
|
114
|
+
const batch = waiting.get(boardPort) ?? [];
|
|
115
|
+
waiting.delete(boardPort);
|
|
116
|
+
if (!batch.length)
|
|
117
|
+
return;
|
|
118
|
+
busy.add(boardPort);
|
|
119
|
+
run(windowsPeerScript(boardPort)).catch(() => null).then((out) => {
|
|
120
|
+
const parsed = out === null ? null : parseWindowsPeers(out);
|
|
121
|
+
for (const w of batch)
|
|
122
|
+
w.done(parsed ? windowsVerdict(parsed, w.port) : "unknown");
|
|
123
|
+
}).finally(() => { busy.delete(boardPort); go(boardPort); });
|
|
124
|
+
};
|
|
125
|
+
return (clientPort, boardPort) => new Promise((done) => {
|
|
126
|
+
if (!isPort(clientPort) || !isPort(boardPort)) {
|
|
127
|
+
done("unknown");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
waiting.set(boardPort, [...(waiting.get(boardPort) ?? []), { port: clientPort, done }]);
|
|
131
|
+
if (!busy.has(boardPort))
|
|
132
|
+
setImmediate(() => { if (!busy.has(boardPort))
|
|
133
|
+
go(boardPort); });
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=board-peer.js.map
|
package/dist/board.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
|
-
import { chmodSync, mkdirSync,
|
|
4
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { CONFIG_DIR } from "./config.js";
|
|
9
|
+
import { macPeer, windowsPeerCheck } from "./board-peer.js";
|
|
9
10
|
import { LIMITS } from "./protocol.js";
|
|
10
11
|
import { childNodeFlags } from "./managed.js";
|
|
11
12
|
import { which } from "./which.js";
|
|
@@ -61,9 +62,23 @@ export function defaultBoardDeps(home = CONFIG_DIR) {
|
|
|
61
62
|
actionFile: join(home, "bin", ".hud-action.json"),
|
|
62
63
|
openTerminal: (actionId) => openExplainTerminal(home, actionId),
|
|
63
64
|
now: Date.now,
|
|
64
|
-
|
|
65
|
+
peer: defaultPeerCheck(),
|
|
65
66
|
};
|
|
66
67
|
}
|
|
68
|
+
export function defaultPeerCheck(os = platform()) {
|
|
69
|
+
const me = typeof process.getuid === "function" ? process.getuid() : null;
|
|
70
|
+
if (os === "linux") {
|
|
71
|
+
return async (c, b) => {
|
|
72
|
+
const uid = loopbackPeerUid(c, b);
|
|
73
|
+
return uid === null || me === null ? "unknown" : uid === me ? "same" : "other";
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (os === "darwin")
|
|
77
|
+
return (c, b) => macPeer(c, b, me);
|
|
78
|
+
if (os === "win32")
|
|
79
|
+
return windowsPeerCheck();
|
|
80
|
+
return async () => "unknown";
|
|
81
|
+
}
|
|
67
82
|
export function loopbackPeerUid(clientPort, boardPort, read = (p) => readFileSync(p, "utf8"), os = platform()) {
|
|
68
83
|
if (os !== "linux")
|
|
69
84
|
return null;
|
|
@@ -290,15 +305,13 @@ export async function startBoardServer(o = {}) {
|
|
|
290
305
|
const known = peerChecked.get(sock);
|
|
291
306
|
if (known !== undefined)
|
|
292
307
|
return known;
|
|
293
|
-
const me = typeof process.getuid === "function" ? process.getuid() : null;
|
|
294
308
|
const addr = sock.remoteAddress ?? "";
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return ok;
|
|
309
|
+
const local = addr === "127.0.0.1" || addr === "::ffff:127.0.0.1";
|
|
310
|
+
const check = local && sock.remotePort
|
|
311
|
+
? deps.peer(sock.remotePort, port).then((v) => v === "same", () => false)
|
|
312
|
+
: Promise.resolve(false);
|
|
313
|
+
peerChecked.set(sock, check);
|
|
314
|
+
return check;
|
|
302
315
|
};
|
|
303
316
|
const status = async () => {
|
|
304
317
|
const s = await deps.status();
|
|
@@ -370,10 +383,6 @@ export async function startBoardServer(o = {}) {
|
|
|
370
383
|
sendJson(res, 403, { ok: false, error: "wrong host" });
|
|
371
384
|
return;
|
|
372
385
|
}
|
|
373
|
-
if (!samePerson(req)) {
|
|
374
|
-
sendJson(res, 403, { ok: false, error: "this board belongs to another user on this computer" });
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
386
|
const site = req.headers["sec-fetch-site"];
|
|
378
387
|
if (typeof site === "string" && site !== "same-origin" && site !== "none") {
|
|
379
388
|
sendJson(res, 403, { ok: false, error: "cross-site request" });
|
|
@@ -410,6 +419,10 @@ export async function startBoardServer(o = {}) {
|
|
|
410
419
|
if (method === "POST" && url.pathname === "/api/session") {
|
|
411
420
|
const nonce = bearerOf(req);
|
|
412
421
|
const body = strictObject(await readBody(req), []);
|
|
422
|
+
if (!(await samePerson(req))) {
|
|
423
|
+
sendJson(res, 403, { ok: false, error: "this board belongs to another user on this computer — or this computer could not show that this connection is yours" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
413
426
|
if (!nonce || !useLink(nonce)) {
|
|
414
427
|
sendJson(res, 401, { ok: false, error: "this board link has expired or was already used — run pantheon board for a fresh one" });
|
|
415
428
|
return;
|
|
@@ -565,7 +578,8 @@ export async function startBoardServer(o = {}) {
|
|
|
565
578
|
}
|
|
566
579
|
catch { } });
|
|
567
580
|
});
|
|
568
|
-
server.
|
|
581
|
+
server.keepAliveTimeout = 30_000;
|
|
582
|
+
server.headersTimeout = 35_000;
|
|
569
583
|
server.requestTimeout = 60_000;
|
|
570
584
|
await new Promise((ok, fail) => { server.once("error", fail); server.listen(0, "127.0.0.1", () => { server.off("error", fail); ok(); }); });
|
|
571
585
|
port = server.address().port;
|
|
@@ -620,31 +634,6 @@ export function browserCommand(url, os = platform(), find = (n) => which(n), o =
|
|
|
620
634
|
const scoped = o.scope ? scopeArgv([x, url], find) : null;
|
|
621
635
|
return scoped ? { cmd: scoped[0], args: scoped.slice(1), opts } : { cmd: x, args: [url], opts };
|
|
622
636
|
}
|
|
623
|
-
export function redirectPage(url, dir, now = Date.now()) {
|
|
624
|
-
if (!LINK_RE.test(url))
|
|
625
|
-
throw new Error("not a board link");
|
|
626
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
627
|
-
for (const f of readdirSync(dir)) {
|
|
628
|
-
if (!/^board-open-[0-9a-f]{16}\.html$/.test(f))
|
|
629
|
-
continue;
|
|
630
|
-
try {
|
|
631
|
-
if (now - statSync(join(dir, f)).mtimeMs > 60_000)
|
|
632
|
-
rmSync(join(dir, f), { force: true });
|
|
633
|
-
}
|
|
634
|
-
catch { }
|
|
635
|
-
}
|
|
636
|
-
const file = join(dir, `board-open-${randomBytes(8).toString("hex")}.html`);
|
|
637
|
-
writeFileSync(file, `<!doctype html><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${url}"><title>Pantheon</title><script>location.replace(${JSON.stringify(url)})</script>\n`, { mode: 0o600 });
|
|
638
|
-
return file;
|
|
639
|
-
}
|
|
640
|
-
export function fileOpenCommand(file, os = platform()) {
|
|
641
|
-
const opts = { detached: true, stdio: "ignore", windowsHide: true };
|
|
642
|
-
if (os === "darwin")
|
|
643
|
-
return { cmd: "open", args: [file], opts };
|
|
644
|
-
if (os === "win32")
|
|
645
|
-
return { cmd: "explorer.exe", args: [file], opts };
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
637
|
export async function openBrowser(url, d = {}) {
|
|
649
638
|
if (!LINK_RE.test(url))
|
|
650
639
|
throw new Error("not a board link");
|
|
@@ -655,11 +644,6 @@ export async function openBrowser(url, d = {}) {
|
|
|
655
644
|
if (await portal(url).catch(() => false))
|
|
656
645
|
return "portal";
|
|
657
646
|
}
|
|
658
|
-
if (os === "darwin" || os === "win32") {
|
|
659
|
-
const page = redirectPage(url, d.pageDir ?? join(CONFIG_DIR, "bin"));
|
|
660
|
-
const how = fileOpenCommand(page, os);
|
|
661
|
-
return (await (d.launch ?? launch)(how)) ? "direct" : false;
|
|
662
|
-
}
|
|
663
647
|
const scope = os === "linux" && (d.inService ?? (() => inServiceCgroup()))();
|
|
664
648
|
const how = browserCommand(url, os, find, { scope });
|
|
665
649
|
if (!how)
|
|
@@ -721,7 +705,7 @@ async function openExplainTerminal(home, actionId) {
|
|
|
721
705
|
return { ok: false, why: `${t.error} — copy the question into your agent instead` };
|
|
722
706
|
return (await launch(t)) ? { ok: true } : { ok: false, why: "the terminal could not be started — copy the question into your agent instead" };
|
|
723
707
|
}
|
|
724
|
-
export async function ping(s, timeoutMs =
|
|
708
|
+
export async function ping(s, timeoutMs = 3000) {
|
|
725
709
|
const challenge = randomBytes(16).toString("hex");
|
|
726
710
|
try {
|
|
727
711
|
const res = await fetch(`http://127.0.0.1:${s.port}/api/ping?challenge=${challenge}`, { signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
|
package/dist/cli.js
CHANGED
|
@@ -914,7 +914,8 @@ ${B("control")}
|
|
|
914
914
|
(auto-detects claude / codex / cursor / copilot / kiro on this machine)
|
|
915
915
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
916
916
|
pantheon leave remove what pantheon put on this machine
|
|
917
|
-
pantheon uninstall leave, then remove Pantheon itself: its own Node, the pantheon command, the PATH lines
|
|
917
|
+
pantheon uninstall leave, then remove Pantheon itself: its own Node, the pantheon command, the PATH lines,
|
|
918
|
+
and all it keeps in ~/.pantheon (files of yours there stay)
|
|
918
919
|
pantheon panic emergency stop: ends the run in flight and the worker, and switches off wake rights in every team you are in
|
|
919
920
|
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
920
921
|
pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
|
|
@@ -1038,7 +1039,7 @@ async function teamStanding(all, m) {
|
|
|
1038
1039
|
return { others: null, founder: false };
|
|
1039
1040
|
}
|
|
1040
1041
|
finally {
|
|
1041
|
-
c.
|
|
1042
|
+
c.dispose();
|
|
1042
1043
|
}
|
|
1043
1044
|
}
|
|
1044
1045
|
function slipDistance(a, b) {
|
|
@@ -1080,7 +1081,7 @@ async function main() {
|
|
|
1080
1081
|
await installNetwork();
|
|
1081
1082
|
}
|
|
1082
1083
|
catch { }
|
|
1083
|
-
if (!QUIET.has(cmd)) {
|
|
1084
|
+
if (!QUIET.has(cmd) && cmd !== "uninstall") {
|
|
1084
1085
|
try {
|
|
1085
1086
|
console.error(D(updateBanner(VERSION)));
|
|
1086
1087
|
}
|
|
@@ -2507,7 +2508,8 @@ async function main() {
|
|
|
2507
2508
|
if (uninstalling && !argv.includes("--yes")) {
|
|
2508
2509
|
const teams = plan.teams.length && !keepIdentity ? `leaves ${plan.teams.map((t) => `"${t.name}"`).join(", ")} (this machine's key goes), ` : "";
|
|
2509
2510
|
const what = `this removes Pantheon from this machine: it ${teams}uninstalls its services, hooks and agent entries` +
|
|
2510
|
-
(install?.present ? `, and deletes Pantheon's own Node in ${install.runtime}, the pantheon command and the PATH lines the installer added
|
|
2511
|
+
(install?.present ? `, and deletes Pantheon's own Node in ${install.runtime}, the pantheon command and the PATH lines the installer added` : "") +
|
|
2512
|
+
`. Everything Pantheon keeps in ${CONFIG_DIR} goes${keepIdentity ? " (but your identity, --keep-identity)" : ""}; files of your own there stay.`;
|
|
2511
2513
|
if (!process.stdin.isTTY)
|
|
2512
2514
|
die(`${what}\n If you mean it: pantheon uninstall --yes`);
|
|
2513
2515
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -2528,6 +2530,8 @@ async function main() {
|
|
|
2528
2530
|
die(`nothing was changed.${plan.teams.length > 1 ? " Leave one team with: pantheon leave <team>" : ""}`);
|
|
2529
2531
|
}
|
|
2530
2532
|
console.error(`${B(uninstalling ? "uninstalling pantheon" : "leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
|
|
2533
|
+
const { pantheonHomeRefusal, refusedHomeLine } = await import("./home-safety.js");
|
|
2534
|
+
const homeRefusal = pantheonHomeRefusal(CONFIG_DIR);
|
|
2531
2535
|
const { uninstallMachine, realUninstallIo } = await import("./machine.js");
|
|
2532
2536
|
const { removeUserMcpEntries } = await import("./install.js");
|
|
2533
2537
|
const { killRecordedRun } = await import("./worker.js");
|
|
@@ -2537,6 +2541,7 @@ async function main() {
|
|
|
2537
2541
|
hasIntent: () => readIntent() !== null,
|
|
2538
2542
|
clearIntent: () => clearIntent(),
|
|
2539
2543
|
killRun: () => killRecordedRun(dirname(CONFIG_PATH)),
|
|
2544
|
+
homeRefusal,
|
|
2540
2545
|
}));
|
|
2541
2546
|
for (const l of done.lines)
|
|
2542
2547
|
console.error(` ${l}`);
|
|
@@ -2557,66 +2562,75 @@ async function main() {
|
|
|
2557
2562
|
left.push(...u.left);
|
|
2558
2563
|
}
|
|
2559
2564
|
const { unlinkSync, existsSync: exists, rmSync, readdirSync } = await import("node:fs");
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
+
const spare = [];
|
|
2566
|
+
const hasConfig = !!all && exists(CONFIG_PATH);
|
|
2567
|
+
if (homeRefusal) {
|
|
2568
|
+
left.push(refusedHomeLine(CONFIG_DIR, homeRefusal));
|
|
2569
|
+
}
|
|
2570
|
+
else if (hasConfig && keepIdentity) {
|
|
2571
|
+
left.push(`your identity and ${plan.teams.length} team membership${plan.teams.length === 1 ? "" : "s"}, ${CONFIG_PATH} (--keep-identity)`);
|
|
2572
|
+
}
|
|
2573
|
+
else if (!keepIdentity && (hasConfig || uninstalling)) {
|
|
2574
|
+
if (hasConfig) {
|
|
2565
2575
|
unlinkSync(CONFIG_PATH);
|
|
2566
2576
|
removed.push(`${CONFIG_PATH} — your key and ${plan.teams.length ? `the token${plan.teams.length === 1 ? "" : "s"} for ${plan.teams.map((t) => `"${t.name}"`).join(", ")}` : "account"}; rejoining means a new invite`);
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2577
|
+
}
|
|
2578
|
+
const runHomes = join(CONFIG_PATH, "..", "run-homes");
|
|
2579
|
+
const { restorePendingQuarantines, pendingQuarantines } = await import("./agents.js");
|
|
2580
|
+
const put = restorePendingQuarantines(runHomes);
|
|
2581
|
+
if (put.length)
|
|
2582
|
+
removed.push(`nothing of your repos' — what interrupted runs had held aside: ${put.join("; ")}`);
|
|
2583
|
+
const still = pendingQuarantines(runHomes);
|
|
2584
|
+
const keptAside = (dir) => { try {
|
|
2585
|
+
return readdirSync(join(dir, "quarantine-kept")).length > 0;
|
|
2586
|
+
}
|
|
2587
|
+
catch {
|
|
2588
|
+
return false;
|
|
2589
|
+
} };
|
|
2590
|
+
let homes = [];
|
|
2591
|
+
try {
|
|
2592
|
+
homes = readdirSync(runHomes);
|
|
2593
|
+
}
|
|
2594
|
+
catch { }
|
|
2595
|
+
const keep = new Set([...still.map((x) => x.runHome), ...homes.map((h) => join(runHomes, h)).filter(keptAside)]);
|
|
2596
|
+
for (const h of homes) {
|
|
2597
|
+
const dir = join(runHomes, h);
|
|
2598
|
+
if (keep.has(dir))
|
|
2599
|
+
continue;
|
|
2580
2600
|
try {
|
|
2581
|
-
|
|
2601
|
+
rmSync(dir, { recursive: true, force: true });
|
|
2582
2602
|
}
|
|
2583
2603
|
catch { }
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
continue;
|
|
2589
|
-
try {
|
|
2590
|
-
rmSync(dir, { recursive: true, force: true });
|
|
2591
|
-
}
|
|
2592
|
-
catch { }
|
|
2593
|
-
}
|
|
2594
|
-
if (!keep.size) {
|
|
2595
|
-
try {
|
|
2596
|
-
rmSync(runHomes, { recursive: true, force: true });
|
|
2597
|
-
}
|
|
2598
|
-
catch { }
|
|
2604
|
+
}
|
|
2605
|
+
if (!keep.size) {
|
|
2606
|
+
try {
|
|
2607
|
+
rmSync(runHomes, { recursive: true, force: true });
|
|
2599
2608
|
}
|
|
2600
|
-
|
|
2601
|
-
left.push(`${x.runHome} — it still holds ${x.paths.join(", ")} from ${x.worktree}, which could not be put back safely; compare and move them back by hand, then delete the folder`);
|
|
2602
|
-
for (const dir of keep)
|
|
2603
|
-
if (keptAside(dir) && !still.some((x) => x.runHome === dir))
|
|
2604
|
-
left.push(`${join(dir, "quarantine-kept")} — agent config an interrupted run held, set aside because the repo had moved on; check it, then delete the folder`);
|
|
2609
|
+
catch { }
|
|
2605
2610
|
}
|
|
2611
|
+
else
|
|
2612
|
+
spare.push(runHomes);
|
|
2613
|
+
for (const x of still)
|
|
2614
|
+
left.push(`${x.runHome} — it still holds ${x.paths.join(", ")} from ${x.worktree}, which could not be put back safely; compare and move them back by hand, then delete the folder`);
|
|
2615
|
+
for (const dir of keep)
|
|
2616
|
+
if (keptAside(dir) && !still.some((x) => x.runHome === dir))
|
|
2617
|
+
left.push(`${join(dir, "quarantine-kept")} — agent config an interrupted run held, set aside because the repo had moved on; check it, then delete the folder`);
|
|
2606
2618
|
}
|
|
2607
2619
|
{
|
|
2608
2620
|
const { removePantheonWorktrees, removeHomeLeftovers } = await import("./machine.js");
|
|
2609
2621
|
const repos = [project, ...Object.values(all?.bindings ?? {}).map((b) => b.path).filter((p) => !!p)];
|
|
2610
|
-
const trees = removePantheonWorktrees(repos, [join(CONFIG_DIR, "worktrees"), ...repos.map((r) => join(r, ".pantheon", "worktrees"))]);
|
|
2622
|
+
const trees = removePantheonWorktrees(repos, [...(homeRefusal ? [] : [join(CONFIG_DIR, "worktrees")]), ...repos.map((r) => join(r, ".pantheon", "worktrees"))]);
|
|
2611
2623
|
removed.push(...trees.removed);
|
|
2612
2624
|
left.push(...trees.left);
|
|
2613
|
-
|
|
2614
|
-
|
|
2625
|
+
if (!homeRefusal) {
|
|
2626
|
+
try {
|
|
2627
|
+
rmSync(join(CONFIG_DIR, "worktrees"), { recursive: true, force: true });
|
|
2628
|
+
}
|
|
2629
|
+
catch { }
|
|
2630
|
+
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity, { uninstalling, refusal: null });
|
|
2631
|
+
removed.push(...rest.removed);
|
|
2632
|
+
left.push(...rest.left);
|
|
2615
2633
|
}
|
|
2616
|
-
catch { }
|
|
2617
|
-
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity, { uninstalling });
|
|
2618
|
-
removed.push(...rest.removed);
|
|
2619
|
-
left.push(...rest.left);
|
|
2620
2634
|
}
|
|
2621
2635
|
if (uninstalling) {
|
|
2622
2636
|
if (install?.present) {
|
|
@@ -2626,6 +2640,12 @@ async function main() {
|
|
|
2626
2640
|
}
|
|
2627
2641
|
if (!runningManaged)
|
|
2628
2642
|
left.push(`this pantheon itself (${process.argv[1] ?? "the one you ran"}), which npm installed — remove it with: ${NPM_UNINSTALL}`);
|
|
2643
|
+
const { sweepPantheonHome } = await import("./machine.js");
|
|
2644
|
+
if (!homeRefusal) {
|
|
2645
|
+
const swept = sweepPantheonHome(CONFIG_DIR, { keepIdentity, spare, refusal: null });
|
|
2646
|
+
removed.push(...swept.removed);
|
|
2647
|
+
left.push(...swept.left);
|
|
2648
|
+
}
|
|
2629
2649
|
}
|
|
2630
2650
|
console.error(`\n${B("Removed")}`);
|
|
2631
2651
|
for (const r of removed)
|
|
@@ -3554,6 +3574,25 @@ async function main() {
|
|
|
3554
3574
|
st = null;
|
|
3555
3575
|
raw = null;
|
|
3556
3576
|
}
|
|
3577
|
+
if (raw !== null) {
|
|
3578
|
+
let nonce;
|
|
3579
|
+
try {
|
|
3580
|
+
nonce = JSON.parse(raw).nonce;
|
|
3581
|
+
}
|
|
3582
|
+
catch {
|
|
3583
|
+
nonce = undefined;
|
|
3584
|
+
}
|
|
3585
|
+
const stale = st !== null && Date.now() - st.mtimeMs > HUD_ACTION_MAX_AGE_MS;
|
|
3586
|
+
if (stale && typeof nonce === "string" && nonce !== want) {
|
|
3587
|
+
try {
|
|
3588
|
+
rmSync(f, { force: true });
|
|
3589
|
+
}
|
|
3590
|
+
catch { }
|
|
3591
|
+
st = null;
|
|
3592
|
+
raw = null;
|
|
3593
|
+
nonce = undefined;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3557
3596
|
if (raw !== null) {
|
|
3558
3597
|
let nonce;
|
|
3559
3598
|
try {
|
package/dist/client.js
CHANGED
|
@@ -21,6 +21,7 @@ export class PantheonClient {
|
|
|
21
21
|
seen = new Map();
|
|
22
22
|
backoff = 500;
|
|
23
23
|
closed = false;
|
|
24
|
+
disposed = false;
|
|
24
25
|
everConnected = false;
|
|
25
26
|
localOverride = null;
|
|
26
27
|
usingLoopback = false;
|
|
@@ -170,6 +171,11 @@ export class PantheonClient {
|
|
|
170
171
|
}
|
|
171
172
|
attempt(address) {
|
|
172
173
|
return new Promise((resolve, reject) => {
|
|
174
|
+
if (this.disposed) {
|
|
175
|
+
this.closed = true;
|
|
176
|
+
reject(new Error("this client was disposed"));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
173
179
|
let settled = false;
|
|
174
180
|
let heard = false;
|
|
175
181
|
const fail = reject;
|
|
@@ -195,6 +201,8 @@ export class PantheonClient {
|
|
|
195
201
|
this.watchdog.unref?.();
|
|
196
202
|
ws.on("open", () => { });
|
|
197
203
|
ws.on("message", (raw) => {
|
|
204
|
+
if (this.disposed)
|
|
205
|
+
return;
|
|
198
206
|
this.lastInbound = Date.now();
|
|
199
207
|
let msg;
|
|
200
208
|
try {
|
|
@@ -438,6 +446,14 @@ export class PantheonClient {
|
|
|
438
446
|
this.waiters = [];
|
|
439
447
|
this.ws?.close(4000, "handover");
|
|
440
448
|
}
|
|
449
|
+
dispose() {
|
|
450
|
+
this.disposed = true;
|
|
451
|
+
this.close();
|
|
452
|
+
if (this.watchdog) {
|
|
453
|
+
clearInterval(this.watchdog);
|
|
454
|
+
this.watchdog = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
441
457
|
close() {
|
|
442
458
|
this.closed = true;
|
|
443
459
|
for (const w of this.waiters) {
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join, resolve, sep } from "node:path";
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
7
|
import { repoName } from "./repo-route.js";
|
|
8
|
+
import { markPantheonHome } from "./home-safety.js";
|
|
8
9
|
export const CONFIG_DIR = process.env.PANTHEON_HOME ?? join(homedir(), ".pantheon");
|
|
9
10
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
10
11
|
export const DEFAULT_RELAY = process.env.PANTHEON_DEFAULT_RELAY
|
|
@@ -146,7 +147,7 @@ export function saveAll(f) {
|
|
|
146
147
|
return CONFIG_PATH;
|
|
147
148
|
}
|
|
148
149
|
function privateDir() {
|
|
149
|
-
|
|
150
|
+
ensurePantheonHome();
|
|
150
151
|
if (process.platform === "win32")
|
|
151
152
|
return;
|
|
152
153
|
try {
|
|
@@ -202,6 +203,11 @@ function writeConfigAtomic(data) {
|
|
|
202
203
|
catch { }
|
|
203
204
|
}
|
|
204
205
|
}
|
|
206
|
+
export function ensurePantheonHome() {
|
|
207
|
+
const made = !existsSync(CONFIG_DIR);
|
|
208
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
209
|
+
markPantheonHome(CONFIG_DIR, made);
|
|
210
|
+
}
|
|
205
211
|
const CONFIG_LOCK_STALE_MS = 5_000;
|
|
206
212
|
const CONFIG_LOCK_WAIT_MS = 15_000;
|
|
207
213
|
const sleepSync = (ms) => { try {
|