@join-pantheon/cli 0.31.2 → 0.32.0-rc.1
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 +4 -1
- package/dist/claude-confine.js +12 -2
- package/dist/cli.js +56 -17
- package/dist/doctor.js +64 -6
- package/dist/helpers-build.js +69 -1
- package/dist/install.js +25 -11
- package/dist/installer.js +204 -0
- package/dist/machine.js +15 -1
- package/dist/managed.js +238 -0
- package/dist/mcp-supervisor.js +2 -1
- package/dist/measure.js +2 -1
- package/dist/notifier.js +7 -17
- package/dist/notify.js +7 -1
- package/dist/relay.js +67 -6
- package/dist/service.js +14 -6
- package/dist/tray.js +5 -10
- package/dist/uninstall.js +194 -0
- package/dist/upgrade.js +44 -5
- package/dist/which.js +11 -3
- package/dist/worker-policy.js +2 -0
- package/dist/worker.js +30 -7
- package/helper/macos/prebuilt/manifest.json +29 -0
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/macos/prebuilt/pantheon-tray +0 -0
- package/installer/install.ps1 +547 -0
- package/installer/install.sh +612 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -325,7 +325,10 @@ pantheon service install menubar
|
|
|
325
325
|
|
|
326
326
|
It is a thin shell: every fact comes from `pantheon tray-status` and every action runs a
|
|
327
327
|
pantheon command, so it holds no state, no network of its own, and no credentials. Like the
|
|
328
|
-
notification HUD it ships as Swift source
|
|
328
|
+
notification HUD it ships as Swift source plus a universal (Apple silicon and Intel), ad-hoc
|
|
329
|
+
signed binary built from exactly that source at publish time, so installing it needs no Xcode
|
|
330
|
+
Command Line Tools. Only if the shipped binary does not match its source, or does not run on
|
|
331
|
+
this Mac, is it compiled locally — and that alone needs the Command Line Tools.
|
|
329
332
|
|
|
330
333
|
Human decisions use a separate, model-free path. `ESCALATE` creates a durable
|
|
331
334
|
`HUMAN` obligation; install native desktop alerts with:
|
package/dist/claude-confine.js
CHANGED
|
@@ -3,7 +3,8 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { writeInside } from "./safe-write.js";
|
|
5
5
|
import { homedir, platform, tmpdir } from "node:os";
|
|
6
|
-
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
7
|
+
import { expectedLauncher } from "./managed.js";
|
|
7
8
|
import { agentSpawn } from "./which.js";
|
|
8
9
|
export const NO_CLAUDE_FEATURES = { version: null, permissionPrompts: false, settings: false, settingSources: false, disallowedTools: false };
|
|
9
10
|
export function parseClaudeHelp(help, version = null) {
|
|
@@ -139,7 +140,7 @@ function userExcludedCommands(file) {
|
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
const SECRET_FILE_GLOBS = [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*", ".npmrc", ".netrc", "credentials*", "*.keystore", "service-account*.json"];
|
|
142
|
-
const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "bin", "ops", "run-homes", "logs", "mcp", "data"];
|
|
143
|
+
const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "bin", "runtime", "ops", "run-homes", "logs", "mcp", "data"];
|
|
143
144
|
function aroundWorktree(top, keeps, named) {
|
|
144
145
|
const keep = (typeof keeps === "string" ? [keeps] : [...keeps]).map((k) => resolve(k));
|
|
145
146
|
const out = [];
|
|
@@ -167,6 +168,14 @@ function aroundWorktree(top, keeps, named) {
|
|
|
167
168
|
walk(top, true);
|
|
168
169
|
return out;
|
|
169
170
|
}
|
|
171
|
+
function managedTargets(home, os) {
|
|
172
|
+
const launcherAt = expectedLauncher(home, os);
|
|
173
|
+
if (!launcherAt)
|
|
174
|
+
return [];
|
|
175
|
+
const P = os === "win32" ? win32 : posix;
|
|
176
|
+
const bin = P.dirname(launcherAt);
|
|
177
|
+
return [{ abs: bin, dir: true }, { abs: P.join(P.dirname(bin), "runtime"), dir: true }];
|
|
178
|
+
}
|
|
170
179
|
export function claudeRunSettings(i) {
|
|
171
180
|
const home = resolve(i.home ?? homedir());
|
|
172
181
|
const os = i.os ?? platform();
|
|
@@ -176,6 +185,7 @@ export function claudeRunSettings(i) {
|
|
|
176
185
|
...SECRET_HOME_PATHS.map((p) => ({ abs: join(home, ...p.rel.split("/")), dir: p.dir })),
|
|
177
186
|
{ abs: resolve(i.pantheonHome), dir: true },
|
|
178
187
|
{ abs: resolve(i.runHome), dir: true },
|
|
188
|
+
...managedTargets(home, os),
|
|
179
189
|
];
|
|
180
190
|
const seen = new Set();
|
|
181
191
|
const denied = [];
|
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,7 @@ import { connectFailure, signInHint } from "./refusal.js";
|
|
|
47
47
|
import * as mcpWindows from "./mcp-windows.js";
|
|
48
48
|
import { updateBanner } from "./update-check.js";
|
|
49
49
|
import { inWokenRun } from "./worker-policy.js";
|
|
50
|
+
import { joinInstructions, managedLayout, relayServesInstaller } from "./managed.js";
|
|
50
51
|
const argv = process.argv.slice(2);
|
|
51
52
|
for (let i = argv.length - 1; i >= 1; i--) {
|
|
52
53
|
const m = /^(--[a-z][a-z0-9-]*)=(.*)$/i.exec(argv[i]);
|
|
@@ -565,14 +566,16 @@ function windowsOverridesForBoundRepos() {
|
|
|
565
566
|
for (const r of windowsRepoOverrides(dirs))
|
|
566
567
|
console.error(` ${D("claude code in")} ${r.dir} ${D(r.status)}`);
|
|
567
568
|
}
|
|
568
|
-
function printJoinSteps(invite) {
|
|
569
|
+
async function printJoinSteps(invite, relay) {
|
|
570
|
+
const j = joinInstructions(invite, relay, await relayServesInstaller(relay));
|
|
569
571
|
console.error(`\n${B("For a teammate who'd rather not type commands, forward this whole block:")}\n`);
|
|
570
|
-
console.error(D(`
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
572
|
+
console.error(D(` ${j.intro}`));
|
|
573
|
+
for (const s of j.steps) {
|
|
574
|
+
console.error(D(` ${s.label}`));
|
|
575
|
+
for (const l of s.lines)
|
|
576
|
+
console.error(` ${C(l)}`);
|
|
577
|
+
}
|
|
578
|
+
console.error(D(` ${j.outro}`));
|
|
576
579
|
}
|
|
577
580
|
const HUD_ACTION_MAX_AGE_MS = 30_000;
|
|
578
581
|
function unattendedProject() {
|
|
@@ -917,6 +920,7 @@ ${B("control")}
|
|
|
917
920
|
(auto-detects claude / codex / cursor / copilot / kiro on this machine)
|
|
918
921
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
919
922
|
pantheon leave remove what pantheon put on this machine
|
|
923
|
+
pantheon uninstall leave, then remove Pantheon itself: its own Node, the pantheon command, the PATH lines
|
|
920
924
|
pantheon panic emergency stop: ends the run in flight and the worker, and switches off wake rights in every team you are in
|
|
921
925
|
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
922
926
|
pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
|
|
@@ -973,6 +977,7 @@ const COMMAND_FLAGS = {
|
|
|
973
977
|
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
974
978
|
network: "--proxy <url> [--no-proxy <hosts>] | --direct | --clear",
|
|
975
979
|
leave: "[<team>] --yes --keep-identity --stop-relay --project <dir>",
|
|
980
|
+
uninstall: "--yes --keep-identity --stop-relay --project <dir>",
|
|
976
981
|
stop: "[relay|worker|notifier|menubar|all] --port <n>",
|
|
977
982
|
logs: "[relay|worker|notifier|menubar] --follow --n <lines>",
|
|
978
983
|
worker: "--agent <claude-code|codex|cursor|copilot|kiro|auto> --persona <name> --project <dir> --allow-bash --max-runs <n> --max-runs-day <n> --grace <s> --once",
|
|
@@ -1074,7 +1079,7 @@ async function main() {
|
|
|
1074
1079
|
if (helpAt.some((a) => a === "--help" || a === "-h")) {
|
|
1075
1080
|
const u = usageFor(cmd, HELP_ALL);
|
|
1076
1081
|
const flags = COMMAND_FLAGS[cmd];
|
|
1077
|
-
const teamCmd = !["start", "join", "signup", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status"].includes(cmd);
|
|
1082
|
+
const teamCmd = !["start", "join", "signup", "login", "verify", "whoami", "relay", "help", "version", "upgrade", "network", "ps", "stop", "service", "hook", "mcp", "pending", "tray-status", "uninstall"].includes(cmd);
|
|
1078
1083
|
const shown = flags && teamCmd && !/--space/.test(flags) ? `${flags}\n --space <team> (the team to act on, when this folder is bound to another or none)` : flags;
|
|
1079
1084
|
console.log(u || shown ? `${u ?? ` pantheon ${cmd}`}${shown ? `\n\n${B("flags")}\n ${shown}` : ""}\n\n${D("nothing was run · all commands: pantheon help --all")}` : HELP);
|
|
1080
1085
|
return;
|
|
@@ -1188,7 +1193,7 @@ async function main() {
|
|
|
1188
1193
|
console.error(r.inviteMayWake
|
|
1189
1194
|
? ` ${B("It also lets them wake workers:")} ${D("their requests can start an agent run on this machine while you are away (you chose wake \"anyone\"). Take it back: pantheon admit <name> --revoke")}`
|
|
1190
1195
|
: D(` It lets them join and talk to your agent. It does not let them wake this machine on its own: the first time their request needs it while you are away, you are asked (Always / Just this once / No). Approve ahead: pantheon wake allow <name>`));
|
|
1191
|
-
printJoinSteps(link ?? r.invite);
|
|
1196
|
+
await printJoinSteps(link ?? r.invite, r.cfg?.relay);
|
|
1192
1197
|
}
|
|
1193
1198
|
if (!r.reach.shareable && r.reach.hint) {
|
|
1194
1199
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
@@ -1250,7 +1255,7 @@ async function main() {
|
|
|
1250
1255
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
1251
1256
|
if (inv.note)
|
|
1252
1257
|
console.error(`\x1b[33m ! ${inv.note}\x1b[0m`);
|
|
1253
|
-
printJoinSteps(link ?? raw);
|
|
1258
|
+
await printJoinSteps(link ?? raw, cfg.relay);
|
|
1254
1259
|
client.close();
|
|
1255
1260
|
return;
|
|
1256
1261
|
}
|
|
@@ -1279,6 +1284,7 @@ async function main() {
|
|
|
1279
1284
|
dataDir: resolveDataDir(flag("data")).dir,
|
|
1280
1285
|
});
|
|
1281
1286
|
const { port, host } = await relay.listen();
|
|
1287
|
+
relay.startInstallerCheck();
|
|
1282
1288
|
console.error(`${B("pantheon relay")} listening on ${C(`ws://${host}:${port}`)}`);
|
|
1283
1289
|
console.error(D(`ledger: ${resolveDataDir(flag("data")).dir}`));
|
|
1284
1290
|
console.error(D(`health: http://localhost:${port}/health`));
|
|
@@ -1521,7 +1527,7 @@ async function main() {
|
|
|
1521
1527
|
for (const s of r.steps)
|
|
1522
1528
|
console.error(` ${s.ok ? C("ok") : "\x1b[31mFAIL\x1b[0m"} ${s.name.padEnd(6)} ${s.detail}`);
|
|
1523
1529
|
if (!r.ok) {
|
|
1524
|
-
console.error(`\n${B("upgrade stopped")} — nothing was changed. Fix the above and run
|
|
1530
|
+
console.error(`\n${B("upgrade stopped")} — nothing was changed.${r.needsPerson ? "" : " Fix the above and run `pantheon upgrade` again."}`);
|
|
1525
1531
|
process.exit(1);
|
|
1526
1532
|
}
|
|
1527
1533
|
if (r.alreadyCurrent) {
|
|
@@ -2394,8 +2400,17 @@ async function main() {
|
|
|
2394
2400
|
console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound} folder binding${bound === 1 ? "" : "s"}` : ""} left this machine`);
|
|
2395
2401
|
return;
|
|
2396
2402
|
}
|
|
2403
|
+
case "uninstall":
|
|
2397
2404
|
case "leave": {
|
|
2398
|
-
|
|
2405
|
+
const uninstalling = cmd === "uninstall";
|
|
2406
|
+
const self = uninstalling ? "uninstall" : "leave";
|
|
2407
|
+
if (uninstalling) {
|
|
2408
|
+
if (inWokenRun())
|
|
2409
|
+
die("a woken run cannot uninstall Pantheon — a person has to run this here");
|
|
2410
|
+
if (argv[1] && !argv[1].startsWith("--"))
|
|
2411
|
+
die(`pantheon uninstall takes no team. To drop just one: pantheon leave ${argv[1]}`);
|
|
2412
|
+
}
|
|
2413
|
+
if (!uninstalling && argv[1] && !argv[1].startsWith("--")) {
|
|
2399
2414
|
const all = loadAll() ?? die("not configured");
|
|
2400
2415
|
const m = findMembership(all, argv[1]) ?? die(`you are not in a team called "${argv[1]}". Yours: ${Object.values(all.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
2401
2416
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).map((b) => b.origin ?? b.path);
|
|
@@ -2431,14 +2446,29 @@ async function main() {
|
|
|
2431
2446
|
const stranded = [...standing].filter(([, v]) => v.founder && v.others?.length === 0).map(([k]) => k);
|
|
2432
2447
|
if (stranded.length && !argv.includes("--yes")) {
|
|
2433
2448
|
die(`you founded ${stranded.map((t) => `"${t}"`).join(", ")} and nobody else is in ${stranded.length === 1 ? "it" : "them"}. Leaving keeps ${stranded.length === 1 ? "it" : "them"} on the relay — holding your plan's team slot — and takes away the only way to delete ${stranded.length === 1 ? "it" : "them"}.\n` +
|
|
2434
|
-
` Delete first: ${stranded.map((t) => `pantheon space delete ${t} --yes`).join(" · ")}\n Leave anyway: pantheon
|
|
2449
|
+
` Delete first: ${stranded.map((t) => `pantheon space delete ${t} --yes`).join(" · ")}\n Leave anyway: pantheon ${self} --yes`);
|
|
2435
2450
|
}
|
|
2436
2451
|
const orphaned = [...standing].filter(([, v]) => v.founder && (v.others?.length ?? 0) > 0).map(([k]) => k);
|
|
2437
2452
|
if (orphaned.length && !argv.includes("--yes")) {
|
|
2438
2453
|
die(`you founded ${orphaned.map((t) => `"${t}"`).join(", ")}, and others are in ${orphaned.length === 1 ? "it" : "them"}. Leaving removes this machine's key: you could not rejoin as yourself, and only a founder can remove a member or delete a team — nobody there could, ever again.\n` +
|
|
2439
|
-
` To step out but keep the way back: ${orphaned.map((t) => `pantheon leave ${t}`).join(" · ")} (keeps your key)\n Leave anyway: pantheon
|
|
2454
|
+
` To step out but keep the way back: ${orphaned.map((t) => `pantheon leave ${t}`).join(" · ")} (keeps your key)\n Leave anyway: pantheon ${self} --yes`);
|
|
2455
|
+
}
|
|
2456
|
+
const { managedInstall, removeManagedInstall, NPM_UNINSTALL } = await import("./uninstall.js");
|
|
2457
|
+
const install = uninstalling ? managedInstall(homedir()) : null;
|
|
2458
|
+
const runningManaged = managedLayout();
|
|
2459
|
+
if (uninstalling && !argv.includes("--yes")) {
|
|
2460
|
+
const teams = plan.teams.length && !keepIdentity ? `leaves ${plan.teams.map((t) => `"${t.name}"`).join(", ")} (this machine's key goes), ` : "";
|
|
2461
|
+
const what = `this removes Pantheon from this machine: it ${teams}uninstalls its services, hooks and agent entries` +
|
|
2462
|
+
(install?.present ? `, and deletes Pantheon's own Node in ${install.runtime}, the pantheon command and the PATH lines the installer added.` : ".");
|
|
2463
|
+
if (!process.stdin.isTTY)
|
|
2464
|
+
die(`${what}\n If you mean it: pantheon uninstall --yes`);
|
|
2465
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2466
|
+
const answer = (await questionOrNull(rl, `${what[0].toUpperCase()}${what.slice(1)} Type "yes" to continue: `)) ?? "";
|
|
2467
|
+
rl.close();
|
|
2468
|
+
if (answer.toLowerCase() !== "yes")
|
|
2469
|
+
die("nothing was changed.");
|
|
2440
2470
|
}
|
|
2441
|
-
if (plan.teams.length && !keepIdentity && !argv.includes("--yes")) {
|
|
2471
|
+
if (!uninstalling && plan.teams.length && !keepIdentity && !argv.includes("--yes")) {
|
|
2442
2472
|
const list = plan.teams.map((t) => `"${t.name}"`).join(", ");
|
|
2443
2473
|
const what = plan.teams.length > 1 ? `this machine is in ${plan.teams.length} teams — ${list}. Leaving removes its key, so it leaves ALL of them.` : `this removes Pantheon and this machine's key, so it leaves ${list}.`;
|
|
2444
2474
|
if (!process.stdin.isTTY)
|
|
@@ -2449,7 +2479,7 @@ async function main() {
|
|
|
2449
2479
|
if (answer.toLowerCase() !== "yes")
|
|
2450
2480
|
die(`nothing was changed.${plan.teams.length > 1 ? " Leave one team with: pantheon leave <team>" : ""}`);
|
|
2451
2481
|
}
|
|
2452
|
-
console.error(`${B("leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
|
|
2482
|
+
console.error(`${B(uninstalling ? "uninstalling pantheon" : "leaving pantheon")}${plan.teams.length ? D(` — ${plan.teams.map((t) => t.name).join(", ")}`) : ""}\n`);
|
|
2453
2483
|
const { uninstallMachine, realUninstallIo } = await import("./machine.js");
|
|
2454
2484
|
const { removeUserMcpEntries } = await import("./install.js");
|
|
2455
2485
|
const { killRecordedRun } = await import("./worker.js");
|
|
@@ -2529,10 +2559,19 @@ async function main() {
|
|
|
2529
2559
|
rmSync(join(CONFIG_DIR, "worktrees"), { recursive: true, force: true });
|
|
2530
2560
|
}
|
|
2531
2561
|
catch { }
|
|
2532
|
-
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity);
|
|
2562
|
+
const rest = removeHomeLeftovers(CONFIG_DIR, keepIdentity, { uninstalling });
|
|
2533
2563
|
removed.push(...rest.removed);
|
|
2534
2564
|
left.push(...rest.left);
|
|
2535
2565
|
}
|
|
2566
|
+
if (uninstalling) {
|
|
2567
|
+
if (install?.present) {
|
|
2568
|
+
const r = removeManagedInstall({ home: homedir(), install, running: runningManaged });
|
|
2569
|
+
removed.push(...r.removed);
|
|
2570
|
+
left.push(...r.left);
|
|
2571
|
+
}
|
|
2572
|
+
if (!runningManaged)
|
|
2573
|
+
left.push(`this pantheon itself (${process.argv[1] ?? "the one you ran"}), which npm installed — remove it with: ${NPM_UNINSTALL}`);
|
|
2574
|
+
}
|
|
2536
2575
|
console.error(`\n${B("Removed")}`);
|
|
2537
2576
|
for (const r of removed)
|
|
2538
2577
|
console.error(` ${C("✓")} ${r}`);
|
package/dist/doctor.js
CHANGED
|
@@ -8,9 +8,11 @@ import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
|
8
8
|
import { loadProfile, profileInitialized, reservedDomains, teamProfile, wakeMode } from "./profile.js";
|
|
9
9
|
import { isApprovedWaker, loadPolicy, relayKnowsAdmitters, unsandboxedAllowed } from "./worker-policy.js";
|
|
10
10
|
import { commandState } from "./command-state.js";
|
|
11
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
12
12
|
import { homedir, platform } from "node:os";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
13
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
14
|
+
import { expectedLauncher, managedLayout, runtimeNodeFit, runtimeVerdict } from "./managed.js";
|
|
15
|
+
import { PKG_NAME } from "./upgrade.js";
|
|
14
16
|
import { PantheonClient } from "./client.js";
|
|
15
17
|
import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
|
|
16
18
|
import { fingerprint, signChallenge, verifyChallenge } from "./identity.js";
|
|
@@ -19,11 +21,14 @@ import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
|
19
21
|
import { loadPersonas } from "./persona.js";
|
|
20
22
|
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
21
23
|
import { agentById, chooseAgent, describeChoice, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
22
|
-
import { onPathNamed } from "./which.js";
|
|
24
|
+
import { loginShellLookup, npmSpawn, onPathNamed, which, whichWindows } from "./which.js";
|
|
23
25
|
import { VERSION } from "./version.js";
|
|
24
26
|
import { serviceInstalled, servicesUnavailable } from "./service.js";
|
|
27
|
+
import { hasPrebuilt } from "./helpers-build.js";
|
|
28
|
+
import { traySourcePath, TRAY_PREBUILT } from "./tray.js";
|
|
29
|
+
import { hudSourcePath, HUD_PREBUILT } from "./notifier.js";
|
|
25
30
|
import { hostEnv, platformName } from "./host-env.js";
|
|
26
|
-
import { currentPlan, explainNetError, routeFor, redactProxy } from "./network.js";
|
|
31
|
+
import { currentPlan, explainNetError, proxyEnvFor, routeFor, redactProxy } from "./network.js";
|
|
27
32
|
import { refusalOf } from "./refusal.js";
|
|
28
33
|
const HOSTED_RELAY = "relay.joinpantheon.network";
|
|
29
34
|
const git = (args, cwd) => {
|
|
@@ -109,12 +114,64 @@ export function nodeSupported(version) {
|
|
|
109
114
|
const [major = 0, minor = 0] = version.replace(/^v/, "").split(".").map(Number);
|
|
110
115
|
return major > 23 || (major === 23 && minor >= 4) || (major === 22 && minor >= 13);
|
|
111
116
|
}
|
|
117
|
+
function runtimeFacts() {
|
|
118
|
+
const os = platform();
|
|
119
|
+
const layout = managedLayout(process.execPath, os);
|
|
120
|
+
const expected = expectedLauncher(homedir(), os);
|
|
121
|
+
const root = layout?.root ?? (expected ? dirname(dirname(expected)) : null);
|
|
122
|
+
const real = (p) => { try {
|
|
123
|
+
return realpathSync(p);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return resolve(p);
|
|
127
|
+
} };
|
|
128
|
+
const rootReal = root ? real(root) : null;
|
|
129
|
+
const ours = (p) => {
|
|
130
|
+
if (!rootReal)
|
|
131
|
+
return false;
|
|
132
|
+
const r = real(p);
|
|
133
|
+
return r === rootReal || r.startsWith(rootReal + sep);
|
|
134
|
+
};
|
|
135
|
+
const firstOnPath = os === "win32"
|
|
136
|
+
? [whichWindows("pantheon", process.env)]
|
|
137
|
+
: [which("pantheon", os, process.env, { login: () => null }), loginShellLookup("pantheon")?.found ?? null];
|
|
138
|
+
return { layout, nodeVersion: process.versions.node, expected, expectedExists: !!expected && existsSync(expected), firstOnPath, ours };
|
|
139
|
+
}
|
|
140
|
+
function newestRelease() {
|
|
141
|
+
if (process.env["PANTHEON_NO_UPDATE_CHECK"])
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const s = npmSpawn(["view", PKG_NAME, "version", "engines.node", "--json"], undefined, undefined, proxyEnvFor());
|
|
145
|
+
const j = JSON.parse(execFileSync(s.command, s.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 8000, ...s.options }));
|
|
146
|
+
return typeof j.version === "string" && typeof j["engines.node"] === "string" ? { version: j.version, range: j["engines.node"] } : null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
112
152
|
export async function doctor(projectDir) {
|
|
113
153
|
const out = [];
|
|
114
154
|
const add = (level, name, detail, fix, share) => out.push({ level, name, detail, ...(fix ? { fix } : {}), ...(share ? { share } : {}) });
|
|
115
155
|
nodeSupported(process.versions.node)
|
|
116
156
|
? add("pass", "node", `v${process.versions.node}`, undefined, `v${process.versions.node}`)
|
|
117
157
|
: add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22.13 or newer — node:sqlite, which the relay uses, is behind a flag through 22.12 — install a current Node LTS and re-run", `v${process.versions.node}, too old`);
|
|
158
|
+
{
|
|
159
|
+
const facts = runtimeFacts();
|
|
160
|
+
const v = runtimeVerdict(facts);
|
|
161
|
+
if (v)
|
|
162
|
+
add(v.level, "runtime", v.detail, v.fix, v.share);
|
|
163
|
+
if (facts.layout) {
|
|
164
|
+
const relay = (() => { try {
|
|
165
|
+
return load()?.relay ?? null;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return null;
|
|
169
|
+
} })();
|
|
170
|
+
const fit = runtimeNodeFit(facts.layout, process.versions.node, newestRelease(), relay);
|
|
171
|
+
if (fit)
|
|
172
|
+
add(fit.level, "runtime node", fit.detail, fit.fix, fit.share);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
118
175
|
const host = hostEnv();
|
|
119
176
|
if (host.ephemeral) {
|
|
120
177
|
add("warn", "machine", `${platformName(host.platform)} in ${host.kind === "container" ? "a container" : "a cloud workspace"} — ${host.evidence.join("; ")}`, `this is not the computer you work on, which is why it reports ${platformName(host.platform)} to someone who may be sitting at a Mac. The team and your account are fine here, but the menu bar, desktop alerts and the worker belong on your own machine — run the setup from a terminal there.`, `${host.platform} in ${host.kind === "container" ? "a container" : "a cloud workspace"}`);
|
|
@@ -489,8 +546,9 @@ export async function doctor(projectDir) {
|
|
|
489
546
|
}
|
|
490
547
|
}
|
|
491
548
|
if (host.desktop) {
|
|
549
|
+
const needsTools = (name, source) => hasPrebuilt(name, source) ? "" : "needs the Xcode Command Line Tools — xcode-select --install — because this build has no prebuilt helper for it; ";
|
|
492
550
|
if (!serviceInstalled("menubar")) {
|
|
493
|
-
add("warn", "menu bar", "not installed on a Mac that can run it",
|
|
551
|
+
add("warn", "menu bar", "not installed on a Mac that can run it", `install it with: pantheon service install menubar (${needsTools(TRAY_PREBUILT, traySourcePath())}from a setup that ran on this computer, not in a cloud agent). --no-menubar at setup also leaves it off; that is fine if it was your choice`, "not installed");
|
|
494
552
|
}
|
|
495
553
|
else if (!menubarProc().running) {
|
|
496
554
|
add("warn", "menu bar", "installed, but not running — launchd is not holding it, so there is no icon right now", "pantheon service restart menubar (loads it back; something unloaded it — a stop, a crash loop, or a test run on this machine)", "installed, not running");
|
|
@@ -499,7 +557,7 @@ export async function doctor(projectDir) {
|
|
|
499
557
|
add("pass", "menu bar", "installed and running — status, pause/resume, and a badge when a decision needs you", undefined, "installed and running");
|
|
500
558
|
}
|
|
501
559
|
if (!serviceInstalled("notifier")) {
|
|
502
|
-
add("warn", "alerts", "not installed — no desktop notification when a teammate needs you",
|
|
560
|
+
add("warn", "alerts", "not installed — no desktop notification when a teammate needs you", `pantheon service install notifier (${needsTools(HUD_PREBUILT, hudSourcePath())}--no-notifications at setup also leaves it off; that is fine if it was your choice)`, "not installed");
|
|
503
561
|
}
|
|
504
562
|
else if (!notifierProc().running) {
|
|
505
563
|
add("warn", "alerts", "installed, but not running — nothing on this Mac will show you a teammate's message", "pantheon service restart notifier (loads it back; something unloaded it — a stop, a crash loop, or a test run on this machine)", "installed, not running");
|
package/dist/helpers-build.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { chmodSync, copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
export const helperHashPath = (binary) => `${binary}.source.sha256`;
|
|
7
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export const prebuiltDir = () => resolve(HERE, "..", "helper", "macos", "prebuilt");
|
|
4
9
|
export function sourceHash(source) {
|
|
5
10
|
return createHash("sha256").update(readFileSync(source)).digest("hex");
|
|
6
11
|
}
|
|
@@ -20,4 +25,67 @@ export function helperStale(binary, source) {
|
|
|
20
25
|
return true;
|
|
21
26
|
}
|
|
22
27
|
}
|
|
28
|
+
export function findPrebuilt(name, source, dir = prebuiltDir()) {
|
|
29
|
+
let manifest;
|
|
30
|
+
try {
|
|
31
|
+
manifest = JSON.parse(readFileSync(join(dir, "manifest.json"), "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { reason: "this Pantheon build carries no prebuilt helpers" };
|
|
35
|
+
}
|
|
36
|
+
const entry = manifest?.helpers?.[name];
|
|
37
|
+
if (!entry)
|
|
38
|
+
return { reason: `no prebuilt ${name} in this build` };
|
|
39
|
+
if (entry.source_sha256 !== sourceHash(source))
|
|
40
|
+
return { reason: `the prebuilt ${name} was built from a different ${entry.source} than this release ships` };
|
|
41
|
+
const path = join(dir, name);
|
|
42
|
+
try {
|
|
43
|
+
if (createHash("sha256").update(readFileSync(path)).digest("hex") !== entry.sha256)
|
|
44
|
+
return { reason: `the prebuilt ${name} does not match its manifest` };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { reason: `the prebuilt ${name} is missing` };
|
|
48
|
+
}
|
|
49
|
+
return { path };
|
|
50
|
+
}
|
|
51
|
+
export const hasPrebuilt = (name, source, dir = prebuiltDir()) => existsSync(source) && "path" in findPrebuilt(name, source, dir);
|
|
52
|
+
export function requireDeveloperTools(what, service, why) {
|
|
53
|
+
try {
|
|
54
|
+
execFileSync("/usr/bin/xcode-select", ["-p"], { stdio: "pipe" });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error(`building ${what} needs the Xcode Command Line Tools${why ? ` (${why}, so it is compiled here)` : ""} — run \`xcode-select --install\`, then \`pantheon service install ${service}\``);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function installHelper(o, deps = {}) {
|
|
61
|
+
const found = findPrebuilt(o.name, o.source, deps.prebuiltDir ?? prebuiltDir());
|
|
62
|
+
let why = "reason" in found ? found.reason : "";
|
|
63
|
+
if ("path" in found) {
|
|
64
|
+
const tmp = `${o.binary}.${process.pid}.new`;
|
|
65
|
+
try {
|
|
66
|
+
copyFileSync(found.path, tmp);
|
|
67
|
+
chmodSync(tmp, 0o755);
|
|
68
|
+
try {
|
|
69
|
+
execFileSync(deps.xattr ?? "/usr/bin/xattr", ["-d", "com.apple.quarantine", tmp], { stdio: "pipe" });
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
execFileSync(tmp, ["version"], { stdio: "pipe", timeout: 10_000 });
|
|
73
|
+
renameSync(tmp, o.binary);
|
|
74
|
+
recordHelperBuild(o.binary, o.source);
|
|
75
|
+
return { binary: o.binary, via: "prebuilt" };
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(tmp, { force: true });
|
|
80
|
+
}
|
|
81
|
+
catch { }
|
|
82
|
+
why = `the prebuilt ${o.name} does not run on this Mac (${e.message.split("\n")[0]})`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
(deps.requireTools ?? requireDeveloperTools)(o.what, o.service, why);
|
|
86
|
+
execFileSync(deps.swiftc ?? "/usr/bin/swiftc", ["-O", o.source, "-o", o.binary], { stdio: "pipe" });
|
|
87
|
+
chmodSync(o.binary, 0o755);
|
|
88
|
+
recordHelperBuild(o.binary, o.source);
|
|
89
|
+
return { binary: o.binary, via: "compiled", reason: why };
|
|
90
|
+
}
|
|
23
91
|
//# sourceMappingURL=helpers-build.js.map
|
package/dist/install.js
CHANGED
|
@@ -11,17 +11,20 @@ import { agentSpawn, onPathNamed, which } from "./which.js";
|
|
|
11
11
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
12
12
|
import { kiroDir, kiroAgentFile, refreshKiroAgentAllowed } from "./kiro-files.js";
|
|
13
13
|
import { eolOf, isJsonc, stripBom, stripJsonc, withEol } from "./text-file.js";
|
|
14
|
+
import { childNodeFlags, managedLayout, managedLauncher } from "./managed.js";
|
|
14
15
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
export function onPath() {
|
|
16
17
|
return onPathNamed("pantheon");
|
|
17
18
|
}
|
|
18
|
-
export function launcher(force, os = platform()) {
|
|
19
|
+
export function launcher(force, os = platform(), managed = managedLauncher()) {
|
|
19
20
|
const portable = force === "portable" || (force !== "local" && onPath());
|
|
21
|
+
if (!portable && managed && os !== "win32")
|
|
22
|
+
return { command: managed, args: ["mcp"], portable: false };
|
|
20
23
|
if (os === "win32")
|
|
21
|
-
return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
24
|
+
return { command: process.execPath, args: [...childNodeFlags(), resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
22
25
|
return portable
|
|
23
26
|
? { command: "pantheon", args: ["mcp"], portable: true }
|
|
24
|
-
: { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
27
|
+
: { command: process.execPath, args: [...childNodeFlags(), resolve(HERE, "cli.js"), "mcp"], portable: false };
|
|
25
28
|
}
|
|
26
29
|
function readConfigJson(file) {
|
|
27
30
|
return readConfigDoc(file).doc;
|
|
@@ -200,19 +203,29 @@ export function install(projectDir, cfg, force, os = platform(), claudeLocal = c
|
|
|
200
203
|
return out;
|
|
201
204
|
}
|
|
202
205
|
function localEntry() {
|
|
203
|
-
return { command: process.execPath, args: [resolve(HERE, "cli.js"), "mcp"] };
|
|
206
|
+
return { command: process.execPath, args: [...childNodeFlags(), resolve(HERE, "cli.js"), "mcp"] };
|
|
204
207
|
}
|
|
205
|
-
|
|
208
|
+
const realEntryDeps = (os) => ({
|
|
209
|
+
execPath: process.execPath, cli: resolve(HERE, "cli.js"), exists: existsSync, find: (n) => which(n),
|
|
210
|
+
managed: managedLayout(process.execPath, os),
|
|
211
|
+
});
|
|
212
|
+
export function userEntry(os = platform(), d = realEntryDeps(os)) {
|
|
213
|
+
if (d.managed) {
|
|
214
|
+
if (os === "win32" && d.exists(d.managed.cli))
|
|
215
|
+
return { command: d.managed.node, args: ["--use-system-ca", d.managed.cli, "mcp"] };
|
|
216
|
+
if (os !== "win32" && d.exists(d.managed.launcher))
|
|
217
|
+
return { command: d.managed.launcher, args: ["mcp"] };
|
|
218
|
+
}
|
|
206
219
|
if (os === "win32") {
|
|
207
|
-
const node =
|
|
208
|
-
const entry =
|
|
209
|
-
if (
|
|
220
|
+
const node = d.execPath;
|
|
221
|
+
const entry = d.cli;
|
|
222
|
+
if (d.exists(entry))
|
|
210
223
|
return { command: node, args: [entry, "mcp"] };
|
|
211
|
-
const abs =
|
|
224
|
+
const abs = d.find("pantheon");
|
|
212
225
|
return abs ? { command: abs, args: ["mcp"] } : { command: "pantheon", args: ["mcp"] };
|
|
213
226
|
}
|
|
214
227
|
try {
|
|
215
|
-
const abs =
|
|
228
|
+
const abs = d.find("pantheon") ?? "";
|
|
216
229
|
if (abs && !isVersionManagedPath(abs))
|
|
217
230
|
return { command: abs, args: ["mcp"] };
|
|
218
231
|
if (abs)
|
|
@@ -698,7 +711,8 @@ export function isOurMcpEntry(e) {
|
|
|
698
711
|
const base = command.split(/[\\/]/).pop() ?? "";
|
|
699
712
|
if (/^pantheon(\.cmd|\.exe)?$/i.test(base))
|
|
700
713
|
return true;
|
|
701
|
-
|
|
714
|
+
const script = args.find((a) => !(typeof a === "string" && a.startsWith("--")));
|
|
715
|
+
return args.length >= 2 && typeof script === "string" && /[\\/]cli\.js$/.test(script);
|
|
702
716
|
}
|
|
703
717
|
export function removeMcpJsonEntry(file, key = "mcpServers") {
|
|
704
718
|
try {
|