@join-pantheon/cli 0.33.3 → 0.33.4-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accounts.js +53 -0
- package/dist/board.js +1 -2
- package/dist/cli.js +183 -22
- package/dist/config.js +2 -1
- package/dist/disk-writer.js +404 -0
- package/dist/doctor.js +6 -3
- package/dist/ledger.js +31 -14
- package/dist/notifier.js +1 -1
- package/dist/quickstart.js +18 -6
- package/dist/recover-offer.js +125 -0
- package/dist/relay.js +258 -74
- package/dist/roster-file.js +11 -6
- package/dist/store.js +28 -7
- package/dist/team-name.js +58 -0
- package/dist/tray.js +5 -0
- package/dist/worker.js +5 -3
- package/helper/macos/main.swift +5 -3
- package/helper/macos/prebuilt/manifest.json +3 -3
- package/helper/macos/prebuilt/pantheon-hud +0 -0
- package/installer/install.ps1 +12 -2
- package/installer/install.sh +5 -0
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -2,9 +2,31 @@ import { DEFAULT_PLANS, parsePlans, parseLimits, planLimits } from "./plans.js";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { chmodSync, closeSync, mkdirSync, openSync, readdirSync, rmSync } from "node:fs";
|
|
5
|
+
import { Worker } from "node:worker_threads";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { fingerprint } from "./identity.js";
|
|
7
8
|
const nodeRequire = createRequire(import.meta.url);
|
|
9
|
+
const BACKUP_WORKER = `
|
|
10
|
+
const { workerData, parentPort } = require("node:worker_threads");
|
|
11
|
+
const fs = require("node:fs"), path = require("node:path");
|
|
12
|
+
const warn = process.emitWarning.bind(process);
|
|
13
|
+
process.emitWarning = (w, ...r) => { if (String((w && w.message) || w).includes("SQLite")) return; return warn(w, ...r); };
|
|
14
|
+
try {
|
|
15
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
16
|
+
const { db, dir, file, keep } = workerData;
|
|
17
|
+
const conn = new DatabaseSync(db);
|
|
18
|
+
try {
|
|
19
|
+
conn.exec("PRAGMA busy_timeout = 10000");
|
|
20
|
+
conn.exec("VACUUM INTO '" + file.replace(/'/g, "''") + "'");
|
|
21
|
+
} finally { conn.close(); }
|
|
22
|
+
try { fs.chmodSync(file, 0o600); } catch {}
|
|
23
|
+
const snaps = fs.readdirSync(dir).filter((f) => f.startsWith("accounts-") && f.endsWith(".db")).sort();
|
|
24
|
+
for (const old of snaps.slice(0, Math.max(0, snaps.length - keep))) { try { fs.rmSync(path.join(dir, old)); } catch {} }
|
|
25
|
+
parentPort.postMessage({ ok: true });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
parentPort.postMessage({ ok: false, error: (e && e.message) || String(e) });
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
8
30
|
const CODE_TTL_MS = 10 * 60 * 1_000;
|
|
9
31
|
const CODE_RESEND_COOLDOWN_MS = 60 * 1_000;
|
|
10
32
|
const MAX_ATTEMPTS = 5;
|
|
@@ -28,8 +50,10 @@ export function validEmail(email) {
|
|
|
28
50
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
29
51
|
}
|
|
30
52
|
export class Accounts {
|
|
53
|
+
dbPath;
|
|
31
54
|
db;
|
|
32
55
|
constructor(dbPath) {
|
|
56
|
+
this.dbPath = dbPath;
|
|
33
57
|
if (dbPath !== ":memory:") {
|
|
34
58
|
mkdirSync(dirname(dbPath), { recursive: true, mode: 0o700 });
|
|
35
59
|
try {
|
|
@@ -660,6 +684,35 @@ export class Accounts {
|
|
|
660
684
|
}
|
|
661
685
|
return file;
|
|
662
686
|
}
|
|
687
|
+
backupAsync(dir, keep = 14) {
|
|
688
|
+
if (this.dbPath === ":memory:") {
|
|
689
|
+
try {
|
|
690
|
+
return Promise.resolve(this.backup(dir, keep));
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
return Promise.reject(e);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
mkdirSync(dir, { recursive: true });
|
|
697
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
698
|
+
const file = join(dir, `accounts-${stamp}.db`);
|
|
699
|
+
return new Promise((resolve, reject) => {
|
|
700
|
+
const w = new Worker(BACKUP_WORKER, { eval: true, workerData: { db: this.dbPath, dir, file, keep } });
|
|
701
|
+
let settled = false;
|
|
702
|
+
w.once("message", (m) => { settled = true; if (m.ok)
|
|
703
|
+
resolve(file);
|
|
704
|
+
else
|
|
705
|
+
reject(new Error(m.error ?? "backup failed")); });
|
|
706
|
+
w.once("error", (e) => { if (!settled) {
|
|
707
|
+
settled = true;
|
|
708
|
+
reject(e);
|
|
709
|
+
} });
|
|
710
|
+
w.once("exit", (code) => { if (!settled) {
|
|
711
|
+
settled = true;
|
|
712
|
+
reject(new Error(`backup worker exited ${code}`));
|
|
713
|
+
} });
|
|
714
|
+
});
|
|
715
|
+
}
|
|
663
716
|
auditLog(opts = {}) {
|
|
664
717
|
const where = [];
|
|
665
718
|
const args = [];
|
package/dist/board.js
CHANGED
|
@@ -527,8 +527,7 @@ export async function startBoardServer(o = {}) {
|
|
|
527
527
|
sendJson(res, 400, { ok: false, error: "bad request" });
|
|
528
528
|
return;
|
|
529
529
|
}
|
|
530
|
-
const
|
|
531
|
-
const r = await deps.cli(["history", "--goal", goal, "--json", "--last", "40", ...(teams.length > 1 ? ["--space", space] : [])], 20_000);
|
|
530
|
+
const r = await deps.cli(["history", "--goal", goal, "--json", "--last", "40", "--space", space], 20_000);
|
|
532
531
|
try {
|
|
533
532
|
const acts = JSON.parse(r.out);
|
|
534
533
|
sendJson(res, 200, { ok: true, acts: acts.map((a) => ({ id: String(a.id ?? ""), act: String(a.act ?? ""), from: String(a.from ?? ""), ts: Number(a.ts) || 0, body: String(a.body ?? "").slice(0, 6000), ...(a.summary ? { summary: String(a.summary) } : {}) })) });
|
package/dist/cli.js
CHANGED
|
@@ -38,7 +38,9 @@ import { homedir } from "node:os";
|
|
|
38
38
|
import { removeHook, decideStop, writeIntent, clearIntent, readIntent, repairNote, installHooks, agentHookStates } from "./notify.js";
|
|
39
39
|
import { pending, render as renderPending } from "./pending.js";
|
|
40
40
|
import { signedRead } from "./reads.js";
|
|
41
|
-
import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel, bindableFolder } from "./quickstart.js";
|
|
41
|
+
import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel, bindableFolder, startReconnects } from "./quickstart.js";
|
|
42
|
+
import { alreadyMemberLine, askedLine, configureLines, fetchSeats, inUseLine, offerSeats, seatsForTeam, seatsToMove, startGate, RECOVER_WAITING_EXIT } from "./recover-offer.js";
|
|
43
|
+
import { shellArg, teamNameProblem } from "./team-name.js";
|
|
42
44
|
import { folderRefusalHere, isTemporaryRefusal, projectFolderAnswer } from "./folder-guard.js";
|
|
43
45
|
import { installService, serviceInstalled, serviceLogFile, uninstallService } from "./service.js";
|
|
44
46
|
import { LEGACY_TASK_NAME, hostAlive, hostFiles, readHostState, recordedTaskName } from "./win-host.js";
|
|
@@ -114,6 +116,39 @@ async function requireSignInOnAccountsRelay(relayFlag) {
|
|
|
114
116
|
if (hasAccounts)
|
|
115
117
|
die(`sign in first — ${relayFlag} verifies email.${signInHint("NOT_SIGNED_IN", relayFlag)}\n Then run this same pantheon start again.`);
|
|
116
118
|
}
|
|
119
|
+
async function accountSeats(relay) {
|
|
120
|
+
const id = identity();
|
|
121
|
+
if (!id?.account)
|
|
122
|
+
return null;
|
|
123
|
+
if (relay && !sameHost(relay.replace(/^ws/i, "http"), id.relay.replace(/^ws/i, "http")))
|
|
124
|
+
return null;
|
|
125
|
+
const base = id.relay.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
126
|
+
return Promise.race([
|
|
127
|
+
fetchSeats(() => signedFetch(id, "GET", "/account/recover").then(async (x) => ({ status: x.status, j: await relayJson(x, `${base}/account/recover`) }))),
|
|
128
|
+
new Promise((res) => setTimeout(() => res(null), 8_000).unref()),
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
async function askOnce(q) {
|
|
132
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
133
|
+
try {
|
|
134
|
+
return await questionOrNull(rl, q);
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function runRecover(team) {
|
|
141
|
+
const { spawnSync } = await import("node:child_process");
|
|
142
|
+
const keep = argv.filter((a) => /^--(no-worker|no-hook|no-notifications|no-menubar|no-guide|yes)$/.test(a));
|
|
143
|
+
return spawnSync(process.execPath, [process.argv[1], "recover", team, ...keep], { stdio: "inherit" }).status ?? 1;
|
|
144
|
+
}
|
|
145
|
+
async function offerSeatsHere(seats, o = {}) {
|
|
146
|
+
const list = seats === undefined ? await accountSeats() : seats;
|
|
147
|
+
if (!list)
|
|
148
|
+
return null;
|
|
149
|
+
configureLines({ launcher: managedLayout()?.launcher ?? null });
|
|
150
|
+
return offerSeats(list, { tty: o.tty ?? !!process.stdin.isTTY, ask: askOnce, recover: runRecover, log: (l) => console.error(l) });
|
|
151
|
+
}
|
|
117
152
|
async function interactiveOnboard(relayArg, opts = {}) {
|
|
118
153
|
const prior = identity();
|
|
119
154
|
const relay = relayArg ?? prior?.relay ?? process.env.PANTHEON_RELAY ?? DEFAULT_RELAY;
|
|
@@ -257,6 +292,10 @@ async function interactiveOnboard(relayArg, opts = {}) {
|
|
|
257
292
|
console.error(`\n ${C("You're in")} — signed in as ${B(acct.username)} <${acct.email}> on this machine.\n`);
|
|
258
293
|
if (opts.joining || opts.starting)
|
|
259
294
|
return true;
|
|
295
|
+
rl.close();
|
|
296
|
+
const res = await offerSeatsHere();
|
|
297
|
+
if (res?.offered.length)
|
|
298
|
+
console.error("");
|
|
260
299
|
if (opts.signin) {
|
|
261
300
|
console.error(` ${B("Signed in — tell your agent to continue;")} it will run the join or start again.`);
|
|
262
301
|
console.error(D(` (On your own? ${C("pantheon start")} creates a team; ${C("pantheon join <invite-link>")} joins one.)`));
|
|
@@ -626,11 +665,23 @@ async function teamLimitAdvice(warning) {
|
|
|
626
665
|
const repoLabel = (r) => r.origin ?? basename(r.root);
|
|
627
666
|
const repoShare = (r) => (r.origin ? r.origin.replace(/^.*[:/]([^:/]+)$/, "$1") : basename(r.root));
|
|
628
667
|
const teamFlag = (cfg) => (flag("space") ? ` --space ${cfg.space}` : "");
|
|
668
|
+
function sayDefaultAfter(before, gone) {
|
|
669
|
+
if (before.default_space !== gone.id)
|
|
670
|
+
return;
|
|
671
|
+
const now = loadAll();
|
|
672
|
+
const left = Object.values(now?.spaces ?? {});
|
|
673
|
+
if (left.length === 1)
|
|
674
|
+
console.error(D(` "${left[0].name}" is your default team now.`));
|
|
675
|
+
else if (left.length > 1)
|
|
676
|
+
console.error(D(` It was your default team; none is picked now. A command run from a folder bound to no team asks which (pantheon use <team> picks a default). The menu bar, alerts and worker cover all ${left.length} teams either way.`));
|
|
677
|
+
}
|
|
629
678
|
function need() {
|
|
630
679
|
const r = resolveFor(process.cwd());
|
|
631
680
|
if (flag("space") && !["start", "profile"].includes(cmd)) {
|
|
632
681
|
const f = loadAll();
|
|
633
682
|
const m = f && findMembership(f, flag("space"));
|
|
683
|
+
if ((!f || !m) && r.cfg && r.cfg.space === flag("space") && r.reason === "env")
|
|
684
|
+
return r.cfg;
|
|
634
685
|
if (!f || !m)
|
|
635
686
|
die(`you are not in a team called "${flag("space")}". Yours: ${Object.values(f?.spaces ?? {}).map((x) => x.name).join(", ") || "none"}`);
|
|
636
687
|
return r.cfg && (r.cfg.spaceId === m.id || r.cfg.space === m.name) && r.reason !== "default" ? r.cfg : configFor(f, m, null);
|
|
@@ -1191,13 +1242,13 @@ function usageFor(command, help) {
|
|
|
1191
1242
|
return out.length ? out.join("\n") : null;
|
|
1192
1243
|
}
|
|
1193
1244
|
const COMMAND_FLAGS = {
|
|
1194
|
-
start: "--space <name> --relay <url> --me <name> --port <n> --ttl <minutes> --no-worker --no-hook --no-notifications --no-menubar --reset --yes\n and every setup answer as a flag (--name, --title, --wake, --commands, --alerts, --decides …): pantheon setup --questions",
|
|
1245
|
+
start: "--space <name> --relay <url> --me <name> --port <n> --ttl <minutes> --no-worker --no-hook --no-notifications --no-menubar --reset --yes --new-team (a team even though you have a seat to bring here)\n and every setup answer as a flag (--name, --title, --wake, --commands, --alerts, --decides …): pantheon setup --questions",
|
|
1195
1246
|
join: "<invite link | line | code> --relay <url> --me <name> --no-worker --no-hook --no-notifications --no-menubar --reset --yes\n and every setup answer as a flag: pantheon setup --questions",
|
|
1196
1247
|
invite: "--list --revoke <token|all> --may-wake | --no-wake --ttl <minutes>",
|
|
1197
1248
|
admit: "<member> --may-wake | --no-wake --revoke --forget --yes",
|
|
1198
1249
|
wake: "allow <member> | deny <member> | list --space <team>",
|
|
1199
1250
|
trust: "[<member>] --yes --space <team>",
|
|
1200
|
-
recover: "[<team>] | withdraw <team> | confirm <member> | cancel <member> [--yes] --space <team> --no-worker --no-hook --no-notifications --no-menubar",
|
|
1251
|
+
recover: "[<team>] | withdraw <team> | confirm <member> | cancel <member> [--yes] --space <team> --no-worker --no-hook --no-notifications --no-menubar\n exits 3 when the move was asked for and waits for a teammate (or a day); 0 when it moved",
|
|
1201
1252
|
say: "<ACT> <text> --to <member>[,<member>] --goal <id> --ref <id | seq | last> --summary '<one line>' --domains <a,b> --branch --commit --remote (DELIVERED) --space <team>",
|
|
1202
1253
|
goal: "list | open \"<title>\" --with <member>[,…] [--acceptance \"…\"] [--deadline <ISO time>] [--list] | close <id> \"<summary>\"",
|
|
1203
1254
|
history: "--goal <id> --from <member> --last <n> --since <seq> --short --presence --json",
|
|
@@ -1371,8 +1422,48 @@ async function main() {
|
|
|
1371
1422
|
if (setupFlags.errors.length)
|
|
1372
1423
|
die(setupFlags.errors.join("\n") + "\n pantheon setup --questions lists every answer");
|
|
1373
1424
|
const wp = unattendedProject();
|
|
1425
|
+
const named = flag("space") ?? null;
|
|
1426
|
+
const startFile = loadAll();
|
|
1427
|
+
const localTeam = named && startFile ? findMembership(startFile, named) : undefined;
|
|
1374
1428
|
await requireSignInOnHostedRelay(flag("relay"));
|
|
1375
1429
|
await requireSignInOnAccountsRelay(flag("relay"));
|
|
1430
|
+
const reconnect = startReconnects({ ...(named ? { space: named } : {}), reset: argv.includes("--reset") });
|
|
1431
|
+
const newTeam = argv.includes("--new-team") || argv.includes("--yes");
|
|
1432
|
+
let seatsKnown = false;
|
|
1433
|
+
if (!reconnect && !localTeam && identity()?.account) {
|
|
1434
|
+
const seats = await accountSeats(flag("relay"));
|
|
1435
|
+
seatsKnown = !!seats;
|
|
1436
|
+
if (seats) {
|
|
1437
|
+
const tty = !!process.stdin.isTTY;
|
|
1438
|
+
const mine = named ? seats.find((x) => x.space.toLowerCase() === named.toLowerCase()) : undefined;
|
|
1439
|
+
if (mine) {
|
|
1440
|
+
console.error(`\n\x1b[33m ${alreadyMemberLine(mine.space)}\x1b[0m`);
|
|
1441
|
+
if (mine.in_use === true)
|
|
1442
|
+
console.error(` ${inUseLine(mine)}`);
|
|
1443
|
+
if (!tty || mine.in_use === true)
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
const a = ((await askOnce(` Move your seat in ${mine.space} to this machine now? [Y/n] `)) ?? "n").trim().toLowerCase();
|
|
1446
|
+
process.exit(a === "" || a === "y" || a === "yes" ? await runRecover(mine.space) : 1);
|
|
1447
|
+
}
|
|
1448
|
+
const res = named || tty || newTeam || !seatsToMove(seats).length
|
|
1449
|
+
? await offerSeatsHere(seats, { tty: tty && !named }) : null;
|
|
1450
|
+
const gate = await startGate({ named, seats, moved: res?.moved ?? [], asked: res?.asked ?? [], tty, newTeam, ask: askOnce });
|
|
1451
|
+
if (!gate.go) {
|
|
1452
|
+
console.error("");
|
|
1453
|
+
for (const l of gate.lines)
|
|
1454
|
+
console.error(gate.code === 1 ? `\x1b[33m ${l}\x1b[0m` : ` ${l}`);
|
|
1455
|
+
process.exit(gate.code);
|
|
1456
|
+
}
|
|
1457
|
+
if (!named && !argv.includes("--new-team") && argv.includes("--yes") && seatsToMove(seats).some((x) => !res?.moved.includes(x.space) && !res?.asked.includes(x.space))) {
|
|
1458
|
+
console.error(D(` (--yes for a new team beside a seat you have is going away: use --new-team)`));
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (named && !localTeam && seatsKnown) {
|
|
1463
|
+
const bad = teamNameProblem(named);
|
|
1464
|
+
if (bad)
|
|
1465
|
+
die(bad);
|
|
1466
|
+
}
|
|
1376
1467
|
console.error(`${B("pantheon start")}\n`);
|
|
1377
1468
|
if (!flag("space") && !repoInfo(process.cwd()))
|
|
1378
1469
|
console.error(D(` No git repo here, so the team will be named after this folder: "${teamNameFor(process.cwd())}". Pass --space <name> to choose.\n`));
|
|
@@ -1544,6 +1635,14 @@ async function main() {
|
|
|
1544
1635
|
}
|
|
1545
1636
|
case "init": {
|
|
1546
1637
|
const had = argv.includes("--reset") ? null : identity();
|
|
1638
|
+
if (flag("space") && !flag("token")) {
|
|
1639
|
+
const f0 = loadAll();
|
|
1640
|
+
if (!(f0 && findMembership(f0, flag("space")))) {
|
|
1641
|
+
const bad = teamNameProblem(flag("space"));
|
|
1642
|
+
if (bad)
|
|
1643
|
+
die(bad);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1547
1646
|
const kp = had?.publicKey && had.privateKey ? { publicKey: had.publicKey, privateKey: had.privateKey } : newKeypair();
|
|
1548
1647
|
const cfg = {
|
|
1549
1648
|
relay: flag("relay", "ws://localhost:8787"),
|
|
@@ -1614,6 +1713,17 @@ async function main() {
|
|
|
1614
1713
|
const ok = await interactiveOnboard(parsed.relay, { joining: parsed.space });
|
|
1615
1714
|
if (!ok || !identity()?.account)
|
|
1616
1715
|
die(`not signed in yet — the invite is still good.\n When the code arrives: pantheon verify <code> (check spam if it is not in your inbox)\n Then run this same \`pantheon join\` command again.`);
|
|
1716
|
+
const mineAll = await accountSeats(parsed.relay);
|
|
1717
|
+
const own = mineAll ? seatsForTeam(mineAll, parsed.space) : null;
|
|
1718
|
+
const offer = own ? await offerSeatsHere(own) : null;
|
|
1719
|
+
if (offer?.moved.length) {
|
|
1720
|
+
console.error(`\n ${C("done")} — your seat in ${offer.moved[0]} is on this machine; the invite was not needed.`);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
if (offer?.asked.length) {
|
|
1724
|
+
console.error(`\n The invite is not needed for a team you are already in. ${askedLine(offer.asked[0])}`);
|
|
1725
|
+
process.exit(RECOVER_WAITING_EXIT);
|
|
1726
|
+
}
|
|
1617
1727
|
console.error("");
|
|
1618
1728
|
}
|
|
1619
1729
|
if (!identity()?.account && sameHost(parsed.relay, DEFAULT_RELAY)) {
|
|
@@ -2698,6 +2808,7 @@ async function main() {
|
|
|
2698
2808
|
const bound = Object.values(all.bindings ?? {}).filter((b) => b.space === m.id).length;
|
|
2699
2809
|
removeMembership(m.id);
|
|
2700
2810
|
console.error(`${B("deleted")} "${m.name}" — everyone in it was disconnected; its token${bound ? ` and ${bound} folder binding${bound === 1 ? "" : "s"}` : ""} left this machine`);
|
|
2811
|
+
sayDefaultAfter(all, m);
|
|
2701
2812
|
return;
|
|
2702
2813
|
}
|
|
2703
2814
|
case "uninstall":
|
|
@@ -2721,6 +2832,7 @@ async function main() {
|
|
|
2721
2832
|
}
|
|
2722
2833
|
removeMembership(m.id);
|
|
2723
2834
|
console.error(`${B("left")} "${m.name}" — its token is gone from this machine${bound.length ? `, and ${bound.length} binding${bound.length === 1 ? "" : "s"} with it (${bound.join(", ")})` : ""}.`);
|
|
2835
|
+
sayDefaultAfter(all, m);
|
|
2724
2836
|
if (others === null)
|
|
2725
2837
|
console.error(D(`The relay could not be reached to check who else is in "${m.name}". If there are teammates, they still list you; one of them removes you with: pantheon admit ${all.member} --revoke`));
|
|
2726
2838
|
else if (others.length)
|
|
@@ -3204,6 +3316,13 @@ async function main() {
|
|
|
3204
3316
|
delete nextCfg.pendingAuth;
|
|
3205
3317
|
save(nextCfg);
|
|
3206
3318
|
console.error(`${C("verified")} — you are ${B(acct.username)} <${acct.email}> on this device.`);
|
|
3319
|
+
const offer = await offerSeatsHere();
|
|
3320
|
+
if (offer?.moved.length)
|
|
3321
|
+
return;
|
|
3322
|
+
if (offer?.offered.length) {
|
|
3323
|
+
console.error(D(`\nOr, for a team you are not in yet: pantheon join <invite>`));
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3207
3326
|
console.error(pending.purpose === "signup"
|
|
3208
3327
|
? D(`\nNext: pantheon start (create a team) · or pantheon join <invite> (join one)`)
|
|
3209
3328
|
: D(`\nThis device is now signed in. pantheon start / pantheon join work as your account.`));
|
|
@@ -3648,6 +3767,7 @@ async function main() {
|
|
|
3648
3767
|
console.log(`${C("withdrawn")} — your seat in ${team} stays where it was.`);
|
|
3649
3768
|
return;
|
|
3650
3769
|
}
|
|
3770
|
+
let picked = null;
|
|
3651
3771
|
if (!sub) {
|
|
3652
3772
|
const r = await call("GET", "/account/recover");
|
|
3653
3773
|
if (r.status === 404)
|
|
@@ -3658,18 +3778,28 @@ async function main() {
|
|
|
3658
3778
|
console.log(`${B(`Your seats as ${acct.username}`)} ${D(`(this device: ${fingerprint(id.publicKey)})`)}\n`);
|
|
3659
3779
|
if (!seats.length)
|
|
3660
3780
|
console.log(D(" none — you are in no team on this relay yet"));
|
|
3781
|
+
const pickable = seats.filter((s) => s.state === "ready" || s.state === "movable");
|
|
3661
3782
|
for (const s of seats) {
|
|
3662
|
-
const
|
|
3783
|
+
const n = pickable.indexOf(s);
|
|
3784
|
+
const name = `${n >= 0 ? `${n + 1}. ` : " "}${s.space}`.padEnd(21);
|
|
3663
3785
|
const line = s.state === "here" ? D("on this machine") :
|
|
3664
|
-
s.state === "ready" ? `moved here — save it on this machine: ${C(`pantheon recover ${s.space}`)}` :
|
|
3786
|
+
s.state === "ready" ? `moved here — save it on this machine: ${C(`pantheon recover ${shellArg(s.space)}`)}` :
|
|
3665
3787
|
s.state === "waiting" ? `waiting — moves ${new Date(s.due_at).toLocaleString()} unless someone cancels; a teammate can confirm it sooner` :
|
|
3666
|
-
s.state === "movable" ? `on another of your devices (${s.fp}) — move it here: ${C(`pantheon recover ${s.space}`)}${D(s.alone ? " (you are alone in it: at once)" : " (a teammate confirms, or it waits a day)")}` :
|
|
3788
|
+
s.state === "movable" ? `on another of your devices (${s.fp}) — move it here: ${C(`pantheon recover ${shellArg(s.space)}`)}${D(s.alone ? " (you are alone in it: at once)" : " (a teammate confirms, or it waits a day)")}` :
|
|
3667
3789
|
D(`its key ${s.fp} was never signed in to your account here, so it cannot be moved by email — a teammate who has checked it is you: pantheon admit ${acct.username} --forget, then a new invite`);
|
|
3668
3790
|
console.log(` ${name}${line}${s.founder ? D(" (founder)") : ""}`);
|
|
3669
3791
|
}
|
|
3670
|
-
|
|
3792
|
+
if (!pickable.length || !process.stdin.isTTY)
|
|
3793
|
+
return;
|
|
3794
|
+
const a = ((await askOnce(`\n Move which seat to this machine? Its number (Enter for none): `)) ?? "").trim();
|
|
3795
|
+
const i = Number(a);
|
|
3796
|
+
if (!a)
|
|
3797
|
+
return;
|
|
3798
|
+
if (!Number.isInteger(i) || i < 1 || i > pickable.length)
|
|
3799
|
+
die(`no seat numbered ${a} — run pantheon recover again`);
|
|
3800
|
+
picked = pickable[i - 1].space;
|
|
3671
3801
|
}
|
|
3672
|
-
const team = sub;
|
|
3802
|
+
const team = sub ?? picked;
|
|
3673
3803
|
let r = await call("POST", "/account/recover", { space: team });
|
|
3674
3804
|
if (r.status === 403 && r.j.reason === "sign_in_again" && process.stdin.isTTY) {
|
|
3675
3805
|
console.error(`${B("First, prove it is you")} — a code goes to ${C(acct.email)} (look in spam too if it is not there in a minute).`);
|
|
@@ -3694,7 +3824,7 @@ async function main() {
|
|
|
3694
3824
|
}
|
|
3695
3825
|
rl.close();
|
|
3696
3826
|
if (!ok)
|
|
3697
|
-
die(`not verified — run pantheon recover ${team} again for a new code`);
|
|
3827
|
+
die(`not verified — run pantheon recover ${shellArg(team)} again for a new code`);
|
|
3698
3828
|
r = await call("POST", "/account/recover", { space: team });
|
|
3699
3829
|
}
|
|
3700
3830
|
if (r.status === 404 && !r.j.reason)
|
|
@@ -3705,9 +3835,9 @@ async function main() {
|
|
|
3705
3835
|
const others = (r.j.others ?? []);
|
|
3706
3836
|
console.log(`${B(`Asked: your seat in ${team} moves to this machine`)} ${D(`(new key ${String(r.j.fp)})`)}`);
|
|
3707
3837
|
console.log(` It moves when a teammate confirms it — ${others.join(", ") || "your teammates"} ${others.length === 1 ? "was" : "were"} told — or on its own ${new Date(Number(r.j.due_at)).toLocaleString()} if nobody cancels.`);
|
|
3708
|
-
console.log(` Speed it up: ask one of them to check your key with you (read them "this device" from pantheon whoami ) and run ${C(`pantheon recover confirm ${acct.username} --space ${team}`)}`);
|
|
3709
|
-
console.log(D(` Once it has moved, run pantheon recover ${team} again here to save the team on this machine.`));
|
|
3710
|
-
|
|
3838
|
+
console.log(` Speed it up: ask one of them to check your key with you (read them "this device" from pantheon whoami ) and run ${C(`pantheon recover confirm ${acct.username} --space ${shellArg(team)}`)}`);
|
|
3839
|
+
console.log(D(` Once it has moved, run pantheon recover ${shellArg(team)} again here to save the team on this machine.`));
|
|
3840
|
+
process.exit(RECOVER_WAITING_EXIT);
|
|
3711
3841
|
}
|
|
3712
3842
|
const token = String(r.j.invite ?? "");
|
|
3713
3843
|
if (r.j.state !== "moved" || !token)
|
|
@@ -3717,7 +3847,7 @@ async function main() {
|
|
|
3717
3847
|
console.error(D(` If the old machine is lost or not yours any more, sign it out of your account too: pantheon sessions (then pantheon sessions revoke <device-id> --device)\n`));
|
|
3718
3848
|
const { spawnSync: run } = await import("node:child_process");
|
|
3719
3849
|
const invite = toInvite({ ...id, space: String(r.j.space ?? team), token });
|
|
3720
|
-
const keep = argv.slice(
|
|
3850
|
+
const keep = argv.slice(1).filter((a) => /^--(no-worker|no-hook|no-notifications|no-menubar|no-guide|yes)$/.test(a));
|
|
3721
3851
|
const res = run(process.execPath, [process.argv[1], "join", invite, ...keep], { stdio: "inherit" });
|
|
3722
3852
|
process.exit(res.status ?? 1);
|
|
3723
3853
|
}
|
|
@@ -4087,9 +4217,21 @@ async function main() {
|
|
|
4087
4217
|
if (argv.includes("--only-action") && !hud)
|
|
4088
4218
|
refuse("that action was already taken or is no longer there — click it again");
|
|
4089
4219
|
}
|
|
4090
|
-
|
|
4091
|
-
if (!hud?.space)
|
|
4220
|
+
let cfg = (() => {
|
|
4221
|
+
if (!hud?.space) {
|
|
4222
|
+
const r = resolveFor(process.cwd());
|
|
4223
|
+
if (r.cfg?.space)
|
|
4224
|
+
return need();
|
|
4225
|
+
const f = loadAll();
|
|
4226
|
+
const first = Object.values(f?.spaces ?? {})[0];
|
|
4227
|
+
if (f && first && (!hud || ["details", "open", "fix", "done", "wake"].includes(hud.action)))
|
|
4228
|
+
return configFor(f, first, null);
|
|
4229
|
+
if (hud && (hud.action === "reply" || hud.action === "retract")) {
|
|
4230
|
+
console.log(`not sent: which team it belongs to was not recorded — answer it from the board`);
|
|
4231
|
+
process.exit(1);
|
|
4232
|
+
}
|
|
4092
4233
|
return need();
|
|
4234
|
+
}
|
|
4093
4235
|
const f = loadAll();
|
|
4094
4236
|
const m = f ? findMembership(f, hud.space) : undefined;
|
|
4095
4237
|
if (f && m)
|
|
@@ -4222,6 +4364,13 @@ async function main() {
|
|
|
4222
4364
|
const wa = ref ? loadPolicy().wakeAsks?.[ref] : undefined;
|
|
4223
4365
|
if (!wa || !hud.member || wa.member !== hud.member)
|
|
4224
4366
|
notSent("that question is no longer open on this machine — pantheon wake list shows who may wake it");
|
|
4367
|
+
if (!hud.space) {
|
|
4368
|
+
const f = loadAll();
|
|
4369
|
+
const m = f ? findMembership(f, wa.team) : undefined;
|
|
4370
|
+
if (!f || !m)
|
|
4371
|
+
notSent(`that question is about the team "${wa.team}", which this machine is no longer in`);
|
|
4372
|
+
cfg = configFor(f, m, null);
|
|
4373
|
+
}
|
|
4225
4374
|
if (![cfg.spaceId, cfg.space].includes(wa.team))
|
|
4226
4375
|
notSent(`that question is about the team "${wa.team}", not "${hud.space ?? cfg.space}" — answer it from that team's board`);
|
|
4227
4376
|
const allow = hud.allow === true;
|
|
@@ -4252,11 +4401,22 @@ async function main() {
|
|
|
4252
4401
|
return;
|
|
4253
4402
|
}
|
|
4254
4403
|
if (hud?.action === "done") {
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4404
|
+
const tries = hud.space ? [cfg] : (() => { const f = loadAll(); const all = f ? Object.values(f.spaces ?? {}).map((m) => configFor(f, m, null)) : []; return all.length ? all : [cfg]; })();
|
|
4405
|
+
let c2 = null;
|
|
4406
|
+
let o;
|
|
4407
|
+
for (const tc of tries) {
|
|
4408
|
+
const c = new PantheonClient(tc, undefined, { role: "cli" });
|
|
4409
|
+
const ok = await c.connect().then(() => true, (e) => { if (tries.length === 1)
|
|
4410
|
+
die(connectFailure(e, tc.relay)); return false; });
|
|
4411
|
+
const hit = ok && hud.decision ? c.obligations.find((x) => x.id === hud.decision) : undefined;
|
|
4412
|
+
if (hit) {
|
|
4413
|
+
c2 = c;
|
|
4414
|
+
o = hit;
|
|
4415
|
+
break;
|
|
4416
|
+
}
|
|
4417
|
+
c.close();
|
|
4418
|
+
}
|
|
4419
|
+
if (!c2 || !o) {
|
|
4260
4420
|
console.error(D("that decision is no longer waiting on you"));
|
|
4261
4421
|
return;
|
|
4262
4422
|
}
|
|
@@ -4810,8 +4970,9 @@ async function main() {
|
|
|
4810
4970
|
return;
|
|
4811
4971
|
}
|
|
4812
4972
|
case "menubar": {
|
|
4813
|
-
|
|
4814
|
-
|
|
4973
|
+
const { ensureTrayBinary, writeTrayCommands, menubarRefusal } = await import("./tray.js");
|
|
4974
|
+
if (menubarRefusal(loadAll(), load()))
|
|
4975
|
+
need();
|
|
4815
4976
|
const { spawn } = await import("node:child_process");
|
|
4816
4977
|
let binary;
|
|
4817
4978
|
try {
|
package/dist/config.js
CHANGED
|
@@ -6,6 +6,7 @@ import { join, resolve, sep } from "node:path";
|
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
7
|
import { repoName } from "./repo-route.js";
|
|
8
8
|
import { markPantheonHome } from "./home-safety.js";
|
|
9
|
+
import { TEAM_NAME } from "./team-name.js";
|
|
9
10
|
export const CONFIG_DIR = process.env.PANTHEON_HOME ?? join(homedir(), ".pantheon");
|
|
10
11
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
11
12
|
export const DEFAULT_RELAY = process.env.PANTHEON_DEFAULT_RELAY
|
|
@@ -596,7 +597,7 @@ export function toInvite(cfg) {
|
|
|
596
597
|
}
|
|
597
598
|
export const JOIN_LINK_BASE = "https://www.joinpantheon.network/join";
|
|
598
599
|
const HOSTED_RELAY = "wss://relay.joinpantheon.network";
|
|
599
|
-
const JOIN_SLUG =
|
|
600
|
+
const JOIN_SLUG = TEAM_NAME;
|
|
600
601
|
export function toJoinLink(cfg) {
|
|
601
602
|
if (cfg.relay.replace(/\/+$/, "") !== HOSTED_RELAY || !JOIN_SLUG.test(cfg.space))
|
|
602
603
|
return null;
|