@join-pantheon/cli 0.29.1 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -22
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +197 -14
- package/dist/claude-confine.js +283 -0
- package/dist/cli.js +725 -120
- package/dist/client.js +37 -2
- package/dist/command-state.js +79 -0
- package/dist/config.js +211 -10
- package/dist/doctor.js +103 -13
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +210 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +24 -0
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +25 -6
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +25 -10
- package/dist/network.js +39 -8
- package/dist/notify.js +17 -6
- package/dist/pending.js +8 -14
- package/dist/persona.js +21 -0
- package/dist/profile.js +4 -1
- package/dist/quickstart.js +82 -14
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1143 -180
- package/dist/roster-file.js +169 -23
- package/dist/run-failure.js +83 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +84 -34
- package/dist/setup-guide.js +28 -9
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +11 -2
- package/dist/update-check.js +2 -0
- package/dist/which.js +69 -12
- package/dist/worker-policy.js +137 -3
- package/dist/worker.js +702 -81
- package/helper/macos/menubar.swift +15 -1
- package/package.json +2 -2
package/dist/machine.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, statSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname, networkInterfaces, platform } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
6
|
-
import { disableService, plistPath, serviceInstalled, serviceLogFile, unitPath, uninstallService } from "./service.js";
|
|
6
|
+
import { borrowedHome, disableService, existingUnitPath, plistPath, serviceInstalled, serviceLogFile, unitPath, unitPaths, uninstallService } from "./service.js";
|
|
7
7
|
import { copilotMcpConfigFile, vsCodeUserDir, kiroMcpConfigFile, kiroDir } from "./install.js";
|
|
8
8
|
const sh = (cmd) => {
|
|
9
9
|
try {
|
|
@@ -13,19 +13,48 @@ const sh = (cmd) => {
|
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
|
+
const SYSTEM_PATH = ["/usr/bin", "/bin", "/usr/sbin", "/sbin"];
|
|
17
|
+
export function runTool(cmd, args) {
|
|
18
|
+
try {
|
|
19
|
+
const PATH = [...new Set([...(process.env.PATH ?? "").split(":"), ...SYSTEM_PATH].filter(Boolean))].join(":");
|
|
20
|
+
return execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000, env: { ...process.env, PATH, LC_ALL: "C" } }).trim() || null;
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
const out = e.stdout;
|
|
24
|
+
return typeof out === "string" && out.trim() ? out.trim() : null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function launchctlRow(list, label) {
|
|
28
|
+
for (const line of list.split("\n")) {
|
|
29
|
+
const cols = line.trim().split(/\s+/);
|
|
30
|
+
if (cols.length < 3 || cols[2] !== label)
|
|
31
|
+
continue;
|
|
32
|
+
const pid = /^\d+$/.test(cols[0]) ? Number(cols[0]) : null;
|
|
33
|
+
return { pid, status: cols[1] };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function firstPid(out) {
|
|
38
|
+
for (const line of (out ?? "").split("\n"))
|
|
39
|
+
if (/^\s*\d+\s*$/.test(line) && Number(line) > 0)
|
|
40
|
+
return Number(line);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
16
43
|
function uptime(pid) {
|
|
17
|
-
|
|
18
|
-
|
|
44
|
+
if (platform() === "win32")
|
|
45
|
+
return undefined;
|
|
46
|
+
const out = runTool("ps", ["-o", "etime=", "-p", String(pid)]);
|
|
47
|
+
return out && /^[\d:-]+$/.test(out.trim()) ? out.trim() : undefined;
|
|
19
48
|
}
|
|
20
49
|
function fromLaunchd(kind) {
|
|
21
50
|
if (platform() !== "darwin")
|
|
22
51
|
return {};
|
|
23
52
|
const label = `dev.pantheon.${kind}`;
|
|
24
|
-
const row =
|
|
53
|
+
const row = launchctlRow(runTool("launchctl", ["list"]) ?? "", label);
|
|
25
54
|
if (!row)
|
|
26
55
|
return {};
|
|
27
|
-
const
|
|
28
|
-
const
|
|
56
|
+
const pid = row.pid ?? NaN;
|
|
57
|
+
const statusRaw = row.status;
|
|
29
58
|
const info = {
|
|
30
59
|
managedBy: "launchd",
|
|
31
60
|
label,
|
|
@@ -58,9 +87,9 @@ export function parseSystemdShow(out) {
|
|
|
58
87
|
};
|
|
59
88
|
}
|
|
60
89
|
function fromSystemd(kind) {
|
|
61
|
-
if (platform() !== "linux" || !
|
|
90
|
+
if (platform() !== "linux" || !existingUnitPath(kind))
|
|
62
91
|
return {};
|
|
63
|
-
const out =
|
|
92
|
+
const out = runTool("systemctl", ["--user", "show", `pantheon-${kind}`, "-p", "MainPID", "-p", "ActiveState", "-p", "ExecMainStatus"]);
|
|
64
93
|
const label = `pantheon-${kind}.service`;
|
|
65
94
|
if (!out)
|
|
66
95
|
return { managedBy: "systemd", label, running: false };
|
|
@@ -76,9 +105,10 @@ const fromManager = (kind) => {
|
|
|
76
105
|
return l.managedBy ? l : fromSystemd(kind);
|
|
77
106
|
};
|
|
78
107
|
function listenerOn(port) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
if (platform() === "win32")
|
|
109
|
+
return null;
|
|
110
|
+
const pid = firstPid(runTool("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]));
|
|
111
|
+
if (pid === null)
|
|
82
112
|
return null;
|
|
83
113
|
return { pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
84
114
|
}
|
|
@@ -92,20 +122,220 @@ export function relayProc(port) {
|
|
|
92
122
|
}
|
|
93
123
|
return { kind: "relay", running: false, managedBy: svc.managedBy ?? "none", ...svc };
|
|
94
124
|
}
|
|
95
|
-
function bareProc(kind
|
|
125
|
+
function bareProc(kind) {
|
|
96
126
|
const svc = fromManager(kind);
|
|
97
127
|
if (svc.running && svc.pid)
|
|
98
128
|
return { kind, ...svc, running: true };
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (Number.isFinite(pid)) {
|
|
129
|
+
const pid = findBareProcess(kind);
|
|
130
|
+
if (pid !== null) {
|
|
102
131
|
return { kind, running: true, managedBy: "foreground", pid, ...(uptime(pid) ? { since: uptime(pid) } : {}) };
|
|
103
132
|
}
|
|
104
133
|
return { kind, running: false, managedBy: svc.managedBy ?? "none", ...svc };
|
|
105
134
|
}
|
|
106
|
-
export function workerProc() { return bareProc("worker"
|
|
107
|
-
export function notifierProc() { return bareProc("notifier"
|
|
108
|
-
export function menubarProc() { return bareProc("menubar"
|
|
135
|
+
export function workerProc() { return bareProc("worker"); }
|
|
136
|
+
export function notifierProc() { return bareProc("notifier"); }
|
|
137
|
+
export function menubarProc() { return bareProc("menubar"); }
|
|
138
|
+
const BARE_PATTERN = {
|
|
139
|
+
worker: /pantheon.* worker/,
|
|
140
|
+
notifier: /pantheon.* notifier/,
|
|
141
|
+
menubar: /pantheon.* menubar|pantheon-tray/,
|
|
142
|
+
relay: /pantheon.* relay/,
|
|
143
|
+
};
|
|
144
|
+
const PGREP_PATTERN = {
|
|
145
|
+
worker: "[p]antheon.* worker",
|
|
146
|
+
notifier: "[p]antheon.* notifier",
|
|
147
|
+
menubar: "[p]antheon.* menubar|[p]antheon-tray",
|
|
148
|
+
relay: "[p]antheon.* relay",
|
|
149
|
+
};
|
|
150
|
+
export const workerPidFile = (home = CONFIG_DIR) => join(home, "worker.pid");
|
|
151
|
+
const aliveReal = (pid) => {
|
|
152
|
+
try {
|
|
153
|
+
process.kill(pid, 0);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
return e.code === "EPERM";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
function runPowerShell(args) {
|
|
161
|
+
const root = process.env.SystemRoot ?? process.env.windir;
|
|
162
|
+
const exe = root && existsSync(join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"))
|
|
163
|
+
? join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : "powershell.exe";
|
|
164
|
+
try {
|
|
165
|
+
return execFileSync(exe, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 6_000, windowsHide: true }).trim() || null;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
export function windowsProcessInfo(pid, run = runPowerShell) {
|
|
172
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
173
|
+
return null;
|
|
174
|
+
const script = `$p = Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}'; if ($p) { [DateTimeOffset]::new($p.CreationDate).ToUnixTimeMilliseconds(); $p.CommandLine }`;
|
|
175
|
+
const out = run(["-NoProfile", "-NonInteractive", "-Command", script]);
|
|
176
|
+
if (!out)
|
|
177
|
+
return null;
|
|
178
|
+
const [first, ...rest] = out.split(/\r?\n/);
|
|
179
|
+
const ms = Number(first?.trim());
|
|
180
|
+
return { startedMs: Number.isFinite(ms) && ms > 0 ? ms : null, cmd: rest.join(" ").trim() || null };
|
|
181
|
+
}
|
|
182
|
+
export const realProcScanIo = () => {
|
|
183
|
+
const win = new Map();
|
|
184
|
+
const winInfo = (pid) => { if (!win.has(pid))
|
|
185
|
+
win.set(pid, windowsProcessInfo(pid)); return win.get(pid) ?? null; };
|
|
186
|
+
return {
|
|
187
|
+
platform: platform(),
|
|
188
|
+
selfPid: process.pid,
|
|
189
|
+
procDir: "/proc",
|
|
190
|
+
pgrep: (pattern) => {
|
|
191
|
+
try {
|
|
192
|
+
return execFileSync("pgrep", ["-f", pattern], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 });
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
const st = e.status;
|
|
196
|
+
return st === 1 ? "" : null;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
ppidOf: (pid) => firstPid(runTool("ps", ["-o", "ppid=", "-p", String(pid)])),
|
|
200
|
+
alive: aliveReal,
|
|
201
|
+
cmdlineOf: (pid) => {
|
|
202
|
+
const p = platform();
|
|
203
|
+
if (p === "linux") {
|
|
204
|
+
try {
|
|
205
|
+
return readFileSync(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean).join(" ") || null;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (p === "darwin")
|
|
212
|
+
return runTool("ps", ["-o", "command=", "-p", String(pid)]);
|
|
213
|
+
if (p === "win32")
|
|
214
|
+
return winInfo(pid)?.cmd ?? null;
|
|
215
|
+
return null;
|
|
216
|
+
},
|
|
217
|
+
startedAt: (pid) => (platform() === "win32" ? winInfo(pid)?.startedMs ?? null : null),
|
|
218
|
+
...(typeof process.getuid === "function" ? { uid: process.getuid() } : {}),
|
|
219
|
+
pidFile: workerPidFile(),
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
export function readProcTable(procDir) {
|
|
223
|
+
let names;
|
|
224
|
+
try {
|
|
225
|
+
names = readdirSync(procDir);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
const out = [];
|
|
231
|
+
for (const n of names) {
|
|
232
|
+
if (!/^\d+$/.test(n))
|
|
233
|
+
continue;
|
|
234
|
+
try {
|
|
235
|
+
const cmd = readFileSync(join(procDir, n, "cmdline"), "utf8").split("\0").filter(Boolean).join(" ");
|
|
236
|
+
const stat = readFileSync(join(procDir, n, "stat"), "utf8");
|
|
237
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/)[1]);
|
|
238
|
+
let uid;
|
|
239
|
+
try {
|
|
240
|
+
const m = /^Uid:\s+(\d+)/m.exec(readFileSync(join(procDir, n, "status"), "utf8"));
|
|
241
|
+
if (m)
|
|
242
|
+
uid = Number(m[1]);
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
if (uid === undefined) {
|
|
246
|
+
try {
|
|
247
|
+
uid = statSync(join(procDir, n)).uid;
|
|
248
|
+
}
|
|
249
|
+
catch { }
|
|
250
|
+
}
|
|
251
|
+
out.push({ pid: Number(n), ppid: Number.isFinite(ppid) ? ppid : 0, cmd, ...(uid !== undefined ? { uid } : {}) });
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
export const isPantheonKindCommand = (kind, cmd) => BARE_PATTERN[kind].test(cmd) || (/pantheon|cli\.js/i.test(cmd) && new RegExp(`\\b${kind}\\b`).test(cmd));
|
|
258
|
+
const START_SLACK_MS = 10_000;
|
|
259
|
+
function pidFileWorker(io) {
|
|
260
|
+
let rec = null;
|
|
261
|
+
try {
|
|
262
|
+
rec = JSON.parse(readFileSync(io.pidFile, "utf8"));
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const pid = Number(rec?.pid);
|
|
268
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === io.selfPid)
|
|
269
|
+
return null;
|
|
270
|
+
const stale = () => { try {
|
|
271
|
+
rmSync(io.pidFile, { force: true });
|
|
272
|
+
}
|
|
273
|
+
catch { } return null; };
|
|
274
|
+
if (!io.alive(pid))
|
|
275
|
+
return stale();
|
|
276
|
+
const cmd = io.cmdlineOf(pid);
|
|
277
|
+
if (cmd !== null && !isPantheonKindCommand("worker", cmd))
|
|
278
|
+
return stale();
|
|
279
|
+
const born = io.startedAt?.(pid) ?? null;
|
|
280
|
+
if (born !== null) {
|
|
281
|
+
if (typeof rec?.procStart === "number") {
|
|
282
|
+
if (Math.abs(born - rec.procStart) > START_SLACK_MS)
|
|
283
|
+
return stale();
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const wrote = Date.parse(rec?.started ?? "");
|
|
287
|
+
if (Number.isFinite(wrote) && born > wrote + START_SLACK_MS)
|
|
288
|
+
return stale();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { pid, confirmed: cmd !== null };
|
|
292
|
+
}
|
|
293
|
+
export function findBareProcess(kind, io = realProcScanIo()) {
|
|
294
|
+
const fromPidFile = () => (kind === "worker" ? pidFileWorker(io)?.pid ?? null : null);
|
|
295
|
+
if (io.platform === "linux") {
|
|
296
|
+
const table = readProcTable(io.procDir);
|
|
297
|
+
if (!table)
|
|
298
|
+
return fromPidFile();
|
|
299
|
+
const parent = new Map(table.map((p) => [p.pid, p.ppid]));
|
|
300
|
+
const skip = new Set();
|
|
301
|
+
for (let p = io.selfPid, n = 0; p && p > 0 && n < 64; p = parent.get(p), n++)
|
|
302
|
+
skip.add(p);
|
|
303
|
+
const mine = (p) => io.uid === undefined || p.uid === io.uid;
|
|
304
|
+
const hit = table.find((p) => !skip.has(p.pid) && mine(p) && BARE_PATTERN[kind].test(p.cmd));
|
|
305
|
+
return hit ? hit.pid : fromPidFile();
|
|
306
|
+
}
|
|
307
|
+
if (io.platform === "darwin") {
|
|
308
|
+
const out = io.pgrep(PGREP_PATTERN[kind]);
|
|
309
|
+
if (out === null)
|
|
310
|
+
return fromPidFile();
|
|
311
|
+
const skip = new Set();
|
|
312
|
+
for (let p = io.selfPid, n = 0; p && p > 1 && n < 64; p = io.ppidOf(p), n++)
|
|
313
|
+
skip.add(p);
|
|
314
|
+
for (const line of out.split("\n")) {
|
|
315
|
+
const pid = Number(line.trim());
|
|
316
|
+
if (Number.isInteger(pid) && pid > 0 && !skip.has(pid))
|
|
317
|
+
return pid;
|
|
318
|
+
}
|
|
319
|
+
return fromPidFile();
|
|
320
|
+
}
|
|
321
|
+
return fromPidFile();
|
|
322
|
+
}
|
|
323
|
+
export function recordWorkerPid(file = workerPidFile()) {
|
|
324
|
+
try {
|
|
325
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
326
|
+
const procStart = Math.round(Date.now() - process.uptime() * 1000);
|
|
327
|
+
writeFileSync(file, JSON.stringify({ pid: process.pid, started: new Date().toISOString(), procStart, argv: process.argv.slice(1) }) + "\n");
|
|
328
|
+
const pid = process.pid;
|
|
329
|
+
process.on("exit", () => {
|
|
330
|
+
try {
|
|
331
|
+
if (JSON.parse(readFileSync(file, "utf8")).pid === pid)
|
|
332
|
+
rmSync(file, { force: true });
|
|
333
|
+
}
|
|
334
|
+
catch { }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch { }
|
|
338
|
+
}
|
|
109
339
|
export function wiring() {
|
|
110
340
|
const out = [];
|
|
111
341
|
const claude = sh(`claude mcp list 2>/dev/null | grep -i "^pantheon"`);
|
|
@@ -166,13 +396,50 @@ export function dataSummary(dir) {
|
|
|
166
396
|
const spaces = files.length;
|
|
167
397
|
return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
|
|
168
398
|
}
|
|
399
|
+
const killBySh = (cmd) => {
|
|
400
|
+
const m = /^kill (\d+)$/.exec(cmd);
|
|
401
|
+
if (!m)
|
|
402
|
+
return sh(cmd);
|
|
403
|
+
try {
|
|
404
|
+
process.kill(Number(m[1]), "SIGTERM");
|
|
405
|
+
return "";
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const killPid = (pid) => {
|
|
412
|
+
try {
|
|
413
|
+
process.kill(pid, "SIGTERM");
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
return e.code ?? "failed";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const killHint = (pid) => platform() === "win32" ? `taskkill /PID ${pid ?? "<pid>"} /T /F` : `kill ${pid ?? "<pid>"}`;
|
|
421
|
+
export function managerDefinitionPath(kind, plat = platform(), run = runTool) {
|
|
422
|
+
if (plat === "darwin") {
|
|
423
|
+
const out = run("launchctl", ["print", `gui/${process.getuid?.() ?? 501}/dev.pantheon.${kind}`]);
|
|
424
|
+
return /^\s*path = (.+)$/m.exec(out ?? "")?.[1]?.trim() ?? null;
|
|
425
|
+
}
|
|
426
|
+
if (plat === "linux") {
|
|
427
|
+
const out = run("systemctl", ["--user", "show", `pantheon-${kind}`, "-p", "FragmentPath"]);
|
|
428
|
+
return /^FragmentPath=(.+)$/m.exec(out ?? "")?.[1]?.trim() || null;
|
|
429
|
+
}
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
169
432
|
const defaultStopIo = (port = 8787) => ({
|
|
170
433
|
proc: (kind) => kind === "relay" ? relayProc(port) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
|
|
171
434
|
installed: (kind) => serviceInstalled(kind),
|
|
172
|
-
sh,
|
|
435
|
+
sh: killBySh,
|
|
173
436
|
disable: (kind) => disableService(kind),
|
|
174
|
-
cmdline: (pid) =>
|
|
437
|
+
cmdline: (pid) => (platform() === "win32" ? windowsProcessInfo(pid)?.cmd ?? null : runTool("ps", ["-o", "command=", "-p", String(pid)])),
|
|
438
|
+
borrowed: () => borrowedHome(),
|
|
439
|
+
loadedFrom: (kind) => managerDefinitionPath(kind),
|
|
440
|
+
kill: killPid,
|
|
175
441
|
});
|
|
442
|
+
export const borrowedHomeLine = (kind) => `${kind === "services" ? "services" : `${kind}:`} left alone — HOME (${homedir()}) is not your own home (a test, a sandbox or another account's folder), so the service manager's jobs here are the real user's, not this home's. Nothing was stopped; run this from your own login to stop your services`;
|
|
176
443
|
export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
|
|
177
444
|
export function stopProcResult(kind, io = defaultStopIo()) {
|
|
178
445
|
const p = io.proc(kind);
|
|
@@ -180,16 +447,43 @@ export function stopProcResult(kind, io = defaultStopIo()) {
|
|
|
180
447
|
if (!ours && (p.managedBy === "launchd" || p.managedBy === "systemd") && p.running) {
|
|
181
448
|
return { lines: [`${kind}: running under ${p.managedBy}, but not installed from this home — left alone`], stopped: false };
|
|
182
449
|
}
|
|
450
|
+
const borrowed = (io.borrowed ?? borrowedHome)();
|
|
183
451
|
const killLive = (q) => {
|
|
184
452
|
if (!q.running || !q.pid)
|
|
185
453
|
return null;
|
|
454
|
+
if (q.managedBy === "launchd" || q.managedBy === "systemd") {
|
|
455
|
+
if (borrowed)
|
|
456
|
+
return { line: borrowedHomeLine(kind), killed: false };
|
|
457
|
+
const want = q.managedBy === "launchd" ? plistPath(kind) : unitPath(kind);
|
|
458
|
+
const oursAt = q.managedBy === "launchd" ? [want] : unitPaths(kind);
|
|
459
|
+
const got = io.loadedFrom?.(kind) ?? null;
|
|
460
|
+
if (io.loadedFrom && (!got || !oursAt.some((w) => resolve(w) === resolve(got)))) {
|
|
461
|
+
return { line: `${kind}: pid ${q.pid} runs under ${q.managedBy} from ${got ?? "a definition it did not name"}, not ${want} — not this home's, left alone`, killed: false };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
186
464
|
if (kind === "relay") {
|
|
187
465
|
const cmd = (io.cmdline ?? (() => null))(q.pid);
|
|
188
466
|
if (!cmd || !isPantheonCommand(cmd)) {
|
|
189
467
|
return { line: `relay: the port is held by pid ${q.pid}${cmd ? ` (${cmd.slice(0, 80)})` : ""}, which is not Pantheon's relay — left alone`, killed: false };
|
|
190
468
|
}
|
|
191
469
|
}
|
|
192
|
-
|
|
470
|
+
else if (q.managedBy !== "launchd" && q.managedBy !== "systemd" && io.cmdline) {
|
|
471
|
+
const cmd = io.cmdline(q.pid);
|
|
472
|
+
if (cmd === null) {
|
|
473
|
+
return { line: `${kind}: pid ${q.pid} could not be confirmed as Pantheon's ${kind} (its command line could not be read), so it was left alone — a pid is soon reused, and this one may be another program by now. If it is the ${kind}: ${killHint(q.pid)}`, killed: false };
|
|
474
|
+
}
|
|
475
|
+
if (!isPantheonKindCommand(kind, cmd)) {
|
|
476
|
+
return { line: `${kind}: pid ${q.pid} is not Pantheon's ${kind} any more (${cmd.slice(0, 80)}) — left alone`, killed: false };
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const err = io.kill ? io.kill(q.pid) : (io.sh(`kill ${q.pid}`), null);
|
|
480
|
+
if (err === "ESRCH")
|
|
481
|
+
return { line: `${kind}: pid ${q.pid} had already exited`, killed: true };
|
|
482
|
+
if (err === "EPERM") {
|
|
483
|
+
return { line: `${kind}: pid ${q.pid} is not yours to stop (EPERM) — it belongs to another user on this machine; nothing was stopped`, killed: false };
|
|
484
|
+
}
|
|
485
|
+
if (err)
|
|
486
|
+
return { line: `${kind}: could not stop pid ${q.pid} (${err}) — try: ${killHint(q.pid)}`, killed: false };
|
|
193
487
|
return { line: `${kind}: stopped pid ${q.pid}${q.managedBy === "foreground" ? " (it was started by hand, not as a service)" : ""}`, killed: true };
|
|
194
488
|
};
|
|
195
489
|
if (ours) {
|
|
@@ -200,10 +494,15 @@ export function stopProcResult(kind, io = defaultStopIo()) {
|
|
|
200
494
|
disabled = true;
|
|
201
495
|
}
|
|
202
496
|
catch (e) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
497
|
+
if (borrowed) {
|
|
498
|
+
lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — HOME (${homedir()}) is not your own home, so the service manager was left alone; run this from your own login to stop your services`);
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const how = platform() === "darwin"
|
|
502
|
+
? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
|
|
503
|
+
: `systemctl --user disable --now pantheon-${kind}`;
|
|
504
|
+
lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
|
|
505
|
+
}
|
|
207
506
|
}
|
|
208
507
|
const after = io.proc(kind);
|
|
209
508
|
const k = killLive(after);
|
|
@@ -271,6 +570,11 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
|
|
|
271
570
|
lines.push(...r.lines);
|
|
272
571
|
if (kind === "relay")
|
|
273
572
|
relayStopped = r.stopped;
|
|
573
|
+
if (kind === "worker" && opts.killRun) {
|
|
574
|
+
const k = opts.killRun();
|
|
575
|
+
if (k)
|
|
576
|
+
lines.push(`worker: ${k}`);
|
|
577
|
+
}
|
|
274
578
|
}
|
|
275
579
|
if (!relayStopped)
|
|
276
580
|
return { lines };
|
|
@@ -283,6 +587,76 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
|
|
|
283
587
|
const elsewhere = [...new Set(teams.map((t) => t.relay))];
|
|
284
588
|
return { lines, note: `Your team${teams.length === 1 ? "'s relay is" : "s' relays are"} ${elsewhere.join(", ") || "elsewhere"}, not this one — your teammates are not affected.` };
|
|
285
589
|
}
|
|
590
|
+
export function ownWorkerPid(io = realProcScanIo()) {
|
|
591
|
+
const w = pidFileWorker(io);
|
|
592
|
+
return w?.confirmed ? w.pid : null;
|
|
593
|
+
}
|
|
594
|
+
export function removePantheonWorktrees(repos, roots, git = (repo, args) => execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 30_000 })) {
|
|
595
|
+
const removed = [], left = [];
|
|
596
|
+
const real = (p) => { try {
|
|
597
|
+
return realpathSync(p);
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return resolve(p);
|
|
601
|
+
} };
|
|
602
|
+
const rootsReal = roots.map(real);
|
|
603
|
+
const under = (p) => { const pp = real(p); return rootsReal.some((rr) => pp === rr || pp.startsWith(rr + "/") || pp.startsWith(rr + "\\")); };
|
|
604
|
+
for (const repo of [...new Set(repos.map((r) => resolve(r)))]) {
|
|
605
|
+
let list;
|
|
606
|
+
try {
|
|
607
|
+
list = git(repo, ["worktree", "list", "--porcelain"]);
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const trees = [...list.matchAll(/^worktree (.+)$/gm)].map((m) => m[1].trim()).filter(under);
|
|
613
|
+
let n = 0;
|
|
614
|
+
for (const t of trees) {
|
|
615
|
+
try {
|
|
616
|
+
git(repo, ["worktree", "remove", "--force", t]);
|
|
617
|
+
n++;
|
|
618
|
+
}
|
|
619
|
+
catch (e) {
|
|
620
|
+
left.push(`the worktree ${t} in ${repo} (${e.message.split("\n")[0]}) — git -C ${repo} worktree remove --force ${t}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
git(repo, ["worktree", "prune"]);
|
|
625
|
+
}
|
|
626
|
+
catch { }
|
|
627
|
+
if (n)
|
|
628
|
+
removed.push(`${n} worktree${n === 1 ? "" : "s"} the worker made in ${repo}`);
|
|
629
|
+
}
|
|
630
|
+
return { removed, left };
|
|
631
|
+
}
|
|
632
|
+
export function removeHomeLeftovers(home, keep) {
|
|
633
|
+
const removed = [], left = [];
|
|
634
|
+
const items = [
|
|
635
|
+
["worker-policy.json", "the worker's wake policy (who may wake it, and self-upgrade) — a rejoin starts from no grants"],
|
|
636
|
+
["profile.json", "your profile (name, title, what you decide)"],
|
|
637
|
+
["spend.json", "the spend ledger"],
|
|
638
|
+
["worker.pid", "the worker's pid file"],
|
|
639
|
+
["bin", "the menu bar's and alerts' files (the .command scripts, pantheon-hud, pantheon-tray)"],
|
|
640
|
+
];
|
|
641
|
+
for (const [name, what] of items) {
|
|
642
|
+
const f = join(home, name);
|
|
643
|
+
if (!existsSync(f))
|
|
644
|
+
continue;
|
|
645
|
+
if (keep && name !== "worker.pid" && name !== "bin") {
|
|
646
|
+
left.push(`${what}, ${f} (--keep-identity)`);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
rmSync(f, { recursive: true, force: true });
|
|
651
|
+
removed.push(`${what} (${f})`);
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
left.push(`${what}, ${f} — could not remove (${e.message.split("\n")[0]})`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return { removed, left };
|
|
658
|
+
}
|
|
659
|
+
export const accountStillExists = (email) => `your account${email ? ` ${email}` : ""} still exists — this machine is signed out. Sign back in with: pantheon login${email ? ` --email ${email}` : ""} (pantheon signup would fail: the email is taken). To delete the account itself: pantheon account delete, before leaving`;
|
|
286
660
|
const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
|
|
287
661
|
export function uninstallMachine(opts, io) {
|
|
288
662
|
const lines = [], removed = [], left = [];
|
|
@@ -290,9 +664,11 @@ export function uninstallMachine(opts, io) {
|
|
|
290
664
|
for (const kind of kinds) {
|
|
291
665
|
if (io.installed(kind)) {
|
|
292
666
|
try {
|
|
293
|
-
|
|
667
|
+
const said = io.uninstall(kind);
|
|
668
|
+
for (const l of said)
|
|
294
669
|
lines.push(`${kind}: ${l.trim()}`);
|
|
295
|
-
|
|
670
|
+
const files = said.map((l) => /^\s*removed (.+)$/.exec(l)?.[1]?.trim()).filter((f) => !!f);
|
|
671
|
+
removed.push(`the ${kind} service — stopped, and its definition deleted so it cannot return at login${files.length ? ` (${files.join(", ")})` : ""}`);
|
|
296
672
|
}
|
|
297
673
|
catch (e) {
|
|
298
674
|
left.push(`the ${kind} service (${e.message.split("\n")[0]}) — pantheon service uninstall ${kind}`);
|
|
@@ -303,8 +679,22 @@ export function uninstallMachine(opts, io) {
|
|
|
303
679
|
if (kind === "relay")
|
|
304
680
|
for (const l of io.stop(kind))
|
|
305
681
|
lines.push(l);
|
|
682
|
+
else if (kind === "worker" && p.pid && io.ownWorkerPid?.() === p.pid && io.kill) {
|
|
683
|
+
if (io.kill(p.pid))
|
|
684
|
+
removed.push(`the worker started by hand (pid ${p.pid}) — stopped; with no config it could only idle`);
|
|
685
|
+
else
|
|
686
|
+
left.push(`a worker started by hand is still running (pid ${p.pid}) and could not be stopped — close its terminal, or: ${killHint(p.pid)}`);
|
|
687
|
+
}
|
|
688
|
+
else if (io.borrowed?.()) {
|
|
689
|
+
left.push(`a ${kind} (pid ${p.pid ?? "?"}) is running on this machine — with HOME borrowed, it is probably another home's, so it was left alone`);
|
|
690
|
+
}
|
|
306
691
|
else
|
|
307
|
-
left.push(`a ${kind} started by hand is still running (pid ${p.pid ?? "?"}) — close its terminal, or:
|
|
692
|
+
left.push(`a ${kind} started by hand is still running (pid ${p.pid ?? "?"}) — close its terminal, or: ${killHint(p.pid)}`);
|
|
693
|
+
}
|
|
694
|
+
if (kind === "worker" && io.killRun) {
|
|
695
|
+
const k = io.killRun();
|
|
696
|
+
if (k)
|
|
697
|
+
removed.push(`${k.replace(/^stopped /, "")} — stopped`);
|
|
308
698
|
}
|
|
309
699
|
}
|
|
310
700
|
if (!opts.stopRelay && (io.installed("relay") || io.proc("relay").running)) {
|
|
@@ -359,8 +749,12 @@ export function uninstallMachine(opts, io) {
|
|
|
359
749
|
export async function panicMachine(io) {
|
|
360
750
|
const lines = io.stopWorker().map((l) => ` ${l}`);
|
|
361
751
|
const workerDown = await io.workerDown();
|
|
362
|
-
if (!workerDown)
|
|
363
|
-
|
|
752
|
+
if (!workerDown) {
|
|
753
|
+
const plat = io.platform ?? platform();
|
|
754
|
+
lines.push((io.borrowed ?? borrowedHome)()
|
|
755
|
+
? ` ! a worker is still running, and it is not this home's to stop: HOME (${homedir()}) is not your own home, so the service manager's worker here is the real user's. Run pantheon panic from your own login`
|
|
756
|
+
: ` ! the worker is still running. Stop it by hand: ${plat === "darwin" ? "launchctl bootout gui/$(id -u)/dev.pantheon.worker or kill its pid (pantheon ps)" : plat === "win32" ? "taskkill /PID <pid> /F (the pid is in pantheon ps)" : "systemctl --user disable --now pantheon-worker or kill its pid (pantheon ps)"}`);
|
|
757
|
+
}
|
|
364
758
|
let allRevoked = true;
|
|
365
759
|
if (!io.teams.length)
|
|
366
760
|
lines.push(" no team on this machine, so there are no wake grants to revoke");
|
|
@@ -384,6 +778,70 @@ export function leavePlan(all) {
|
|
|
384
778
|
const teams = Object.values(all?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
|
|
385
779
|
return { teams, ...(all?.member ? { member: all.member } : {}) };
|
|
386
780
|
}
|
|
781
|
+
export function tailLines(file, n) {
|
|
782
|
+
if (n <= 0)
|
|
783
|
+
return "";
|
|
784
|
+
let fd;
|
|
785
|
+
try {
|
|
786
|
+
fd = openSync(file, "r");
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return "";
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const size = fstatSync(fd).size;
|
|
793
|
+
let pos = size, text = "";
|
|
794
|
+
const chunk = 64 * 1024;
|
|
795
|
+
while (pos > 0 && (text.match(/\n/g)?.length ?? 0) <= n) {
|
|
796
|
+
const len = Math.min(chunk, pos);
|
|
797
|
+
pos -= len;
|
|
798
|
+
const buf = Buffer.alloc(len);
|
|
799
|
+
readSync(fd, buf, 0, len, pos);
|
|
800
|
+
text = buf.toString("utf8") + text;
|
|
801
|
+
}
|
|
802
|
+
const lines = text.split("\n");
|
|
803
|
+
const trailing = text.endsWith("\n");
|
|
804
|
+
if (trailing)
|
|
805
|
+
lines.pop();
|
|
806
|
+
return lines.slice(-n).join("\n") + (lines.length ? "\n" : "");
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
closeSync(fd);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
export async function followFile(file, onData, opts = {}) {
|
|
813
|
+
let pos = (() => { try {
|
|
814
|
+
return statSync(file).size;
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return 0;
|
|
818
|
+
} })();
|
|
819
|
+
const pollMs = opts.pollMs ?? 500;
|
|
820
|
+
while (!opts.signal?.aborted) {
|
|
821
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
822
|
+
let size;
|
|
823
|
+
try {
|
|
824
|
+
size = statSync(file).size;
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (size < pos)
|
|
830
|
+
pos = 0;
|
|
831
|
+
if (size === pos)
|
|
832
|
+
continue;
|
|
833
|
+
const fd = openSync(file, "r");
|
|
834
|
+
try {
|
|
835
|
+
const buf = Buffer.alloc(size - pos);
|
|
836
|
+
readSync(fd, buf, 0, buf.length, pos);
|
|
837
|
+
pos = size;
|
|
838
|
+
onData(buf.toString("utf8"));
|
|
839
|
+
}
|
|
840
|
+
finally {
|
|
841
|
+
closeSync(fd);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
387
845
|
export function statFile(f) {
|
|
388
846
|
try {
|
|
389
847
|
const s = statSync(f);
|
|
@@ -409,6 +867,10 @@ export function realUninstallIo(deps) {
|
|
|
409
867
|
hasIntent: deps.hasIntent,
|
|
410
868
|
clearIntent: deps.clearIntent,
|
|
411
869
|
logDir: join(CONFIG_DIR, "logs"),
|
|
870
|
+
ownWorkerPid: () => ownWorkerPid(),
|
|
871
|
+
borrowed: () => borrowedHome(),
|
|
872
|
+
kill: (pid) => killPid(pid) === null,
|
|
873
|
+
...(deps.killRun ? { killRun: deps.killRun } : {}),
|
|
412
874
|
};
|
|
413
875
|
}
|
|
414
876
|
//# sourceMappingURL=machine.js.map
|