@join-pantheon/cli 0.33.1 → 0.33.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/README.md +6 -5
- package/dist/accounts.js +10 -0
- package/dist/agents.js +11 -0
- package/dist/board.js +55 -9
- package/dist/claude-confine.js +1 -1
- package/dist/cli.js +556 -89
- package/dist/client.js +8 -4
- package/dist/config.js +18 -4
- package/dist/continued.js +5 -0
- package/dist/desktop-app.js +103 -0
- package/dist/doctor.js +84 -32
- package/dist/folder-guard.js +12 -0
- package/dist/folder-snapshot.js +432 -0
- package/dist/install.js +15 -8
- package/dist/machine.js +84 -4
- package/dist/managed.js +2 -2
- package/dist/mcp.js +5 -3
- package/dist/network.js +156 -0
- package/dist/notice-linux.js +6 -5
- package/dist/notice-windows.js +2 -2
- package/dist/notice.js +13 -0
- package/dist/notifier.js +29 -2
- package/dist/notify.js +11 -0
- package/dist/onboard-defaults.js +37 -1
- package/dist/pins.js +7 -1
- package/dist/plans.js +23 -0
- package/dist/protocol.js +5 -0
- package/dist/quickstart.js +17 -11
- package/dist/refusal.js +4 -4
- package/dist/relay.js +332 -22
- package/dist/repo-find.js +6 -3
- package/dist/roster-file.js +50 -5
- package/dist/tray.js +12 -1
- package/dist/uninstall.js +38 -3
- package/dist/uri-handler.js +3 -2
- package/dist/verify-act.js +17 -0
- package/dist/worker-fleet.js +140 -5
- package/dist/worker.js +201 -63
- package/installer/install.ps1 +251 -15
- package/installer/install.sh +147 -18
- package/package.json +1 -1
- package/helper/macos/prebuilt/manifest.json +0 -29
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/helper/macos/prebuilt/pantheon-tray +0 -0
package/dist/network.js
CHANGED
|
@@ -614,6 +614,8 @@ export function explainNetError(e, target, planArg) {
|
|
|
614
614
|
? { why: `the connection through the proxy ${shown} was cut`, fix: `the proxy may not allow WebSockets; ask for ${host} to be allowed`, share: "connection reset" }
|
|
615
615
|
: { why: "the connection dropped before the relay answered", fix: "try again", share: "connection reset" };
|
|
616
616
|
}
|
|
617
|
+
if (UNEXPECTED_REPLY.test(text))
|
|
618
|
+
return unexpectedReply(text, url ? host : "");
|
|
617
619
|
return null;
|
|
618
620
|
}
|
|
619
621
|
export function describeNetError(e, target, plan) {
|
|
@@ -623,4 +625,158 @@ export function describeNetError(e, target, plan) {
|
|
|
623
625
|
const fix = x ? x.fix.charAt(0).toUpperCase() + x.fix.slice(1) : "";
|
|
624
626
|
return x ? `${x.why}. ${fix}. (${first})`.replace(/\.\. \(/, ". (").replace(/\?\. \(/, "? (") : raw;
|
|
625
627
|
}
|
|
628
|
+
export class RelayReplyError extends Error {
|
|
629
|
+
status;
|
|
630
|
+
kind;
|
|
631
|
+
code = "RELAY_UNEXPECTED_REPLY";
|
|
632
|
+
constructor(status, kind, url) {
|
|
633
|
+
super(`${url} answered HTTP ${status} with ${kind === "empty" ? "an empty body" : kind === "html" ? "a web page" : "something that is not JSON"}, where the relay always answers JSON`);
|
|
634
|
+
this.status = status;
|
|
635
|
+
this.kind = kind;
|
|
636
|
+
this.name = "RelayReplyError";
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
export function replyKind(body, contentType) {
|
|
640
|
+
if (!body.trim())
|
|
641
|
+
return "empty";
|
|
642
|
+
return /html/i.test(contentType) || /^\s*<(!doctype|html|head|body|\?xml)/i.test(body) ? "html" : "other";
|
|
643
|
+
}
|
|
644
|
+
export async function relayJson(res, url) {
|
|
645
|
+
const text = await res.text();
|
|
646
|
+
try {
|
|
647
|
+
const j = JSON.parse(text);
|
|
648
|
+
if (j && typeof j === "object" && !Array.isArray(j))
|
|
649
|
+
return j;
|
|
650
|
+
}
|
|
651
|
+
catch { }
|
|
652
|
+
throw new RelayReplyError(res.status, replyKind(text, res.headers.get("content-type") ?? ""), url ?? (res.url || "the relay"));
|
|
653
|
+
}
|
|
654
|
+
const UNEXPECTED_REPLY = /RELAY_UNEXPECTED_REPLY|Unexpected server response: \d+|answered HTTP \d+/;
|
|
655
|
+
const CHAIN_NOT_PUBLIC = /SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_GET_ISSUER_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE|CERT_UNTRUSTED|self[- ]signed certificate|unable to get local issuer/i;
|
|
656
|
+
export const worthCertificateCheck = (e, o = {}) => {
|
|
657
|
+
const t = errText(e);
|
|
658
|
+
const status = replyStatus(t);
|
|
659
|
+
if (status !== null && status >= 502 && status <= 504)
|
|
660
|
+
return false;
|
|
661
|
+
if (UNEXPECTED_REPLY.test(t) || CHAIN_NOT_PUBLIC.test(t))
|
|
662
|
+
return true;
|
|
663
|
+
return !!o.nonJsonSeen && /ECONNRESET|socket hang up/i.test(t);
|
|
664
|
+
};
|
|
665
|
+
const replyStatus = (text) => {
|
|
666
|
+
const m = /Unexpected server response: (\d+)|answered HTTP (\d+)/.exec(text);
|
|
667
|
+
return m ? Number(m[1] ?? m[2]) : null;
|
|
668
|
+
};
|
|
669
|
+
export const isPantheonHost = (host) => /(^|\.)joinpantheon\.network$/i.test(host.replace(/\.$/, ""));
|
|
670
|
+
function unexpectedReply(text, host) {
|
|
671
|
+
const status = replyStatus(text);
|
|
672
|
+
if (status !== null && status >= 502 && status <= 504) {
|
|
673
|
+
return { why: `the relay's host answered ${status}: the relay is restarting or briefly down`, fix: "try again in a minute; pantheon doctor shows more", share: `relay host answered ${status}` };
|
|
674
|
+
}
|
|
675
|
+
const what = /empty body|Unexpected end of JSON input/.test(text) ? `an empty ${status ?? "reply"}`
|
|
676
|
+
: /web page|Unexpected token '?<|<!doctype|<html/i.test(text) ? "a web page" : status ? `HTTP ${status}` : "something that is not the relay";
|
|
677
|
+
const allow = !host ? "the relay's address" : isPantheonHost(host) ? "*.joinpantheon.network" : host;
|
|
678
|
+
return {
|
|
679
|
+
why: `something other than the relay answered (${what}), which is what an antivirus, a company proxy or a captive portal does when it blocks a site`,
|
|
680
|
+
fix: `allow ${allow} in your antivirus or proxy (with HTTPS scanning), or ask your IT; pantheon doctor names who signed the connection`,
|
|
681
|
+
share: "something other than the relay answered",
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const firstOf = (v) => (Array.isArray(v) ? v[0] : v)?.trim() ?? "";
|
|
685
|
+
export function issuerName(issuer) {
|
|
686
|
+
const cn = firstOf(issuer?.CN), o = firstOf(issuer?.O);
|
|
687
|
+
if (cn && o && !cn.toLowerCase().includes(o.toLowerCase()))
|
|
688
|
+
return `${cn}, ${o}`;
|
|
689
|
+
return cn || o || firstOf(issuer?.OU) || "an unnamed certificate authority";
|
|
690
|
+
}
|
|
691
|
+
export function judgeCertificate(host, authorized, authorizationError, issuer) {
|
|
692
|
+
const name = issuerName(issuer);
|
|
693
|
+
if (authorized)
|
|
694
|
+
return { host, issuer: name, publicCa: true };
|
|
695
|
+
const err = authorizationError instanceof Error ? (authorizationError.code ?? authorizationError.message) : String(authorizationError ?? "");
|
|
696
|
+
return { host, issuer: name, publicCa: CHAIN_NOT_PUBLIC.test(err) ? false : null, ...(err ? { error: err } : {}) };
|
|
697
|
+
}
|
|
698
|
+
export async function checkRelayCertificate(target, opts = {}) {
|
|
699
|
+
let u;
|
|
700
|
+
try {
|
|
701
|
+
u = new URL(target.replace(/^ws/i, "http"));
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
if (u.protocol !== "https:")
|
|
707
|
+
return null;
|
|
708
|
+
const host = u.hostname.replace(/^\[|\]$/g, "");
|
|
709
|
+
const port = Number(u.port || 443);
|
|
710
|
+
const proxy = routeFor(u.href, opts.plan ?? currentPlan());
|
|
711
|
+
const options = {
|
|
712
|
+
host, port, ca: opts.ca ?? [...tls.rootCertificates], rejectUnauthorized: false,
|
|
713
|
+
...(net.isIP(host) ? {} : { servername: host }),
|
|
714
|
+
};
|
|
715
|
+
return new Promise((done) => {
|
|
716
|
+
let settled = false;
|
|
717
|
+
let sock = null;
|
|
718
|
+
const finish = (r) => {
|
|
719
|
+
if (settled)
|
|
720
|
+
return;
|
|
721
|
+
settled = true;
|
|
722
|
+
clearTimeout(timer);
|
|
723
|
+
sock?.destroy();
|
|
724
|
+
done(r);
|
|
725
|
+
};
|
|
726
|
+
const timer = setTimeout(() => finish(null), opts.timeoutMs ?? 4_000);
|
|
727
|
+
timer.unref?.();
|
|
728
|
+
const read = (s) => {
|
|
729
|
+
sock = s;
|
|
730
|
+
const c = s.getPeerCertificate(false);
|
|
731
|
+
if (!c || !Object.keys(c).length)
|
|
732
|
+
return finish(null);
|
|
733
|
+
finish(judgeCertificate(host, s.authorized, s.authorizationError, c.issuer));
|
|
734
|
+
};
|
|
735
|
+
if (proxy) {
|
|
736
|
+
tunnelAgent(proxy, true)
|
|
737
|
+
.createConnection(options, (err, s) => {
|
|
738
|
+
if (settled) {
|
|
739
|
+
s?.destroy();
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
if (err || !s)
|
|
743
|
+
finish(null);
|
|
744
|
+
else
|
|
745
|
+
read(s);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
const s = tls.connect(options, () => read(s));
|
|
750
|
+
sock = s;
|
|
751
|
+
s.once("error", () => finish(null));
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
export function interceptionExplanation(c) {
|
|
756
|
+
if (isPantheonHost(c.host)) {
|
|
757
|
+
return {
|
|
758
|
+
why: `your antivirus or proxy (${c.issuer}) intercepts ${c.host}`,
|
|
759
|
+
fix: "allow *.joinpantheon.network in its web/anti-phishing settings (with HTTPS scanning), or ask your IT",
|
|
760
|
+
share: "relay intercepted: its certificate is not from a public CA (antivirus or proxy)",
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
return {
|
|
764
|
+
why: `${c.host}'s certificate here does not check out against the public certificate authorities (it names ${c.issuer}): the relay may use a private certificate authority or send an incomplete chain, or an antivirus or proxy may be in between`,
|
|
765
|
+
fix: `ask whoever runs ${c.host} which it is — a private root goes in NODE_EXTRA_CA_CERTS=/path/to/root.pem; an antivirus or proxy needs ${c.host} allowed`,
|
|
766
|
+
share: "relay certificate not from a public CA",
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
export async function interceptedText(e, target, check = checkRelayCertificate, o = {}) {
|
|
770
|
+
if (!target || !worthCertificateCheck(e, o))
|
|
771
|
+
return null;
|
|
772
|
+
const c = await check(target).catch(() => null);
|
|
773
|
+
if (!c || c.publicCa !== false)
|
|
774
|
+
return null;
|
|
775
|
+
const x = interceptionExplanation(c);
|
|
776
|
+
const raw = (e?.message ?? String(e)).split("\n")[0].slice(0, 120);
|
|
777
|
+
return `${x.why} — ${x.fix}. (${raw})`;
|
|
778
|
+
}
|
|
779
|
+
export async function describeRelayFailure(e, target, check) {
|
|
780
|
+
return (await interceptedText(e, target, check)) ?? describeNetError(e, target);
|
|
781
|
+
}
|
|
626
782
|
//# sourceMappingURL=network.js.map
|
package/dist/notice-linux.js
CHANGED
|
@@ -46,7 +46,7 @@ export function noticeActions(n) {
|
|
|
46
46
|
const out = [["default", "Open board"]];
|
|
47
47
|
if (n.kind === "decision" && n.details?.decision)
|
|
48
48
|
out.push(["done", "Done"]);
|
|
49
|
-
out.push(["board", "Open board"]);
|
|
49
|
+
out.push(["board", n.focus ? "Approve or decline" : "Open board"]);
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
52
|
export function noticeUrgency(n) {
|
|
@@ -157,7 +157,7 @@ export class LinuxAlerts {
|
|
|
157
157
|
run,
|
|
158
158
|
cli,
|
|
159
159
|
log: deps.log ?? ((l) => console.error(l)),
|
|
160
|
-
openBoard: deps.openBoard ?? ((env) => openBoardWith(cli, env)),
|
|
160
|
+
openBoard: deps.openBoard ?? ((env, focus) => openBoardWith(cli, env, focus)),
|
|
161
161
|
sessionEnv: deps.sessionEnv ?? realSessionEnv,
|
|
162
162
|
};
|
|
163
163
|
}
|
|
@@ -296,7 +296,7 @@ export class LinuxAlerts {
|
|
|
296
296
|
else if (action === "board" || action === "default") {
|
|
297
297
|
const display = this.d.env.DISPLAY || this.d.env.WAYLAND_DISPLAY ? {} : pickDisplay(this.d.sessionEnv());
|
|
298
298
|
const env = { ...this.d.env, ...display, ...(token ? { XDG_ACTIVATION_TOKEN: token, DESKTOP_STARTUP_ID: token } : {}) };
|
|
299
|
-
const opening = this.d.openBoard(env).catch(() => false).then(async (opened) => {
|
|
299
|
+
const opening = this.d.openBoard(env, s.notice.focus).catch(() => false).then(async (opened) => {
|
|
300
300
|
if (!opened)
|
|
301
301
|
await this.note("Open the board in a terminal", "Run pantheon board (or pantheon brief) to see everything that needs you.");
|
|
302
302
|
});
|
|
@@ -436,7 +436,7 @@ export function writeHudAction(home, hud) {
|
|
|
436
436
|
renameSync(tmp, f);
|
|
437
437
|
return f;
|
|
438
438
|
}
|
|
439
|
-
function openBoardWith(cli, env) {
|
|
439
|
+
function openBoardWith(cli, env, focus) {
|
|
440
440
|
return new Promise((done) => {
|
|
441
441
|
const [file, ...args] = cli;
|
|
442
442
|
let settled = false;
|
|
@@ -445,8 +445,9 @@ function openBoardWith(cli, env) {
|
|
|
445
445
|
done(v);
|
|
446
446
|
} };
|
|
447
447
|
let child;
|
|
448
|
+
const at = focus && /^[ao]_[A-Za-z0-9]{6,48}$/.test(focus) ? ["--focus", focus] : [];
|
|
448
449
|
try {
|
|
449
|
-
child = spawn(file, [...args, "board", "--open"], { env, stdio: "ignore", detached: true });
|
|
450
|
+
child = spawn(file, [...args, "board", "--open", ...at], { env, stdio: "ignore", detached: true });
|
|
450
451
|
}
|
|
451
452
|
catch {
|
|
452
453
|
settle(false);
|
package/dist/notice-windows.js
CHANGED
|
@@ -42,7 +42,7 @@ export function toastButtons(notice, key) {
|
|
|
42
42
|
else if (d.act) {
|
|
43
43
|
add("Show details", buildUri("details", { act: d.act, decision: d.decision, space }, key));
|
|
44
44
|
}
|
|
45
|
-
add("Open board", buildUri("board", {}, key));
|
|
45
|
+
add(notice.focus ? "Approve or decline" : "Open board", buildUri("board", notice.focus ? { focus: notice.focus } : {}, key));
|
|
46
46
|
return out;
|
|
47
47
|
}
|
|
48
48
|
export function toastTag(notice) {
|
|
@@ -60,7 +60,7 @@ export function toastFields(notice, key) {
|
|
|
60
60
|
return {
|
|
61
61
|
lines,
|
|
62
62
|
who: toastText(notice.who, TOAST_CAPS.who),
|
|
63
|
-
launch: buildUri("board", {}, key),
|
|
63
|
+
launch: (notice.focus && buildUri("board", { focus: notice.focus }, key)) || buildUri("board", {}, key),
|
|
64
64
|
buttons: toastButtons(notice, key).map((b) => ({ content: toastText(b.content, TOAST_CAPS.button), uri: b.uri })),
|
|
65
65
|
scenario: notice.ttl === 0 ? "reminder" : "",
|
|
66
66
|
duration: notice.ttl !== 0 && notice.ttl > 7 ? "long" : "",
|
package/dist/notice.js
CHANGED
|
@@ -174,4 +174,17 @@ export function composeWorker(run, ctx) {
|
|
|
174
174
|
: {}),
|
|
175
175
|
};
|
|
176
176
|
}
|
|
177
|
+
export function composeWakeAsk(a) {
|
|
178
|
+
const where = a.mac ? "in the menu bar, or on the board (pantheon board)" : "on the board";
|
|
179
|
+
return {
|
|
180
|
+
kind: "worker",
|
|
181
|
+
who: fit(`${a.member} · waiting on you`, BUDGET.who),
|
|
182
|
+
heading: fit(`${a.member} is waiting on your approval`, BUDGET.heading),
|
|
183
|
+
context: fit(a.space, BUDGET.context),
|
|
184
|
+
body: fit(`${a.member} asked: ${plain(a.what)} — your worker takes requests only from teammates you approved here. Approve or decline ${where}.`, BUDGET.body),
|
|
185
|
+
ttl: 0,
|
|
186
|
+
sound: true,
|
|
187
|
+
focus: a.focus,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
177
190
|
//# sourceMappingURL=notice.js.map
|
package/dist/notifier.js
CHANGED
|
@@ -9,12 +9,17 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { PantheonClient } from "./client.js";
|
|
10
10
|
import { CONFIG_DIR } from "./config.js";
|
|
11
11
|
import { loadProfile } from "./profile.js";
|
|
12
|
-
import { APP_TOOLS, composeDecision, composeDelivery, TOOL_LABELS } from "./notice.js";
|
|
12
|
+
import { APP_TOOLS, composeDecision, composeDelivery, composeWakeAsk, TOOL_LABELS } from "./notice.js";
|
|
13
|
+
import { loadPolicy } from "./worker-policy.js";
|
|
14
|
+
import { wakeAlerted } from "./continued.js";
|
|
13
15
|
import { cliOnPath, detectAvailable } from "./agents.js";
|
|
14
16
|
import { whichPantheon } from "./service.js";
|
|
15
17
|
export const DEFAULT_REMINDER_MS = 4 * 60 * 60 * 1_000;
|
|
16
18
|
export const MAX_REMINDERS = 4;
|
|
17
19
|
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
20
|
+
export function wakeAskWhat(body) {
|
|
21
|
+
return /answer while you are away:\s*(.+)$/m.exec(body)?.[1]?.trim() ?? body.split("\n")[0] ?? "";
|
|
22
|
+
}
|
|
18
23
|
export function humanObligations(obligations, member) {
|
|
19
24
|
return obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${member}`);
|
|
20
25
|
}
|
|
@@ -164,10 +169,32 @@ export class HumanNotifier {
|
|
|
164
169
|
const labelled = this.opts.label ? { ...n, context: n.context ? `${this.opts.label} · ${n.context}` : this.opts.label } : n;
|
|
165
170
|
return platform() === "darwin" ? labelled : { ...labelled, space: this.cfg.space };
|
|
166
171
|
};
|
|
172
|
+
let asks = {};
|
|
173
|
+
try {
|
|
174
|
+
asks = this.opts.wakeAsks ? this.opts.wakeAsks() : loadPolicy().wakeAsks ?? {};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
asks = {};
|
|
178
|
+
}
|
|
167
179
|
for (const obligation of due) {
|
|
168
180
|
const isReminder = state.notified_at[obligation.id] !== undefined;
|
|
169
181
|
const act = client.seenAct(obligation.ref);
|
|
170
|
-
const
|
|
182
|
+
const wake = asks[obligation.ref];
|
|
183
|
+
const workerSaid = (w) => { try {
|
|
184
|
+
return this.opts.wakeAlerted ? this.opts.wakeAlerted(w, obligation.ref) : wakeAlerted(w.team, w.member, obligation.ref);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return false;
|
|
188
|
+
} };
|
|
189
|
+
if (wake && !isReminder && workerSaid(wake)) {
|
|
190
|
+
state.notified_at[obligation.id] = now();
|
|
191
|
+
dirty = true;
|
|
192
|
+
this.opts.log(`> ${obligation.id}: the worker alerted this one itself (may ${wake.member} start work here)`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const notice = wake
|
|
196
|
+
? withLabel(composeWakeAsk({ member: wake.member, what: wakeAskWhat(act?.body ?? obligation.body), space: this.opts.label ? "" : this.cfg.space, focus: obligation.ref, mac: platform() === "darwin" }))
|
|
197
|
+
: withLabel(composeDecision(obligation, act, obligation.goal ? client.goals.get(obligation.goal) : undefined, peerOf(act?.from), { tools, preferred: preferredTool(profile, tools), now: now(), member: this.cfg.member }));
|
|
171
198
|
const ok = await send(notice);
|
|
172
199
|
if (ok) {
|
|
173
200
|
state.notified_at[obligation.id] = now();
|
package/dist/notify.js
CHANGED
|
@@ -95,6 +95,17 @@ export function readIntent(home) {
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
export function repairNote(repairs) {
|
|
99
|
+
if (!repairs || repairs < 1)
|
|
100
|
+
return null;
|
|
101
|
+
if (repairs === 1)
|
|
102
|
+
return { said: "put back once (it had been removed)", why: null, recurring: false };
|
|
103
|
+
return {
|
|
104
|
+
said: `put back ${repairs} times after something removed it`,
|
|
105
|
+
why: "something on this machine rewrites ~/.claude/settings.json and drops the hooks key",
|
|
106
|
+
recurring: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
98
109
|
export function writeIntent(next, home) {
|
|
99
110
|
try {
|
|
100
111
|
const f = intentPath(home);
|
package/dist/onboard-defaults.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { validUsername } from "./accounts.js";
|
|
2
|
+
import { validEmail, validUsername } from "./accounts.js";
|
|
3
3
|
export function gitConfigValue(key) {
|
|
4
4
|
try {
|
|
5
5
|
return execFileSync("git", ["config", "--get", key], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 }).trim();
|
|
@@ -15,4 +15,40 @@ export function suggestUsername(email, n) {
|
|
|
15
15
|
const u = n ? `${local}${n}` : local;
|
|
16
16
|
return validUsername(u) ? u : "";
|
|
17
17
|
}
|
|
18
|
+
const GITHUB_NOREPLY = /@users\.noreply\.github\.com$/i;
|
|
19
|
+
export function isGithubNoreply(email) {
|
|
20
|
+
return GITHUB_NOREPLY.test(email.trim());
|
|
21
|
+
}
|
|
22
|
+
export function githubNoreplyLogin(email) {
|
|
23
|
+
if (!isGithubNoreply(email))
|
|
24
|
+
return "";
|
|
25
|
+
const local = email.trim().split("@")[0];
|
|
26
|
+
return /^(?:\d+\+)?([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/i.exec(local)?.[1]?.toLowerCase() ?? "";
|
|
27
|
+
}
|
|
28
|
+
export function emailSuggestion(gitEmail) {
|
|
29
|
+
const e = gitEmail.trim();
|
|
30
|
+
return validEmail(e) && !isGithubNoreply(e) ? e : "";
|
|
31
|
+
}
|
|
32
|
+
export function usernameSuggestion(email, gitEmail = "", n) {
|
|
33
|
+
const fromEmail = isGithubNoreply(email) ? "" : suggestUsername(email, n);
|
|
34
|
+
if (fromEmail)
|
|
35
|
+
return fromEmail;
|
|
36
|
+
const login = githubNoreplyLogin(email) || githubNoreplyLogin(gitEmail);
|
|
37
|
+
if (!login)
|
|
38
|
+
return "";
|
|
39
|
+
const u = n ? `${login.slice(0, 28)}${n}` : login.slice(0, 32);
|
|
40
|
+
return validUsername(u) ? u : "";
|
|
41
|
+
}
|
|
42
|
+
export function signinCommand(launcher, platform = process.platform) {
|
|
43
|
+
return pantheonCommand(launcher, "signin", platform);
|
|
44
|
+
}
|
|
45
|
+
export function pantheonCommand(launcher, rest, platform = process.platform) {
|
|
46
|
+
if (!launcher)
|
|
47
|
+
return `pantheon ${rest}`;
|
|
48
|
+
if (!/\s/.test(launcher))
|
|
49
|
+
return `${launcher} ${rest}`;
|
|
50
|
+
if (platform === "win32")
|
|
51
|
+
return `& "${launcher}" ${rest} (Command Prompt: "${launcher}" ${rest})`;
|
|
52
|
+
return `"${launcher}" ${rest}`;
|
|
53
|
+
}
|
|
18
54
|
//# sourceMappingURL=onboard-defaults.js.map
|
package/dist/pins.js
CHANGED
|
@@ -74,7 +74,7 @@ export function writeWhole(f, text) {
|
|
|
74
74
|
throw e;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
export function observeMembers(p, team, members, me, at = Date.now()) {
|
|
77
|
+
export function observeMembers(p, team, members, me, at = Date.now(), vouch) {
|
|
78
78
|
const t = (p.teams[team] ??= { members: dict() });
|
|
79
79
|
const out = [];
|
|
80
80
|
const mine = fingerprint(me.publicKey);
|
|
@@ -98,6 +98,12 @@ export function observeMembers(p, team, members, me, at = Date.now()) {
|
|
|
98
98
|
delete cur.pending;
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
|
+
const by = vouch?.(m, fp, p) ?? null;
|
|
102
|
+
if (by) {
|
|
103
|
+
t.members[m.member] = { fp, ...(key ? { key } : {}), since: at, ...(cur.signs ? { signs: cur.signs } : {}) };
|
|
104
|
+
out.push({ member: m.member, kind: "moved", fp, was: cur.fp, by });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
101
107
|
if (cur.pending?.fp !== fp) {
|
|
102
108
|
cur.pending = { fp, ...(key ? { key } : {}), since: at };
|
|
103
109
|
out.push({ member: m.member, kind: "changed", fp, was: cur.fp });
|
package/dist/plans.js
CHANGED
|
@@ -52,4 +52,27 @@ export function planLimits(plans, plan, override) {
|
|
|
52
52
|
return plans[plan] ?? plans["free"] ?? DEFAULT_PLANS["free"];
|
|
53
53
|
}
|
|
54
54
|
export const fmtLimit = (n) => n >= UNLIMITED ? "unlimited" : String(n);
|
|
55
|
+
export function lastSlotNotice(plan) {
|
|
56
|
+
if (!plan || plan.limits.spaces >= UNLIMITED)
|
|
57
|
+
return null;
|
|
58
|
+
if (plan.usage.spaces + 1 !== plan.limits.spaces)
|
|
59
|
+
return null;
|
|
60
|
+
return `this is the last team your ${plan.name} plan allows (${plan.limits.spaces}); if one of your teams was only a test, pantheon space delete <name> frees its place`;
|
|
61
|
+
}
|
|
62
|
+
export function ownedTeamsIn(message) {
|
|
63
|
+
const m = /you own ((?:"[^"]+"(?:, )?)+)/.exec(message);
|
|
64
|
+
return m ? [...m[1].matchAll(/"([^"]+)"/g)].map((x) => x[1]) : [];
|
|
65
|
+
}
|
|
66
|
+
export function spaceLimitAdvice(teams) {
|
|
67
|
+
if (!teams.length)
|
|
68
|
+
return ["Free a slot: pantheon space delete <team> --yes (from a machine in that team), or upgrade at https://www.joinpantheon.network"];
|
|
69
|
+
const out = ["Free a slot by deleting a team you no longer need (it ends that team for everyone in it):"];
|
|
70
|
+
const width = Math.max(...teams.map((t) => t.name.length));
|
|
71
|
+
for (const t of teams) {
|
|
72
|
+
const note = !t.here ? "not on this machine: run it from one that is in it" : t.alone ? "nobody else is in it" : t.alone === false ? "others are in it" : "";
|
|
73
|
+
out.push(` pantheon space delete ${t.name.padEnd(width)} --yes${note ? ` (${note})` : ""}`);
|
|
74
|
+
}
|
|
75
|
+
out.push("Then run the same pantheon start again. Or upgrade at https://www.joinpantheon.network");
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
55
78
|
//# sourceMappingURL=plans.js.map
|
package/dist/protocol.js
CHANGED
|
@@ -20,6 +20,10 @@ export const ACTS = [
|
|
|
20
20
|
"CLOSE",
|
|
21
21
|
];
|
|
22
22
|
export const SENDABLE = ACTS.filter((a) => a !== "JOIN" && a !== "LEAVE");
|
|
23
|
+
export const UPKEEP_AGENT = "pantheon-upkeep";
|
|
24
|
+
export function siblingSessions(roster, member, session) {
|
|
25
|
+
return (roster.find((m) => m.id === member)?.sessions ?? []).filter((s) => s.id !== session && s.agent !== UPKEEP_AGENT);
|
|
26
|
+
}
|
|
23
27
|
export const LIMITS = {
|
|
24
28
|
bodyChars: 32000,
|
|
25
29
|
summaryRequiredOver: 2000,
|
|
@@ -132,6 +136,7 @@ export const Hello = z.object({
|
|
|
132
136
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
133
137
|
repo_origin: z.string().max(200).optional(),
|
|
134
138
|
delegate: DelegateCertSchema.optional(),
|
|
139
|
+
upkeep: z.boolean().optional(),
|
|
135
140
|
});
|
|
136
141
|
export const OrganizationProfileSchema = z.object({
|
|
137
142
|
version: z.literal(1),
|
package/dist/quickstart.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createServer } from "node:net";
|
|
|
4
4
|
import { basename, join as pathJoin, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
6
6
|
import { lookup } from "node:dns/promises";
|
|
7
|
-
import {
|
|
7
|
+
import { describeRelayFailure, interceptedText, RelayReplyError } from "./network.js";
|
|
8
8
|
import { PantheonClient } from "./client.js";
|
|
9
9
|
import { holdsRepo } from "./repo-find.js";
|
|
10
10
|
import { folderFacts, folderRefusal, folderRefusalHere, isInside, isTemporaryRefusal } from "./folder-guard.js";
|
|
@@ -13,7 +13,7 @@ import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.
|
|
|
13
13
|
import { newKeypair } from "./identity.js";
|
|
14
14
|
import { installForUser } from "./install.js";
|
|
15
15
|
import { writeIntent, installHooks } from "./notify.js";
|
|
16
|
-
import { detectAvailable, agentById, agentsNeedingUnsandboxed } from "./agents.js";
|
|
16
|
+
import { detectAvailable, agentById, agentsNeedingUnsandboxed, folderReaderLine } from "./agents.js";
|
|
17
17
|
import { installService, servicesUnavailable } from "./service.js";
|
|
18
18
|
import { clearTrayQuit } from "./win-host.js";
|
|
19
19
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
@@ -147,16 +147,16 @@ export function wireUp(opts, deps = {}) {
|
|
|
147
147
|
warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
|
|
148
148
|
}
|
|
149
149
|
else {
|
|
150
|
-
tools.push({ tool: "worker", status: `on — answers peers from this folder while you are away (it
|
|
151
|
-
granted.push(`${wakeChosen() === "approved" ? "A teammate you approve from this machine (pantheon wake allow <name>)" : "Your teammates"} can start an agent run that READS ${project} and answers them. It never changes that folder:
|
|
152
|
-
const
|
|
153
|
-
return detectAvailable()
|
|
150
|
+
tools.push({ tool: "worker", status: `on — answers peers from this folder while you are away (it works in a copy; anything it writes is kept as drafts for you)` });
|
|
151
|
+
granted.push(`${wakeChosen() === "approved" ? "A teammate you approve from this machine (pantheon wake allow <name>)" : "Your teammates"} can start an agent run that READS a copy of ${project} and answers them. It never changes that folder: anything it writes is kept as drafts for you to review.`);
|
|
152
|
+
const reader = (() => { try {
|
|
153
|
+
return folderReaderLine(detectAvailable());
|
|
154
154
|
}
|
|
155
155
|
catch {
|
|
156
|
-
return
|
|
156
|
+
return null;
|
|
157
157
|
} })();
|
|
158
|
-
if (!
|
|
159
|
-
warnings.push(
|
|
158
|
+
if (reader && !reader.ok)
|
|
159
|
+
warnings.push(`${reader.line} — until then, teammates' requests for this folder wait for your live session`);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
catch (e) {
|
|
@@ -352,6 +352,10 @@ export async function probeRelay(url, timeoutMs, fetchImpl = fetch) {
|
|
|
352
352
|
const health = url.replace(/^ws/i, "http").replace(/\/+$/, "") + "/health";
|
|
353
353
|
try {
|
|
354
354
|
const r = await fetchImpl(health, { signal: AbortSignal.timeout(timeoutMs) });
|
|
355
|
+
if (r.ok && /html/i.test(r.headers.get("content-type") ?? "")) {
|
|
356
|
+
void r.body?.cancel().catch(() => { });
|
|
357
|
+
return { ok: false, error: new RelayReplyError(r.status, "html", health) };
|
|
358
|
+
}
|
|
355
359
|
if (r.ok)
|
|
356
360
|
return { ok: true };
|
|
357
361
|
return { ok: false, error: new Error(`${health} answered HTTP ${r.status}`) };
|
|
@@ -421,7 +425,7 @@ export async function start(opts) {
|
|
|
421
425
|
if (chosen && !hostHere) {
|
|
422
426
|
const probe = await probeRelay(reach.url, opts.remoteTimeoutMs ?? 10_000, opts.fetchImpl);
|
|
423
427
|
if (!probe.ok) {
|
|
424
|
-
return nothing(`cannot reach the relay at ${reach.url}: ${
|
|
428
|
+
return nothing(`cannot reach the relay at ${reach.url}: ${await describeRelayFailure(probe.error, reach.url)}` +
|
|
425
429
|
(place === "remote" ? `\n If that address is a tunnel to this machine, start the tunnel first (cloudflared tunnel --url http://localhost:${opts.port}) and run this again — Pantheon hosts the relay here when it sees the tunnel.` : ""));
|
|
426
430
|
}
|
|
427
431
|
opts.log(` relay ${reach.url} (${reach.how})`);
|
|
@@ -522,11 +526,13 @@ export async function start(opts) {
|
|
|
522
526
|
client.close();
|
|
523
527
|
}
|
|
524
528
|
};
|
|
529
|
+
if (!reuse && opts.beforeCreate)
|
|
530
|
+
await opts.beforeCreate(cfg).catch(() => { });
|
|
525
531
|
try {
|
|
526
532
|
await claim(cfg);
|
|
527
533
|
}
|
|
528
534
|
catch (e) {
|
|
529
|
-
const msg = e.message;
|
|
535
|
+
const msg = (await interceptedText(e, cfg.relay)) ?? e.message;
|
|
530
536
|
const nameTaken = !reuse && /BAD_TOKEN/.test(msg);
|
|
531
537
|
if (nameTaken && !opts.space) {
|
|
532
538
|
const alt = `${cfg.space}-${cfg.member}`;
|
package/dist/refusal.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const NET_CODE = /^(ECONN\w*|ENOTFOUND|ETIMEDOUT|EAI_\w+|EPROTO|EHOSTUNREACH|ENETUNREACH|EPIPE|ECONNRESET|UND_ERR_\w*|ERR_\w+)$/;
|
|
2
2
|
const FIX = {
|
|
3
|
-
NOT_SIGNED_IN: "sign in on this machine: pantheon
|
|
4
|
-
KEY_MISMATCH: "a member of the team runs pantheon admit <you> --forget and sends you a new invite (or pick another name with --me)",
|
|
3
|
+
NOT_SIGNED_IN: "sign in on this machine, in a terminal: pantheon signin (your email, then the emailed code — check spam), then try again",
|
|
4
|
+
KEY_MISMATCH: "if this is you on a new machine: run pantheon recover <team> here in a terminal (it signs you in with your email first) to move your seat; otherwise a member of the team runs pantheon admit <you> --forget and sends you a new invite (or pick another name with --me)",
|
|
5
5
|
REVOKED: "ask a member of the team to let you back in: pantheon admit <you>",
|
|
6
6
|
PLAN_LIMIT: "the team's owner frees a seat (pantheon invite --revoke all cancels unused invites) or upgrades the plan",
|
|
7
7
|
BAD_TOKEN: "this machine's copy of the team token is wrong — rejoin with a fresh invite: pantheon join <invite>",
|
|
@@ -36,7 +36,7 @@ export function signInHint(code, relay, hosted = process.env.PANTHEON_DEFAULT_RE
|
|
|
36
36
|
catch {
|
|
37
37
|
return "";
|
|
38
38
|
}
|
|
39
|
-
return `\n This team is on ${relay}, so sign in there: pantheon
|
|
40
|
-
`\n (new here: pantheon signup --email you@work.com --username you --relay ${relay})
|
|
39
|
+
return `\n This team is on ${relay}, so sign in there: pantheon signin --relay ${relay} (in a terminal: your email, then the emailed code — check spam)` +
|
|
40
|
+
`\n Or two commands: pantheon login --email you@work.com --relay ${relay} (new here: pantheon signup --email you@work.com --username you --relay ${relay}), then pantheon verify <code>`;
|
|
41
41
|
}
|
|
42
42
|
//# sourceMappingURL=refusal.js.map
|