@join-pantheon/cli 0.32.1 → 0.33.0-rc.2
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/accounts.js +1 -1
- package/dist/board.js +1417 -0
- package/dist/claude-config.js +189 -0
- package/dist/cli.js +351 -101
- package/dist/client.js +16 -0
- package/dist/command-state.js +2 -1
- package/dist/dbus.js +92 -13
- package/dist/doctor.js +36 -5
- package/dist/folder-guard.js +89 -0
- package/dist/git-safe.js +3 -3
- package/dist/host-env.js +47 -1
- package/dist/install.js +76 -10
- package/dist/installer.js +2 -1
- package/dist/ledger.js +2 -2
- package/dist/machine.js +174 -3
- package/dist/managed.js +27 -1
- package/dist/notice-linux.js +501 -0
- package/dist/notice-windows.js +323 -0
- package/dist/notifier.js +12 -3
- package/dist/quickstart.js +28 -11
- package/dist/relay.js +64 -9
- package/dist/service.js +455 -29
- package/dist/setup-guide.js +22 -5
- package/dist/store.js +18 -2
- package/dist/uninstall.js +1 -1
- package/dist/upgrade.js +249 -4
- package/dist/uri-handler.js +335 -0
- package/dist/win-host.js +993 -0
- package/dist/worker-policy.js +3 -0
- package/dist/worker.js +11 -11
- package/helper/macos/prebuilt/manifest.json +2 -2
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/windows/pantheon-host.cs +170 -0
- package/helper/windows/pantheon-notice.ps1 +127 -0
- package/helper/windows/pantheon-tray.ps1 +731 -0
- package/helper/windows/pantheon.ico +0 -0
- package/installer/install.ps1 +238 -19
- package/installer/install.sh +36 -1
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -21,6 +21,7 @@ export class PantheonClient {
|
|
|
21
21
|
seen = new Map();
|
|
22
22
|
backoff = 500;
|
|
23
23
|
closed = false;
|
|
24
|
+
disposed = false;
|
|
24
25
|
everConnected = false;
|
|
25
26
|
localOverride = null;
|
|
26
27
|
usingLoopback = false;
|
|
@@ -170,6 +171,11 @@ export class PantheonClient {
|
|
|
170
171
|
}
|
|
171
172
|
attempt(address) {
|
|
172
173
|
return new Promise((resolve, reject) => {
|
|
174
|
+
if (this.disposed) {
|
|
175
|
+
this.closed = true;
|
|
176
|
+
reject(new Error("this client was disposed"));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
173
179
|
let settled = false;
|
|
174
180
|
let heard = false;
|
|
175
181
|
const fail = reject;
|
|
@@ -195,6 +201,8 @@ export class PantheonClient {
|
|
|
195
201
|
this.watchdog.unref?.();
|
|
196
202
|
ws.on("open", () => { });
|
|
197
203
|
ws.on("message", (raw) => {
|
|
204
|
+
if (this.disposed)
|
|
205
|
+
return;
|
|
198
206
|
this.lastInbound = Date.now();
|
|
199
207
|
let msg;
|
|
200
208
|
try {
|
|
@@ -438,6 +446,14 @@ export class PantheonClient {
|
|
|
438
446
|
this.waiters = [];
|
|
439
447
|
this.ws?.close(4000, "handover");
|
|
440
448
|
}
|
|
449
|
+
dispose() {
|
|
450
|
+
this.disposed = true;
|
|
451
|
+
this.close();
|
|
452
|
+
if (this.watchdog) {
|
|
453
|
+
clearInterval(this.watchdog);
|
|
454
|
+
this.watchdog = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
441
457
|
close() {
|
|
442
458
|
this.closed = true;
|
|
443
459
|
for (const w of this.waiters) {
|
package/dist/command-state.js
CHANGED
|
@@ -5,6 +5,7 @@ import { chooseAgent, claudeSandboxable, NOT_FOR_UNATTENDED_RUNS, parseRepoComma
|
|
|
5
5
|
import { detectClaudeFeatures } from "./claude-confine.js";
|
|
6
6
|
import { loadPersonas } from "./persona.js";
|
|
7
7
|
import { plistPath, existingUnitPath } from "./service.js";
|
|
8
|
+
import { readDefinition } from "./win-host.js";
|
|
8
9
|
import { loadPolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
9
10
|
export function workerServiceAllowsBash(plat = platform()) {
|
|
10
11
|
try {
|
|
@@ -17,7 +18,7 @@ export function workerServiceAllowsBash(plat = platform()) {
|
|
|
17
18
|
return args.includes("--allow-bash");
|
|
18
19
|
}
|
|
19
20
|
if (plat === "win32")
|
|
20
|
-
return null;
|
|
21
|
+
return readDefinition("worker")?.args.includes("--allow-bash") ?? null;
|
|
21
22
|
const u = existingUnitPath("worker");
|
|
22
23
|
if (!u)
|
|
23
24
|
return null;
|
package/dist/dbus.js
CHANGED
|
@@ -193,6 +193,7 @@ export function encodeMessage(m) {
|
|
|
193
193
|
f(FIELD.error_name, "s", m.error_name);
|
|
194
194
|
f(FIELD.reply_serial, "u", m.reply_serial);
|
|
195
195
|
f(FIELD.destination, "s", m.destination);
|
|
196
|
+
f(FIELD.sender, "s", m.sender);
|
|
196
197
|
f(FIELD.signature, "g", m.signature || undefined);
|
|
197
198
|
const head = new Writer();
|
|
198
199
|
head.byte("l".charCodeAt(0));
|
|
@@ -363,6 +364,32 @@ export class DBus {
|
|
|
363
364
|
onClose(h) { this.closedHandlers.push(h); }
|
|
364
365
|
close() { this.sock?.end(); this.sock = null; }
|
|
365
366
|
}
|
|
367
|
+
export async function portalOpenUri(bus, uri, waitMs = 3_000) {
|
|
368
|
+
const REQ = "org.freedesktop.portal.Request";
|
|
369
|
+
await bus.call({ destination: "org.freedesktop.DBus", path: "/org/freedesktop/DBus", interface: "org.freedesktop.DBus", member: "AddMatch", signature: "s",
|
|
370
|
+
body: [`type='signal',interface='${REQ}',member='Response'`] });
|
|
371
|
+
const responses = new Map();
|
|
372
|
+
let handle = null;
|
|
373
|
+
let answered = null;
|
|
374
|
+
bus.onSignal((m) => {
|
|
375
|
+
if (m.interface !== REQ || m.member !== "Response" || !m.path)
|
|
376
|
+
return;
|
|
377
|
+
const code = Number(m.body[0]);
|
|
378
|
+
if (handle === m.path && answered)
|
|
379
|
+
answered(code);
|
|
380
|
+
else
|
|
381
|
+
responses.set(m.path, code);
|
|
382
|
+
});
|
|
383
|
+
const r = await bus.call({ destination: "org.freedesktop.portal.Desktop", path: "/org/freedesktop/portal/desktop", interface: "org.freedesktop.portal.OpenURI",
|
|
384
|
+
member: "OpenURI", signature: "ssa{sv}", body: ["", uri, []] }, 10_000);
|
|
385
|
+
handle = String(r.body[0] ?? "");
|
|
386
|
+
const early = responses.get(handle);
|
|
387
|
+
const code = early ?? await new Promise((done) => {
|
|
388
|
+
const t = setTimeout(() => done(null), waitMs);
|
|
389
|
+
answered = (c) => { clearTimeout(t); done(c); };
|
|
390
|
+
});
|
|
391
|
+
return code === null || code === 0 || code === 1;
|
|
392
|
+
}
|
|
366
393
|
const N = { destination: "org.freedesktop.Notifications", path: "/org/freedesktop/Notifications", interface: "org.freedesktop.Notifications" };
|
|
367
394
|
export class Notifications {
|
|
368
395
|
bus;
|
|
@@ -377,33 +404,84 @@ export class Notifications {
|
|
|
377
404
|
const n = new Notifications(b);
|
|
378
405
|
await b.call({ destination: "org.freedesktop.DBus", path: "/org/freedesktop/DBus", interface: "org.freedesktop.DBus", member: "AddMatch", signature: "s",
|
|
379
406
|
body: ["type='signal',interface='org.freedesktop.Notifications',path='/org/freedesktop/Notifications'"] });
|
|
407
|
+
await b.call({ destination: "org.freedesktop.DBus", path: "/org/freedesktop/DBus", interface: "org.freedesktop.DBus", member: "AddMatch", signature: "s",
|
|
408
|
+
body: [`type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='${N.destination}'`] });
|
|
380
409
|
b.onSignal((m) => {
|
|
381
|
-
if (m.interface
|
|
410
|
+
if (m.sender === "org.freedesktop.DBus" && m.interface === "org.freedesktop.DBus" && m.member === "NameOwnerChanged" && m.body[0] === N.destination) {
|
|
411
|
+
n.serverChanged(String(m.body[2] ?? "") || null);
|
|
382
412
|
return;
|
|
383
|
-
const id = Number(m.body[0]);
|
|
384
|
-
if (m.member === "ActivationToken")
|
|
385
|
-
n.tokens.set(id, String(m.body[1] ?? ""));
|
|
386
|
-
else if (m.member === "ActionInvoked") {
|
|
387
|
-
const tok = n.tokens.get(id);
|
|
388
|
-
for (const h of n.actionHandlers)
|
|
389
|
-
h(id, String(m.body[1] ?? ""), tok);
|
|
390
|
-
}
|
|
391
|
-
else if (m.member === "NotificationClosed") {
|
|
392
|
-
n.tokens.delete(id);
|
|
393
|
-
for (const h of n.closedHandlers)
|
|
394
|
-
h(id, Number(m.body[1] ?? 0));
|
|
395
413
|
}
|
|
414
|
+
if (m.interface !== N.interface || m.path !== N.path)
|
|
415
|
+
return;
|
|
416
|
+
void n.fromServer(m.sender).then((ok) => {
|
|
417
|
+
if (!ok)
|
|
418
|
+
return;
|
|
419
|
+
const id = Number(m.body[0]);
|
|
420
|
+
if (m.member === "ActivationToken")
|
|
421
|
+
n.tokens.set(id, String(m.body[1] ?? ""));
|
|
422
|
+
else if (m.member === "ActionInvoked") {
|
|
423
|
+
const tok = n.tokens.get(id);
|
|
424
|
+
for (const h of n.actionHandlers)
|
|
425
|
+
h(id, String(m.body[1] ?? ""), tok);
|
|
426
|
+
}
|
|
427
|
+
else if (m.member === "NotificationClosed") {
|
|
428
|
+
n.tokens.delete(id);
|
|
429
|
+
for (const h of n.closedHandlers)
|
|
430
|
+
h(id, Number(m.body[1] ?? 0));
|
|
431
|
+
}
|
|
432
|
+
});
|
|
396
433
|
});
|
|
397
434
|
return n;
|
|
398
435
|
}
|
|
436
|
+
owner = null;
|
|
437
|
+
lookup = null;
|
|
438
|
+
serverChangeHandlers = [];
|
|
439
|
+
serverChanged(owner) {
|
|
440
|
+
const was = this.owner;
|
|
441
|
+
this.owner = owner;
|
|
442
|
+
if (was === owner)
|
|
443
|
+
return;
|
|
444
|
+
this.tokens.clear();
|
|
445
|
+
for (const h of this.serverChangeHandlers) {
|
|
446
|
+
try {
|
|
447
|
+
h();
|
|
448
|
+
}
|
|
449
|
+
catch { }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
fromServer(sender) {
|
|
453
|
+
if (!sender)
|
|
454
|
+
return Promise.resolve(false);
|
|
455
|
+
if (sender === this.owner)
|
|
456
|
+
return Promise.resolve(true);
|
|
457
|
+
this.lookup ??= this.bus.call({ destination: "org.freedesktop.DBus", path: "/org/freedesktop/DBus", interface: "org.freedesktop.DBus", member: "GetNameOwner", signature: "s", body: [N.destination] })
|
|
458
|
+
.then((r) => String(r.body[0] ?? "") || null, () => null)
|
|
459
|
+
.finally(() => { this.lookup = null; });
|
|
460
|
+
return this.lookup.then((o) => {
|
|
461
|
+
if (o && this.owner && o !== this.owner)
|
|
462
|
+
this.serverChanged(o);
|
|
463
|
+
else if (o)
|
|
464
|
+
this.owner = o;
|
|
465
|
+
return !!o && sender === o;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
onServerChange(h) { this.serverChangeHandlers.push(h); }
|
|
399
469
|
async capabilities() {
|
|
400
470
|
const r = await this.bus.call({ ...N, member: "GetCapabilities" });
|
|
401
471
|
return r.body[0] ?? [];
|
|
402
472
|
}
|
|
473
|
+
async serverInfo() {
|
|
474
|
+
const r = await this.bus.call({ ...N, member: "GetServerInformation" });
|
|
475
|
+
const [name = "", vendor = "", version = "", spec = ""] = r.body.map(String);
|
|
476
|
+
return { name, vendor, version, spec };
|
|
477
|
+
}
|
|
403
478
|
async notify(n) {
|
|
404
479
|
const hints = [["urgency", { sig: "y", value: n.urgency ?? 1 }]];
|
|
405
480
|
if (n.urgency === 2)
|
|
406
481
|
hints.push(["resident", { sig: "b", value: true }]);
|
|
482
|
+
for (const h of n.hints ?? [])
|
|
483
|
+
if (!hints.some(([k]) => k === h[0]))
|
|
484
|
+
hints.push(h);
|
|
407
485
|
const r = await this.bus.call({ ...N, member: "Notify", signature: "susssasa{sv}i", body: [
|
|
408
486
|
n.appName ?? "Pantheon", n.replacesId ?? 0, n.icon ?? "", n.summary, n.body ?? "",
|
|
409
487
|
(n.actions ?? []).flat(), hints, n.timeoutMs ?? -1,
|
|
@@ -413,6 +491,7 @@ export class Notifications {
|
|
|
413
491
|
async close(id) { await this.bus.call({ ...N, member: "CloseNotification", signature: "u", body: [id] }).catch(() => { }); }
|
|
414
492
|
onAction(h) { this.actionHandlers.push(h); }
|
|
415
493
|
onClosed(h) { this.closedHandlers.push(h); }
|
|
494
|
+
onDisconnect(h) { this.bus.onClose(h); }
|
|
416
495
|
disconnect() { this.bus.close(); }
|
|
417
496
|
}
|
|
418
497
|
//# sourceMappingURL=dbus.js.map
|
package/dist/doctor.js
CHANGED
|
@@ -12,6 +12,7 @@ import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
|
12
12
|
import { homedir, platform } from "node:os";
|
|
13
13
|
import { dirname, join, resolve, sep } from "node:path";
|
|
14
14
|
import { expectedLauncher, managedLayout, runtimeNodeFit, runtimeVerdict } from "./managed.js";
|
|
15
|
+
import { compareVersions } from "./installer.js";
|
|
15
16
|
import { PKG_NAME } from "./upgrade.js";
|
|
16
17
|
import { PantheonClient } from "./client.js";
|
|
17
18
|
import { load, resolveFor, loadAll, CONFIG_PATH } from "./config.js";
|
|
@@ -23,7 +24,8 @@ import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
|
23
24
|
import { agentById, chooseAgent, describeChoice, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
24
25
|
import { loginShellLookup, npmSpawn, onPathNamed, which, whichWindows } from "./which.js";
|
|
25
26
|
import { VERSION } from "./version.js";
|
|
26
|
-
import { serviceInstalled, servicesUnavailable } from "./service.js";
|
|
27
|
+
import { realSys, serviceInstalled, servicesUnavailable } from "./service.js";
|
|
28
|
+
import { windowsDoctorRows, windowsFacts } from "./win-host.js";
|
|
27
29
|
import { hasPrebuilt } from "./helpers-build.js";
|
|
28
30
|
import { traySourcePath, TRAY_PREBUILT } from "./tray.js";
|
|
29
31
|
import { hudSourcePath, HUD_PREBUILT } from "./notifier.js";
|
|
@@ -63,7 +65,7 @@ export function userScopeWiring(home = homedir(), present = onPathNamed, env = p
|
|
|
63
65
|
};
|
|
64
66
|
const server = (key) => (d) => !!d[key]?.["pantheon"];
|
|
65
67
|
const out = [];
|
|
66
|
-
if (present("claude")) {
|
|
68
|
+
if (present("claude") || existsSync(join(home, ".claude"))) {
|
|
67
69
|
const f = join(home, ".claude.json");
|
|
68
70
|
out.push({ agent: "Claude Code", file: f, state: json(f, server("mcpServers")) });
|
|
69
71
|
}
|
|
@@ -176,8 +178,13 @@ export async function doctor(projectDir) {
|
|
|
176
178
|
if (host.ephemeral) {
|
|
177
179
|
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"}`);
|
|
178
180
|
}
|
|
181
|
+
else if (host.platform === "win32") {
|
|
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
|
+
for (const r of windowsDoctorRows(windowsFacts(realSys().exec)))
|
|
184
|
+
add(r.level, r.name, r.detail, r.fix, r.share);
|
|
185
|
+
}
|
|
179
186
|
else {
|
|
180
|
-
add("pass", "machine", `${platformName(host.platform)}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts here yet; ask your agent for pantheon_status, or run pantheon brief, to see what needs you"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
|
|
187
|
+
add("pass", "machine", `${platformName(host.platform)}${host.desktop ? (host.platform === "linux" ? " — desktop alerts can run here (the menu bar is macOS-only; pantheon board is the view)" : " — menu bar and desktop alerts can run here") : " — no menu bar or desktop alerts here yet; ask your agent for pantheon_status, or run pantheon brief, to see what needs you"}`, undefined, `${host.platform}${host.desktop ? ", desktop" : ""}`);
|
|
181
188
|
}
|
|
182
189
|
let cfg = null;
|
|
183
190
|
try {
|
|
@@ -386,12 +393,19 @@ export async function doctor(projectDir) {
|
|
|
386
393
|
add("pass", "relay", `reachable at ${health}`, undefined, `reachable (${where})`);
|
|
387
394
|
}
|
|
388
395
|
if (body.ok || body.storage) {
|
|
389
|
-
|
|
396
|
+
const held = typeof body.upgrades_held_at === "string" ? body.upgrades_held_at : null;
|
|
397
|
+
if (held && body.pkg === held && VERSION !== held && compareVersions(VERSION, held) > 0) {
|
|
398
|
+
add("pass", "version", `${VERSION}; the relay is holding upgrades at ${held} for now, so this machine stays where it is`, undefined, `${VERSION}, upgrades held at ${held}`);
|
|
399
|
+
}
|
|
400
|
+
else if (body.pkg && body.pkg !== VERSION) {
|
|
390
401
|
add("warn", "version", `this machine is ${VERSION}, the relay is ${body.pkg}`, "run `pantheon upgrade` on the older side — features land on both halves of the wire, and the protocol version cannot tell you they are missing", `this machine ${VERSION}, relay ${body.pkg}`);
|
|
391
402
|
}
|
|
392
403
|
else if (body.pkg) {
|
|
393
404
|
add("pass", "version", `${VERSION}, matching the relay`, undefined, `${VERSION}, matching the relay`);
|
|
394
405
|
}
|
|
406
|
+
else if (typeof body.relay_pkg === "string") {
|
|
407
|
+
add("pass", "version", `${VERSION}; the relay runs ${body.relay_pkg}, a prerelease, and tells no machine to upgrade`, undefined, `relay prerelease ${body.relay_pkg}`);
|
|
408
|
+
}
|
|
395
409
|
else {
|
|
396
410
|
add("warn", "version", `this machine is ${VERSION}; the relay is too old to report its own`, "update the relay host, then re-run this", `this machine ${VERSION}; relay too old to say`);
|
|
397
411
|
}
|
|
@@ -545,7 +559,24 @@ export async function doctor(projectDir) {
|
|
|
545
559
|
add("warn", "sandbox", `${missing.join(" and ")} not found — Claude Code's sandbox cannot start, so every woken command run will be refused`, "install them: sudo apt install bubblewrap socat (Debian/Ubuntu) or sudo dnf install bubblewrap socat (Fedora)", `sandbox tools missing: ${missing.join(", ")}`);
|
|
546
560
|
}
|
|
547
561
|
}
|
|
548
|
-
if (host.
|
|
562
|
+
if (host.platform === "linux" && !host.ephemeral) {
|
|
563
|
+
const { probeLinuxAlerts } = await import("./notice-linux.js");
|
|
564
|
+
const probe = await probeLinuxAlerts().catch((e) => ({ mode: "none", line: `none — ${e.message.split("\n")[0]}` }));
|
|
565
|
+
const installedN = serviceInstalled("notifier");
|
|
566
|
+
if (probe.mode === "none") {
|
|
567
|
+
add(host.desktop || installedN ? "warn" : "pass", "alerts", probe.line, host.desktop || installedN ? "decisions still wait in pantheon board --watch and pantheon brief; on a desktop, a notification server (GNOME Shell, Plasma, dunst, mako) shows them" : undefined, "none");
|
|
568
|
+
}
|
|
569
|
+
else if (!installedN) {
|
|
570
|
+
add("warn", "alerts", `not installed — this desktop would show them as ${probe.line}`, "pantheon service install notifier (--no-notifications at setup also leaves it off; that is fine if it was your choice)", "not installed");
|
|
571
|
+
}
|
|
572
|
+
else if (!notifierProc().running) {
|
|
573
|
+
add("warn", "alerts", "installed, but not running — nothing on this desktop will show you a teammate's message", "pantheon service restart notifier (journalctl --user -u pantheon-notifier says why it stopped)", "installed, not running");
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
add("pass", "alerts", probe.line, undefined, probe.mode);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (host.desktop && host.platform === "darwin") {
|
|
549
580
|
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; ";
|
|
550
581
|
if (!serviceInstalled("menubar")) {
|
|
551
582
|
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");
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { homedir, platform, tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
function isOneDriveRoot(dir, f, p) {
|
|
5
|
+
if ((f.oneDriveRoots ?? []).some((r) => same(dir, r, p)))
|
|
6
|
+
return true;
|
|
7
|
+
const d = p.resolve(dir);
|
|
8
|
+
return same(p.dirname(d), f.home, p) && /^OneDrive( - .+)?$/i.test(p.basename(d));
|
|
9
|
+
}
|
|
10
|
+
export const TEMP_FOLDER_ADVICE = "open your agent in your project folder, or bind one with pantheon use <team> --here from there";
|
|
11
|
+
export function isInside(child, parent, p = path) {
|
|
12
|
+
if (!child || !parent)
|
|
13
|
+
return false;
|
|
14
|
+
const rel = p.relative(p.resolve(parent), p.resolve(child));
|
|
15
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${p.sep}`) && !p.isAbsolute(rel));
|
|
16
|
+
}
|
|
17
|
+
const same = (a, b, p) => p.relative(p.resolve(a), p.resolve(b)) === "";
|
|
18
|
+
export function folderRefusal(here, f, p = path) {
|
|
19
|
+
const dir = p.resolve(here);
|
|
20
|
+
if (p.dirname(dir) === dir)
|
|
21
|
+
return { kind: "root", why: "this is the root of the disk" };
|
|
22
|
+
if (same(dir, f.home, p))
|
|
23
|
+
return { kind: "home", why: "this is your home folder" };
|
|
24
|
+
if (isInside(f.home, dir, p))
|
|
25
|
+
return { kind: "above-home", why: "this folder holds your home folder" };
|
|
26
|
+
if (["Downloads", "Desktop", "Documents", "Library", "tmp"].some((n) => same(dir, p.join(f.home, n), p))) {
|
|
27
|
+
return { kind: "catch-all", why: `this is your ${p.basename(dir)} folder, which holds unrelated things` };
|
|
28
|
+
}
|
|
29
|
+
if (p.sep === "\\") {
|
|
30
|
+
if (isOneDriveRoot(dir, f, p))
|
|
31
|
+
return { kind: "catch-all", why: `this is your OneDrive folder, which holds everything you sync` };
|
|
32
|
+
if (/^(Desktop|Documents|Pictures)$/i.test(p.basename(dir)) && isOneDriveRoot(p.dirname(dir), f, p)) {
|
|
33
|
+
return { kind: "catch-all", why: `this is your ${p.basename(dir)} folder (in OneDrive), which holds unrelated things` };
|
|
34
|
+
}
|
|
35
|
+
const appData = p.join(f.home, "AppData");
|
|
36
|
+
if (same(dir, appData, p) || ["Local", "LocalLow", "Roaming"].some((n) => same(dir, p.join(appData, n), p))) {
|
|
37
|
+
return { kind: "catch-all", why: `this is ${p.basename(dir) === "AppData" ? "your AppData folder" : `AppData\\${p.basename(dir)}`}, where every program keeps its own data` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const parts = dir.split(p.sep === "\\" ? /[\\/]+/ : /\/+/);
|
|
41
|
+
const scratch = parts.findIndex((s, i) => i > 0 && s.toLowerCase() === "scratch-workspaces" && parts[i - 1].toLowerCase() === "claude");
|
|
42
|
+
if (scratch > 0 || f.appDirs.some((a) => isInside(dir, a, p))) {
|
|
43
|
+
return { kind: "app-scratch", why: `this is a scratch workspace of the Claude app, a temporary folder that goes away; ${TEMP_FOLDER_ADVICE}` };
|
|
44
|
+
}
|
|
45
|
+
const root = f.tempRoots.find((r) => isInside(dir, r, p) && !isInside(f.home, r, p));
|
|
46
|
+
if (root)
|
|
47
|
+
return { kind: "temporary", why: `this is a temporary folder (${root}) that goes away; ${TEMP_FOLDER_ADVICE}` };
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const real = (d) => { try {
|
|
51
|
+
return realpathSync.native(d);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return path.resolve(d);
|
|
55
|
+
} };
|
|
56
|
+
export function folderFacts(env = process.env, os = platform()) {
|
|
57
|
+
const home = real(homedir());
|
|
58
|
+
const roots = new Set();
|
|
59
|
+
const add = (set, d) => { if (d) {
|
|
60
|
+
set.add(path.resolve(d));
|
|
61
|
+
set.add(real(d));
|
|
62
|
+
} };
|
|
63
|
+
add(roots, tmpdir());
|
|
64
|
+
for (const k of ["TMPDIR", "TEMP", "TMP"])
|
|
65
|
+
add(roots, env[k]);
|
|
66
|
+
if (env["LOCALAPPDATA"])
|
|
67
|
+
add(roots, path.join(env["LOCALAPPDATA"], "Temp"));
|
|
68
|
+
if (os !== "win32")
|
|
69
|
+
for (const d of ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", "/var/folders", "/private/var/folders"])
|
|
70
|
+
add(roots, d);
|
|
71
|
+
const apps = new Set();
|
|
72
|
+
if (os === "darwin")
|
|
73
|
+
add(apps, path.join(home, "Library", "Application Support", "Claude"));
|
|
74
|
+
if (os === "win32") {
|
|
75
|
+
add(apps, path.join(env["APPDATA"] ?? path.join(home, "AppData", "Roaming"), "Claude"));
|
|
76
|
+
}
|
|
77
|
+
if (os === "linux")
|
|
78
|
+
add(apps, path.join(env["XDG_CONFIG_HOME"] ?? path.join(home, ".config"), "Claude"));
|
|
79
|
+
const oneDrive = new Set();
|
|
80
|
+
if (os === "win32")
|
|
81
|
+
for (const k of ["OneDrive", "OneDriveCommercial", "OneDriveConsumer"])
|
|
82
|
+
add(oneDrive, env[k]);
|
|
83
|
+
return { home, tempRoots: [...roots], appDirs: [...apps], ...(oneDrive.size ? { oneDriveRoots: [...oneDrive] } : {}) };
|
|
84
|
+
}
|
|
85
|
+
export function folderRefusalHere(dir) {
|
|
86
|
+
return folderRefusal(real(dir), folderFacts());
|
|
87
|
+
}
|
|
88
|
+
export const isTemporaryRefusal = (r) => r?.kind === "temporary" || r?.kind === "app-scratch";
|
|
89
|
+
//# sourceMappingURL=folder-guard.js.map
|
package/dist/git-safe.js
CHANGED
|
@@ -77,7 +77,7 @@ const hasGitLfs = () => {
|
|
|
77
77
|
if (lfsBinary !== null)
|
|
78
78
|
return lfsBinary;
|
|
79
79
|
try {
|
|
80
|
-
execFileSync("git-lfs", ["version"], { stdio: "ignore", timeout: 10_000 });
|
|
80
|
+
execFileSync("git-lfs", ["version"], { windowsHide: true, stdio: "ignore", timeout: 10_000 });
|
|
81
81
|
lfsBinary = true;
|
|
82
82
|
}
|
|
83
83
|
catch {
|
|
@@ -94,7 +94,7 @@ export function lfsInUse(cwd) {
|
|
|
94
94
|
}
|
|
95
95
|
catch { }
|
|
96
96
|
try {
|
|
97
|
-
return execFileSync("git", ["-c", "core.fsmonitor=false", "-c", `core.hooksPath=${noHooksDir()}`, "config", "--get", "filter.lfs.clean"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim().length > 0;
|
|
97
|
+
return execFileSync("git", ["-c", "core.fsmonitor=false", "-c", `core.hooksPath=${noHooksDir()}`, "config", "--get", "filter.lfs.clean"], { cwd, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim().length > 0;
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
100
100
|
return false;
|
|
@@ -217,7 +217,7 @@ function inspect(worktree, a, snapshot = false) {
|
|
|
217
217
|
}
|
|
218
218
|
function commonOf(project) {
|
|
219
219
|
try {
|
|
220
|
-
const out = execFileSync("git", [...gitHarden(), "rev-parse", "--git-common-dir"], { cwd: project, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1" }, timeout: 10_000 }).trim();
|
|
220
|
+
const out = execFileSync("git", [...gitHarden(), "rev-parse", "--git-common-dir"], { cwd: project, encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "ignore"], env: { ...process.env, GIT_CONFIG_NOSYSTEM: "1" }, timeout: 10_000 }).trim();
|
|
221
221
|
return real(resolve(project, out));
|
|
222
222
|
}
|
|
223
223
|
catch {
|
package/dist/host-env.js
CHANGED
|
@@ -34,6 +34,8 @@ export function hostEnv(env = process.env, plat = process.platform, exists = exi
|
|
|
34
34
|
if (plat === "darwin") {
|
|
35
35
|
return { platform: plat, kind: "local", evidence: [], ephemeral: false, desktop: true };
|
|
36
36
|
}
|
|
37
|
+
if (plat === "win32")
|
|
38
|
+
return windowsHostEnv(env);
|
|
37
39
|
const evidence = [];
|
|
38
40
|
let kind = "local";
|
|
39
41
|
for (const [name, label] of CLOUD_VARS) {
|
|
@@ -82,9 +84,43 @@ export function hostEnv(env = process.env, plat = process.platform, exists = exi
|
|
|
82
84
|
kind,
|
|
83
85
|
evidence,
|
|
84
86
|
ephemeral: kind !== "local",
|
|
85
|
-
desktop:
|
|
87
|
+
desktop: plat === "linux" && kind === "local" && linuxGraphicalSession(env, wsl),
|
|
86
88
|
};
|
|
87
89
|
}
|
|
90
|
+
export function windowsHostEnv(env = process.env) {
|
|
91
|
+
const evidence = [];
|
|
92
|
+
let kind = "local";
|
|
93
|
+
for (const [name, label] of CLOUD_VARS) {
|
|
94
|
+
if (env[name]) {
|
|
95
|
+
evidence.push(label);
|
|
96
|
+
kind = "cloud-workspace";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [name, label] of CONTAINER_VARS)
|
|
100
|
+
if (env[name])
|
|
101
|
+
evidence.push(label);
|
|
102
|
+
const user = (env.USERNAME ?? "").toLowerCase();
|
|
103
|
+
if (user === "containeradministrator" || user === "containeruser")
|
|
104
|
+
evidence.push(`the user is ${env.USERNAME}, a Windows container's account`);
|
|
105
|
+
if (evidence.length && kind === "local")
|
|
106
|
+
kind = "container";
|
|
107
|
+
const ssh = !!(env.SSH_CONNECTION || env.SSH_CLIENT);
|
|
108
|
+
return { platform: "win32", kind, evidence, ephemeral: kind !== "local", desktop: kind === "local" && !ssh };
|
|
109
|
+
}
|
|
110
|
+
export function linuxGraphicalSession(env, wsl = !!env.WSL_DISTRO_NAME) {
|
|
111
|
+
if (wsl)
|
|
112
|
+
return false;
|
|
113
|
+
const display = env.DISPLAY ?? "";
|
|
114
|
+
const forwarded = /^[^:]+:/.test(display);
|
|
115
|
+
const ssh = !!(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
|
|
116
|
+
if (env.WAYLAND_DISPLAY)
|
|
117
|
+
return true;
|
|
118
|
+
if (ssh)
|
|
119
|
+
return !!display && !forwarded && env.XDG_SESSION_TYPE !== "tty";
|
|
120
|
+
if (display)
|
|
121
|
+
return true;
|
|
122
|
+
return env.XDG_SESSION_TYPE === "x11" || env.XDG_SESSION_TYPE === "wayland";
|
|
123
|
+
}
|
|
88
124
|
export const platformName = (p) => p === "win32" ? "Windows" : p === "darwin" ? "macOS" : p === "linux" ? "Linux" : p;
|
|
89
125
|
export function ephemeralWarning(host) {
|
|
90
126
|
return `this looks like ${host.kind === "container" ? "a container" : "a cloud workspace"}, not the computer you work on `
|
|
@@ -93,11 +129,21 @@ export function ephemeralWarning(host) {
|
|
|
93
129
|
+ `the session. Your account and team are fine — run the same setup from a terminal on your own computer to finish it.`;
|
|
94
130
|
}
|
|
95
131
|
export function desktopUnavailable(host, what) {
|
|
132
|
+
if (what === "the menu bar" && host.platform === "linux" && !host.ephemeral)
|
|
133
|
+
return "not available — the menu bar is macOS-only; on Linux, pantheon board shows what needs you";
|
|
96
134
|
if (host.desktop)
|
|
97
135
|
return null;
|
|
136
|
+
if (host.platform === "linux" && !host.ephemeral && what === "desktop alerts") {
|
|
137
|
+
return "not available — desktop alerts need a graphical login session, and this Linux shell has none (SSH or headless); pantheon board --watch shows what needs you";
|
|
138
|
+
}
|
|
98
139
|
if (host.ephemeral) {
|
|
99
140
|
return `not available — ${what} ${what === "desktop alerts" ? "need" : "needs"} your own computer, and this is ${host.kind === "container" ? "a container" : "a cloud workspace"} reporting ${platformName(host.platform)}`;
|
|
100
141
|
}
|
|
142
|
+
if (host.platform === "win32") {
|
|
143
|
+
return what === "the menu bar"
|
|
144
|
+
? "not available — the menu bar is macOS's; on Windows the Pantheon tray icon runs with the background host, from a session at the desktop"
|
|
145
|
+
: "not available — desktop alerts need the Windows desktop, and this is a remote shell (SSH); set Pantheon up from a session at the desktop";
|
|
146
|
+
}
|
|
101
147
|
return `not available — ${what} ${host.platform === "darwin" ? "could not be built here" : `${what === "desktop alerts" ? "are" : "is"} macOS-only, and this machine is ${platformName(host.platform)}`}`;
|
|
102
148
|
}
|
|
103
149
|
//# sourceMappingURL=host-env.js.map
|
package/dist/install.js
CHANGED
|
@@ -12,6 +12,8 @@ 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
14
|
import { childNodeFlags, managedLayout, managedLauncher } from "./managed.js";
|
|
15
|
+
import { claudeHomeConflict, claudeJsonFile, claudeUserEntryShape, mergeClaudeUserEntry, removeClaudeUserEntry } from "./claude-config.js";
|
|
16
|
+
import { inWokenRun } from "./worker-policy.js";
|
|
15
17
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
export function onPath() {
|
|
17
19
|
return onPathNamed("pantheon");
|
|
@@ -255,11 +257,38 @@ export function claudeAddFailed(e, command) {
|
|
|
255
257
|
const why = said || (err.message ?? "failed").split("\n")[0];
|
|
256
258
|
return `failed: claude mcp add did not work (${why}) — fix: run \`${command}\` yourself to see why, then pantheon install --user`;
|
|
257
259
|
}
|
|
258
|
-
|
|
260
|
+
export const CLAUDE_DIRECT_NOTE = "written to ~/.claude.json directly (no claude CLI here) — the Claude desktop app and Claude Code read it";
|
|
261
|
+
export function claudeUserScope(entry, present = true, claudeBin = which("claude"), home = homedir(), direct = false) {
|
|
259
262
|
const tool = "claude code";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
if (!claudeBin) {
|
|
264
|
+
if (!present)
|
|
265
|
+
return { tool, where: "-", status: "skipped (claude not on PATH)" };
|
|
266
|
+
const file = claudeJsonFile(home);
|
|
267
|
+
const conflict = claudeHomeConflict();
|
|
268
|
+
if (conflict)
|
|
269
|
+
return { tool, where: "-", status: `skipped (claude not on PATH) — ${conflict}` };
|
|
270
|
+
if (!direct || inWokenRun()) {
|
|
271
|
+
const there = (() => { try {
|
|
272
|
+
const d = JSON.parse(stripBom(readFileSync(file, "utf8")));
|
|
273
|
+
return d?.mcpServers?.["pantheon"];
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return undefined;
|
|
277
|
+
} })();
|
|
278
|
+
return there !== undefined && JSON.stringify(there) === JSON.stringify(claudeUserEntryShape(entry))
|
|
279
|
+
? { tool, where: file, status: `unchanged — ${CLAUDE_DIRECT_NOTE}` }
|
|
280
|
+
: { tool, where: "-", status: "skipped (claude not on PATH here) — run pantheon install --user yourself to write ~/.claude.json directly" };
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const r = mergeClaudeUserEntry(file, entry, isOurMcpEntry);
|
|
284
|
+
if (r === "not ours")
|
|
285
|
+
return { tool, where: file, status: `failed: ${file} already has an MCP server called "pantheon" that Pantheon did not write — rename or remove it, then run pantheon install --user` };
|
|
286
|
+
return { tool, where: file, status: `${r} — ${CLAUDE_DIRECT_NOTE}` };
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
return { tool, where: file, status: `failed: ${e.message}` };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
263
292
|
try {
|
|
264
293
|
const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "user"]);
|
|
265
294
|
const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "user", "--", entry.command, ...entry.args]);
|
|
@@ -277,8 +306,11 @@ function claudeUserScope(entry) {
|
|
|
277
306
|
function claudeLocalScope(entry, projectDir) {
|
|
278
307
|
const tool = "claude code";
|
|
279
308
|
const claudeBin = which("claude");
|
|
280
|
-
if (!claudeBin)
|
|
281
|
-
return
|
|
309
|
+
if (!claudeBin) {
|
|
310
|
+
return existsSync(join(homedir(), ".claude"))
|
|
311
|
+
? { tool, where: "-", status: "skipped (claude not on PATH) — the user-scope entry is in ~/.claude.json, but this folder's own entry can only be written by the claude CLI, and this repo's committed .mcp.json (which starts pantheon by a name Windows cannot run) may be read first; once claude is on PATH, run `pantheon install` here again" }
|
|
312
|
+
: { tool, where: "-", status: "skipped (claude not on PATH) — once Claude Code is installed, run `pantheon install` here again" };
|
|
313
|
+
}
|
|
282
314
|
try {
|
|
283
315
|
const remove = agentSpawn(claudeBin, ["mcp", "remove", "pantheon", "-s", "local"]);
|
|
284
316
|
const add = agentSpawn(claudeBin, ["mcp", "add", "pantheon", "-s", "local", "--", entry.command, ...entry.args]);
|
|
@@ -686,13 +718,13 @@ export function chosenApproval() {
|
|
|
686
718
|
return "approve";
|
|
687
719
|
}
|
|
688
720
|
}
|
|
689
|
-
export function installForUser(approval = chosenApproval(), present = agentPresence()) {
|
|
721
|
+
export function installForUser(approval = chosenApproval(), present = agentPresence(), opts = {}) {
|
|
690
722
|
const entry = userEntry();
|
|
691
723
|
const said = (s) => /^(granted|already granted)/.test(s)
|
|
692
724
|
? `${s} — ${approval === "ask-before-sending" ? "reads run without asking; send, open_goal and claim prompt" : `the ${PANTHEON_TOOL_NAMES.length} pantheon tools run without asking`}`
|
|
693
725
|
: s;
|
|
694
726
|
return [
|
|
695
|
-
claudeUserScope(entry), cursorUserScope(entry, present.cursor), codexUserScope(entry, approval, present.codex),
|
|
727
|
+
claudeUserScope(entry, present.claude, undefined, undefined, !!opts.direct), cursorUserScope(entry, present.cursor), codexUserScope(entry, approval, present.codex),
|
|
696
728
|
copilotUserScope(entry), vsCodeUserScope(entry), kiroUserScope(entry, approval),
|
|
697
729
|
present.claude
|
|
698
730
|
? { tool: "claude perms", where: claudeSettingsFile(), status: said(grantClaudeToolPermissions(undefined, approval)) }
|
|
@@ -702,6 +734,24 @@ export function installForUser(approval = chosenApproval(), present = agentPrese
|
|
|
702
734
|
: { tool: "cursor perms", where: "-", status: "skipped (Cursor not found on this machine)" },
|
|
703
735
|
];
|
|
704
736
|
}
|
|
737
|
+
export function agentAsksOnce(tools) {
|
|
738
|
+
const asks = [
|
|
739
|
+
[/^claude code$/i, "claude", `claude say yes once if it asks to trust this folder`],
|
|
740
|
+
[/^cursor$/i, "cursor", `cursor enable the "pantheon" MCP server if it asks`],
|
|
741
|
+
[/^codex$/i, "codex", `codex run /hooks inside codex once to approve the wake-up hook`],
|
|
742
|
+
[/^kiro$/i, "kiro", `kiro run kiro-cli chat --agent pantheon (or kiro-cli agent set-default pantheon) to keep working while a teammate waits; stay signed in — a woken run never starts Kiro signed out, since Kiro would open a browser`],
|
|
743
|
+
[/^copilot$/i, "copilot", `copilot say yes once when it asks to trust this folder; on a Copilot Business or Enterprise plan an org owner must have the "MCP servers in Copilot" policy on`],
|
|
744
|
+
];
|
|
745
|
+
const out = new Map();
|
|
746
|
+
for (const t of tools) {
|
|
747
|
+
if (/^(skipped|failed|could not)|unavailable|not found/i.test(t.status))
|
|
748
|
+
continue;
|
|
749
|
+
for (const [row, key, line] of asks)
|
|
750
|
+
if (row.test(t.tool))
|
|
751
|
+
out.set(key, line);
|
|
752
|
+
}
|
|
753
|
+
return [...out.values()];
|
|
754
|
+
}
|
|
705
755
|
export function isOurMcpEntry(e) {
|
|
706
756
|
if (!e || typeof e !== "object")
|
|
707
757
|
return false;
|
|
@@ -759,8 +809,24 @@ export function removeUserMcpEntries(deps = {}) {
|
|
|
759
809
|
if (os === "win32" && deps.project)
|
|
760
810
|
claudeRemove("local", `Claude Code's pantheon entry for ${deps.project} (local scope)`, deps.project);
|
|
761
811
|
}
|
|
762
|
-
else
|
|
763
|
-
|
|
812
|
+
else {
|
|
813
|
+
const file = claudeJsonFile(home);
|
|
814
|
+
const conflict = deps.home === undefined ? claudeHomeConflict() : null;
|
|
815
|
+
if (conflict)
|
|
816
|
+
left.push(`Claude Code's pantheon MCP server, if any — ${conflict}`);
|
|
817
|
+
else
|
|
818
|
+
try {
|
|
819
|
+
const r = removeClaudeUserEntry(file, isOurMcpEntry);
|
|
820
|
+
if (r === "removed")
|
|
821
|
+
removed.push(`Claude Code's pantheon MCP server (user scope, ${file})`);
|
|
822
|
+
else if (r === "not ours")
|
|
823
|
+
left.push(`an entry named "pantheon" in ${file} that Pantheon did not write — yours to keep or delete`);
|
|
824
|
+
}
|
|
825
|
+
catch (e) {
|
|
826
|
+
left.push(`Claude Code's pantheon MCP server (${e.message}) — delete the "pantheon" server under "mcpServers" in ${file} by hand, with Claude closed`);
|
|
827
|
+
}
|
|
828
|
+
if (os === "win32" && deps.project)
|
|
829
|
+
left.push(`any Claude Code entry for ${deps.project} (local scope) — claude is not on PATH here; once it is: claude mcp remove pantheon -s local, run in that folder`);
|
|
764
830
|
}
|
|
765
831
|
const files = [
|
|
766
832
|
["Cursor", join(home, ".cursor", "mcp.json"), "mcpServers"],
|
package/dist/installer.js
CHANGED
|
@@ -29,8 +29,9 @@ export function installerScript(name, dir = INSTALLER_DIR, version = VERSION) {
|
|
|
29
29
|
export const NPM_PACKAGE = "@join-pantheon/cli";
|
|
30
30
|
export const NPM_PACKUMENT_URL = "https://registry.npmjs.org/@join-pantheon%2fcli";
|
|
31
31
|
export const INSTALLER_MIN_CLI = "0.32.0";
|
|
32
|
+
export const isVersion = (v) => /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(v);
|
|
32
33
|
function parseVer(v) {
|
|
33
|
-
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(
|
|
34
|
+
const m = isVersion(v) ? /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v) : null;
|
|
34
35
|
return m ? { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]), pre: m[4] ?? null } : null;
|
|
35
36
|
}
|
|
36
37
|
export function compareVersions(a, b) {
|