@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.
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 -120
  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 +494 -32
  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 +82 -14
  29. package/dist/reads.js +30 -0
  30. package/dist/refusal.js +14 -1
  31. package/dist/relay.js +1143 -180
  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 +84 -34
  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"`);
@@ -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) => 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,
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
- 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 };
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
- const how = platform() === "darwin"
204
- ? `launchctl disable gui/${process.getuid?.() ?? 0}/dev.pantheon.${kind}`
205
- : `systemctl --user disable --now pantheon-${kind}`;
206
- 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
+ }
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
- for (const l of io.uninstall(kind))
667
+ const said = io.uninstall(kind);
668
+ for (const l of said)
294
669
  lines.push(`${kind}: ${l.trim()}`);
295
- 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(", ")})` : ""}`);
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: 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`);
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
- 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
+ }
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