@join-pantheon/cli 0.29.0 → 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.
Files changed (46) hide show
  1. package/README.md +45 -22
  2. package/bin/pantheon.js +12 -0
  3. package/dist/accounts.js +151 -26
  4. package/dist/agents.js +197 -14
  5. package/dist/claude-confine.js +283 -0
  6. package/dist/cli.js +725 -119
  7. package/dist/client.js +37 -2
  8. package/dist/command-state.js +79 -0
  9. package/dist/config.js +211 -10
  10. package/dist/doctor.js +103 -13
  11. package/dist/grants.js +1 -1
  12. package/dist/history-view.js +2 -1
  13. package/dist/home-guard.js +210 -0
  14. package/dist/hooks-install.js +2 -1
  15. package/dist/identity.js +24 -0
  16. package/dist/install.js +76 -26
  17. package/dist/kiro-files.js +2 -2
  18. package/dist/ledger.js +111 -28
  19. package/dist/machine.js +507 -38
  20. package/dist/mcp-supervisor.js +25 -6
  21. package/dist/mcp-windows.js +47 -25
  22. package/dist/mcp.js +25 -10
  23. package/dist/network.js +39 -8
  24. package/dist/notify.js +17 -6
  25. package/dist/pending.js +8 -14
  26. package/dist/persona.js +21 -0
  27. package/dist/profile.js +4 -1
  28. package/dist/quickstart.js +88 -15
  29. package/dist/reads.js +30 -0
  30. package/dist/refusal.js +14 -1
  31. package/dist/relay.js +1193 -185
  32. package/dist/roster-file.js +169 -23
  33. package/dist/run-failure.js +83 -3
  34. package/dist/secret-guard.js +141 -0
  35. package/dist/service.js +105 -36
  36. package/dist/setup-guide.js +28 -9
  37. package/dist/spend.js +6 -2
  38. package/dist/store.js +94 -10
  39. package/dist/text-file.js +74 -0
  40. package/dist/tray.js +11 -2
  41. package/dist/update-check.js +2 -0
  42. package/dist/which.js +69 -12
  43. package/dist/worker-policy.js +137 -3
  44. package/dist/worker.js +702 -81
  45. package/helper/macos/menubar.swift +15 -1
  46. 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
- const out = sh(`ps -o etime= -p ${pid}`);
18
- return out ? out.trim() : undefined;
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 = sh(`launchctl list | grep -w ${label}`);
53
+ const row = launchctlRow(runTool("launchctl", ["list"]) ?? "", label);
25
54
  if (!row)
26
55
  return {};
27
- const [pidRaw, statusRaw] = row.split(/\s+/);
28
- const pid = Number(pidRaw);
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" || !existsSync(unitPath(kind)))
90
+ if (platform() !== "linux" || !existingUnitPath(kind))
62
91
  return {};
63
- const out = sh(`systemctl --user show pantheon-${kind} -p MainPID -p ActiveState -p ExecMainStatus 2>/dev/null`);
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
- const out = sh(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t 2>/dev/null | head -1`);
80
- const pid = out ? Number(out) : NaN;
81
- if (!Number.isFinite(pid))
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, pattern) {
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 bare = sh(`pgrep -f "${pattern}" | head -1`);
100
- const pid = bare ? Number(bare) : NaN;
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", "pantheon.* worker"); }
107
- export function notifierProc() { return bareProc("notifier", "pantheon.* notifier"); }
108
- export function menubarProc() { return bareProc("menubar", "pantheon.* menubar|pantheon-tray"); }
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"`);
@@ -148,24 +378,68 @@ export function logFileFor(kind, proc) {
148
378
  export function dataSummary(dir) {
149
379
  if (!dir || !existsSync(dir))
150
380
  return "no ledger yet";
151
- const files = sh(`ls -1 "${dir}"/*.jsonl 2>/dev/null`);
152
- if (!files)
381
+ let files = [];
382
+ try {
383
+ files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f));
384
+ }
385
+ catch { }
386
+ if (!files.length)
153
387
  return `${dir} (empty)`;
154
388
  let acts = 0;
