@join-pantheon/cli 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/cli.js +116 -26
- package/dist/client.js +42 -0
- package/dist/config.js +15 -3
- package/dist/doctor.js +13 -0
- package/dist/email.js +47 -23
- package/dist/host-env.js +57 -0
- package/dist/notice.js +157 -0
- package/dist/notifier.js +25 -14
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +15 -4
- package/dist/relay.js +180 -74
- package/dist/roster-file.js +19 -0
- package/dist/service.js +25 -2
- package/dist/setup-guide.js +4 -2
- package/dist/tray.js +10 -4
- package/dist/worker.js +7 -11
- package/helper/macos/main.swift +231 -65
- package/helper/macos/menubar.swift +110 -43
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,11 @@ pantheon start
|
|
|
57
57
|
pantheon join pantheons://relay.joinpantheon.network/acme#C1qhJfdHBaPjuG3jF48vcqrP
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
On the hosted relay, `pantheon start` and `pantheon invite` also print the same invite as a link,
|
|
61
|
+
`https://www.joinpantheon.network/join/acme#C1qhJfdHBaPjuG3jF48vcqrP`. Pasted into a chat it
|
|
62
|
+
unfurls with the team's name and hands the recipient the line for their agent. The token stays in the
|
|
63
|
+
URL fragment, which browsers never send to any server. `pantheon join` accepts either form.
|
|
64
|
+
|
|
60
65
|
Signed in, `pantheon start` puts your space on the hosted relay, so that link works from any
|
|
61
66
|
network — no tunnel, no VPN. The space is named after the folder you run it from (your repo,
|
|
62
67
|
usually); if another team already took that name, start picks `<name>-<you>` and says so. Choose
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
11
|
import { basename, dirname, join, resolve } from "node:path";
|
|
12
12
|
import { PantheonClient } from "./client.js";
|
|
13
|
-
import { fromInvite, load, save, toInvite, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
13
|
+
import { fromInvite, load, save, toInvite, toJoinLink, resolveFor, loadAll, repoInfo, bindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
|
|
14
14
|
import { fmtLimit } from "./plans.js";
|
|
15
15
|
import { validEmail, validUsername } from "./accounts.js";
|
|
16
16
|
import { install, launcher } from "./install.js";
|
|
@@ -18,6 +18,7 @@ import { Relay } from "./relay.js";
|
|
|
18
18
|
import { runMcp } from "./mcp.js";
|
|
19
19
|
import { Worker } from "./worker.js";
|
|
20
20
|
import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
|
|
21
|
+
import { desktopUnavailable, hostEnv } from "./host-env.js";
|
|
21
22
|
import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
|
|
22
23
|
import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
|
|
23
24
|
import { selectActs, formatAct, resolveRef } from "./history-view.js";
|
|
@@ -375,8 +376,9 @@ ${B("first time — verify your email once")}
|
|
|
375
376
|
|
|
376
377
|
${B("getting going — two commands, from your project's repo if you have one")}
|
|
377
378
|
pantheon start you: sets up this whole machine + one line to send your teammate
|
|
378
|
-
pantheon join <
|
|
379
|
-
pantheon invite another single-use
|
|
379
|
+
pantheon join <invite> them: the link (or the line) sets up their whole machine
|
|
380
|
+
pantheon invite another single-use invite, for a third person or a second machine
|
|
381
|
+
(--list shows the open ones; --revoke <token|all> cancels)
|
|
380
382
|
|
|
381
383
|
Both take the setup answers as flags (--name, --title, --wake, --commands, … — pantheon setup
|
|
382
384
|
--questions lists them), so an agent sets a person up in one command. No repo? Messages, questions
|
|
@@ -510,12 +512,21 @@ async function main() {
|
|
|
510
512
|
process.exit(1);
|
|
511
513
|
}
|
|
512
514
|
if (r.invite) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
+
const link = toJoinLink(fromInvite(r.invite));
|
|
516
|
+
if (link) {
|
|
517
|
+
console.error(`\n${B("Send your teammate this link — it unfurls in chat, and it sets up their whole machine:")}\n`);
|
|
518
|
+
console.error(` ${C(link)}\n`);
|
|
519
|
+
console.error(D(` The same invite as a command, for pasting straight into an agent:`));
|
|
520
|
+
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.error(`\n${B("Send your teammate this one line — it sets up their whole machine:")}\n`);
|
|
524
|
+
console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
|
|
525
|
+
}
|
|
515
526
|
console.error(D(` From their project's repo if they have one — a repo is only needed to hand over code or to be woken. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
|
|
516
527
|
console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
|
|
517
528
|
console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
|
|
518
|
-
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${r.invite}`)}`);
|
|
529
|
+
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? r.invite}`)}`);
|
|
519
530
|
}
|
|
520
531
|
if (!r.reach.shareable && r.reach.hint) {
|
|
521
532
|
console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
|
|
@@ -535,13 +546,43 @@ async function main() {
|
|
|
535
546
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
536
547
|
await client.connect().catch((e) => die(client.pending ?? e.message));
|
|
537
548
|
await new Promise((r) => setTimeout(r, 250));
|
|
549
|
+
const revokeAt = argv.indexOf("--revoke");
|
|
550
|
+
const revokeArg = revokeAt >= 0 ? (argv[revokeAt + 1] && !argv[revokeAt + 1].startsWith("--") ? argv[revokeAt + 1] : "all") : undefined;
|
|
551
|
+
if (argv.includes("--list") || revokeArg !== undefined) {
|
|
552
|
+
const open = await client.listInvites().catch((e) => die(e.message));
|
|
553
|
+
if (revokeArg !== undefined) {
|
|
554
|
+
const targets = revokeArg === "all" ? open.filter((i) => i.mine) : open.filter((i) => i.token === revokeArg || i.token.endsWith(revokeArg.replace(/^…/, "")));
|
|
555
|
+
if (!targets.length) {
|
|
556
|
+
client.close();
|
|
557
|
+
die(revokeArg === "all" ? "no open invites of yours to revoke" : `no open invite matches "${revokeArg}" — pantheon invite --list shows them`);
|
|
558
|
+
}
|
|
559
|
+
for (const i of targets) {
|
|
560
|
+
await client.revokeInvite(i.token).catch((e) => { client.close(); return die(e.message); });
|
|
561
|
+
console.log(`revoked …${i.token.slice(-6)} (minted by ${i.created_by}, would have expired ${new Date(i.expires_at).toLocaleString()})`);
|
|
562
|
+
}
|
|
563
|
+
client.close();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!open.length)
|
|
567
|
+
console.log(D("no open invites"));
|
|
568
|
+
for (const i of open)
|
|
569
|
+
console.log(`${i.mine ? C(i.token) : D(i.token)} by ${i.created_by} expires ${new Date(i.expires_at).toLocaleString()}${i.may_wake ? " may wake" : ""}${i.mine ? "" : D(" (not yours — masked)")}`);
|
|
570
|
+
if (open.length)
|
|
571
|
+
console.log(D(`\nrevoke one: pantheon invite --revoke <token or its last 6 characters> · all of yours: pantheon invite --revoke all`));
|
|
572
|
+
client.close();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
538
575
|
const ttl = Number(flag("ttl", "1440"));
|
|
539
576
|
const mayWake = argv.includes("--may-wake") ? true : argv.includes("--no-wake") ? false : wakeDefault(loadProfile(cfg));
|
|
540
577
|
const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
|
|
541
|
-
|
|
578
|
+
const raw = toInvite({ ...cfg, token: inv.url });
|
|
579
|
+
const link = toJoinLink({ relay: cfg.relay, space: cfg.space, token: inv.url });
|
|
580
|
+
console.log(`pantheon join ${raw}`);
|
|
581
|
+
if (link)
|
|
582
|
+
console.error(`\n${D("Or send this — it unfurls in chat with the team's name, and pantheon join accepts it too:")}\n ${C(link)}`);
|
|
542
583
|
console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
|
|
543
584
|
console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
|
|
544
|
-
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${
|
|
585
|
+
console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${link ?? raw}`)}`);
|
|
545
586
|
client.close();
|
|
546
587
|
return;
|
|
547
588
|
}
|
|
@@ -616,7 +657,7 @@ async function main() {
|
|
|
616
657
|
return;
|
|
617
658
|
}
|
|
618
659
|
case "join": {
|
|
619
|
-
const invite = argv[1] ?? die("usage: pantheon join
|
|
660
|
+
const invite = argv[1] ?? die("usage: pantheon join <invite> (pantheons://host/space#token, or https://www.joinpantheon.network/join/<team>#<token>) [setup answers as flags — pantheon setup --questions]");
|
|
620
661
|
const setupFlags = parseSetupFlags(argv);
|
|
621
662
|
if (setupFlags.errors.length)
|
|
622
663
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
@@ -674,8 +715,8 @@ async function main() {
|
|
|
674
715
|
console.error(`${B("setting this machine up")} ${D("(user scope)")}\n`);
|
|
675
716
|
const wired = wireUp({
|
|
676
717
|
hook: !argv.includes("--no-hook"),
|
|
677
|
-
notifications: !argv.includes("--no-notifications")
|
|
678
|
-
menubar: !argv.includes("--no-menubar")
|
|
718
|
+
notifications: !argv.includes("--no-notifications"),
|
|
719
|
+
menubar: !argv.includes("--no-menubar"),
|
|
679
720
|
unattended: wp.project,
|
|
680
721
|
workerHow: wp.how,
|
|
681
722
|
});
|
|
@@ -1088,7 +1129,8 @@ async function main() {
|
|
|
1088
1129
|
console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
|
|
1089
1130
|
}
|
|
1090
1131
|
else {
|
|
1091
|
-
|
|
1132
|
+
const why = desktopUnavailable(hostEnv(), "the menu bar");
|
|
1133
|
+
console.log(` menu bar ${D("not running")} ${D(why ?? "pantheon service install menubar — status and pause/resume in the menu bar")}`);
|
|
1092
1134
|
}
|
|
1093
1135
|
if (notifier.running) {
|
|
1094
1136
|
console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" ? `service ${notifier.label}` : "started by hand"} pid ${notifier.pid}`);
|
|
@@ -2066,6 +2108,60 @@ async function main() {
|
|
|
2066
2108
|
if (id === "all")
|
|
2067
2109
|
id = undefined;
|
|
2068
2110
|
}
|
|
2111
|
+
let hud = null;
|
|
2112
|
+
if (argv.includes("--queued")) {
|
|
2113
|
+
const f = join(CONFIG_DIR, "bin", ".hud-action.json");
|
|
2114
|
+
try {
|
|
2115
|
+
hud = JSON.parse(readFileSync(f, "utf8"));
|
|
2116
|
+
rmSync(f, { force: true });
|
|
2117
|
+
}
|
|
2118
|
+
catch {
|
|
2119
|
+
hud = null;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
if (hud?.action === "details") {
|
|
2123
|
+
const acts = await fetchHistory(cfg).catch(() => []);
|
|
2124
|
+
const a = hud.act ? acts.find((x) => x.id === hud.act) : undefined;
|
|
2125
|
+
if (a) {
|
|
2126
|
+
console.log(formatAct(a));
|
|
2127
|
+
if (a.ref) {
|
|
2128
|
+
const r = acts.find((x) => x.id === a.ref);
|
|
2129
|
+
if (r)
|
|
2130
|
+
console.log(D("\nin reply to:\n") + formatAct(r, { short: true }));
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
else if (!hud.decision)
|
|
2134
|
+
console.log(D("that act is no longer in what the relay holds"));
|
|
2135
|
+
if (hud.decision) {
|
|
2136
|
+
const c2 = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2137
|
+
await c2.connect().catch((e) => die(e.message));
|
|
2138
|
+
const o = c2.obligations.find((x) => x.id === hud.decision);
|
|
2139
|
+
c2.close();
|
|
2140
|
+
if (o)
|
|
2141
|
+
console.log(`\n${B("Decision required")} ${o.id}\n ${o.body}\n\nAnswer: ${C(`pantheon approvals answer ${o.id} 'your decision and rationale'`)}\n${D(`or have your agent explain it: pantheon explain ${o.id}`)}`);
|
|
2142
|
+
else
|
|
2143
|
+
console.log(D("\nthat decision is no longer waiting on you"));
|
|
2144
|
+
}
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
if (hud?.action === "open") {
|
|
2148
|
+
const p = loadProfile(cfg);
|
|
2149
|
+
if (hud.tool && isAgentSurface(hud.tool) && p.agent.primary_surface !== hud.tool) {
|
|
2150
|
+
saveProfile({ ...p, agent: { ...p.agent, primary_surface: hud.tool }, updated_at: Date.now() });
|
|
2151
|
+
console.error(D(`${hud.tool} is now where your decisions open`));
|
|
2152
|
+
}
|
|
2153
|
+
if (hud.prompt) {
|
|
2154
|
+
const agent = pickExplainAgent(hud.tool ?? p.agent.primary_surface, detectAvailable());
|
|
2155
|
+
if (!agent)
|
|
2156
|
+
die("no agent CLI found on this machine (claude, cursor-agent, or codex)");
|
|
2157
|
+
console.error(D(`opening ${agent.id}…`));
|
|
2158
|
+
const { spawnSync } = await import("node:child_process");
|
|
2159
|
+
spawnSync(agent.binary, [hud.prompt], { stdio: "inherit" });
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (hud.decision)
|
|
2163
|
+
id = hud.decision;
|
|
2164
|
+
}
|
|
2069
2165
|
const client = new PantheonClient(cfg, undefined, { role: "cli" });
|
|
2070
2166
|
await client.connect().catch((e) => die(e.message));
|
|
2071
2167
|
const waiting = client.obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${cfg.member}`);
|
|
@@ -2188,14 +2284,10 @@ async function main() {
|
|
|
2188
2284
|
graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
|
|
2189
2285
|
once: argv.includes("--once"),
|
|
2190
2286
|
...(argv.includes("--no-notify") ? {} : {
|
|
2191
|
-
notify: (n) => {
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
sendDesktopNotification(n.title, n.subtitle, n.body, loadProfile(c), "", n.eyebrow);
|
|
2196
|
-
}
|
|
2197
|
-
catch { }
|
|
2198
|
-
},
|
|
2287
|
+
notify: (n) => { try {
|
|
2288
|
+
sendDesktopNotification(n);
|
|
2289
|
+
}
|
|
2290
|
+
catch { } },
|
|
2199
2291
|
}),
|
|
2200
2292
|
log: (l) => console.error(l),
|
|
2201
2293
|
});
|
|
@@ -2223,12 +2315,10 @@ async function main() {
|
|
|
2223
2315
|
graceMs: numFlag("grace", LIMITS.workerGraceMs / 1000) * 1000,
|
|
2224
2316
|
once: argv.includes("--once"),
|
|
2225
2317
|
...(argv.includes("--no-notify") ? {} : {
|
|
2226
|
-
notify: (n) => {
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
catch { }
|
|
2231
|
-
},
|
|
2318
|
+
notify: (n) => { try {
|
|
2319
|
+
sendDesktopNotification(n);
|
|
2320
|
+
}
|
|
2321
|
+
catch { } },
|
|
2232
2322
|
}),
|
|
2233
2323
|
log: (l) => console.error(l),
|
|
2234
2324
|
});
|
package/dist/client.js
CHANGED
|
@@ -41,6 +41,7 @@ export class PantheonClient {
|
|
|
41
41
|
plan = null;
|
|
42
42
|
enroll = null;
|
|
43
43
|
inviteUrl = null;
|
|
44
|
+
invitesList = null;
|
|
44
45
|
constructor(cfg, onEvent, opts) {
|
|
45
46
|
this.cfg = cfg;
|
|
46
47
|
this.onEvent = onEvent;
|
|
@@ -138,6 +139,9 @@ export class PantheonClient {
|
|
|
138
139
|
break;
|
|
139
140
|
case "claimed":
|
|
140
141
|
break;
|
|
142
|
+
case "invites":
|
|
143
|
+
this.invitesList = msg.invites;
|
|
144
|
+
break;
|
|
141
145
|
case "invite":
|
|
142
146
|
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
|
|
143
147
|
break;
|
|
@@ -417,6 +421,36 @@ export class PantheonClient {
|
|
|
417
421
|
}
|
|
418
422
|
});
|
|
419
423
|
}
|
|
424
|
+
listInvites() {
|
|
425
|
+
if (!this.ws || this.ws.readyState !== 1)
|
|
426
|
+
return Promise.reject(new Error(`not connected to relay (${this.pending ?? this.lastError ?? "offline"})`));
|
|
427
|
+
return new Promise((res, rej) => {
|
|
428
|
+
this.invitesList = null;
|
|
429
|
+
const cid = `c${++this.cid}`;
|
|
430
|
+
const finish = () => { clearInterval(poll); clearTimeout(t); this.acks.delete(cid); };
|
|
431
|
+
const t = setTimeout(() => { finish(); rej(new Error("relay did not list the invites in 10s")); }, 10_000);
|
|
432
|
+
const poll = setInterval(() => { if (this.invitesList) {
|
|
433
|
+
finish();
|
|
434
|
+
const v = this.invitesList;
|
|
435
|
+
this.invitesList = null;
|
|
436
|
+
res(v);
|
|
437
|
+
} }, 40);
|
|
438
|
+
this.acks.set(cid, (v) => { if ("error" in v) {
|
|
439
|
+
finish();
|
|
440
|
+
rej(new Error(v.error));
|
|
441
|
+
} });
|
|
442
|
+
try {
|
|
443
|
+
this.ws?.send(JSON.stringify({ t: "invites", cid }));
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
finish();
|
|
447
|
+
rej(e);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
revokeInvite(token) {
|
|
452
|
+
return this.rpc({ t: "revoke_invite", token: token.replace(/^…/, "") });
|
|
453
|
+
}
|
|
420
454
|
myObligations() {
|
|
421
455
|
return this.obligations.filter((o) => o.owner === this.cfg.member || o.owner === `human:${this.cfg.member}`);
|
|
422
456
|
}
|
|
@@ -435,6 +469,14 @@ export class PantheonClient {
|
|
|
435
469
|
recentFrom(id) {
|
|
436
470
|
return this.seen.get(id)?.from ?? null;
|
|
437
471
|
}
|
|
472
|
+
seenAct(id) {
|
|
473
|
+
return this.seen.get(id) ?? null;
|
|
474
|
+
}
|
|
475
|
+
mineSince(ts, goal) {
|
|
476
|
+
return [...this.seen.values()]
|
|
477
|
+
.filter((r) => r.from === this.cfg.member && r.ts >= ts && r.act !== "JOIN" && r.act !== "LEAVE" && (goal == null || r.goal === goal))
|
|
478
|
+
.sort((a, b) => a.seq - b.seq);
|
|
479
|
+
}
|
|
438
480
|
takenBySibling() {
|
|
439
481
|
return this.myObligations().filter((o) => o.claimed_by && o.claimed_by.session !== this.session);
|
|
440
482
|
}
|
package/dist/config.js
CHANGED
|
@@ -275,17 +275,29 @@ export function toInvite(cfg) {
|
|
|
275
275
|
const scheme = u.protocol === "wss:" ? "pantheons" : "pantheon";
|
|
276
276
|
return `${scheme}://${u.host}/${encodeURIComponent(cfg.space)}#${cfg.token}`;
|
|
277
277
|
}
|
|
278
|
+
export const JOIN_LINK_BASE = "https://www.joinpantheon.network/join";
|
|
279
|
+
const HOSTED_RELAY = "wss://relay.joinpantheon.network";
|
|
280
|
+
const JOIN_SLUG = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
281
|
+
export function toJoinLink(cfg) {
|
|
282
|
+
if (cfg.relay.replace(/\/+$/, "") !== HOSTED_RELAY || !JOIN_SLUG.test(cfg.space))
|
|
283
|
+
return null;
|
|
284
|
+
return `${JOIN_LINK_BASE}/${cfg.space}#${cfg.token}`;
|
|
285
|
+
}
|
|
278
286
|
export function fromInvite(invite) {
|
|
279
287
|
const clean = invite
|
|
280
288
|
.replace(/\u001b\[[0-9;]*m/g, "")
|
|
281
289
|
.replace(/[\u0000-\u001f\u007f]/g, "")
|
|
282
290
|
.trim()
|
|
283
291
|
.replace(/^['"`]|['"`]$/g, "")
|
|
284
|
-
.replace(/^pantheon\s+join\s+/i, "")
|
|
285
292
|
.trim();
|
|
286
|
-
const
|
|
293
|
+
const web = /https?:\/\/(?:www\.)?joinpantheon\.network\/join\/([^\s#?/"'`]+)(?:\?[^\s#"'`]*)?#([^\s"'`]+)/i.exec(clean);
|
|
294
|
+
if (web) {
|
|
295
|
+
const [, slug, token] = web;
|
|
296
|
+
return { relay: HOSTED_RELAY, space: decodeURIComponent(slug), token };
|
|
297
|
+
}
|
|
298
|
+
const m = /(pantheons?):\/\/([^/\s"'`]+)\/([^#\s"'`]+)#([^\s"'`]+)/.exec(clean);
|
|
287
299
|
if (!m)
|
|
288
|
-
throw new Error("not a pantheon invite — expected
|
|
300
|
+
throw new Error("not a pantheon invite — expected pantheons://host/space#token (pantheon:// without TLS), or https://www.joinpantheon.network/join/<team>#<token>");
|
|
289
301
|
const [, scheme, host, space, token] = m;
|
|
290
302
|
return {
|
|
291
303
|
relay: `${scheme === "pantheons" ? "wss" : "ws"}://${host}`,
|
package/dist/doctor.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadPersonas } from "./persona.js";
|
|
|
11
11
|
import { workerProc } from "./machine.js";
|
|
12
12
|
import { VERSION } from "./version.js";
|
|
13
13
|
import { serviceInstalled } from "./service.js";
|
|
14
|
+
import { hostEnv } from "./host-env.js";
|
|
14
15
|
const sh = (cmd, cwd) => {
|
|
15
16
|
try {
|
|
16
17
|
return execFileSync("sh", ["-lc", cmd], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
@@ -46,6 +47,13 @@ export async function doctor(projectDir) {
|
|
|
46
47
|
major >= 22
|
|
47
48
|
? add("pass", "node", `v${process.versions.node}`)
|
|
48
49
|
: add("fail", "node", `v${process.versions.node} is too old`, "pantheon needs Node 22 or newer (the relay uses node:sqlite) — install it and re-run");
|
|
50
|
+
const host = hostEnv();
|
|
51
|
+
if (host.ephemeral) {
|
|
52
|
+
add("warn", "machine", `${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 ${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.`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
add("pass", "machine", `${host.platform}${host.desktop ? " — menu bar and desktop alerts can run here" : " — no menu bar or desktop alerts (macOS only); use phone or chat delivery"}`);
|
|
56
|
+
}
|
|
49
57
|
let cfg = null;
|
|
50
58
|
try {
|
|
51
59
|
cfg = load();
|
|
@@ -193,6 +201,11 @@ export async function doctor(projectDir) {
|
|
|
193
201
|
add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, "start one with `pantheon service install worker --project <a git repo>`; until then a peer's act waits for you to type");
|
|
194
202
|
}
|
|
195
203
|
}
|
|
204
|
+
if (host.desktop) {
|
|
205
|
+
serviceInstalled("menubar")
|
|
206
|
+
? add("pass", "menu bar", "installed — status, pause/resume, and a badge when a decision needs you")
|
|
207
|
+
: add("warn", "menu bar", "not installed on a Mac that can run it", "install it with: pantheon service install menubar (needs the Xcode Command Line Tools — xcode-select --install — and 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");
|
|
208
|
+
}
|
|
196
209
|
if (!hookStates.length && !hook.installed) {
|
|
197
210
|
add("warn", "wake-up hook", "not installed — your agent will not notice a peer's message until you ask it to", "pantheon hook install --user (auto-detects claude / codex / cursor on PATH)");
|
|
198
211
|
}
|
package/dist/email.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
function subjectAndBody(code, purpose) {
|
|
2
2
|
const action = purpose === "signup" ? "confirm your email and finish signing up" : "add this device to your account";
|
|
3
3
|
const eyebrow = purpose === "signup" ? "Confirm your email" : "New device sign-in";
|
|
4
|
-
const subject = `Your
|
|
4
|
+
const subject = `Your Pantheon code: ${code}`;
|
|
5
5
|
const text = [
|
|
6
|
-
`
|
|
6
|
+
`Pantheon — ${eyebrow.toLowerCase()}`,
|
|
7
7
|
``,
|
|
8
8
|
`Your verification code is ${code}.`,
|
|
9
9
|
``,
|
|
@@ -12,37 +12,61 @@ function subjectAndBody(code, purpose) {
|
|
|
12
12
|
``,
|
|
13
13
|
`The code expires in 10 minutes and works once. If you did not request it,`,
|
|
14
14
|
`ignore this email — nothing happens without the code.`,
|
|
15
|
+
``,
|
|
16
|
+
`joinpantheon.network — the execution layer for companies`,
|
|
15
17
|
].join("\n");
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
+
const paper = "#eae4da", card = "#f2ede5", stone = "#ddd4c3", ink = "#292927", faint = "#7c766d", gold = "#d8a33a";
|
|
19
|
+
const mono = `'SF Mono',Menlo,Consolas,'Liberation Mono',monospace`;
|
|
20
|
+
const serif = `Georgia,'Times New Roman',Times,serif`;
|
|
18
21
|
const html = `<!doctype html>
|
|
19
22
|
<html lang="en">
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8">
|
|
25
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
26
|
+
<meta name="color-scheme" content="light">
|
|
27
|
+
<meta name="supported-color-schemes" content="light">
|
|
28
|
+
<title>Your Pantheon code</title>
|
|
29
|
+
</head>
|
|
30
|
+
<body style="margin:0;padding:0;background:${paper}">
|
|
31
|
+
<div style="display:none;max-height:0;overflow:hidden;opacity:0">Your code is ${code}. It expires in 10 minutes and works once. ‌ ‌ ‌ ‌ ‌</div>
|
|
32
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:${paper};margin:0;padding:0">
|
|
33
|
+
<tr><td align="center" style="padding:40px 16px">
|
|
34
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:480px;width:100%">
|
|
25
35
|
|
|
26
|
-
<tr><td style="padding:0
|
|
27
|
-
<
|
|
36
|
+
<tr><td align="left" style="padding:0 0 28px 0">
|
|
37
|
+
<img src="https://www.joinpantheon.network/email/mark.png" width="28" height="28" alt="Pantheon" style="display:inline-block;vertical-align:middle;border:0">
|
|
38
|
+
<span style="display:inline-block;vertical-align:middle;padding-left:10px;font-family:${serif};font-size:19px;letter-spacing:.01em;color:${ink}">Pantheon</span>
|
|
28
39
|
</td></tr>
|
|
29
40
|
|
|
30
|
-
<tr><td style="background
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
<tr><td style="background:${card};border:1px solid ${stone};border-radius:2px;padding:36px 32px 32px 32px">
|
|
42
|
+
|
|
43
|
+
<p style="margin:0 0 22px 0;font-family:${mono};font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:${faint}">${eyebrow}</p>
|
|
44
|
+
|
|
45
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
46
|
+
<tr><td align="center" style="background:${paper};border:1px solid ${stone};border-radius:2px;padding:22px 12px">
|
|
47
|
+
<span style="font-family:${mono};font-size:34px;line-height:1.1;letter-spacing:.22em;color:${ink};font-weight:600">${code}</span>
|
|
35
48
|
</td></tr>
|
|
36
|
-
<tr><td style="font-family:${serif};font-size:16px;line-height:1.55;color:#3B4A4D;padding:18px 0 12px">Enter it where the CLI is waiting, to ${action}:</td></tr>
|
|
37
|
-
<tr><td style="background-color:#DFE5E4;border:1px solid #C3CDCD;padding:11px 14px;font-family:${mono};font-size:13px;color:#101A1C"><span style="color:#0E6F78">$</span> pantheon verify ${code}</td></tr>
|
|
38
|
-
<tr><td style="padding-top:18px"></td></tr>
|
|
39
|
-
<tr><td style="font-family:${mono};font-size:12px;line-height:1.7;color:#5A686A;padding-top:14px;border-top:1px solid #C3CDCD">Expires in 10 minutes. Works once.<br>Didn't request it? Ignore this email — nothing happens without the code.</td></tr>
|
|
40
49
|
</table>
|
|
50
|
+
|
|
51
|
+
<p style="margin:24px 0 14px 0;font-family:${serif};font-size:16px;line-height:1.55;color:${ink}">Enter it where the CLI is waiting, to ${action}:</p>
|
|
52
|
+
|
|
53
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
54
|
+
<tr><td style="background:${paper};border-left:2px solid ${gold};padding:13px 16px">
|
|
55
|
+
<span style="font-family:${mono};font-size:14px;color:${ink}">pantheon verify ${code}</span>
|
|
56
|
+
</td></tr>
|
|
57
|
+
</table>
|
|
58
|
+
|
|
59
|
+
<p style="margin:26px 0 0 0;padding-top:20px;border-top:1px solid ${stone};font-family:${serif};font-size:14px;line-height:1.6;color:${faint}">
|
|
60
|
+
Expires in 10 minutes. Works once.<br>
|
|
61
|
+
Didn't request it? Ignore this email — nothing happens without the code.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
41
64
|
</td></tr>
|
|
42
65
|
|
|
43
|
-
<tr><td align="
|
|
44
|
-
<a href="https://joinpantheon.network" style="color
|
|
45
|
-
|
|
66
|
+
<tr><td align="left" style="padding:22px 2px 0 2px;font-family:${mono};font-size:11.5px;letter-spacing:.04em;color:${faint}">
|
|
67
|
+
<a href="https://www.joinpantheon.network" style="color:${faint};text-decoration:underline">joinpantheon.network</a>
|
|
68
|
+
<span style="color:${gold};padding:0 8px">·</span>
|
|
69
|
+
The execution layer for companies
|
|
46
70
|
</td></tr>
|
|
47
71
|
|
|
48
72
|
</table>
|
package/dist/host-env.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
const CLOUD_VARS = [
|
|
3
|
+
["CODESPACES", "GitHub Codespaces ($CODESPACES)"],
|
|
4
|
+
["REMOTE_CONTAINERS", "a VS Code dev container ($REMOTE_CONTAINERS)"],
|
|
5
|
+
["REMOTE_CONTAINERS_IPC", "a VS Code dev container ($REMOTE_CONTAINERS_IPC)"],
|
|
6
|
+
["DEVCONTAINER", "a dev container ($DEVCONTAINER)"],
|
|
7
|
+
["GITPOD_WORKSPACE_ID", "a Gitpod workspace ($GITPOD_WORKSPACE_ID)"],
|
|
8
|
+
["CLOUD_SHELL", "a cloud shell ($CLOUD_SHELL)"],
|
|
9
|
+
];
|
|
10
|
+
export function hostEnv(env = process.env, plat = process.platform, exists = existsSync, read = (p) => readFileSync(p, "utf8")) {
|
|
11
|
+
if (plat === "darwin") {
|
|
12
|
+
return { platform: plat, kind: "local", evidence: [], ephemeral: false, desktop: true };
|
|
13
|
+
}
|
|
14
|
+
const evidence = [];
|
|
15
|
+
let kind = "local";
|
|
16
|
+
for (const [name, label] of CLOUD_VARS) {
|
|
17
|
+
if (env[name]) {
|
|
18
|
+
evidence.push(label);
|
|
19
|
+
kind = "cloud-workspace";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (exists("/.dockerenv"))
|
|
23
|
+
evidence.push("/.dockerenv exists");
|
|
24
|
+
if (exists("/run/.containerenv"))
|
|
25
|
+
evidence.push("/run/.containerenv exists (podman)");
|
|
26
|
+
let cgroup = "";
|
|
27
|
+
try {
|
|
28
|
+
cgroup = exists("/proc/1/cgroup") ? read("/proc/1/cgroup") : "";
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
if (/docker|containerd|kubepods|lxc/.test(cgroup))
|
|
32
|
+
evidence.push("pid 1 is in a container cgroup (/proc/1/cgroup)");
|
|
33
|
+
if (evidence.length && kind === "local")
|
|
34
|
+
kind = "container";
|
|
35
|
+
return {
|
|
36
|
+
platform: plat,
|
|
37
|
+
kind,
|
|
38
|
+
evidence,
|
|
39
|
+
ephemeral: kind !== "local",
|
|
40
|
+
desktop: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function ephemeralWarning(host) {
|
|
44
|
+
return `this looks like ${host.kind === "container" ? "a container" : "a cloud workspace"}, not the computer you work on `
|
|
45
|
+
+ `(${host.evidence.join("; ")}). That is why this machine reports ${host.platform}: your editor's agent is running `
|
|
46
|
+
+ `somewhere else, so the menu bar and desktop alerts cannot be installed, and anything installed here disappears with `
|
|
47
|
+
+ `the session. Your account and team are fine — run the same setup from a terminal on your own computer to finish it.`;
|
|
48
|
+
}
|
|
49
|
+
export function desktopUnavailable(host, what) {
|
|
50
|
+
if (host.desktop)
|
|
51
|
+
return null;
|
|
52
|
+
if (host.ephemeral) {
|
|
53
|
+
return `not available — ${what} ${what === "desktop alerts" ? "need" : "needs"} your own computer, and this is ${host.kind === "container" ? "a container" : "a cloud workspace"} reporting ${host.platform}`;
|
|
54
|
+
}
|
|
55
|
+
return `not available — ${what} ${host.platform === "darwin" ? "could not be built here" : `${what === "desktop alerts" ? "are" : "is"} macOS-only, and this machine is ${host.platform}`}`;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=host-env.js.map
|