@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.
- 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 -119
- 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 +507 -38
- 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 +88 -15
- package/dist/reads.js +30 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1193 -185
- 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 +105 -36
- 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"`);
|
|
@@ -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
|
-
|
|
152
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
667
|
+
const said = io.uninstall(kind);
|
|
668
|
+
for (const l of said)
|
|
287
669
|
lines.push(`${kind}: ${l.trim()}`);
|
|
288
|
-
|
|
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:
|
|
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
|
-
|
|
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
|