@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/roster-file.js
CHANGED
|
@@ -29,6 +29,7 @@ export class Allowlist {
|
|
|
29
29
|
name = null;
|
|
30
30
|
extra = {};
|
|
31
31
|
repos = new Map();
|
|
32
|
+
recoveries = new Map();
|
|
32
33
|
file;
|
|
33
34
|
constructor(dir, space) {
|
|
34
35
|
this.file = join(dir, `${space}.members.json`);
|
|
@@ -69,7 +70,13 @@ export class Allowlist {
|
|
|
69
70
|
this.removedAt = doc.removed_at;
|
|
70
71
|
if (legacyRemoval && this.removedAt === null)
|
|
71
72
|
this.removedAt = Date.now();
|
|
72
|
-
const known = new Set(["members", "invites", "tokenHash", "retiredTokenHashes", "removed_at", "id", "name", "repos", "founder", "founder_guessed"]);
|
|
73
|
+
const known = new Set(["members", "invites", "tokenHash", "retiredTokenHashes", "removed_at", "id", "name", "repos", "founder", "founder_guessed", "recoveries"]);
|
|
74
|
+
if (Array.isArray(doc.recoveries)) {
|
|
75
|
+
for (const r of doc.recoveries) {
|
|
76
|
+
if (r && typeof r.member === "string" && typeof r.pubkey === "string" && typeof r.due_at === "number")
|
|
77
|
+
this.recoveries.set(r.member, r);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
73
80
|
if (!Array.isArray(raw))
|
|
74
81
|
for (const [k, v] of Object.entries(raw))
|
|
75
82
|
if (!known.has(k))
|
|
@@ -123,6 +130,7 @@ export class Allowlist {
|
|
|
123
130
|
...(this.retiredHashes.length ? { retiredTokenHashes: this.retiredHashes } : {}),
|
|
124
131
|
...(this.removedAt !== null ? { removed_at: this.removedAt } : {}),
|
|
125
132
|
...(this.repos.size ? { repos: [...this.repos.values()] } : {}),
|
|
133
|
+
...(this.recoveries.size ? { recoveries: [...this.recoveries.values()] } : {}),
|
|
126
134
|
}, null, 2) + "\n");
|
|
127
135
|
}
|
|
128
136
|
peekId() { return this.id; }
|
|
@@ -211,11 +219,12 @@ export class Allowlist {
|
|
|
211
219
|
get hasToken() {
|
|
212
220
|
return this.tokenHash !== null;
|
|
213
221
|
}
|
|
214
|
-
mintInvite(by, mayWake, ttlMinutes, token, spaceToken) {
|
|
222
|
+
mintInvite(by, mayWake, ttlMinutes, token, spaceToken, recoveryFor) {
|
|
215
223
|
const inv = {
|
|
216
224
|
token, created_by: by, created_at: Date.now(),
|
|
217
225
|
expires_at: Date.now() + ttlMinutes * 60_000, may_wake: mayWake,
|
|
218
226
|
...(spaceToken ? { space_token: spaceToken } : {}),
|
|
227
|
+
...(recoveryFor ? { recovery_for: recoveryFor } : {}),
|
|
219
228
|
};
|
|
220
229
|
this.invites.set(token, inv);
|
|
221
230
|
this.flush();
|
|
@@ -231,7 +240,16 @@ export class Allowlist {
|
|
|
231
240
|
if (inv.used_by)
|
|
232
241
|
return { ok: false, why: `that invite was already used by "${inv.used_by}" — ask for a fresh one` };
|
|
233
242
|
if (Date.now() > inv.expires_at)
|
|
234
|
-
return { ok: false, why: "that invite has expired — ask for a fresh one" };
|
|
243
|
+
return { ok: false, why: inv.recovery_for ? "that recovery link has expired — run pantheon recover again on the new machine" : "that invite has expired — ask for a fresh one" };
|
|
244
|
+
if (inv.recovery_for) {
|
|
245
|
+
if (member !== inv.recovery_for || !prior?.admitted || prior.publicKey !== publicKey) {
|
|
246
|
+
return { ok: false, why: `that link is for ${inv.recovery_for}'s new device after a recovery, and only that device can use it` };
|
|
247
|
+
}
|
|
248
|
+
inv.used_by = member;
|
|
249
|
+
inv.used_at = Date.now();
|
|
250
|
+
this.flush();
|
|
251
|
+
return { ok: true, entry: prior };
|
|
252
|
+
}
|
|
235
253
|
const existing = this.entries.get(member);
|
|
236
254
|
const removedAt = this.removedWhen(member);
|
|
237
255
|
if (removedAt !== null && (inv.created_at <= removedAt || (this.founderName && inv.created_by !== this.founderName))) {
|
|
@@ -244,7 +262,8 @@ export class Allowlist {
|
|
|
244
262
|
why: `"${member}" founded this team and is registered with a different key — a founder's key cannot be replaced`,
|
|
245
263
|
fix: [
|
|
246
264
|
`If you ARE "${member}" on a new machine: restore your original ~/.pantheon/config.json if you still have it — the key is in it — then redeem this same invite again.`,
|
|
247
|
-
` Without it,
|
|
265
|
+
` Without it, on a relay that verifies email (the hosted one): run pantheon recover <team> in a terminal here: it signs you in with your email and moves your seat to this machine.`,
|
|
266
|
+
` Otherwise nobody can let you back in under "${member}": a team's founder is never removed, so the old key cannot be retired.`,
|
|
248
267
|
` You can join under another name instead (--me <new-name>, this same invite), or start a new team: pantheon start --space <name>.`,
|
|
249
268
|
` If you are not "${member}", join under your own name: --me <your-name>.`,
|
|
250
269
|
].join("\n"),
|
|
@@ -256,6 +275,7 @@ export class Allowlist {
|
|
|
256
275
|
fix: [
|
|
257
276
|
`If you are not "${member}", join under your own name: --me <your-name> (this same invite is still unused).`,
|
|
258
277
|
` If you ARE "${member}" on a new machine: restore your original ~/.pantheon/config.json if you still have it — the key is in it.`,
|
|
278
|
+
` Or, on a relay that verifies email (the hosted one): run pantheon recover <team> in a terminal here: it signs you in with your email and moves your seat to this machine.`,
|
|
259
279
|
` Failing that, a teammate who has confirmed it is you retires the old key (this removes whoever holds "${member}" now): pantheon admit ${member} --forget`,
|
|
260
280
|
` Then redeem this same invite again.`,
|
|
261
281
|
].join("\n"),
|
|
@@ -273,7 +293,7 @@ export class Allowlist {
|
|
|
273
293
|
return { ok: true, entry };
|
|
274
294
|
}
|
|
275
295
|
openInvites() {
|
|
276
|
-
return [...this.invites.values()].filter((i) => !i.used_by && Date.now() < i.expires_at);
|
|
296
|
+
return [...this.invites.values()].filter((i) => !i.used_by && !i.recovery_for && Date.now() < i.expires_at);
|
|
277
297
|
}
|
|
278
298
|
findInvite(token) {
|
|
279
299
|
const exact = this.invites.get(token);
|
|
@@ -418,6 +438,31 @@ export class Allowlist {
|
|
|
418
438
|
this.flush();
|
|
419
439
|
return true;
|
|
420
440
|
}
|
|
441
|
+
recoveryOf(member) { return this.recoveries.get(member) ?? null; }
|
|
442
|
+
recoveryList() { return [...this.recoveries.values()]; }
|
|
443
|
+
requestRecovery(r) {
|
|
444
|
+
this.recoveries.set(r.member, r);
|
|
445
|
+
this.flush();
|
|
446
|
+
}
|
|
447
|
+
cancelRecovery(member) {
|
|
448
|
+
const r = this.recoveries.get(member) ?? null;
|
|
449
|
+
if (r) {
|
|
450
|
+
this.recoveries.delete(member);
|
|
451
|
+
this.flush();
|
|
452
|
+
}
|
|
453
|
+
return r;
|
|
454
|
+
}
|
|
455
|
+
moveSeat(member, pubkey, move) {
|
|
456
|
+
const e = this.entries.get(member);
|
|
457
|
+
if (!e || !e.admitted)
|
|
458
|
+
return null;
|
|
459
|
+
e.moved = { from_fp: fingerprint(e.publicKey), ...move };
|
|
460
|
+
e.publicKey = pubkey;
|
|
461
|
+
this.recoveries.delete(member);
|
|
462
|
+
this.cancelInvitesBy(member);
|
|
463
|
+
this.flush();
|
|
464
|
+
return e;
|
|
465
|
+
}
|
|
421
466
|
describe() {
|
|
422
467
|
if (this.empty)
|
|
423
468
|
return "(no members yet)";
|
package/dist/tray.js
CHANGED
|
@@ -86,6 +86,16 @@ export function arrangeBoard(items, o) {
|
|
|
86
86
|
}
|
|
87
87
|
return out.filter((i) => i.lane).sort(boardOrder);
|
|
88
88
|
}
|
|
89
|
+
export function localWakeAsk(ref) {
|
|
90
|
+
if (!ref)
|
|
91
|
+
return null;
|
|
92
|
+
try {
|
|
93
|
+
return loadPolicy().wakeAsks?.[ref]?.member ?? null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
89
99
|
export function localChoices(ref) {
|
|
90
100
|
if (!ref)
|
|
91
101
|
return null;
|
|
@@ -192,7 +202,8 @@ async function trayStatusOne(cfg) {
|
|
|
192
202
|
const choices = localChoices(o.ref);
|
|
193
203
|
const r = row(o, "decision", !from || from === cfg.member ? "your agent" : from, decisionPrompt(o.id, decisionTitle(o.body)));
|
|
194
204
|
const own = new RegExp(`^${cfg.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (?:escalated|needs a decision):\\s*`, "i");
|
|
195
|
-
|
|
205
|
+
const wake = localWakeAsk(o.ref);
|
|
206
|
+
return { ...r, detail: o.body.replace(own, ""), decider: cfg.member, ...(choices ? { choices } : {}), ...(wake ? { wake } : {}) };
|
|
196
207
|
}),
|
|
197
208
|
...owed.map((o) => row(o, "owed", "you", owedPrompt(decisionTitle(o.body), o.goal))),
|
|
198
209
|
...waiting.map((o) => {
|
package/dist/uninstall.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
4
|
import { posix, win32 } from "node:path";
|
|
5
5
|
import { expectedLauncher, windowsManagedRoots } from "./managed.js";
|
|
@@ -7,6 +7,40 @@ export const BEGIN_MARK = "# >>> pantheon >>>";
|
|
|
7
7
|
export const END_MARK = "# <<< pantheon <<<";
|
|
8
8
|
export const PROFILES = [".profile", ".bash_profile", ".bash_login", ".bashrc", ".zshrc", ".zprofile"];
|
|
9
9
|
export const FISH_CONF = ".config/fish/conf.d/pantheon.fish";
|
|
10
|
+
export const ZSH_PROFILES = [".zshrc", ".zprofile"];
|
|
11
|
+
export function zshDir(home, env = process.env, ask = () => askZsh(env)) {
|
|
12
|
+
const z = (ask() ?? env.ZDOTDIR ?? "").replace(/\/+$/, "");
|
|
13
|
+
if (!z)
|
|
14
|
+
return home;
|
|
15
|
+
const rz = realDir(z), rh = realDir(home);
|
|
16
|
+
return rz && rh && rz.startsWith(`${rh === "/" ? "" : rh}/`) ? posix.join(home, rz.slice(rh.length)) : home;
|
|
17
|
+
}
|
|
18
|
+
function realDir(p) {
|
|
19
|
+
let d = posix.resolve(p);
|
|
20
|
+
const rest = [];
|
|
21
|
+
for (;;) {
|
|
22
|
+
try {
|
|
23
|
+
return posix.join(realpathSync(d), ...rest);
|
|
24
|
+
}
|
|
25
|
+
catch { }
|
|
26
|
+
const up = posix.dirname(d);
|
|
27
|
+
if (up === d)
|
|
28
|
+
return null;
|
|
29
|
+
rest.unshift(posix.basename(d));
|
|
30
|
+
d = up;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function askZsh(env) {
|
|
34
|
+
const mark = "__pantheon_zdotdir__";
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync("zsh", ["-c", `printf "\\n${mark}%s\\n" "\${ZDOTDIR:-}"`], { encoding: "utf8", env, stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 });
|
|
37
|
+
const line = out.split("\n").filter((l) => l.startsWith(mark)).at(-1);
|
|
38
|
+
return line === undefined ? null : line.slice(mark.length);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
10
44
|
export function withoutPathBlock(text) {
|
|
11
45
|
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
12
46
|
const bare = (l) => l.replace(/\n$/, "").replace(/\r$/, "");
|
|
@@ -161,8 +195,9 @@ export function removeManagedInstall(o) {
|
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
197
|
else {
|
|
164
|
-
|
|
165
|
-
|
|
198
|
+
const zdot = o.zdotdir ?? zshDir(o.home, env);
|
|
199
|
+
const files = [...PROFILES.map((n) => p.join(o.home, n)), ...(p.resolve(zdot) !== p.resolve(o.home) ? ZSH_PROFILES.map((n) => p.join(zdot, n)) : [])];
|
|
200
|
+
for (const f of files) {
|
|
166
201
|
if (!fs.exists(f))
|
|
167
202
|
continue;
|
|
168
203
|
let text;
|
package/dist/uri-handler.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join, win32 } from "node:path";
|
|
|
5
5
|
export const URI_SCHEME = "pantheon";
|
|
6
6
|
export const URI_HOSTS = ["board", "done", "explain", "details"];
|
|
7
7
|
const FORMS = {
|
|
8
|
-
board: { required: [], optional: [] },
|
|
8
|
+
board: { required: [], optional: ["focus"] },
|
|
9
9
|
done: { required: ["decision", "k"], optional: ["act", "space"] },
|
|
10
10
|
explain: { required: ["decision", "k"], optional: ["act", "space"] },
|
|
11
11
|
details: { required: ["act", "k"], optional: ["decision", "space"] },
|
|
@@ -15,6 +15,7 @@ export const URI_VALUE = {
|
|
|
15
15
|
act: /^a_[A-Za-z0-9]{6,48}$/,
|
|
16
16
|
space: /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/,
|
|
17
17
|
k: /^[0-9a-f]{32}$/,
|
|
18
|
+
focus: /^[ao]_[A-Za-z0-9]{6,48}$/,
|
|
18
19
|
};
|
|
19
20
|
export const MAX_URI = 400;
|
|
20
21
|
function canonical(host, params) {
|
|
@@ -201,7 +202,7 @@ export async function handleUri(args, d) {
|
|
|
201
202
|
if (d.openBoard) {
|
|
202
203
|
let ok = false;
|
|
203
204
|
try {
|
|
204
|
-
ok = await d.openBoard();
|
|
205
|
+
ok = await d.openBoard(p.params.focus);
|
|
205
206
|
}
|
|
206
207
|
catch {
|
|
207
208
|
ok = false;
|
package/dist/verify-act.js
CHANGED
|
@@ -90,6 +90,23 @@ export function verifyGoal(g, teams, pinnedFp, memberKey, at = Date.now()) {
|
|
|
90
90
|
at = Math.max(sig.ts, g.opened_at || 0);
|
|
91
91
|
return signer(sig, team, g.opened_by, pinnedFp, memberKey, at, "goal");
|
|
92
92
|
}
|
|
93
|
+
export function vouchedMove(m, newFp, teams, pinOf, admitted = () => true) {
|
|
94
|
+
const mv = m.moved;
|
|
95
|
+
if (!mv || mv.how !== "confirmed" || typeof mv.by !== "string" || mv.by === m.member)
|
|
96
|
+
return null;
|
|
97
|
+
const v = mv.vouch;
|
|
98
|
+
if (!v || typeof v !== "object" || v.from !== mv.by || v.act !== "NOTE")
|
|
99
|
+
return null;
|
|
100
|
+
const r = v.meta?.recovery;
|
|
101
|
+
if (!r || r.action !== "confirm" || r.member !== m.member || r.fp !== newFp)
|
|
102
|
+
return null;
|
|
103
|
+
if (r.from_fp !== pinOf(m.member)?.fp || !admitted(mv.by))
|
|
104
|
+
return null;
|
|
105
|
+
const pin = pinOf(mv.by);
|
|
106
|
+
if (!pin || pin.pending)
|
|
107
|
+
return null;
|
|
108
|
+
return verifyAct(v, teams, pin.fp, pin.key).trust === "verified" ? mv.by : null;
|
|
109
|
+
}
|
|
93
110
|
export const trusted = (t) => t === "verified" || t === "verified-run";
|
|
94
111
|
export function trustLabel(r, from) {
|
|
95
112
|
switch (r.trust) {
|
package/dist/worker-fleet.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
|
-
import { holdsRepo } from "./repo-find.js";
|
|
3
|
+
import { holdsRepo, repoInside } from "./repo-find.js";
|
|
4
4
|
import { CONFIG_DIR, configFor, loadAll, repoInfo } from "./config.js";
|
|
5
5
|
import { Worker, workspaceOf } from "./worker.js";
|
|
6
|
+
import { folderReaderLine } from "./agents.js";
|
|
6
7
|
export class WorkerFleet {
|
|
7
8
|
opts;
|
|
8
9
|
dupSaid = new Set();
|
|
9
10
|
slots = new Map();
|
|
10
11
|
stopped = false;
|
|
12
|
+
unservedSaid = new Set();
|
|
13
|
+
idleSaid = "";
|
|
14
|
+
keeper = null;
|
|
15
|
+
alertDue = new Map();
|
|
16
|
+
alertRun = null;
|
|
17
|
+
alertFailSaid = new Map();
|
|
11
18
|
constructor(opts) {
|
|
12
19
|
this.opts = opts;
|
|
13
20
|
}
|
|
@@ -32,6 +39,13 @@ export class WorkerFleet {
|
|
|
32
39
|
continue;
|
|
33
40
|
}
|
|
34
41
|
const ws = teamWorkspaces(f, m.id);
|
|
42
|
+
for (const u of unservedFolders(f, m.id, ws)) {
|
|
43
|
+
const key = `${m.id}|${u.path}|${u.why}`;
|
|
44
|
+
if (this.unservedSaid.has(key))
|
|
45
|
+
continue;
|
|
46
|
+
this.unservedSaid.add(key);
|
|
47
|
+
this.opts.log(`fleet: [${m.name}] ${u.path} is bound but not served: ${u.why}`);
|
|
48
|
+
}
|
|
35
49
|
const home = ws.find((w) => w.kind === "repo") ?? ws[0];
|
|
36
50
|
if (!home)
|
|
37
51
|
continue;
|
|
@@ -39,11 +53,16 @@ export class WorkerFleet {
|
|
|
39
53
|
wanted.set(m.id, { path: home.path, cfg: configFor(f, m, repo) });
|
|
40
54
|
void several;
|
|
41
55
|
}
|
|
56
|
+
if (wanted.size)
|
|
57
|
+
this.stopKeeper();
|
|
58
|
+
else
|
|
59
|
+
this.startKeeper(f);
|
|
60
|
+
this.alertChecks(f, Object.values(f.spaces ?? {}).filter((m) => (idCount.get(m.id) ?? 0) === 1 && !wanted.has(m.id) && this.keeper?.id !== m.id));
|
|
42
61
|
for (const [id, w] of wanted) {
|
|
43
62
|
if (this.slots.has(id))
|
|
44
63
|
continue;
|
|
45
64
|
const home = this.opts.home ?? CONFIG_DIR;
|
|
46
|
-
const { pollMs: _p, readConfig: _r, makeWorker: _m, home: _h, ...rest } = this.opts;
|
|
65
|
+
const { pollMs: _p, readConfig: _r, makeWorker: _m, home: _h, alertsEveryMs: _a, ...rest } = this.opts;
|
|
47
66
|
const wopts = {
|
|
48
67
|
...rest,
|
|
49
68
|
project: w.path,
|
|
@@ -60,6 +79,8 @@ export class WorkerFleet {
|
|
|
60
79
|
started.push(w.cfg.space);
|
|
61
80
|
}
|
|
62
81
|
}
|
|
82
|
+
else
|
|
83
|
+
this.stopKeeper();
|
|
63
84
|
for (const [id, s] of this.slots) {
|
|
64
85
|
if (wanted.has(id))
|
|
65
86
|
continue;
|
|
@@ -69,6 +90,65 @@ export class WorkerFleet {
|
|
|
69
90
|
}
|
|
70
91
|
return { started, stopped };
|
|
71
92
|
}
|
|
93
|
+
startKeeper(f) {
|
|
94
|
+
const teams = Object.values(f.spaces ?? {});
|
|
95
|
+
const m = (f.default_space ? f.spaces?.[f.default_space] : undefined) ?? teams[0];
|
|
96
|
+
if (!m) {
|
|
97
|
+
this.stopKeeper();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.keeper?.id === m.id)
|
|
101
|
+
return;
|
|
102
|
+
this.stopKeeper();
|
|
103
|
+
const home = this.opts.home ?? CONFIG_DIR;
|
|
104
|
+
const { pollMs: _p, readConfig: _r, makeWorker: _m, home: _h, alertsEveryMs: _a, ...rest } = this.opts;
|
|
105
|
+
const cfg = configFor(f, m, null);
|
|
106
|
+
const wopts = {
|
|
107
|
+
...rest, housekeeping: true, once: false, project: home, workspaces: () => [],
|
|
108
|
+
scratch: join(home, "worktrees", encodeURIComponent(m.id)), home,
|
|
109
|
+
log: (l) => this.opts.log(`[${cfg.space}] ${l}`),
|
|
110
|
+
};
|
|
111
|
+
const worker = (this.opts.makeWorker ?? ((c, o) => new Worker(c, o)))(cfg, wopts);
|
|
112
|
+
const done = worker.run().catch((e) => this.opts.log(`[${cfg.space}] ! upkeep stopped: ${e.message}`))
|
|
113
|
+
.finally(() => { if (this.keeper?.worker === worker)
|
|
114
|
+
this.keeper = null; });
|
|
115
|
+
this.keeper = { id: m.id, worker, done, path: home, space: cfg.space };
|
|
116
|
+
}
|
|
117
|
+
alertChecks(f, teams) {
|
|
118
|
+
const ids = new Set(teams.map((m) => m.id));
|
|
119
|
+
for (const id of this.alertDue.keys())
|
|
120
|
+
if (!ids.has(id))
|
|
121
|
+
this.alertDue.delete(id);
|
|
122
|
+
if (this.alertRun || this.stopped)
|
|
123
|
+
return;
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const m = teams.filter((t) => (this.alertDue.get(t.id) ?? 0) <= now).sort((a, b) => (this.alertDue.get(a.id) ?? 0) - (this.alertDue.get(b.id) ?? 0))[0];
|
|
126
|
+
if (!m)
|
|
127
|
+
return;
|
|
128
|
+
this.alertDue.set(m.id, now + (this.opts.alertsEveryMs ?? 5 * 60_000));
|
|
129
|
+
const home = this.opts.home ?? CONFIG_DIR;
|
|
130
|
+
const { pollMs: _p, readConfig: _r, makeWorker: _m, home: _h, alertsEveryMs: _a, ...rest } = this.opts;
|
|
131
|
+
const cfg = configFor(f, m, null);
|
|
132
|
+
const wopts = {
|
|
133
|
+
...rest, alertsOnly: true, once: false, project: home, workspaces: () => [],
|
|
134
|
+
scratch: join(home, "worktrees", encodeURIComponent(m.id)), home,
|
|
135
|
+
log: (l) => this.opts.log(`[${cfg.space}] ${l}`),
|
|
136
|
+
};
|
|
137
|
+
const worker = (this.opts.makeWorker ?? ((c, o) => new Worker(c, o)))(cfg, wopts);
|
|
138
|
+
this.alertRun = worker.run()
|
|
139
|
+
.then(() => { this.alertFailSaid.delete(m.id); })
|
|
140
|
+
.catch((e) => { const why = e.message.split("\n")[0] ?? ""; if (this.alertFailSaid.get(m.id) !== why) {
|
|
141
|
+
this.alertFailSaid.set(m.id, why);
|
|
142
|
+
this.opts.log(`[${cfg.space}] could not look for key changes or seat moves: ${why}`);
|
|
143
|
+
} })
|
|
144
|
+
.finally(() => { this.alertRun = null; });
|
|
145
|
+
}
|
|
146
|
+
stopKeeper() {
|
|
147
|
+
if (!this.keeper)
|
|
148
|
+
return;
|
|
149
|
+
this.keeper.worker.stop();
|
|
150
|
+
this.keeper = null;
|
|
151
|
+
}
|
|
72
152
|
async run() {
|
|
73
153
|
const poll = this.opts.pollMs ?? 5000;
|
|
74
154
|
while (!this.stopped) {
|
|
@@ -77,18 +157,27 @@ export class WorkerFleet {
|
|
|
77
157
|
this.opts.log(`fleet: worker for "${s}" started`);
|
|
78
158
|
for (const s of stopped)
|
|
79
159
|
this.opts.log(`fleet: worker for "${s}" stopped — binding gone`);
|
|
80
|
-
|
|
81
|
-
this.
|
|
160
|
+
const idle = !this.slots.size && !started.length
|
|
161
|
+
? (this.unservedSaid.size
|
|
162
|
+
? `fleet: nothing this worker can serve — the folders bound here are not opened to teammates' runs (said above, with why)`
|
|
163
|
+
: `fleet: nothing bound on this machine — bind a repo or a docs folder with \`pantheon use <team> --here\` from inside it; it is served as soon as it is, no restart`)
|
|
164
|
+
: "";
|
|
165
|
+
if (idle && idle !== this.idleSaid)
|
|
166
|
+
this.opts.log(idle);
|
|
167
|
+
this.idleSaid = idle;
|
|
82
168
|
await new Promise((r) => setTimeout(r, poll));
|
|
83
169
|
if (this.opts.once && this.slots.size === 0)
|
|
84
170
|
break;
|
|
85
171
|
}
|
|
172
|
+
const keeper = this.keeper;
|
|
173
|
+
this.stopKeeper();
|
|
86
174
|
for (const s of this.slots.values())
|
|
87
175
|
s.worker.stop();
|
|
88
|
-
await Promise.allSettled([...this.slots.values()].map((s) => s.done));
|
|
176
|
+
await Promise.allSettled([...this.slots.values()].map((s) => s.done).concat(keeper ? [keeper.done] : [], this.alertRun ? [this.alertRun] : []));
|
|
89
177
|
}
|
|
90
178
|
stop() {
|
|
91
179
|
this.stopped = true;
|
|
180
|
+
this.keeper?.worker.stop();
|
|
92
181
|
for (const s of this.slots.values())
|
|
93
182
|
s.worker.stop();
|
|
94
183
|
}
|
|
@@ -102,4 +191,50 @@ export function teamWorkspaces(f, spaceId) {
|
|
|
102
191
|
const seen = new Set();
|
|
103
192
|
return [...repos, ...folders].filter((w) => (seen.has(w.path) ? false : (seen.add(w.path), true)));
|
|
104
193
|
}
|
|
194
|
+
export function unservedFolders(f, spaceId, served = teamWorkspaces(f, spaceId), inside = (d) => repoInside(d)) {
|
|
195
|
+
const all = Object.values(f?.bindings ?? {}).map((b) => resolve(b.path));
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const [k, b] of Object.entries(f?.bindings ?? {})) {
|
|
198
|
+
if (b.space !== spaceId || !k.startsWith("d_"))
|
|
199
|
+
continue;
|
|
200
|
+
if (served.some((w) => resolve(w.path) === resolve(b.path)))
|
|
201
|
+
continue;
|
|
202
|
+
if (!existsSync(b.path)) {
|
|
203
|
+
out.push({ path: b.path, why: "it is not there any more — bind the folder again from where it is now (pantheon use <team> --here)" });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const nested = all.find((p) => p !== resolve(b.path) && p.startsWith(`${resolve(b.path)}${sep}`));
|
|
207
|
+
if (nested) {
|
|
208
|
+
out.push({ path: b.path, why: `another bound folder is inside it (${nested}), so it is not opened whole — the one inside is served` });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const r = inside(b.path);
|
|
212
|
+
if (r === "too-large")
|
|
213
|
+
out.push({ path: b.path, why: "it is too large to check for repos inside it (over 20,000 files and folders), so it is not opened to teammates' runs — bind the folder of documents itself, not one above it" });
|
|
214
|
+
else if (r)
|
|
215
|
+
out.push({ path: b.path, why: `it holds a git repo (${r}), and a folder with repos in it is never opened to a teammate's run — bind the repo itself (pantheon use <team> --here inside it), or a folder of documents only` });
|
|
216
|
+
}
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
export function workerServesReport(f, available) {
|
|
220
|
+
const out = [];
|
|
221
|
+
const teams = Object.values(f?.spaces ?? {});
|
|
222
|
+
let folders = false;
|
|
223
|
+
for (const m of teams) {
|
|
224
|
+
const ws = teamWorkspaces(f, m.id);
|
|
225
|
+
const lead = teams.length > 1 ? `${m.name}: ` : "";
|
|
226
|
+
if (ws.length)
|
|
227
|
+
out.push({ line: `serves ${lead}${ws.map((w) => `${w.name}${w.kind === "folder" ? " (folder)" : ""}`).join(", ")}`, warn: false });
|
|
228
|
+
for (const u of unservedFolders(f, m.id, ws))
|
|
229
|
+
out.push({ line: `not served ${lead}${u.path} — ${u.why}`, warn: true });
|
|
230
|
+
folders ||= ws.some((w) => w.kind === "folder");
|
|
231
|
+
}
|
|
232
|
+
if (teams.length && !out.length)
|
|
233
|
+
out.push({ line: "serves nothing yet — teammates' requests wait for you. Bind a repo or docs folder from inside it: pantheon use <team> --here (served at once, no restart)", warn: true });
|
|
234
|
+
if (folders) {
|
|
235
|
+
const r = folderReaderLine(available);
|
|
236
|
+
out.push({ line: `folders ${r.line}`, warn: !r.ok });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
105
240
|
//# sourceMappingURL=worker-fleet.js.map
|