@join-pantheon/cli 0.33.0 → 0.33.1-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/dist/claude-confine.js +5 -6
- package/dist/cli.js +52 -20
- package/dist/doctor.js +17 -1
- package/dist/home-safety.js +1 -0
- package/dist/install.js +2 -2
- package/dist/machine.js +8 -7
- package/dist/managed.js +14 -4
- package/dist/notify.js +32 -0
- package/dist/relay.js +3 -0
- package/dist/roster-file.js +12 -0
- package/dist/service.js +89 -14
- package/dist/uninstall.js +26 -9
- package/dist/upgrade.js +4 -0
- package/dist/win-host.js +16 -1
- package/dist/win-package.js +70 -0
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +36 -4
- package/installer/install.ps1 +215 -11
- package/package.json +1 -1
package/dist/claude-confine.js
CHANGED
|
@@ -4,7 +4,7 @@ import { lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:
|
|
|
4
4
|
import { writeInside } from "./safe-write.js";
|
|
5
5
|
import { homedir, platform, tmpdir } from "node:os";
|
|
6
6
|
import { isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
7
|
-
import { expectedLauncher } from "./managed.js";
|
|
7
|
+
import { expectedLauncher, windowsManagedRoots } from "./managed.js";
|
|
8
8
|
import { agentSpawn } from "./which.js";
|
|
9
9
|
export const NO_CLAUDE_FEATURES = { version: null, permissionPrompts: false, settings: false, settingSources: false, disallowedTools: false };
|
|
10
10
|
export function parseClaudeHelp(help, version = null) {
|
|
@@ -169,12 +169,11 @@ function aroundWorktree(top, keeps, named) {
|
|
|
169
169
|
return out;
|
|
170
170
|
}
|
|
171
171
|
function managedTargets(home, os) {
|
|
172
|
-
const launcherAt = expectedLauncher(home, os);
|
|
173
|
-
if (!launcherAt)
|
|
174
|
-
return [];
|
|
175
172
|
const P = os === "win32" ? win32 : posix;
|
|
176
|
-
const
|
|
177
|
-
|
|
173
|
+
const roots = os === "win32"
|
|
174
|
+
? Object.values(windowsManagedRoots(home)).filter((r) => !!r)
|
|
175
|
+
: [expectedLauncher(home, os)].filter((l) => !!l).map((l) => P.dirname(P.dirname(l)));
|
|
176
|
+
return roots.flatMap((root) => [{ abs: P.join(root, "bin"), dir: true }, { abs: P.join(root, "runtime"), dir: true }]);
|
|
178
177
|
}
|
|
179
178
|
export function claudeRunSettings(i) {
|
|
180
179
|
const home = resolve(i.home ?? homedir());
|
package/dist/cli.js
CHANGED
|
@@ -914,8 +914,9 @@ ${B("control")}
|
|
|
914
914
|
(auto-detects claude / codex / cursor / copilot / kiro on this machine)
|
|
915
915
|
pantheon spend woken runs on this machine, and what pantheon cannot see
|
|
916
916
|
pantheon leave remove what pantheon put on this machine
|
|
917
|
-
pantheon uninstall
|
|
918
|
-
and
|
|
917
|
+
pantheon uninstall remove Pantheon itself: its services, hooks, its own Node, the pantheon command, the PATH
|
|
918
|
+
lines and what it keeps in ~/.pantheon — but not your identity, so installing again puts
|
|
919
|
+
you back in your teams (--forget-identity deletes that too; files of yours there stay)
|
|
919
920
|
pantheon panic emergency stop: ends the run in flight and the worker, and switches off wake rights in every team you are in
|
|
920
921
|
pantheon doctor [--share] check this machine end to end; --share prints it without names or paths, to paste to us
|
|
921
922
|
pantheon network [--proxy <url> | --direct | --clear] how this machine reaches the relay; save a proxy for every Pantheon process here
|
|
@@ -999,7 +1000,7 @@ const COMMAND_FLAGS = {
|
|
|
999
1000
|
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
1000
1001
|
network: "--proxy <url> [--no-proxy <hosts>] | --direct | --clear",
|
|
1001
1002
|
leave: "[<team>] --yes --keep-identity --stop-relay --project <dir>",
|
|
1002
|
-
uninstall: "--yes --
|
|
1003
|
+
uninstall: "--yes --forget-identity --stop-relay --project <dir>",
|
|
1003
1004
|
stop: "[relay|worker|notifier|menubar|all] --port <n>",
|
|
1004
1005
|
logs: "[relay|worker|notifier|menubar] --follow --n <lines>",
|
|
1005
1006
|
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",
|
|
@@ -1057,7 +1058,8 @@ function slipDistance(a, b) {
|
|
|
1057
1058
|
const EXTRA_FLAGS = "--action-id --agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
|
|
1058
1059
|
"--no-browser --no-continue --no-guide --no-notify --only-action --path --peek --poll --portable --publish --queued --quiet --remind --scratch --serve --session --surface --token --unattended " +
|
|
1059
1060
|
"--until --user --version --all --json " +
|
|
1060
|
-
"--name --title --wake --commands --unsandboxed --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities"
|
|
1061
|
+
"--name --title --wake --commands --unsandboxed --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities" +
|
|
1062
|
+
" --keep-tray-quit --direct";
|
|
1061
1063
|
const KNOWN_FLAGS = new Set([...`${Object.values(COMMAND_FLAGS).join(" ")} ${EXTRA_FLAGS}`.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]));
|
|
1062
1064
|
function refuseUnknownFlags() {
|
|
1063
1065
|
if (["mcp", "hook", "tray-status", "_menubar", "handle-uri"].includes(cmd))
|
|
@@ -1518,7 +1520,18 @@ async function main() {
|
|
|
1518
1520
|
return;
|
|
1519
1521
|
}
|
|
1520
1522
|
case "rewire": {
|
|
1521
|
-
const rows = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
|
|
1523
|
+
const rows = installForUser(undefined, undefined, { direct: argv.includes("--direct") }).map((t) => ({ tool: t.tool, status: t.status }));
|
|
1524
|
+
try {
|
|
1525
|
+
const { refreshUserHooks } = await import("./notify.js");
|
|
1526
|
+
const h = refreshUserHooks();
|
|
1527
|
+
if (h?.refreshed.length)
|
|
1528
|
+
rows.push({ tool: "hooks", status: `pointed at this install (${h.refreshed.join(", ")})${h.refreshed.includes("codex") ? " — Codex runs a changed hook only once approved: run /hooks inside codex once" : ""}` });
|
|
1529
|
+
else if (h?.project.length)
|
|
1530
|
+
rows.push({ tool: "hooks", status: `skipped (in this project's files, ${h.project.join(", ")}) — pantheon hook install there points them here` });
|
|
1531
|
+
}
|
|
1532
|
+
catch (e) {
|
|
1533
|
+
rows.push({ tool: "hooks", status: `failed: ${e.message.split("\n")[0]}` });
|
|
1534
|
+
}
|
|
1522
1535
|
try {
|
|
1523
1536
|
const { refreshServiceDefinition, serviceInstalled: installed, restartService: restart } = await import("./service.js");
|
|
1524
1537
|
for (const kind of ["worker", "notifier", "menubar"]) {
|
|
@@ -2051,8 +2064,11 @@ async function main() {
|
|
|
2051
2064
|
const quit = existsSync(hostFiles().disabled("tray"));
|
|
2052
2065
|
console.log(` tray ${tray?.phase === "running" ? `${C("running")} pid ${tray.pid}` : D(quit ? "quit — back at your next logon, or now: pantheon tray" : tray?.gaveUp === "clm" ? "not shown — PowerShell is in Constrained Language Mode here (pantheon board instead)" : "not running")}`);
|
|
2053
2066
|
}
|
|
2054
|
-
else
|
|
2055
|
-
|
|
2067
|
+
else {
|
|
2068
|
+
const { readLaunchError } = await import("./win-host.js");
|
|
2069
|
+
const why = readLaunchError() ? (await import("./service.js")).hostStartProblem() : null;
|
|
2070
|
+
console.log(` host ${D("not running")} ${D(why ? `cannot start: ${why}` : serviceInstalled("worker") || serviceInstalled("notifier") ? "pantheon service start worker (or log out and back in)" : "pantheon service install worker — the worker, alerts and the tray in the background")}`);
|
|
2071
|
+
}
|
|
2056
2072
|
}
|
|
2057
2073
|
else if (menubar.running) {
|
|
2058
2074
|
console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
|
|
@@ -2485,7 +2501,11 @@ async function main() {
|
|
|
2485
2501
|
const { leavePlan } = await import("./machine.js");
|
|
2486
2502
|
const plan = leavePlan(all);
|
|
2487
2503
|
const project = resolve(flag("project", "."));
|
|
2488
|
-
const
|
|
2504
|
+
const forgetIdentity = argv.includes("--forget-identity");
|
|
2505
|
+
if (forgetIdentity && argv.includes("--keep-identity"))
|
|
2506
|
+
die("--keep-identity and --forget-identity say opposite things — pick one");
|
|
2507
|
+
const keepIdentity = argv.includes("--keep-identity") || (uninstalling && !forgetIdentity);
|
|
2508
|
+
const keptByDefault = uninstalling && keepIdentity && !argv.includes("--keep-identity");
|
|
2489
2509
|
const standing = new Map();
|
|
2490
2510
|
if (all && !keepIdentity) {
|
|
2491
2511
|
const ms = Object.values(all.spaces ?? {});
|
|
@@ -2502,14 +2522,15 @@ async function main() {
|
|
|
2502
2522
|
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` +
|
|
2503
2523
|
` To step out but keep the way back: ${orphaned.map((t) => `pantheon leave ${t}`).join(" · ")} (keeps your key)\n Leave anyway: pantheon ${self} --yes`);
|
|
2504
2524
|
}
|
|
2505
|
-
const {
|
|
2506
|
-
const
|
|
2525
|
+
const { managedInstalls, removeManagedInstall, NPM_UNINSTALL } = await import("./uninstall.js");
|
|
2526
|
+
const installs = uninstalling ? managedInstalls(homedir()) : [];
|
|
2527
|
+
const install = installs[0] ?? null;
|
|
2507
2528
|
const runningManaged = managedLayout();
|
|
2508
2529
|
if (uninstalling && !argv.includes("--yes")) {
|
|
2509
2530
|
const teams = plan.teams.length && !keepIdentity ? `leaves ${plan.teams.map((t) => `"${t.name}"`).join(", ")} (this machine's key goes), ` : "";
|
|
2510
2531
|
const what = `this removes Pantheon from this machine: it ${teams}uninstalls its services, hooks and agent entries` +
|
|
2511
2532
|
(install?.present ? `, and deletes Pantheon's own Node in ${install.runtime}, the pantheon command and the PATH lines the installer added` : "") +
|
|
2512
|
-
`. Everything Pantheon keeps in ${CONFIG_DIR} goes${keepIdentity ? " (but your identity, --keep-identity)" : ""}; files of your own there stay.`;
|
|
2533
|
+
`. Everything Pantheon keeps in ${CONFIG_DIR} goes${keptByDefault ? `, except your identity (${CONFIG_PATH}: this machine's key${plan.teams.length ? ` and your place in ${plan.teams.map((t) => `"${t.name}"`).join(", ")}` : ""}), so installing again puts you straight back — to delete it too: pantheon uninstall --forget-identity` : keepIdentity ? " (but your identity, --keep-identity)" : ""}; files of your own there stay.`;
|
|
2513
2534
|
if (!process.stdin.isTTY)
|
|
2514
2535
|
die(`${what}\n If you mean it: pantheon uninstall --yes`);
|
|
2515
2536
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -2568,7 +2589,9 @@ async function main() {
|
|
|
2568
2589
|
left.push(refusedHomeLine(CONFIG_DIR, homeRefusal));
|
|
2569
2590
|
}
|
|
2570
2591
|
else if (hasConfig && keepIdentity) {
|
|
2571
|
-
left.push(
|
|
2592
|
+
left.push(keptByDefault
|
|
2593
|
+
? `your identity and ${plan.teams.length} team membership${plan.teams.length === 1 ? "" : "s"}, ${CONFIG_PATH} — kept, so installing Pantheon again puts you back in ${plan.teams.length === 1 ? "it" : "them"} as yourself; to delete it: pantheon uninstall --forget-identity`
|
|
2594
|
+
: `your identity and ${plan.teams.length} team membership${plan.teams.length === 1 ? "" : "s"}, ${CONFIG_PATH} (--keep-identity)`);
|
|
2572
2595
|
}
|
|
2573
2596
|
else if (!keepIdentity && (hasConfig || uninstalling)) {
|
|
2574
2597
|
if (hasConfig) {
|
|
@@ -2633,10 +2656,14 @@ async function main() {
|
|
|
2633
2656
|
}
|
|
2634
2657
|
}
|
|
2635
2658
|
if (uninstalling) {
|
|
2636
|
-
|
|
2637
|
-
|
|
2659
|
+
for (const one of installs) {
|
|
2660
|
+
if (!one.present)
|
|
2661
|
+
continue;
|
|
2662
|
+
const r = removeManagedInstall({ home: homedir(), install: one, running: runningManaged });
|
|
2638
2663
|
removed.push(...r.removed);
|
|
2639
2664
|
left.push(...r.left);
|
|
2665
|
+
if (one.os === "win32" && runningManaged && runningManaged.root.toLowerCase() === one.root.toLowerCase())
|
|
2666
|
+
spare.push(one.binDir, one.runtime);
|
|
2640
2667
|
}
|
|
2641
2668
|
if (!runningManaged)
|
|
2642
2669
|
left.push(`this pantheon itself (${process.argv[1] ?? "the one you ran"}), which npm installed — remove it with: ${NPM_UNINSTALL}`);
|
|
@@ -2661,12 +2688,17 @@ async function main() {
|
|
|
2661
2688
|
const name = me?.member ?? plan.member ?? "<your name>";
|
|
2662
2689
|
const listed = plan.teams.filter((t) => { const s = standing.get(t.name); return !s || s.others === null || (s.others.length && !s.founder); }).map((t) => `"${t.name}"`);
|
|
2663
2690
|
const founded = plan.teams.filter((t) => { const s = standing.get(t.name); return s?.founder && s.others?.length; }).map((t) => `"${t.name}"`);
|
|
2664
|
-
if (
|
|
2665
|
-
console.error(`\n ${D(`
|
|
2666
|
-
|
|
2691
|
+
if (keepIdentity && plan.teams.length) {
|
|
2692
|
+
console.error(`\n ${D(`You are still in ${plan.teams.map((t) => `"${t.name}"`).join(", ")} — this machine just has no Pantheon on it now. Install it again (the same one line) and you are back as ${name}, with nothing to rejoin.`)}`);
|
|
2693
|
+
}
|
|
2694
|
+
else {
|
|
2695
|
+
if (listed.length) {
|
|
2696
|
+
console.error(`\n ${D(`Your teammates may still list you in ${listed.join(", ")}. If you are not coming back, ask one in each to run:`)}`);
|
|
2697
|
+
console.error(` ${D(`pantheon admit ${name} --revoke`)}`);
|
|
2698
|
+
}
|
|
2699
|
+
if (founded.length)
|
|
2700
|
+
console.error(`\n ${D(`You founded ${founded.join(", ")}. With this machine's key gone you cannot rejoin as yourself, and nobody there can remove a member or delete the team — only its founder could.`)}`);
|
|
2667
2701
|
}
|
|
2668
|
-
if (founded.length)
|
|
2669
|
-
console.error(`\n ${D(`You founded ${founded.join(", ")}. With this machine's key gone you cannot rejoin as yourself, and nobody there can remove a member or delete the team — only its founder could.`)}`);
|
|
2670
2702
|
}
|
|
2671
2703
|
if (me?.account && !keepIdentity) {
|
|
2672
2704
|
const { accountStillExists } = await import("./machine.js");
|
|
@@ -4404,7 +4436,7 @@ async function main() {
|
|
|
4404
4436
|
if (sub === "start" && process.platform === "win32" && (kind === "worker" || kind === "notifier")) {
|
|
4405
4437
|
const { enableService } = await import("./service.js");
|
|
4406
4438
|
try {
|
|
4407
|
-
for (const l of enableService(kind))
|
|
4439
|
+
for (const l of enableService(kind, undefined, { keepTrayQuit: argv.includes("--keep-tray-quit") }))
|
|
4408
4440
|
console.error(l);
|
|
4409
4441
|
}
|
|
4410
4442
|
catch (e) {
|
package/dist/doctor.js
CHANGED
|
@@ -180,7 +180,23 @@ export async function doctor(projectDir) {
|
|
|
180
180
|
}
|
|
181
181
|
else if (host.platform === "win32") {
|
|
182
182
|
add("pass", "machine", `Windows${host.desktop ? " — the background host, desktop alerts and the tray icon can run here" : " — a remote shell (SSH): the background host's alerts and tray need a session at the desktop"}`, undefined, `win32${host.desktop ? ", desktop" : ""}`);
|
|
183
|
-
|
|
183
|
+
const { thisAppPackage, onlyInsidePackage, packageApp, INSTALL_OUTSIDE_LINE } = await import("./win-package.js");
|
|
184
|
+
const family = thisAppPackage();
|
|
185
|
+
if (family) {
|
|
186
|
+
const own = [process.execPath, process.argv[1] ?? ""].find((p) => p && onlyInsidePackage(p, family));
|
|
187
|
+
if (own)
|
|
188
|
+
add("warn", "app package", `this pantheon runs from ${own}, which exists only inside ${packageApp(family)} — Windows keeps what an app like it installs into AppData private to it, so Pantheon's logon task (outside the app) cannot start the worker, alerts or tray`, `${INSTALL_OUTSIDE_LINE} — it puts Pantheon in your profile folder, which every program sees`, "runs from an app-private folder");
|
|
189
|
+
else
|
|
190
|
+
add("pass", "app package", `this terminal runs inside ${packageApp(family)}; Pantheon runs from a folder every program sees, so its background host can start outside the app`, undefined, "inside an app package");
|
|
191
|
+
}
|
|
192
|
+
const wf = windowsFacts(realSys().exec);
|
|
193
|
+
if (!wf.hostUp) {
|
|
194
|
+
const { readLaunchError } = await import("./win-host.js");
|
|
195
|
+
const { hostStartProblem } = await import("./service.js");
|
|
196
|
+
if (readLaunchError())
|
|
197
|
+
wf.launchProblem = hostStartProblem();
|
|
198
|
+
}
|
|
199
|
+
for (const r of windowsDoctorRows(wf))
|
|
184
200
|
add(r.level, r.name, r.detail, r.fix, r.share);
|
|
185
201
|
}
|
|
186
202
|
else {
|
package/dist/home-safety.js
CHANGED
|
@@ -37,6 +37,7 @@ export const PANTHEON_HOME_ENTRIES = [
|
|
|
37
37
|
{ name: "no-hooks", what: "the empty hooks folder woken runs' git uses", dir: true },
|
|
38
38
|
{ name: "lfs-hooks", what: "the LFS-only hooks folder woken runs' git uses", dir: true },
|
|
39
39
|
{ name: "runtime", what: "Pantheon's own Node", dir: true, kept: "managed" },
|
|
40
|
+
{ name: "install.log", what: "the installer's log", kept: "managed" },
|
|
40
41
|
{ name: "data", what: "the relay's ledger", dir: true, kept: "it is your team's history and may be its only copy; delete it yourself once no relay runs from it" },
|
|
41
42
|
{ name: /^(\.DS_Store|Thumbs\.db|desktop\.ini)$/i, sample: ".DS_Store", what: "the file browser's folder cache" },
|
|
42
43
|
];
|
package/dist/install.js
CHANGED
|
@@ -293,10 +293,10 @@ export function claudeUserScope(entry, present = true, claudeBin = which("claude
|
|
|
293
293
|
const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "user"]);
|
|
294
294
|
const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "user", "--", entry.command, ...entry.args]);
|
|
295
295
|
try {
|
|
296
|
-
execFileSync(remove.command, remove.args, { stdio: "ignore", ...remove.options });
|
|
296
|
+
execFileSync(remove.command, remove.args, { stdio: "ignore", timeout: 60_000, ...remove.options });
|
|
297
297
|
}
|
|
298
298
|
catch { }
|
|
299
|
-
execFileSync(add.command, add.args, { stdio: ["ignore", "ignore", "pipe"], ...add.options });
|
|
299
|
+
execFileSync(add.command, add.args, { stdio: ["ignore", "ignore", "pipe"], timeout: 60_000, ...add.options });
|
|
300
300
|
return { tool, where: "user scope", status: "ready in every project and every Conductor window" };
|
|
301
301
|
}
|
|
302
302
|
catch (e) {
|
package/dist/machine.js
CHANGED
|
@@ -665,7 +665,7 @@ export function removePantheonWorktrees(repos, roots, git = (repo, args) => exec
|
|
|
665
665
|
}
|
|
666
666
|
return { removed, left };
|
|
667
667
|
}
|
|
668
|
-
const
|
|
668
|
+
const MANAGED_LAUNCHERS = ["pantheon", "pantheon.cmd"];
|
|
669
669
|
export function removeHomeLeftovers(home, keep, opts = {}) {
|
|
670
670
|
const removed = [], left = [];
|
|
671
671
|
const refusal = opts.refusal !== undefined ? opts.refusal : pantheonHomeRefusal(home);
|
|
@@ -683,12 +683,12 @@ export function removeHomeLeftovers(home, keep, opts = {}) {
|
|
|
683
683
|
if (!existsSync(f))
|
|
684
684
|
continue;
|
|
685
685
|
if (keep && name !== "worker.pid" && name !== "bin") {
|
|
686
|
-
left.push(`${what}, ${f} (
|
|
686
|
+
left.push(`${what}, ${f} (kept with your identity)`);
|
|
687
687
|
continue;
|
|
688
688
|
}
|
|
689
|
-
if (name === "bin" && existsSync(join(f,
|
|
689
|
+
if (name === "bin" && MANAGED_LAUNCHERS.some((l) => existsSync(join(f, l)))) {
|
|
690
690
|
try {
|
|
691
|
-
const others = readdirSync(f).filter((e) => e
|
|
691
|
+
const others = readdirSync(f).filter((e) => !MANAGED_LAUNCHERS.includes(e));
|
|
692
692
|
for (const e of others)
|
|
693
693
|
rmSync(join(f, e), { recursive: true, force: true });
|
|
694
694
|
if (others.length)
|
|
@@ -730,7 +730,8 @@ export function sweepPantheonHome(home, o = {}) {
|
|
|
730
730
|
const refusal = o.refusal !== undefined ? o.refusal : o.fs ? null : pantheonHomeRefusal(home);
|
|
731
731
|
if (refusal)
|
|
732
732
|
return { removed, left: [refusedHomeLine(home, refusal)], gone: false };
|
|
733
|
-
const
|
|
733
|
+
const key = (p) => (os === "win32" ? resolve(p).toLowerCase() : resolve(p));
|
|
734
|
+
const spare = new Set((o.spare ?? []).map(key));
|
|
734
735
|
const attempt = (p, rm) => {
|
|
735
736
|
let why = "";
|
|
736
737
|
for (let i = 0; i < tries; i++) {
|
|
@@ -765,7 +766,7 @@ export function sweepPantheonHome(home, o = {}) {
|
|
|
765
766
|
}
|
|
766
767
|
if (name === HOME_MARKER)
|
|
767
768
|
continue;
|
|
768
|
-
if (spare.has(
|
|
769
|
+
if (spare.has(key(p)))
|
|
769
770
|
continue;
|
|
770
771
|
if (e.kept === "managed")
|
|
771
772
|
continue;
|
|
@@ -786,7 +787,7 @@ export function sweepPantheonHome(home, o = {}) {
|
|
|
786
787
|
if (gone.length)
|
|
787
788
|
removed.push(`Pantheon's own files in ${home}: ${gone.join(", ")}`);
|
|
788
789
|
if (keptForIdentity.length)
|
|
789
|
-
left.push(`what goes with your identity in ${home}: ${keptForIdentity.join(", ")} (
|
|
790
|
+
left.push(`what goes with your identity in ${home}: ${keptForIdentity.join(", ")} (kept with your identity)`);
|
|
790
791
|
let rest = [];
|
|
791
792
|
try {
|
|
792
793
|
rest = fs.list(home);
|
package/dist/managed.js
CHANGED
|
@@ -15,7 +15,7 @@ export function managedLayout(execPath = process.execPath, os = platform()) {
|
|
|
15
15
|
return null;
|
|
16
16
|
if (win32.basename(nodeDir).toLowerCase() !== "node" || win32.basename(runtime).toLowerCase() !== "runtime")
|
|
17
17
|
return null;
|
|
18
|
-
if (win32.basename(root).toLowerCase()
|
|
18
|
+
if (![".pantheon", "pantheon"].includes(win32.basename(root).toLowerCase()))
|
|
19
19
|
return null;
|
|
20
20
|
const binDir = win32.join(root, "bin");
|
|
21
21
|
const pkgDir = win32.join(nodeDir, "node_modules", ...PKG);
|
|
@@ -98,10 +98,20 @@ export function runtimeVerdict(f) {
|
|
|
98
98
|
share: "managed install present, another pantheon ran",
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
export function
|
|
101
|
+
export function windowsManagedRoots(home, env = process.env) {
|
|
102
|
+
const local = env.LOCALAPPDATA || (home ? win32.join(home, "AppData", "Local") : "");
|
|
103
|
+
return { current: home ? win32.join(home, ".pantheon") : null, legacy: local ? win32.join(local, "Pantheon") : null };
|
|
104
|
+
}
|
|
105
|
+
export function expectedLauncher(home, os = platform(), env = process.env, exists = existsSync) {
|
|
102
106
|
if (os === "win32") {
|
|
103
|
-
const
|
|
104
|
-
|
|
107
|
+
const { current, legacy } = windowsManagedRoots(home, env);
|
|
108
|
+
const at = (root) => (root ? win32.join(root, "bin", "pantheon.cmd") : null);
|
|
109
|
+
const now = at(current), old = at(legacy);
|
|
110
|
+
if (now && exists(now))
|
|
111
|
+
return now;
|
|
112
|
+
if (old && exists(old))
|
|
113
|
+
return old;
|
|
114
|
+
return old ?? now;
|
|
105
115
|
}
|
|
106
116
|
return home ? posix.join(home, ".pantheon", "bin", "pantheon") : null;
|
|
107
117
|
}
|
package/dist/notify.js
CHANGED
|
@@ -8,6 +8,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { whichPantheon } from "./service.js";
|
|
10
10
|
import { managedLayout } from "./managed.js";
|
|
11
|
+
import { inWokenRun } from "./worker-policy.js";
|
|
11
12
|
import { detectForHooks, agentById } from "./agents.js";
|
|
12
13
|
import { codexHookState, cursorHookState, copilotHookState, kiroHookState, installCodexHooks, installCursorHooks, installCopilotHooks, installKiroHooks, removeCodexHooks, removeCursorHooks, removeCopilotHooks, removeKiroHooks, } from "./hooks-install.js";
|
|
13
14
|
export const HOOK_MARKER = "pantheon hook";
|
|
@@ -171,6 +172,37 @@ export function repairHook(home) {
|
|
|
171
172
|
return null;
|
|
172
173
|
}
|
|
173
174
|
}
|
|
175
|
+
export function refreshUserHooks(home) {
|
|
176
|
+
if (inWokenRun())
|
|
177
|
+
return null;
|
|
178
|
+
const intent = readIntent(home);
|
|
179
|
+
const bin = hookLauncher();
|
|
180
|
+
if (!intent || !bin)
|
|
181
|
+
return null;
|
|
182
|
+
const targets = intent.agents?.length ? intent.agents : ["claude-code"];
|
|
183
|
+
if (intent.scope !== "user") {
|
|
184
|
+
const files = agentHookStates(intent.projectDir ?? process.cwd(), "project").map((s) => s.file).filter((f) => !!f);
|
|
185
|
+
return { refreshed: [], project: files };
|
|
186
|
+
}
|
|
187
|
+
const jsonBin = JSON.stringify(bin).slice(1, -1);
|
|
188
|
+
const states = new Map(agentHookStates(intent.projectDir ?? process.cwd(), "user").map((st) => [st.agent, st]));
|
|
189
|
+
const holdsBin = (f) => { try {
|
|
190
|
+
const t = readFileSync(f, "utf8");
|
|
191
|
+
return t.includes(bin) || t.includes(jsonBin);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return true;
|
|
195
|
+
} };
|
|
196
|
+
const stale = targets.filter((id) => {
|
|
197
|
+
const st = states.get(id);
|
|
198
|
+
return !!st?.installed && !!st.file && !holdsBin(st.file);
|
|
199
|
+
});
|
|
200
|
+
if (!stale.length)
|
|
201
|
+
return { refreshed: [], project: [] };
|
|
202
|
+
installHooks("user", intent.projectDir ?? process.cwd(), stale);
|
|
203
|
+
const after = new Map(agentHookStates(intent.projectDir ?? process.cwd(), "user").map((st) => [st.agent, st]));
|
|
204
|
+
return { refreshed: stale.filter((id) => { const st = after.get(id); return !!st?.file && holdsBin(st.file); }), project: [] };
|
|
205
|
+
}
|
|
174
206
|
function installClaudeHook(scope, projectDir = process.cwd()) {
|
|
175
207
|
const file = settingsPath(scope, projectDir);
|
|
176
208
|
const doc = read(file);
|
package/dist/relay.js
CHANGED
|
@@ -2701,6 +2701,9 @@ pantheon start</code></pre>
|
|
|
2701
2701
|
}
|
|
2702
2702
|
if (verdict === "key-mismatch") {
|
|
2703
2703
|
this.log(`[${sp.name}] ! REFUSED ${m.member}: key mismatch (${fingerprint(m.pubkey)})`);
|
|
2704
|
+
if (sp.allow.founder() === m.member) {
|
|
2705
|
+
return reject("KEY_MISMATCH", `"${m.member}" founded "${sp.name}" and is registered here with a different key. A founder's key cannot be replaced: if this is really you on another machine, restore your original ~/.pantheon/config.json (the key is in it). Without it, join under another name (--me <new-name>) or start a new team (pantheon start).`);
|
|
2706
|
+
}
|
|
2704
2707
|
return reject("KEY_MISMATCH", `"${m.member}" is already registered here with a different key. If you are someone else, join under your own name (--me <your-name>). If this is really you on another machine, an admitted member who has checked it is you runs pantheon admit ${m.member} --forget (it removes whoever holds the name now) and sends you a new invite.`);
|
|
2705
2708
|
}
|
|
2706
2709
|
if (verdict === "revoked") {
|
package/dist/roster-file.js
CHANGED
|
@@ -238,6 +238,18 @@ export class Allowlist {
|
|
|
238
238
|
return { ok: false, why: `"${member}" was removed from this team${existing?.revoked_by ? ` by ${existing.revoked_by}` : ""}, so only an invite from the founder${this.founderName ? ` (${this.founderName})` : ""}, made since, can bring them back` };
|
|
239
239
|
}
|
|
240
240
|
if (existing && existing.publicKey !== publicKey) {
|
|
241
|
+
if (this.founderName && member === this.founderName) {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
why: `"${member}" founded this team and is registered with a different key — a founder's key cannot be replaced`,
|
|
245
|
+
fix: [
|
|
246
|
+
`If you ARE "${member}" on a new machine: restore your original ~/.pantheon/config.json if you still have it — the key is in it — then redeem this same invite again.`,
|
|
247
|
+
` Without it, nobody can let you back in under "${member}": a team's founder is never removed, so the old key cannot be retired.`,
|
|
248
|
+
` You can join under another name instead (--me <new-name>, this same invite), or start a new team: pantheon start --space <name>.`,
|
|
249
|
+
` If you are not "${member}", join under your own name: --me <your-name>.`,
|
|
250
|
+
].join("\n"),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
241
253
|
return {
|
|
242
254
|
ok: false,
|
|
243
255
|
why: `"${member}" is registered here with a different key — the name is taken in this team`,
|
package/dist/service.js
CHANGED
|
@@ -6,9 +6,10 @@ import { basename, dirname, isAbsolute, join, posix, resolve } from "node:path";
|
|
|
6
6
|
import { linuxGraphicalSession } from "./host-env.js";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { isVersionManagedPath, launcher } from "./install.js";
|
|
9
|
-
import { childNodeFlags, managedLauncher, managedLayout } from "./managed.js";
|
|
9
|
+
import { childNodeFlags, managedLauncher, managedLayout, windowsManagedRoots } from "./managed.js";
|
|
10
10
|
import { CONFIG_DIR, ensurePantheonHome } from "./config.js";
|
|
11
|
-
import { HOST_KINDS, LEGACY_TASK_NAME, clearTrayQuit, ensureHostShim, fromUtf16le, hostAlive, hostArgv, hostChildRunning, hostFiles, hostLaunch, isHostKind, launchFileText, legacyTaskIsUsers, readDefinition, readHostState, taskName, taskUserId, taskXml, utf16le, windowsPowerShell, writeDefinition, } from "./win-host.js";
|
|
11
|
+
import { HOST_KINDS, LEGACY_TASK_NAME, clearTrayQuit, ensureHostShim, fromUtf16le, hostAlive, hostArgv, hostChildRunning, hostFiles, hostLaunch, isHostKind, launchFileText, legacyTaskIsUsers, readDefinition, readHostState, readLaunchError, taskName, taskUserId, taskXml, utf16le, windowsPowerShell, writeDefinition, } from "./win-host.js";
|
|
12
|
+
import { INSTALL_OUTSIDE_LINE, onlyInsidePackage, packageApp, thisAppPackage } from "./win-package.js";
|
|
12
13
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
13
14
|
const LABEL = (kind) => `dev.pantheon.${kind}`;
|
|
14
15
|
export function borrowedHome() {
|
|
@@ -474,7 +475,14 @@ export function restartService(kind, exec = realExec, opts = {}) {
|
|
|
474
475
|
}
|
|
475
476
|
if (!st.loaded)
|
|
476
477
|
return [` ${kind}: the ${taskName(exec)} task is not registered — pantheon service install ${kind}`];
|
|
477
|
-
|
|
478
|
+
const name = taskName(exec);
|
|
479
|
+
runTask(exec, name);
|
|
480
|
+
const ms = opts.hostStartMs ?? 20_000;
|
|
481
|
+
if (!waitFor(hostUp, ms, opts.sleep ?? blockingSleep)) {
|
|
482
|
+
return [readLaunchError() || registeredLaunch() !== "shim"
|
|
483
|
+
? ` ${kind}: the ${name} task started, but the Pantheon host did not come up within ${Math.round(ms / 1000)} s — ${hostStartProblem()}`
|
|
484
|
+
: ` ${kind}: started the ${name} task — the Pantheon host is not up yet after ${Math.round(ms / 1000)} s; pantheon ps in a minute shows it`];
|
|
485
|
+
}
|
|
478
486
|
return [` ${kind}: the Pantheon host was not running — started it, and the ${kind} with it, on the new code`];
|
|
479
487
|
}
|
|
480
488
|
if (plat === "darwin") {
|
|
@@ -614,9 +622,15 @@ export function hostRuntime(o) {
|
|
|
614
622
|
}
|
|
615
623
|
throw new Error(`this Pantheon runs from ${moving(o.cli) ? o.cli : o.execPath}, a folder a Node version manager replaces when you switch Node versions — a logon task naming it would start nothing after the next switch. Install Pantheon with its own Node (irm https://relay.joinpantheon.network/install.ps1 | iex) or with a Node from nodejs.org (npm install -g @join-pantheon/cli), then run this again`);
|
|
616
624
|
}
|
|
617
|
-
function installedManagedLayout(env) {
|
|
618
|
-
const
|
|
619
|
-
|
|
625
|
+
export function installedManagedLayout(env, home = homedir(), exists = existsSync) {
|
|
626
|
+
const { current, legacy } = windowsManagedRoots(home, env);
|
|
627
|
+
const at = (root) => (root ? managedLayout(`${root.replace(/[\\/]+$/, "")}\\runtime\\node\\node.exe`, "win32") : null);
|
|
628
|
+
const now = at(current), old = at(legacy);
|
|
629
|
+
if (now && exists(now.node))
|
|
630
|
+
return now;
|
|
631
|
+
if (old && exists(old.node))
|
|
632
|
+
return old;
|
|
633
|
+
return now ?? old;
|
|
620
634
|
}
|
|
621
635
|
export function desiredTask(sys, log, rt) {
|
|
622
636
|
const env = sys.env ?? process.env;
|
|
@@ -626,6 +640,11 @@ export function desiredTask(sys, log, rt) {
|
|
|
626
640
|
nodesOnPath: [which("node") ?? ""], exists: existsSync,
|
|
627
641
|
...rt,
|
|
628
642
|
}) : sys.hostRuntime;
|
|
643
|
+
const family = sys.appPackage !== undefined ? sys.appPackage : thisAppPackage();
|
|
644
|
+
if (family)
|
|
645
|
+
for (const f of [runtime.node, runtime.cli])
|
|
646
|
+
if (onlyInsidePackage(f, family, env, sys.exists))
|
|
647
|
+
throw new Error(packagedRuntimeProblem(f, family));
|
|
629
648
|
const shim = ensureHostShim({ exec: (c, a) => sys.exec(c, a), env, ...(log ? { log } : {}) });
|
|
630
649
|
const home = nonDefaultHome();
|
|
631
650
|
const launch = hostLaunch({ shim, node: runtime.node, flags: runtime.flags, cli: runtime.cli, home, launch: hostFiles().launch, powershell: windowsPowerShell(env) });
|
|
@@ -693,6 +712,49 @@ function replaceLegacyTask(sys, name, unattended, log) {
|
|
|
693
712
|
}
|
|
694
713
|
}
|
|
695
714
|
const hostUp = () => hostAlive(readHostState());
|
|
715
|
+
export function packagedRuntimeProblem(program, family) {
|
|
716
|
+
return `${program} exists only inside ${packageApp(family)}: Windows keeps what an app like it installs into AppData private to that app, and Pantheon's logon task runs outside it, so the worker, alerts and tray could never start. To fix it once, ${INSTALL_OUTSIDE_LINE} — it puts Pantheon in your profile folder, which every program sees — then run this again`;
|
|
717
|
+
}
|
|
718
|
+
function runTask(exec, name) {
|
|
719
|
+
if (readLaunchError()) {
|
|
720
|
+
try {
|
|
721
|
+
exec("schtasks", ["/End", "/TN", name]);
|
|
722
|
+
}
|
|
723
|
+
catch { }
|
|
724
|
+
}
|
|
725
|
+
exec("schtasks", ["/Run", "/TN", name]);
|
|
726
|
+
}
|
|
727
|
+
function registeredLaunch() {
|
|
728
|
+
try {
|
|
729
|
+
const cmd = /<Command>([^<]*)<\/Command>/.exec(fromUtf16le(readFileSync(hostFiles().taskXml)))?.[1] ?? "";
|
|
730
|
+
return /pantheon-host-[0-9a-f]+\.exe$/i.test(cmd) ? "shim" : cmd ? "powershell" : null;
|
|
731
|
+
}
|
|
732
|
+
catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
export function confirmHostStart(sys, name) {
|
|
737
|
+
const ms = sys.hostStartMs ?? 20_000;
|
|
738
|
+
if (waitFor(hostUp, ms, sys.sleep ?? blockingSleep))
|
|
739
|
+
return { up: true, line: ` the Pantheon host is up (pid ${readHostState()?.pid ?? "?"})` };
|
|
740
|
+
if (!readLaunchError() && registeredLaunch() === "shim")
|
|
741
|
+
return { up: false, slow: true, line: ` started the ${name} task — the Pantheon host is not up yet after ${Math.round(ms / 1000)} s (a first start can be slow while Windows scans it); pantheon ps in a minute shows it` };
|
|
742
|
+
const why = hostStartProblem(sys);
|
|
743
|
+
return { up: false, line: ` the ${name} task started, but the Pantheon host did not come up within ${Math.round(ms / 1000)} s — ${why}`, reason: why };
|
|
744
|
+
}
|
|
745
|
+
export function hostStartProblem(sys = {}) {
|
|
746
|
+
const e = readLaunchError();
|
|
747
|
+
const env = sys.env ?? process.env;
|
|
748
|
+
if (e?.what === "missing") {
|
|
749
|
+
const family = sys.appPackage !== undefined ? sys.appPackage : thisAppPackage();
|
|
750
|
+
if (family && onlyInsidePackage(e.program, family, env, sys.exists))
|
|
751
|
+
return packagedRuntimeProblem(e.program, family);
|
|
752
|
+
return `it cannot find ${e.program} (since ${e.at}). Run the installer again: irm https://relay.joinpantheon.network/install.ps1 | iex`;
|
|
753
|
+
}
|
|
754
|
+
if (e?.what === "start-failed")
|
|
755
|
+
return `Windows would not start ${e.program} (since ${e.at}) — a security product may be blocking it; ${hostFiles().log} may say more`;
|
|
756
|
+
return `nothing it wrote says why yet. ${hostFiles().log} and pantheon doctor may say more; logging out and back in starts the task again`;
|
|
757
|
+
}
|
|
696
758
|
const windowsBorrowed = (sys) => (sys.borrowed ?? (() => borrowedScheduler(sys.env ?? process.env)))();
|
|
697
759
|
function installWindows(kind, cwd, args, sys) {
|
|
698
760
|
if (!isHostKind(kind)) {
|
|
@@ -718,8 +780,15 @@ function installWindows(kind, cwd, args, sys) {
|
|
|
718
780
|
if (hostUp())
|
|
719
781
|
out.push(` the host is running — it starts the ${kind} within seconds, and restarts it if it stops`);
|
|
720
782
|
else {
|
|
721
|
-
sys.exec
|
|
722
|
-
|
|
783
|
+
runTask(sys.exec, name);
|
|
784
|
+
const c = confirmHostStart(sys, name);
|
|
785
|
+
out.push(c.line);
|
|
786
|
+
if (c.up || c.slow)
|
|
787
|
+
out.push(` started — the ${kind} now starts at logon and restarts if it stops`);
|
|
788
|
+
else {
|
|
789
|
+
loaded = false;
|
|
790
|
+
reason = c.reason;
|
|
791
|
+
}
|
|
723
792
|
}
|
|
724
793
|
}
|
|
725
794
|
catch (e) {
|
|
@@ -827,7 +896,7 @@ function disableWindows(kind, sys) {
|
|
|
827
896
|
writeFileSync(f.disabled(kind), new Date().toISOString());
|
|
828
897
|
waitFor(() => !hostChildRunning(kind), 6_000, sys.sleep ?? blockingSleep);
|
|
829
898
|
}
|
|
830
|
-
export function enableService(kind, sys = realSys()) {
|
|
899
|
+
export function enableService(kind, sys = realSys(), o = {}) {
|
|
831
900
|
if (sys.platform !== "win32")
|
|
832
901
|
throw new Error("service start is the Windows host's; elsewhere: pantheon service install");
|
|
833
902
|
if (!isHostKind(kind) || !readDefinition(kind))
|
|
@@ -835,13 +904,17 @@ export function enableService(kind, sys = realSys()) {
|
|
|
835
904
|
const f = hostFiles();
|
|
836
905
|
const was = existsSync(f.disabled(kind));
|
|
837
906
|
rmSync(f.disabled(kind), { force: true });
|
|
838
|
-
const tray = clearTrayQuit() ? [" the tray icon, which you had quit, shows again"] : [];
|
|
907
|
+
const tray = !o.keepTrayQuit && clearTrayQuit() ? [" the tray icon, which you had quit, shows again"] : [];
|
|
839
908
|
if (hostUp())
|
|
840
909
|
return [` ${kind}: ${was ? "resumed" : "already on"} — the Pantheon host ${was ? "starts it within seconds" : "runs it"}`, ...tray];
|
|
841
910
|
const lines = [];
|
|
842
911
|
ensureTask(sys, (l) => lines.push(l), { migrate: true });
|
|
843
|
-
|
|
844
|
-
|
|
912
|
+
const name = taskName(sys.exec, sys.env);
|
|
913
|
+
runTask(sys.exec, name);
|
|
914
|
+
const c = confirmHostStart(sys, name);
|
|
915
|
+
if (!c.up && !c.slow)
|
|
916
|
+
throw new Error(c.line.trim());
|
|
917
|
+
return [...lines, c.up ? ` ${kind}: ${was ? "resumed" : "on"} — started the Pantheon host, which runs it` : ` ${kind}: ${was ? "resumed" : "on"} —${c.line.replace(/^ {2}started/, " started")}`, ...tray];
|
|
845
918
|
}
|
|
846
919
|
export function showTray(sys = realSys()) {
|
|
847
920
|
const f = hostFiles();
|
|
@@ -856,7 +929,9 @@ export function showTray(sys = realSys()) {
|
|
|
856
929
|
}
|
|
857
930
|
const lines = [];
|
|
858
931
|
ensureTask(sys, (l) => lines.push(l), { migrate: true });
|
|
859
|
-
|
|
860
|
-
|
|
932
|
+
const name = taskName(sys.exec, sys.env);
|
|
933
|
+
runTask(sys.exec, name);
|
|
934
|
+
const c = confirmHostStart(sys, name);
|
|
935
|
+
return [...lines, c.up ? " started the Pantheon host, and the tray icon with it" : c.line];
|
|
861
936
|
}
|
|
862
937
|
//# sourceMappingURL=service.js.map
|
package/dist/uninstall.js
CHANGED
|
@@ -2,7 +2,7 @@ import { execFileSync, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, lstatSync, readFileSync, readdirSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
4
|
import { posix, win32 } from "node:path";
|
|
5
|
-
import { expectedLauncher } from "./managed.js";
|
|
5
|
+
import { expectedLauncher, windowsManagedRoots } from "./managed.js";
|
|
6
6
|
export const BEGIN_MARK = "# >>> pantheon >>>";
|
|
7
7
|
export const END_MARK = "# <<< pantheon <<<";
|
|
8
8
|
export const PROFILES = [".profile", ".bash_profile", ".bash_login", ".bashrc", ".zshrc", ".zprofile"];
|
|
@@ -83,8 +83,23 @@ export const realUninstallFs = {
|
|
|
83
83
|
}
|
|
84
84
|
catch { } },
|
|
85
85
|
};
|
|
86
|
+
export function managedInstalls(home, os = platform(), env = process.env, fs = realUninstallFs) {
|
|
87
|
+
const first = managedInstall(home, os, env, fs);
|
|
88
|
+
if (!first)
|
|
89
|
+
return [];
|
|
90
|
+
if (os !== "win32")
|
|
91
|
+
return [first];
|
|
92
|
+
const { current, legacy } = windowsManagedRoots(home, env);
|
|
93
|
+
const other = [current, legacy].find((r) => r && r.toLowerCase() !== first.root.toLowerCase());
|
|
94
|
+
if (!other)
|
|
95
|
+
return [first];
|
|
96
|
+
const p = win32;
|
|
97
|
+
const at = { os, root: other, runtime: p.join(other, "runtime"), binDir: p.join(other, "bin"), launcher: p.join(other, "bin", "pantheon.cmd"), log: p.join(other, "install.log"), present: false };
|
|
98
|
+
at.present = fs.exists(at.runtime) || fs.exists(at.launcher);
|
|
99
|
+
return at.present ? [first, at] : [first];
|
|
100
|
+
}
|
|
86
101
|
export function managedInstall(home, os = platform(), env = process.env, fs = realUninstallFs) {
|
|
87
|
-
const launcher = expectedLauncher(home, os, env);
|
|
102
|
+
const launcher = expectedLauncher(home, os, env, fs.exists);
|
|
88
103
|
if (!launcher)
|
|
89
104
|
return null;
|
|
90
105
|
const p = os === "win32" ? win32 : posix;
|
|
@@ -94,8 +109,8 @@ export function managedInstall(home, os = platform(), env = process.env, fs = re
|
|
|
94
109
|
const log = os === "win32" ? p.join(root, "install.log") : p.join(root, "logs", "install.log");
|
|
95
110
|
return { os, root, runtime, binDir, launcher, log, present: fs.exists(runtime) || fs.exists(launcher) };
|
|
96
111
|
}
|
|
97
|
-
export function finishAfterExit(paths, pids = [process.pid]) {
|
|
98
|
-
const inner = cleanupScript(paths, pids);
|
|
112
|
+
export function finishAfterExit(paths, pids = [process.pid], ifEmpty = null) {
|
|
113
|
+
const inner = cleanupScript(paths, pids, ifEmpty);
|
|
99
114
|
if (!inner)
|
|
100
115
|
return false;
|
|
101
116
|
const ps = `${process.env.SystemRoot ?? "C:\\Windows"}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
@@ -113,14 +128,15 @@ export function cleanupLauncher(ps, inner) {
|
|
|
113
128
|
const enc = Buffer.from(inner, "utf16le").toString("base64");
|
|
114
129
|
return `Start-Process -FilePath '${ps.replace(/'/g, "''")}' -WindowStyle Hidden -ArgumentList '-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-EncodedCommand','${enc}'`;
|
|
115
130
|
}
|
|
116
|
-
export function cleanupScript(paths, pids) {
|
|
117
|
-
if (!paths.length || paths.some((x) => /[\r\n]/.test(x)))
|
|
131
|
+
export function cleanupScript(paths, pids, ifEmpty = null) {
|
|
132
|
+
if (!paths.length || paths.some((x) => /[\r\n]/.test(x)) || (ifEmpty && /[\r\n]/.test(ifEmpty)))
|
|
118
133
|
return null;
|
|
119
134
|
const ids = pids.filter((n) => Number.isInteger(n) && n > 0);
|
|
120
135
|
const steps = [
|
|
121
136
|
ids.length ? `Wait-Process -Id ${ids.join(",")} -Timeout 60 -ErrorAction SilentlyContinue` : "",
|
|
122
137
|
"Start-Sleep -Milliseconds 500",
|
|
123
138
|
`Remove-Item -LiteralPath ${paths.map((x) => `'${x.replace(/'/g, "''")}'`).join(",")} -Recurse -Force`,
|
|
139
|
+
ifEmpty ? `$d = '${ifEmpty.replace(/'/g, "''")}'; if ((Test-Path -LiteralPath $d) -and -not (Get-ChildItem -LiteralPath $d -Force)) { Remove-Item -LiteralPath $d -Force }` : "",
|
|
124
140
|
].filter(Boolean).join("; ");
|
|
125
141
|
return `& { ${steps} } *> (Join-Path $env:TEMP 'pantheon-uninstall.log')`;
|
|
126
142
|
}
|
|
@@ -200,10 +216,11 @@ export function removeManagedInstall(o) {
|
|
|
200
216
|
}
|
|
201
217
|
const lockedHere = m.os === "win32" && !!o.running && o.running.root.toLowerCase() === m.root.toLowerCase();
|
|
202
218
|
if (lockedHere) {
|
|
203
|
-
const finish =
|
|
219
|
+
const finish = o.finishLater ? o.finishLater([m.runtime, m.binDir, m.log], m.root) : finishAfterExit([m.runtime, m.binDir, m.log], undefined, m.root);
|
|
220
|
+
const byHand = `Remove-Item -Recurse -Force ${[m.runtime, m.binDir].map((x) => `'${x.replace(/'/g, "''")}'`).join(", ")}`;
|
|
204
221
|
left.push(finish
|
|
205
|
-
? `${m.
|
|
206
|
-
: `${m.
|
|
222
|
+
? `${m.runtime} — Windows keeps Pantheon's node.exe locked while this command runs, so it is removed a few seconds after this command exits. If it is still there afterwards, an agent window has it open: close them, then in PowerShell: ${byHand}`
|
|
223
|
+
: `${m.runtime} — Windows keeps Pantheon's node.exe locked while this command runs. Close your agent windows and this one, then in a new PowerShell: ${byHand}`);
|
|
207
224
|
return { removed, left };
|
|
208
225
|
}
|
|
209
226
|
for (const [path, what] of [[m.launcher, "the pantheon command"], [m.log, "the installer's log"], [m.runtime, "Pantheon's own Node and the CLI in it"]]) {
|
package/dist/upgrade.js
CHANGED
|
@@ -8,6 +8,7 @@ import { proxyEnvFor } from "./network.js";
|
|
|
8
8
|
import { relayProc, workerProc, notifierProc, menubarProc } from "./machine.js";
|
|
9
9
|
import { load } from "./config.js";
|
|
10
10
|
import { compareVersions, INSTALLER_MIN_CLI, isVersion } from "./installer.js";
|
|
11
|
+
export const WINDOWS_PROFILE_ROOT_MIN = "0.33.1-0";
|
|
11
12
|
import { childNodeFlags, installerLineHere, launcherWithRepair, managedLayout, nodeSatisfies, nodeTooOldSentence, runtimeNodeVersion } from "./managed.js";
|
|
12
13
|
export function repoRoot() {
|
|
13
14
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -125,6 +126,9 @@ export function upgrade(deps = defaultDeps(), opts = {}) {
|
|
|
125
126
|
if (managed && spec !== "latest" && compareVersions(spec, INSTALLER_MIN_CLI) < 0) {
|
|
126
127
|
return stop("check", `${spec} is older than this install supports (${INSTALLER_MIN_CLI} or newer): it could not upgrade itself again. Nothing was changed.`);
|
|
127
128
|
}
|
|
129
|
+
if (managed && spec !== "latest" && /^\.pantheon$/i.test(managed.root.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "") && /^[A-Za-z]:\\|^\\\\/.test(managed.root) && compareVersions(spec, WINDOWS_PROFILE_ROOT_MIN) < 0) {
|
|
130
|
+
return stop("check", `${spec} predates Pantheon in your profile folder (0.33.1 or newer is needed here): it could not find itself to upgrade again. Nothing was changed.`);
|
|
131
|
+
}
|
|
128
132
|
if (managed && deps.engines) {
|
|
129
133
|
const range = deps.engines(spec);
|
|
130
134
|
const node = deps.runtimeNode ? deps.runtimeNode() : runtimeNodeVersion(managed);
|
package/dist/win-host.js
CHANGED
|
@@ -80,6 +80,7 @@ export const hostFiles = (home = CONFIG_DIR) => {
|
|
|
80
80
|
notices: join(dir, "notices"),
|
|
81
81
|
noticesSkipped: join(dir, "notices.skipped"),
|
|
82
82
|
launch: join(dir, "launch.txt"),
|
|
83
|
+
launchError: join(dir, "launch-error.txt"),
|
|
83
84
|
clm: join(dir, "clm"),
|
|
84
85
|
state: join(dir, "state.json"),
|
|
85
86
|
lock: join(dir, "host.lock"),
|
|
@@ -90,7 +91,7 @@ export const hostFiles = (home = CONFIG_DIR) => {
|
|
|
90
91
|
};
|
|
91
92
|
export function readDefinition(kind, home = CONFIG_DIR) {
|
|
92
93
|
try {
|
|
93
|
-
const d = JSON.parse(readFileSync(hostFiles(home).definition(kind), "utf8"));
|
|
94
|
+
const d = JSON.parse(readFileSync(hostFiles(home).definition(kind), "utf8").replace(/^\uFEFF/, ""));
|
|
94
95
|
if (!d || typeof d.cwd !== "string" || !Array.isArray(d.args) || !d.args.every((a) => typeof a === "string"))
|
|
95
96
|
return null;
|
|
96
97
|
return { kind, cwd: d.cwd, args: d.args };
|
|
@@ -117,6 +118,17 @@ function writeAtomic(f, text) {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
export const HOST_BEAT_STALE_MS = 30_000;
|
|
121
|
+
export function readLaunchError(home = CONFIG_DIR) {
|
|
122
|
+
try {
|
|
123
|
+
const [at, what, ...rest] = readFileSync(hostFiles(home).launchError, "utf8").replace(/^\uFEFF/, "").trim().split("\t");
|
|
124
|
+
if (!at || (what !== "missing" && what !== "start-failed") || !rest.length)
|
|
125
|
+
return null;
|
|
126
|
+
return { at, what, program: rest.join("\t") };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
120
132
|
export function readHostState(home = CONFIG_DIR) {
|
|
121
133
|
try {
|
|
122
134
|
const s = JSON.parse(readFileSync(hostFiles(home).state, "utf8"));
|
|
@@ -966,6 +978,9 @@ export function windowsDoctorRows(x) {
|
|
|
966
978
|
rows.push({ level: "pass", name: "host", detail: `running (pid ${x.state.pid}, ${x.state.version})${restarts.length ? ` — restarted ${restarts.map(([c, n]) => `the ${c} ${n}×`).join(", ")}` : ""}`,
|
|
967
979
|
share: "running" });
|
|
968
980
|
}
|
|
981
|
+
else if (x.launchProblem) {
|
|
982
|
+
rows.push({ level: "warn", name: "host", detail: `not running, and will not start as things are — ${x.launchProblem}`, share: "cannot start" });
|
|
983
|
+
}
|
|
969
984
|
else {
|
|
970
985
|
rows.push({ level: "warn", name: "host", detail: "not running — the worker, alerts and tray are all down until it starts",
|
|
971
986
|
fix: `pantheon service start ${x.installed.worker ? "worker" : "notifier"} (or log out and back in; its log: ~/.pantheon/logs/host.log)`, share: "not running" });
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { win32 } from "node:path";
|
|
4
|
+
export const realPackageProbeFs = {
|
|
5
|
+
mkdir: (p) => mkdirSync(p, { recursive: true }),
|
|
6
|
+
write: (p, text) => writeFileSync(p, text),
|
|
7
|
+
exists: (p) => existsSync(p),
|
|
8
|
+
list: (p) => readdirSync(p),
|
|
9
|
+
remove: (p) => rmSync(p, { recursive: true, force: true }),
|
|
10
|
+
};
|
|
11
|
+
export function appPackage(env = process.env, fs = realPackageProbeFs, rand = () => randomBytes(6).toString("hex")) {
|
|
12
|
+
const local = env.LOCALAPPDATA;
|
|
13
|
+
if (!local)
|
|
14
|
+
return null;
|
|
15
|
+
const name = `pantheon-probe-${rand()}`;
|
|
16
|
+
const dir = win32.join(local, name);
|
|
17
|
+
try {
|
|
18
|
+
fs.mkdir(dir);
|
|
19
|
+
fs.write(win32.join(dir, "probe"), "1");
|
|
20
|
+
let families = [];
|
|
21
|
+
try {
|
|
22
|
+
families = fs.list(win32.join(local, "Packages"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
families = [];
|
|
26
|
+
}
|
|
27
|
+
return families.find((f) => fs.exists(win32.join(local, "Packages", f, "LocalCache", "Local", name, "probe"))) ?? null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
try {
|
|
34
|
+
fs.remove(dir);
|
|
35
|
+
}
|
|
36
|
+
catch { }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
let cached = null;
|
|
40
|
+
export function thisAppPackage() {
|
|
41
|
+
if (process.platform !== "win32")
|
|
42
|
+
return null;
|
|
43
|
+
if (!cached)
|
|
44
|
+
cached = { value: appPackage() };
|
|
45
|
+
return cached.value;
|
|
46
|
+
}
|
|
47
|
+
export function onlyInsidePackage(path, family, env = process.env, exists = existsSync) {
|
|
48
|
+
const local = env.LOCALAPPDATA, roaming = env.APPDATA;
|
|
49
|
+
const norm = (p) => win32.normalize(p).replace(/[\\]+$/, "").toLowerCase();
|
|
50
|
+
const p = norm(path);
|
|
51
|
+
for (const [root, sub] of [[local, "Local"], [roaming, "Roaming"]]) {
|
|
52
|
+
if (!root)
|
|
53
|
+
continue;
|
|
54
|
+
const r = norm(root);
|
|
55
|
+
if (!p.startsWith(r + "\\"))
|
|
56
|
+
continue;
|
|
57
|
+
const rel = win32.normalize(path).slice(win32.normalize(root).replace(/[\\]+$/, "").length + 1);
|
|
58
|
+
if (!local)
|
|
59
|
+
return false;
|
|
60
|
+
if (exists(win32.join(local, "Packages", family, "LocalCache", sub, rel)))
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export function packageApp(family) {
|
|
66
|
+
const name = family.replace(/_[a-z0-9]+$/i, "");
|
|
67
|
+
return /^claude$/i.test(name) ? "the Claude desktop app" : name.replace(/^[A-Za-z0-9]+\./, "") || family;
|
|
68
|
+
}
|
|
69
|
+
export const INSTALL_OUTSIDE_LINE = "open PowerShell from the Start menu (not through the app) and run: irm https://relay.joinpantheon.network/install.ps1 | iex";
|
|
70
|
+
//# sourceMappingURL=win-package.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
|
-
"built_at": "2026-10-
|
|
3
|
+
"built_at": "2026-10-05T16:44:41.416Z",
|
|
4
4
|
"swiftc": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)",
|
|
5
5
|
"helpers": {
|
|
6
6
|
"pantheon-tray": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"pantheon-hud": {
|
|
18
18
|
"source": "main.swift",
|
|
19
19
|
"source_sha256": "bcf55a19b658d1f09a9d117c76030393ab08a2569062dd7413f884ca05f3a0eb",
|
|
20
|
-
"sha256": "
|
|
20
|
+
"sha256": "0414b397a533940e3a5c324add6250e1ed71919e67e462d11dcb2a1d0c2f9b41",
|
|
21
21
|
"archs": [
|
|
22
22
|
"arm64",
|
|
23
23
|
"x86_64"
|
|
Binary file
|
|
@@ -23,7 +23,12 @@
|
|
|
23
23
|
// swapping the runtime folder, a Node upgrade in progress) it waits and looks
|
|
24
24
|
// again, with the same backoff; only after --missing-grace seconds (default
|
|
25
25
|
// ten minutes) of nothing to start does it conclude Pantheon was removed, and
|
|
26
|
-
// exit 3.
|
|
26
|
+
// exit 3. While it waits it says so in launch-error.txt beside the launch file
|
|
27
|
+
// (when, and which program it could not find), so `pantheon service install`,
|
|
28
|
+
// `ps` and `doctor` can tell a host that is starting from one that never will:
|
|
29
|
+
// a Node that exists only inside an app package's private AppData (an install
|
|
30
|
+
// run from the Claude desktop app) looks there to the app and is not there to
|
|
31
|
+
// Task Scheduler. The file goes once a start succeeds.
|
|
27
32
|
//
|
|
28
33
|
// Each start of this shim is one session (PANTHEON_HOST_SESSION, a new GUID):
|
|
29
34
|
// the host uses it to tell a restart (same session) from a new logon.
|
|
@@ -42,7 +47,7 @@ using System.Threading;
|
|
|
42
47
|
[assembly: System.Reflection.AssemblyTitle("Pantheon background host")]
|
|
43
48
|
[assembly: System.Reflection.AssemblyDescription("Keeps Pantheon's worker, alerts and tray running while you are logged on")]
|
|
44
49
|
[assembly: System.Reflection.AssemblyProduct("Pantheon")]
|
|
45
|
-
[assembly: System.Reflection.AssemblyVersion("1.
|
|
50
|
+
[assembly: System.Reflection.AssemblyVersion("1.2.0.0")]
|
|
46
51
|
|
|
47
52
|
static class PantheonHost
|
|
48
53
|
{
|
|
@@ -84,12 +89,13 @@ static class PantheonHost
|
|
|
84
89
|
while (true)
|
|
85
90
|
{
|
|
86
91
|
List<string> argv = ReadLaunch(launchFile);
|
|
92
|
+
string wanted = argv != null ? argv[0] : baked[0];
|
|
87
93
|
if (argv == null || !File.Exists(argv[0])) argv = baked;
|
|
88
94
|
if (!File.Exists(argv[0]))
|
|
89
95
|
{
|
|
90
96
|
// Nothing to start right now: wait for it to come back, and give up
|
|
91
97
|
// only once it has been gone longer than any install or upgrade takes.
|
|
92
|
-
if (missingSince == DateTime.MinValue) missingSince = DateTime.UtcNow;
|
|
98
|
+
if (missingSince == DateTime.MinValue) { missingSince = DateTime.UtcNow; SayLaunchError(launchFile, "missing", wanted); }
|
|
93
99
|
if ((DateTime.UtcNow - missingSince).TotalSeconds >= graceSeconds) return 3;
|
|
94
100
|
Thread.Sleep(delay);
|
|
95
101
|
delay = Math.Min(delay * 2, MaxDelayMs);
|
|
@@ -115,12 +121,14 @@ static class PantheonHost
|
|
|
115
121
|
psi.EnvironmentVariables["PANTHEON_HOST_SESSION"] = session;
|
|
116
122
|
using (Process p = Process.Start(psi))
|
|
117
123
|
{
|
|
124
|
+
ClearLaunchError(launchFile);
|
|
118
125
|
p.WaitForExit();
|
|
119
126
|
code = p.ExitCode;
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
|
-
catch (Exception)
|
|
129
|
+
catch (Exception e)
|
|
123
130
|
{
|
|
131
|
+
SayLaunchError(launchFile, "start-failed", argv[0] + " (" + e.Message + ")");
|
|
124
132
|
code = 1;
|
|
125
133
|
}
|
|
126
134
|
if (code == 0) return 0;
|
|
@@ -131,6 +139,30 @@ static class PantheonHost
|
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
// launch-error.txt beside the launch file: one line, tab-separated, of when
|
|
143
|
+
// (UTC, ISO 8601), what ("missing" or "start-failed") and which program.
|
|
144
|
+
// Best effort: a host that cannot write it still waits and retries.
|
|
145
|
+
static string LaunchErrorFile(string launchFile)
|
|
146
|
+
{
|
|
147
|
+
if (launchFile == null) return null;
|
|
148
|
+
string dir = Path.GetDirectoryName(launchFile);
|
|
149
|
+
return string.IsNullOrEmpty(dir) ? null : Path.Combine(dir, "launch-error.txt");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static void SayLaunchError(string launchFile, string what, string program)
|
|
153
|
+
{
|
|
154
|
+
string f = LaunchErrorFile(launchFile);
|
|
155
|
+
if (f == null) return;
|
|
156
|
+
try { File.WriteAllText(f, DateTime.UtcNow.ToString("o") + "\t" + what + "\t" + program + "\r\n", new UTF8Encoding(false)); } catch (Exception) { }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static void ClearLaunchError(string launchFile)
|
|
160
|
+
{
|
|
161
|
+
string f = LaunchErrorFile(launchFile);
|
|
162
|
+
if (f == null) return;
|
|
163
|
+
try { if (File.Exists(f)) File.Delete(f); } catch (Exception) { }
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
// The launch file: one argument per line (UTF-8), the program first. null
|
|
135
167
|
// when there is none, or it cannot be read, or it names nothing.
|
|
136
168
|
static List<string> ReadLaunch(string file)
|
package/installer/install.ps1
CHANGED
|
@@ -56,15 +56,27 @@
|
|
|
56
56
|
# What it does, and nothing else:
|
|
57
57
|
# 1. downloads one pinned Node release (the win-x64 or win-arm64 zip) from
|
|
58
58
|
# nodejs.org, checks its sha256 against the one pinned in this file, and
|
|
59
|
-
# unpacks it into %
|
|
59
|
+
# unpacks it into %USERPROFILE%\.pantheon\runtime\node. That Node is
|
|
60
60
|
# Pantheon's own; a Node you already have is not touched.
|
|
61
61
|
# 2. installs the Pantheon CLI with THAT Node's npm into that same folder.
|
|
62
62
|
# No administrator rights, no execution-policy change, no WSL.
|
|
63
|
-
# 3. writes %
|
|
64
|
-
# 4. puts %
|
|
63
|
+
# 3. writes %USERPROFILE%\.pantheon\bin\pantheon.cmd, which runs that Node.
|
|
64
|
+
# 4. puts %USERPROFILE%\.pantheon\bin first on your user PATH, and on this
|
|
65
65
|
# window's PATH.
|
|
66
66
|
# 5. runs `pantheon <the arguments you gave>`.
|
|
67
67
|
#
|
|
68
|
+
# An install from before 0.33.1 lives in %LOCALAPPDATA%\Pantheon instead; when
|
|
69
|
+
# one is there and works, it is updated where it is. New installs use the
|
|
70
|
+
# profile folder because of apps from the Microsoft Store or any MSIX package,
|
|
71
|
+
# the Claude desktop app among them: Windows keeps a NEW folder that anything
|
|
72
|
+
# such an app starts (its agent's shell, this script) creates in AppData
|
|
73
|
+
# private to that app. Pantheon then ran inside the app and nowhere else, and
|
|
74
|
+
# its logon task -- run by Task Scheduler, outside every app -- found no Node.
|
|
75
|
+
# The profile folder is never kept private that way. Run from inside such an
|
|
76
|
+
# app, this script also sets your PATH through a one-off task of your own
|
|
77
|
+
# (outside the app, deleted straight after), since Windows 10 keeps that
|
|
78
|
+
# registry change private to the app too.
|
|
79
|
+
#
|
|
68
80
|
# Running it again is safe: whatever is already right is left as it is. Two
|
|
69
81
|
# copies at once are safe too: the second waits for the first.
|
|
70
82
|
# The relay that serves this file fills in the Pantheon version below.
|
|
@@ -129,6 +141,119 @@ function Install-Pantheon {
|
|
|
129
141
|
[IO.File]::WriteAllText($LogFile, $r.out + $r.err)
|
|
130
142
|
return $r.code
|
|
131
143
|
}
|
|
144
|
+
# Is this PowerShell inside an app package (the Claude desktop app, any
|
|
145
|
+
# Microsoft Store or MSIX app)? Windows keeps a NEW folder such a process
|
|
146
|
+
# creates in %LOCALAPPDATA% private to that app, under
|
|
147
|
+
# %LOCALAPPDATA%\Packages\<package>\LocalCache\Local. There is no variable
|
|
148
|
+
# that says so, and asking Windows needs native code; so it is asked the way
|
|
149
|
+
# the redirection answers: a new folder with a file in it, looked for in each
|
|
150
|
+
# package's private copy, then removed. The package's folder name, or ''
|
|
151
|
+
# (not in one, or it cannot tell).
|
|
152
|
+
function Get-PantheonAppPackage([string]$Local) {
|
|
153
|
+
if (-not $Local) { return '' }
|
|
154
|
+
$name = 'pantheon-probe-' + [Guid]::NewGuid().ToString('N').Substring(0, 12)
|
|
155
|
+
$dir = Join-Path $Local $name
|
|
156
|
+
$found = ''
|
|
157
|
+
try {
|
|
158
|
+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
|
159
|
+
[IO.File]::WriteAllText((Join-Path $dir 'probe'), '1')
|
|
160
|
+
$pk = Join-Path $Local 'Packages'
|
|
161
|
+
if (Test-Path -LiteralPath $pk) {
|
|
162
|
+
foreach ($f in @(Get-ChildItem -LiteralPath $pk -Directory -Force -ErrorAction SilentlyContinue)) {
|
|
163
|
+
if (Test-Path -LiteralPath ([IO.Path]::Combine($f.FullName, 'LocalCache', 'Local', $name, 'probe'))) { $found = $f.Name; break }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch { $found = '' }
|
|
167
|
+
finally { Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue }
|
|
168
|
+
return $found
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Where this install goes. Run inside an app package ($Package): <profile>\.pantheon,
|
|
172
|
+
# the one place such an app writes that every program sees, Task Scheduler
|
|
173
|
+
# included. Otherwise %LOCALAPPDATA%\Pantheon, as before 0.33.1 (AppData\Local
|
|
174
|
+
# never roams with an enterprise profile; the profile folder does). Either way
|
|
175
|
+
# an install already in one of them is updated where it is, so a machine that
|
|
176
|
+
# works keeps working the same way -- except an AppData install that exists
|
|
177
|
+
# only inside this app's package, which is never reused: it is what kept the
|
|
178
|
+
# background host from ever starting.
|
|
179
|
+
# @{ root; oldPrivate }: oldPrivate when such an app-only install was passed over.
|
|
180
|
+
function Select-PantheonRoot([string]$ProfileDir, [string]$Local, [string]$Package) {
|
|
181
|
+
$current = [IO.Path]::Combine($ProfileDir, '.pantheon')
|
|
182
|
+
$mine = @{ root = $current; oldPrivate = $false }
|
|
183
|
+
if (Test-Path -LiteralPath ([IO.Path]::Combine($current, 'runtime', 'node', 'node.exe'))) { return $mine }
|
|
184
|
+
if (-not $Local) { return $mine }
|
|
185
|
+
# Its Node only in this app's private copy: passed over (and said), whatever else of it is there.
|
|
186
|
+
if ($Package -and (Test-Path -LiteralPath ([IO.Path]::Combine($Local, 'Packages', $Package, 'LocalCache', 'Local', 'Pantheon', 'runtime', 'node', 'node.exe')))) { return @{ root = $current; oldPrivate = $true } }
|
|
187
|
+
$legacy = [IO.Path]::Combine($Local, 'Pantheon')
|
|
188
|
+
$there = (Test-Path -LiteralPath ([IO.Path]::Combine($legacy, 'runtime', 'node', 'node.exe'))) -and (Test-Path -LiteralPath ([IO.Path]::Combine($legacy, 'bin', 'pantheon.cmd')))
|
|
189
|
+
if ($there -or -not $Package) { return @{ root = $legacy; oldPrivate = $false } }
|
|
190
|
+
return $mine
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# The user PATH, set from OUTSIDE this app's package. Inside one, Windows 10
|
|
194
|
+
# keeps HKCU changes private to the app (Windows 11 lets this key through),
|
|
195
|
+
# so a terminal opened from the Start menu would not find pantheon. Task
|
|
196
|
+
# Scheduler runs a task outside every package: a one-off task of this user's
|
|
197
|
+
# makes the same edit as the installer does below (this folder first, $Drop
|
|
198
|
+
# taken out, every other entry as it was, %VARS% unexpanded), writes how it
|
|
199
|
+
# went to a file, and is deleted. Run the way the logon task's PowerShell
|
|
200
|
+
# fallback runs (-Command, so no execution policy applies, nothing encoded).
|
|
201
|
+
# '' when it worked; otherwise why.
|
|
202
|
+
function Set-PantheonPathOutside([string]$BinDir, [string[]]$Drop, [string]$WorkDir, [string]$Schtasks, [string]$PowerShellExe, [string]$UserId) {
|
|
203
|
+
$tag = [Guid]::NewGuid().ToString('N').Substring(0, 8)
|
|
204
|
+
$result = Join-Path $WorkDir "path-$tag.result"
|
|
205
|
+
$xmlFile = Join-Path $WorkDir "path-$tag.xml"
|
|
206
|
+
$name = "Pantheon-Path-$tag"
|
|
207
|
+
# PowerShell reads the typographic single quotes as quotes too: each doubled, like '.
|
|
208
|
+
$q = { param($v) "'" + ([string]$v -replace "['\u2018\u2019\u201A\u201B]", '$0$0') + "'" }
|
|
209
|
+
$dropList = (@($Drop | Where-Object { $_ }) | ForEach-Object { & $q $_ }) -join ','
|
|
210
|
+
$inner = "`$ErrorActionPreference = 'Stop'; try { `$bin = $(& $q $BinDir); `$drop = @($dropList); " +
|
|
211
|
+
"`$same = { param(`$a, `$b) ([Environment]::ExpandEnvironmentVariables(`$a).TrimEnd([char]92)) -ieq ([Environment]::ExpandEnvironmentVariables(`$b).TrimEnd([char]92)) }; " +
|
|
212
|
+
"`$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment'); " +
|
|
213
|
+
"try { `$raw = [string]`$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames); " +
|
|
214
|
+
"`$parts = @(`$raw -split ';' | Where-Object { `$_ -ne '' }); " +
|
|
215
|
+
"`$rest = @(`$parts | Where-Object { `$p = `$_; -not (& `$same `$p `$bin) -and @(`$drop | Where-Object { & `$same `$p `$_ }).Count -eq 0 }); " +
|
|
216
|
+
"`$new = (@(`$bin) + `$rest) -join ';'; " +
|
|
217
|
+
"if (`$new -ne `$raw) { `$key.SetValue('Path', `$new, [Microsoft.Win32.RegistryValueKind]::ExpandString); [Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', '1', 'User'); [Environment]::SetEnvironmentVariable('PANTHEON_PATH_UPDATED', `$null, 'User') } " +
|
|
218
|
+
"} finally { `$key.Close() }; [IO.File]::WriteAllText($(& $q $result), 'ok') } catch { [IO.File]::WriteAllText($(& $q $result), 'error: ' + `$_.Exception.Message) }"
|
|
219
|
+
$x = { param($v) [Security.SecurityElement]::Escape([string]$v) }
|
|
220
|
+
$taskArgs = '-NoProfile -NonInteractive -WindowStyle Hidden -Command "& { ' + $inner + ' }"'
|
|
221
|
+
$xml = (@(
|
|
222
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
223
|
+
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
224
|
+
' <RegistrationInfo><Description>Pantheon installer: puts Pantheon on your PATH once; deleted straight after.</Description></RegistrationInfo>',
|
|
225
|
+
(' <Principals><Principal id="Author"><UserId>' + (& $x $UserId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>'),
|
|
226
|
+
' <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>false</StartWhenAvailable><RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled><Hidden>true</Hidden><RunOnlyIfIdle>false</RunOnlyIfIdle><WakeToRun>false</WakeToRun><ExecutionTimeLimit>PT2M</ExecutionTimeLimit><Priority>7</Priority></Settings>',
|
|
227
|
+
(' <Actions Context="Author"><Exec><Command>' + (& $x $PowerShellExe) + '</Command><Arguments>' + (& $x $taskArgs) + '</Arguments></Exec></Actions>'),
|
|
228
|
+
'</Task>') -join "`r`n")
|
|
229
|
+
# One left by an installer that was stopped while it waited: gone first.
|
|
230
|
+
try {
|
|
231
|
+
$listed = Invoke-Quiet $Schtasks @('/Query', '/FO', 'CSV', '/NH')
|
|
232
|
+
foreach ($line in ($listed.out -split "`r?`n")) {
|
|
233
|
+
$m = [regex]::Match($line, '^"\\(Pantheon-Path-[0-9a-f]{8})"')
|
|
234
|
+
if ($m.Success) { $null = Invoke-Quiet $Schtasks @('/Delete', '/TN', $m.Groups[1].Value, '/F') }
|
|
235
|
+
}
|
|
236
|
+
} catch { }
|
|
237
|
+
try {
|
|
238
|
+
New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null
|
|
239
|
+
[IO.File]::WriteAllText($xmlFile, $xml, [Text.Encoding]::Unicode)
|
|
240
|
+
$c = Invoke-Quiet $Schtasks @('/Create', '/TN', $name, '/XML', $xmlFile, '/F')
|
|
241
|
+
if ($c.code -ne 0) { return "Task Scheduler would not take the task ($(($c.err + $c.out).Trim()))" }
|
|
242
|
+
try {
|
|
243
|
+
$r = Invoke-Quiet $Schtasks @('/Run', '/TN', $name)
|
|
244
|
+
if ($r.code -ne 0) { return "Task Scheduler would not start the task ($(($r.err + $r.out).Trim()))" }
|
|
245
|
+
for ($i = 0; $i -lt 60 -and -not (Test-Path -LiteralPath $result); $i++) { Start-Sleep -Milliseconds 500 }
|
|
246
|
+
if (-not (Test-Path -LiteralPath $result)) { return 'the task did not finish within 30 seconds' }
|
|
247
|
+
$said = ([IO.File]::ReadAllText($result)).Trim()
|
|
248
|
+
if ($said -eq 'ok') { return '' }
|
|
249
|
+
return $said
|
|
250
|
+
} finally { $null = Invoke-Quiet $Schtasks @('/Delete', '/TN', $name, '/F') }
|
|
251
|
+
} catch { return $_.Exception.Message }
|
|
252
|
+
finally {
|
|
253
|
+
Remove-Item -LiteralPath $xmlFile, $result -Force -ErrorAction SilentlyContinue
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
132
257
|
# This user's logon task: "Pantheon-<SID>" (src/win-host.ts taskNameFor), and
|
|
133
258
|
# "Pantheon", the name builds before 0.33.0 gave it.
|
|
134
259
|
function Get-PantheonTaskNames {
|
|
@@ -419,8 +544,26 @@ function Install-Pantheon {
|
|
|
419
544
|
|
|
420
545
|
$localAppData = [Environment]::GetFolderPath('LocalApplicationData')
|
|
421
546
|
if (-not $localAppData) { $localAppData = $env:LOCALAPPDATA }
|
|
422
|
-
|
|
423
|
-
$
|
|
547
|
+
$profileDir = [Environment]::GetFolderPath('UserProfile')
|
|
548
|
+
if (-not $profileDir) { $profileDir = $env:USERPROFILE }
|
|
549
|
+
if (-not $profileDir) { Fail 'USERPROFILE is not set, so there is nowhere to install. Run this from your own Windows account.' }
|
|
550
|
+
# Inside an app package (the Claude desktop app): said once, and acted on below.
|
|
551
|
+
$appPackage = Get-PantheonAppPackage $localAppData
|
|
552
|
+
$appName = if ($appPackage -match '^Claude_') { 'the Claude desktop app' } elseif ($appPackage) { (($appPackage -replace '_[A-Za-z0-9]+$', '') -replace '^[A-Za-z0-9]+\.', '') } else { '' }
|
|
553
|
+
$where = Select-PantheonRoot $profileDir $localAppData $appPackage
|
|
554
|
+
$root = $where.root
|
|
555
|
+
# A release before 0.33.1 does not know ~\.pantheon (it would not find itself to
|
|
556
|
+
# upgrade, and its hooks would name a bare node): never put one there. This can
|
|
557
|
+
# only be asked for when the relay names an older CLI than this script (npm late).
|
|
558
|
+
$baseVersion = ($PantheonVersion -split '-')[0]
|
|
559
|
+
$isProfileRoot = $root -ieq [IO.Path]::Combine($profileDir, '.pantheon')
|
|
560
|
+
$tooOld = $false
|
|
561
|
+
try { $tooOld = [version]$baseVersion -lt [version]'0.33.1' } catch { $tooOld = $false }
|
|
562
|
+
if ($isProfileRoot -and $tooOld) { Fail "Pantheon $PantheonVersion cannot go in $root (that needs 0.33.1 or newer), and this is all the relay offers right now. Try again in a few minutes." }
|
|
563
|
+
$legacyBin = if ($localAppData) { Join-Path $localAppData 'Pantheon\bin' } else { '' }
|
|
564
|
+
$dropFromPath = @()
|
|
565
|
+
if ($legacyBin -and -not ($root -ieq (Join-Path $localAppData 'Pantheon'))) { $dropFromPath = @($legacyBin) }
|
|
566
|
+
if ($where.oldPrivate) { Note "the Pantheon in $(Join-Path $localAppData 'Pantheon') exists only inside $appName (Windows keeps what an app like it installs into AppData private to it), so its background worker could never start. Installing into $root instead, which every program sees." }
|
|
424
567
|
$runtime = Join-Path $root 'runtime'
|
|
425
568
|
$nodeDir = Join-Path $runtime 'node'
|
|
426
569
|
$nodeExe = Join-Path $nodeDir 'node.exe'
|
|
@@ -491,8 +634,11 @@ function Install-Pantheon {
|
|
|
491
634
|
|
|
492
635
|
# ---- 1. Pantheon's own Node --------------------------------------------
|
|
493
636
|
|
|
637
|
+
# Whole means node.exe AND its npm: an uninstall that ran while something
|
|
638
|
+
# still held node.exe (an agent window) removes the rest of the folder
|
|
639
|
+
# around it, and a node.exe with no npm beside it cannot install the CLI.
|
|
494
640
|
$haveNode = ''
|
|
495
|
-
if (Test-Path -LiteralPath $nodeExe) { try { $haveNode = (& $nodeExe --version) 2>$null } catch { $haveNode = '' } }
|
|
641
|
+
if ((Test-Path -LiteralPath $nodeExe) -and (Test-Path -LiteralPath $npmCli)) { try { $haveNode = (& $nodeExe --version) 2>$null } catch { $haveNode = '' } }
|
|
496
642
|
if ($haveNode -eq "v$NodeVersion") {
|
|
497
643
|
Say "Node v$NodeVersion for Pantheon is already in $nodeDir."
|
|
498
644
|
} else {
|
|
@@ -689,8 +835,9 @@ function Install-Pantheon {
|
|
|
689
835
|
try {
|
|
690
836
|
$raw = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
|
691
837
|
$parts = @($raw -split ';' | Where-Object { $_ -ne '' })
|
|
692
|
-
|
|
693
|
-
|
|
838
|
+
$stale = @($parts | Where-Object { $p = $_; @($dropFromPath | Where-Object { & $same $p $_ }).Count -gt 0 })
|
|
839
|
+
if ($parts.Count -eq 0 -or -not (& $same $parts[0] $binDir) -or $stale.Count -gt 0) {
|
|
840
|
+
$rest = @($parts | Where-Object { $p = $_; -not (& $same $p $binDir) -and @($dropFromPath | Where-Object { & $same $p $_ }).Count -eq 0 })
|
|
694
841
|
$key.SetValue('Path', ((@($binDir) + $rest) -join ';'), [Microsoft.Win32.RegistryValueKind]::ExpandString)
|
|
695
842
|
# SetEnvironmentVariable(..., 'User') is what tells Explorer and new
|
|
696
843
|
# terminals that the user environment changed (WM_SETTINGCHANGE).
|
|
@@ -699,6 +846,17 @@ function Install-Pantheon {
|
|
|
699
846
|
Say "Put $binDir first on your user PATH."
|
|
700
847
|
}
|
|
701
848
|
} finally { $key.Close() }
|
|
849
|
+
# Inside an app package, that change may have stayed inside the app (Windows
|
|
850
|
+
# 10 keeps HKCU writes private to it): made again from outside it.
|
|
851
|
+
if ($appPackage) {
|
|
852
|
+
$State.step = "adding Pantheon to your PATH from outside $appName"
|
|
853
|
+
$meName = ''
|
|
854
|
+
try { $meName = [Security.Principal.WindowsIdentity]::GetCurrent().Name } catch { $meName = "$env:USERDOMAIN\$env:USERNAME" }
|
|
855
|
+
$why = Set-PantheonPathOutside -BinDir $binDir -Drop $dropFromPath -WorkDir $runtime `
|
|
856
|
+
-Schtasks (Join-Path $env:SystemRoot 'System32\schtasks.exe') -PowerShellExe (Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe') -UserId $meName
|
|
857
|
+
if ($why) { Note "this is running inside $appName, and Pantheon could not add $binDir to your PATH from outside it ($why). In a PowerShell window opened from the Start menu, pantheon may not be found by name; $launcher always works." }
|
|
858
|
+
else { Say "Put $binDir on your user PATH from outside $appName, so terminals opened from the Start menu find pantheon too." }
|
|
859
|
+
}
|
|
702
860
|
# This window: the bin folder first, and the runtime's own folder (added
|
|
703
861
|
# for npm above) taken off again, as no new terminal will have it.
|
|
704
862
|
$env:Path = (@($binDir) + @($env:Path -split ';' | Where-Object { $_ -ne '' -and -not (& $same $_ $binDir) -and -not (& $same $_ $nodeDir) })) -join ';'
|
|
@@ -715,12 +873,58 @@ function Install-Pantheon {
|
|
|
715
873
|
$State.lock = $null
|
|
716
874
|
# The background host stopped for the swap starts again, on the new Node.
|
|
717
875
|
Start-PantheonTasks $State
|
|
876
|
+
# A background service set up before (pantheon service install) is pointed
|
|
877
|
+
# at THIS install: its logon task may still name a Node that is gone, or one
|
|
878
|
+
# only an app package could see (why 0.33.1 moved Pantheon out of AppData).
|
|
879
|
+
# `service start` keeps each service's own settings (its folder, its flags),
|
|
880
|
+
# registers the task again for this Node, starts it, and waits until the host
|
|
881
|
+
# says it is up. One the person paused stays paused.
|
|
882
|
+
foreach ($k in @('worker', 'notifier')) {
|
|
883
|
+
if (-not (Test-Path -LiteralPath (Join-Path $hostDir "$k.json"))) { continue }
|
|
884
|
+
if (Test-Path -LiteralPath (Join-Path $hostDir "$k.disabled")) { continue }
|
|
885
|
+
$State.step = "starting Pantheon's background $k on this install"
|
|
886
|
+
$r = Invoke-Quiet $nodeExe ($ca + @($entry, 'service', 'start', $k, '--keep-tray-quit'))
|
|
887
|
+
$said = (($r.err + "`n" + $r.out) -split "`r?`n" | Where-Object { $_.Trim() -and $_ -notmatch '^pantheon \d' } | Select-Object -Last 2) -join ' '
|
|
888
|
+
if ($r.code -eq 0) { Say "Pantheon's background $k runs on this install." }
|
|
889
|
+
else { Note "Pantheon's background $k did not start on this install: $($said.Trim()). Run: pantheon service start $k" }
|
|
890
|
+
}
|
|
891
|
+
# Someone already signed in here (a reinstall, or moving off an install only
|
|
892
|
+
# an app could see): the agents' Pantheon entries and hooks name a Node by
|
|
893
|
+
# its full path, so they are pointed at this one -- what `pantheon upgrade`
|
|
894
|
+
# does after every upgrade.
|
|
895
|
+
$member = ''
|
|
896
|
+
$cfgFile = Join-Path $pantheonHomeDir 'config.json'
|
|
897
|
+
if (Test-Path -LiteralPath $cfgFile) {
|
|
898
|
+
try { $member = [string]([IO.File]::ReadAllText($cfgFile) | ConvertFrom-Json).member } catch { $member = '' }
|
|
899
|
+
$State.step = 'pointing your coding agents at this install'
|
|
900
|
+
$w = Invoke-Quiet $nodeExe ($ca + @($entry, 'rewire', '--direct', '--json'))
|
|
901
|
+
# Windows PowerShell 5.1 hands a JSON array on as ONE object: enumerated
|
|
902
|
+
# with foreach, so each row is its own, on 5.1 and 7 alike.
|
|
903
|
+
$rows = @()
|
|
904
|
+
try {
|
|
905
|
+
$json = $w.out -split "`r?`n" | Where-Object { $_.Trim().StartsWith('[') } | Select-Object -Last 1
|
|
906
|
+
if ($json) { $parsed = ConvertFrom-Json $json; foreach ($x in $parsed) { $rows += , $x } }
|
|
907
|
+
} catch { $rows = @() }
|
|
908
|
+
# What was actually pointed here, said from what rewire reported, never assumed.
|
|
909
|
+
$bad = @($rows | Where-Object { $_.status -match '^failed' })
|
|
910
|
+
$done = @($rows | Where-Object { $_.status -notmatch '^(failed|skipped|unchanged)' -and $_.tool -notmatch 'perms$' } | ForEach-Object { $_.tool })
|
|
911
|
+
foreach ($r in $rows) { if ($r.tool -eq 'hooks' -and $r.status -match '(Codex runs a changed hook.*|pantheon hook install there.*)$') { Note "hooks: $($Matches[1])" } }
|
|
912
|
+
if ($w.code -ne 0 -or $rows.Count -eq 0) { Note 'your coding agents could not be pointed at this install; run: pantheon setup' }
|
|
913
|
+
elseif ($bad.Count) { Note ("these could not be pointed at this install: " + (($bad | ForEach-Object { "$($_.tool) ($($_.status))" }) -join '; ') + '. Run: pantheon setup') }
|
|
914
|
+
elseif ($done.Count) { Say "Your coding agents now use this install: $($done -join ', ') (restart any agent window that was open)." }
|
|
915
|
+
else { Say 'Your coding agents already use this install.' }
|
|
916
|
+
}
|
|
718
917
|
$State.step = 'starting pantheon'
|
|
719
918
|
if ($Arguments.Count -eq 0) {
|
|
720
919
|
Say ''
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
920
|
+
if ($member) {
|
|
921
|
+
Say "Pantheon $PantheonVersion is installed, and you are still signed in as $member -- nothing to rejoin."
|
|
922
|
+
Say 'Open a new terminal (or restart your agent) and carry on; pantheon doctor checks everything.'
|
|
923
|
+
} else {
|
|
924
|
+
Say "Pantheon $PantheonVersion is installed. Open a new terminal, then:"
|
|
925
|
+
Say ' pantheon start create a team'
|
|
926
|
+
Say " pantheon join '<invite link>' join a teammate's team"
|
|
927
|
+
}
|
|
724
928
|
$State.code = 0
|
|
725
929
|
return
|
|
726
930
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@join-pantheon/cli",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.1-rc.1",
|
|
4
4
|
"description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|