155
- for (const f of files.split("\n")) {
156
- const n = sh(`wc -l < "${f}"`);
157
- acts += Number(n ?? 0);
389
+ for (const f of files) {
390
+ try {
391
+ const t = readFileSync(f, "utf8");
392
+ acts += t.split("\n").filter((l) => l.trim()).length;
393
+ }
394
+ catch { }
158
395
  }
159
- const spaces = files.split("\n").length;
396
+ const spaces = files.length;
160
397
  return `${acts} acts across ${spaces} space${spaces > 1 ? "s" : ""}`;
161
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
+ }
162
432
  const defaultStopIo = (port = 8787) => ({
163
433
  proc: (kind) => kind === "relay" ? relayProc(port) : kind === "worker" ? workerProc() : kind === "menubar" ? menubarProc() : notifierProc(),
164
434
  installed: (kind) => serviceInstalled(kind),
165
- sh,
435
+ sh: killBySh,
166
436
  disable: (kind) => disableService(kind),
167
- cmdline: (pid) => sh(`ps -o command= -p ${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,
168
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`;
169
443
  export const isPantheonCommand = (cmd) => /pantheon/i.test(cmd) || /\bcli\.js\s+(relay|start)\b/.test(cmd);
170
444
  export function stopProcResult(kind, io = defaultStopIo()) {
171
445
  const p = io.proc(kind);
@@ -173,16 +447,43 @@ export function stopProcResult(kind, io = defaultStopIo()) {
173
447
  if (!ours && (p.managedBy === "launchd" || p.managedBy === "systemd") && p.running) {
174
448
  return { lines: [`${kind}: running under ${p.managedBy}, but not installed from this home — left alone`], stopped: false };
175
449
  }
450
+ const borrowed = (io.borrowed ?? borrowedHome)();
176
451
  const killLive = (q) => {
177
452
  if (!q.running || !q.pid)
178
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
+ }
179
464
  if (kind === "relay") {
180
465
  const cmd = (io.cmdline ?? (() => null))(q.pid);
181
466
  if (!cmd || !isPantheonCommand(cmd)) {
182
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 };
183
468
  }
184
469
  }
185
- io.sh(`kill ${q.pid}`);
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 };
186
487
  return { line: `${kind}: stopped pid ${q.pid}${q.managedBy === "foreground" ? " (it was started by hand, not as a service)" : ""}`, killed: true };
187
488
  };
188
489
  if (ours) {
@@ -193,10 +494,15 @@ export function stopProcResult(kind, io = defaultStopIo()) {
193
494
  disabled = true;
194
495
  }
195
496
  catch (e) {
196
- const how = platform() === "darwin"
197
- ? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
198
- : `systemctl --user disable --now pantheon-${kind}`;
199
- lines.push(`${kind}: could not disable the service (${e.message.split("\n")[0]}) — it may start again at login; try: ${how}`);
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
+ }
200
506
  }
201
507
  const after = io.proc(kind);
202
508
  const k = killLive(after);
@@ -264,6 +570,11 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
264
570
  lines.push(...r.lines);
265
571
  if (kind === "relay")
266
572
  relayStopped = r.stopped;
573
+ if (kind === "worker" && opts.killRun) {
574
+ const k = opts.killRun();
575
+ if (k)
576
+ lines.push(`worker: ${k}`);
577
+ }
267
578
  }
268
579
  if (!relayStopped)
269
580
  return { lines };
@@ -276,6 +587,76 @@ export function stopMachine(what, opts, io = defaultStopIo(opts.port), hosts = h
276
587
  const elsewhere = [...new Set(teams.map((t) => t.relay))];
277
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.` };
278
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`;
279
660
  const LOG_KINDS = ["worker", "relay", "notifier", "menubar"];
280
661
  export function uninstallMachine(opts, io) {
281
662
  const lines = [], removed = [], left = [];
@@ -283,9 +664,11 @@ export function uninstallMachine(opts, io) {
283
664
  for (const kind of kinds) {
284
665
  if (io.installed(kind)) {
285
666
  try {
286
- for (const l of io.uninstall(kind))
667
+ const said = io.uninstall(kind);
668
+ for (const l of said)
287
669
  lines.push(`${kind}: ${l.trim()}`);
288
- removed.push(`the ${kind} service — stopped, and its definition deleted so it cannot return at login`);
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(", ")})` : ""}`);
289
672
  }
290
673
  catch (e) {
291
674
  left.push(`the ${kind} service (${e.message.split("\n")[0]}) — pantheon service uninstall ${kind}`);
@@ -296,8 +679,22 @@ export function uninstallMachine(opts, io) {
296
679
  if (kind === "relay")
297
680
  for (const l of io.stop(kind))
298
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
+ }
299
691
  else
300
- left.push(`a ${kind} started by hand is still running (pid ${p.pid ?? "?"}) — close its terminal, or: kill ${p.pid ?? "<pid>"}`);
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`);
301
698
  }
302
699
  }
303
700
  if (!opts.stopRelay && (io.installed("relay") || io.proc("relay").running)) {
@@ -352,8 +749,12 @@ export function uninstallMachine(opts, io) {
352
749
  export async function panicMachine(io) {
353
750
  const lines = io.stopWorker().map((l) => ` ${l}`);
354
751
  const workerDown = await io.workerDown();
355
- if (!workerDown)
356
- lines.push(` ! the worker is still running. Stop it by hand: ${platform() === "darwin" ? "launchctl bootout gui/$(id -u)/dev.pantheon.worker" : "systemctl --user disable --now pantheon-worker"} or kill its pid (pantheon ps)`);
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
+ }
357
758
  let allRevoked = true;
358
759
  if (!io.teams.length)
359
760
  lines.push(" no team on this machine, so there are no wake grants to revoke");
@@ -377,6 +778,70 @@ export function leavePlan(all) {
377
778
  const teams = Object.values(all?.spaces ?? {}).map((m) => ({ name: m.name, relay: m.relay }));
378
779
  return { teams, ...(all?.member ? { member: all.member } : {}) };
379
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
+ }
380
845
  export function statFile(f) {
381
846
  try {
382
847
  const s = statSync(f);
@@ -402,6 +867,10 @@ export function realUninstallIo(deps) {
402
867
  hasIntent: deps.hasIntent,
403
868
  clearIntent: deps.clearIntent,
404
869
  logDir: join(CONFIG_DIR, "logs"),
870
+ ownWorkerPid: () => ownWorkerPid(),
871
+ borrowed: () => borrowedHome(),
872
+ kill: (pid) => killPid(pid) === null,
873
+ ...(deps.killRun ? { killRun: deps.killRun } : {}),
405
874
  };
406
875
  }
407
876
  //# sourceMappingURL=machine.js.map