@join-pantheon/cli 0.30.0 → 0.31.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 +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
package/dist/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { describeWindows, scanMcpWindows } from "./mcp-windows.js";
|
|
|
6
6
|
import { grantsSeen, measureMcp, toolsReport, HOST_STARTUP_ALLOWANCE_MS, STARTUP_WARN_MS } from "./measure.js";
|
|
7
7
|
import { FEEDBACK_ADDRESS } from "./feedback.js";
|
|
8
8
|
import { loadProfile, profileInitialized, wakeMode } from "./profile.js";
|
|
9
|
-
import { isApprovedWaker, loadPolicy, relayKnowsAdmitters } from "./worker-policy.js";
|
|
9
|
+
import { isApprovedWaker, loadPolicy, relayKnowsAdmitters, unsandboxedAllowed } from "./worker-policy.js";
|
|
10
10
|
import { commandState } from "./command-state.js";
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
12
|
import { homedir, platform } from "node:os";
|
|
@@ -18,7 +18,7 @@ import { approvalSummary, chosenApproval, copilotMcpConfigFile, kiroMcpConfigFil
|
|
|
18
18
|
import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
19
19
|
import { loadPersonas } from "./persona.js";
|
|
20
20
|
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
21
|
-
import { chooseAgent, describeChoice, unattendedRefusal, skippedSummary } from "./agents.js";
|
|
21
|
+
import { agentById, chooseAgent, describeChoice, unattendedRefusal, skippedSummary, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
22
22
|
import { onPathNamed } from "./which.js";
|
|
23
23
|
import { VERSION } from "./version.js";
|
|
24
24
|
import { serviceInstalled, servicesUnavailable } from "./service.js";
|
|
@@ -138,14 +138,15 @@ export async function doctor(projectDir) {
|
|
|
138
138
|
add("pass", "config", `${cfg.member} in "${cfg.space}" via ${cfg.relay} (${CONFIG_PATH})`, undefined, "present");
|
|
139
139
|
const here = resolveFor(projectDir, { strict: true });
|
|
140
140
|
const nTeams = Object.keys(loadAll()?.spaces ?? {}).length;
|
|
141
|
+
const folderHere = here.reason === "binding" && !here.repo;
|
|
141
142
|
if (here.reason === "binding")
|
|
142
|
-
add("pass", "team", `this repo is bound to "${here.cfg.space}"${here.repo?.origin ? ` (${here.repo.origin})` : ""}`, undefined, `this repo is bound to a team (${nTeams} on this machine)`);
|
|
143
|
+
add("pass", "team", folderHere ? `this folder is bound to "${here.cfg.space}" (${here.binding?.path ?? projectDir}) — no git needed` : `this repo is bound to "${here.cfg.space}"${here.repo?.origin ? ` (${here.repo.origin})` : ""}`, undefined, `this ${folderHere ? "folder" : "repo"} is bound to a team (${nTeams} on this machine)`);
|
|
143
144
|
else if (here.reason === "single")
|
|
144
145
|
add("pass", "team", `"${cfg.space}" — your only team, so every folder resolves to it`, undefined, "one team on this machine");
|
|
145
146
|
else if (here.reason === "unbound")
|
|
146
147
|
add("fail", "team", `this folder is not bound to any of your ${nTeams} teams — agents opened here will refuse to send`, `from the team's checkout: pantheon use <team> --here (pantheon spaces lists them)`, `folder not bound to any of ${nTeams} teams`);
|
|
147
148
|
else if (here.reason === "stale")
|
|
148
|
-
add("fail", "team", `this repo is bound to a team this machine has left (${here.binding?.space})`, "pantheon use <team> --here, or pantheon join <invite>", "bound to a team this machine has left");
|
|
149
|
+
add("fail", "team", `this ${here.repo ? "repo" : "folder"} is bound to a team this machine has left (${here.binding?.space})`, "pantheon use <team> --here, or pantheon join <invite>", "bound to a team this machine has left");
|
|
149
150
|
if (here.repo && !here.repo.shared)
|
|
150
151
|
add("warn", "team", "this repo has no origin remote", "DELIVERED must name a commit a teammate can fetch; add a remote before delivering", "repo has no origin remote");
|
|
151
152
|
const nonce = "doctor-selftest";
|
|
@@ -208,7 +209,10 @@ export async function doctor(projectDir) {
|
|
|
208
209
|
? add("pass", "PATH", "`pantheon` resolves by name", undefined, "pantheon resolves by name")
|
|
209
210
|
: add("warn", "PATH", "`pantheon` is not on PATH", "install it globally (`npm install -g @join-pantheon/cli`) and make sure your npm global bin dir (`npm bin -g`) is on PATH", "pantheon is not on PATH");
|
|
210
211
|
const inRepo = git(["rev-parse", "--is-inside-work-tree"], projectDir) === "true";
|
|
211
|
-
if (!inRepo) {
|
|
212
|
+
if (!inRepo && folderHere) {
|
|
213
|
+
add("pass", "git", "a plain folder — messages, questions and decisions work here; code is handed over from a repo", undefined, "plain folder, no git needed");
|
|
214
|
+
}
|
|
215
|
+
else if (!inRepo) {
|
|
212
216
|
add("warn", "git", `${projectDir} is not a git repo`, "DELIVERED requires a pushed branch + commit, so the work needs to live in a repo you both clone", "not a git repo");
|
|
213
217
|
}
|
|
214
218
|
else {
|
|
@@ -226,20 +230,28 @@ export async function doctor(projectDir) {
|
|
|
226
230
|
: add("warn", "agent cli", "found none of claude / codex / cursor-agent / copilot / kiro-cli on PATH", "fine if you only drive agents through a GUI (Conductor, Cursor, VS Code, the Kiro IDE), but `pantheon worker` needs a CLI to spawn", "none of claude / codex / cursor-agent / copilot / kiro-cli on PATH");
|
|
227
231
|
if (agents.length) {
|
|
228
232
|
try {
|
|
229
|
-
|
|
233
|
+
let unsandboxedOk = false;
|
|
234
|
+
try {
|
|
235
|
+
unsandboxedOk = unsandboxedAllowed(loadPolicy());
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
const choice = chooseAgent("auto", { prefer: prof.agent.primary_surface, unsandboxedOk });
|
|
230
239
|
const line = describeChoice(choice);
|
|
231
240
|
const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
|
|
232
241
|
const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
|
|
233
242
|
const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${skippedSummary(choice)}` : ""}`;
|
|
234
|
-
const refused = unattendedRefusal(choice);
|
|
243
|
+
const refused = unattendedRefusal(choice, { unsandboxedOk });
|
|
244
|
+
const surface = prof.agent.primary_surface ? agentById(prof.agent.primary_surface) : undefined;
|
|
235
245
|
if (refused)
|
|
236
|
-
add("warn", "worker agent", line, `
|
|
246
|
+
add("warn", "worker agent", line, `teammates' requests wait for your next session here: ${refused}`, shareLine);
|
|
237
247
|
else if (bad)
|
|
238
248
|
add("warn", "worker agent", line, `nothing is signed in, so every woken run here fails until you sign in: ${choice.skipped.map((x) => x.why).join("; ") || "sign in to the agent CLI"}`, shareLine);
|
|
239
249
|
else if (fellBack)
|
|
240
250
|
add("warn", "worker agent", line, prof.agent.primary_surface === "kiro"
|
|
241
|
-
? `you chose Kiro at setup; Kiro
|
|
242
|
-
:
|
|
251
|
+
? `you chose Kiro at setup; Kiro answers in your live sessions, and runs while you're away use ${choice.backend.label}`
|
|
252
|
+
: surface && !SANDBOXED_FOR_UNATTENDED.has(surface.id) && !unsandboxedOk
|
|
253
|
+
? `you chose ${surface.label} at setup; runs while you're away use ${choice.backend.label} in its sandbox (to have ${surface.label} answer them: pantheon policy grant unsandboxed-runs)`
|
|
254
|
+
: `you chose ${prof.agent.primary_surface} at setup; runs use ${choice.backend.label} until it is signed in again`, shareLine);
|
|
243
255
|
else
|
|
244
256
|
add("pass", "worker agent", line, undefined, shareLine);
|
|
245
257
|
}
|
|
@@ -371,17 +383,51 @@ export async function doctor(projectDir) {
|
|
|
371
383
|
const hookStates = agentHookStates(projectDir);
|
|
372
384
|
const hook = hookStates[0] ?? hookState(projectDir);
|
|
373
385
|
const reportsActivity = hookStates.some((s) => s.events?.some((e) => /prompt|UserPromptSubmit|beforeSubmitPrompt/i.test(e))) || (hook.events?.includes("UserPromptSubmit") ?? false);
|
|
386
|
+
{
|
|
387
|
+
const { loadPins, pinsPath } = await import("./pins.js");
|
|
388
|
+
const { brokenStateFiles, brokenStateFix } = await import("./file-lock.js");
|
|
389
|
+
const { dirname } = await import("node:path");
|
|
390
|
+
const broken = brokenStateFiles(dirname(pinsPath()));
|
|
391
|
+
if (broken.length)
|
|
392
|
+
add("fail", "signing", `${broken.join(", ")} in ${dirname(pinsPath())} cannot be read and written — they are what stops a request being run twice, so the worker holds every unattended run until they are fixed`, brokenStateFix(broken), "state files broken");
|
|
393
|
+
const all = loadPins();
|
|
394
|
+
if (all.corrupt)
|
|
395
|
+
add("fail", "signing", `${pinsPath()} could not be read — no message is trusted until it can be`, "restore it from a backup, or remove it to pin teammates' keys afresh (then check each with them: pantheon trust)", "keys.json unreadable");
|
|
396
|
+
const t = all.teams[client.pinKey()];
|
|
397
|
+
const pending = Object.entries(t?.members ?? {}).filter(([, p]) => p.pending).map(([m]) => m);
|
|
398
|
+
const n = Object.keys(t?.members ?? {}).filter((m) => m !== cfg.member).length;
|
|
399
|
+
if (client.features.includes("act-sig")) {
|
|
400
|
+
if (pending.length)
|
|
401
|
+
add("warn", "signing", `${pending.join(", ")}'s key changed — their messages are not trusted here until you check the new key with them`, `pantheon trust ${pending[0]}`, `${pending.length} key change${pending.length === 1 ? "" : "s"} waiting`);
|
|
402
|
+
else
|
|
403
|
+
add("pass", "signing", `messages are signed; this machine checks them against ${n} teammate key${n === 1 ? "" : "s"} it pinned (pantheon trust lists them)`, undefined, `signed; ${n} keys pinned`);
|
|
404
|
+
}
|
|
405
|
+
else if (t?.sigs || client.signaturesExpected()) {
|
|
406
|
+
add("fail", "signing", "this team's relay does not carry signatures, though this machine expects them (it signed before, or you joined with 0.31+) — requests are not run here until it does", "check the relay's version with its operator (pantheon doctor on a teammate's machine)", "relay stopped carrying signatures");
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
add("warn", "signing", "this team's relay does not carry signatures yet (a relay before 0.31), so messages cannot be checked against their senders' keys", "upgrade the relay", "relay without signatures");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
374
412
|
const workerUp = workerProc().running;
|
|
375
413
|
const mode = wakeMode(loadProfile(cfg));
|
|
414
|
+
const anyoneWhy = loadProfile(cfg).agent.wake_default === "anyone" ? "you chose \"anyone\"" : "\"anyone\" is the default";
|
|
376
415
|
const policy = loadPolicy();
|
|
377
416
|
const approvedHere = admittedPeers.filter((m) => isApprovedWaker(policy, [client.spaceId ?? cfg.spaceId, cfg.space], m.member));
|
|
378
417
|
const teamKey = client.spaceId ?? cfg.spaceId ?? cfg.space;
|
|
379
418
|
const notCarried = mode === "approved" && !policy.wakersCarried?.[teamKey] && !policy.wakersCarried?.[cfg.space] && !relayKnowsAdmitters(client.members, cfg.member);
|
|
380
419
|
const effective = mode === "anyone" || notCarried ? wakers : mode === "no" ? [] : approvedHere.filter((m) => m.may_wake);
|
|
381
420
|
const blockedByTeam = mode === "approved" ? approvedHere.filter((m) => !m.may_wake) : [];
|
|
382
|
-
|
|
421
|
+
const noSvcHere = servicesUnavailable(host.platform);
|
|
422
|
+
if (!effective.length && mode === "approved" && !blockedByTeam.length && !notCarried) {
|
|
423
|
+
if (workerUp)
|
|
424
|
+
add("pass", "wake rights", "nobody approved yet — you are asked the first time a teammate's request needs this machine (Always / Just this once / No)", undefined, "ask on first request; worker running");
|
|
425
|
+
else
|
|
426
|
+
add("warn", "wake rights", "no worker is running here, so teammates' requests wait for you — and nobody is asked", noSvcHere ? "Pantheon does not run in the background on Windows yet, so a peer's act waits until your agent is open" : "start it: pantheon service install worker (it serves every repo and folder bound here)", "ask on first request; worker not running");
|
|
427
|
+
}
|
|
428
|
+
else if (!effective.length) {
|
|
383
429
|
const why = mode === "no" ? "this machine is set not to be woken"
|
|
384
|
-
: mode === "anyone" ?
|
|
430
|
+
: mode === "anyone" ? `${anyoneWhy}, and the team lets nobody wake workers`
|
|
385
431
|
: blockedByTeam.length ? `approved here: ${blockedByTeam.map((m) => m.member).join(", ")}, but the team does not let them wake workers yet`
|
|
386
432
|
: "nobody is approved on this machine";
|
|
387
433
|
add("warn", "wake rights", `no teammate can wake a worker on this machine — ${why}`, mode === "no" ? "that is what you chose; pantheon setup to change it"
|
|
@@ -389,7 +435,7 @@ export async function doctor(projectDir) {
|
|
|
389
435
|
: "that is a safe default; let one teammate wake this machine with `pantheon wake allow <name>` when you want unattended replies", `nobody can wake this machine (${mode}); worker ${workerUp ? "running" : "not running"}`);
|
|
390
436
|
}
|
|
391
437
|
else {
|
|
392
|
-
const who = `${effective.map((m) => m.member).join(", ")} can wake a worker here (${mode === "anyone" ?
|
|
438
|
+
const who = `${effective.map((m) => m.member).join(", ")} can wake a worker here (${mode === "anyone" ? anyoneWhy : notCarried ? "the team's may_wake decides until this relay is 0.30 or newer" : "approved on this machine"})`;
|
|
393
439
|
const wakers = effective;
|
|
394
440
|
if (workerUp) {
|
|
395
441
|
add("pass", "wake rights", `${who} — worker is running`, undefined, `${wakers.length} may wake; worker running`);
|
|
@@ -398,12 +444,44 @@ export async function doctor(projectDir) {
|
|
|
398
444
|
const noSvc = servicesUnavailable(host.platform);
|
|
399
445
|
add("warn", "wake rights", `${who}, but no worker is running here, so nothing can be woken`, noSvc
|
|
400
446
|
? "Pantheon does not run in the background on Windows yet, so a peer's act waits until your agent is open"
|
|
401
|
-
: "start one with `pantheon service install worker
|
|
447
|
+
: "start one with `pantheon service install worker` (it serves every repo and folder bound here); until then a peer's act waits for you to type", `${wakers.length} may wake; worker not running`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
{
|
|
451
|
+
const f = loadAll();
|
|
452
|
+
const m = f && Object.values(f.spaces ?? {}).find((x) => x.id === (client.spaceId ?? cfg.spaceId) || x.name === cfg.space);
|
|
453
|
+
if (f && m) {
|
|
454
|
+
const { teamWorkspaces } = await import("./worker-fleet.js");
|
|
455
|
+
const ws = teamWorkspaces(f, m.id);
|
|
456
|
+
const fps = new Set(ws.map((w) => w.fp).filter(Boolean));
|
|
457
|
+
const missing = (m.repos ?? []).filter((r) => !fps.has(r.fp)).map((r) => r.name);
|
|
458
|
+
if (ws.length) {
|
|
459
|
+
add("pass", "worker serves", `${ws.map((w) => `${w.name}${w.kind === "folder" ? " (folder)" : ""}`).join(", ")}${missing.length ? ` — not on this machine: ${missing.join(", ")}` : ""}`, missing.length ? "requests about those are told they wait for you; clone them, then: pantheon repo find" : undefined, `${ws.length} workspace${ws.length === 1 ? "" : "s"}${missing.length ? `, ${missing.length} team repo${missing.length === 1 ? "" : "s"} not here` : ""}`);
|
|
460
|
+
}
|
|
402
461
|
}
|
|
403
462
|
}
|
|
404
463
|
{
|
|
405
464
|
const cs = commandState({ project: projectDir });
|
|
406
|
-
|
|
465
|
+
let servesRepo = true;
|
|
466
|
+
try {
|
|
467
|
+
const f = loadAll();
|
|
468
|
+
const m = f && Object.values(f.spaces ?? {}).find((x) => x.id === (client.spaceId ?? cfg.spaceId) || x.name === cfg.space);
|
|
469
|
+
if (f && m) {
|
|
470
|
+
const ws = (await import("./worker-fleet.js")).teamWorkspaces(f, m.id);
|
|
471
|
+
servesRepo = !ws.length || ws.some((w) => w.kind === "repo");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch { }
|
|
475
|
+
if (!servesRepo)
|
|
476
|
+
add("pass", "commands", "not used here — a docs folder is read and answered from, never run in", undefined, "docs folder only; commands not used");
|
|
477
|
+
else {
|
|
478
|
+
const { commandCaveat } = await import("./command-state.js");
|
|
479
|
+
const caveat = cs.level !== "off" ? commandCaveat() : "";
|
|
480
|
+
if (caveat)
|
|
481
|
+
add("warn", "commands", `woken runs: ${cs.summary} — ${caveat}`, undefined, "how woken runs here use commands");
|
|
482
|
+
else
|
|
483
|
+
add("pass", "commands", `woken runs: ${cs.summary}`, undefined, cs.level === "off" ? "woken runs edit files only" : `woken runs may run commands (${cs.allowBash ? "--allow-bash" : "granted here"})`);
|
|
484
|
+
}
|
|
407
485
|
if (cs.level !== "off" && host.platform === "linux") {
|
|
408
486
|
const missing = ["bwrap", "socat"].filter((b) => !onPathNamed(b, "linux"));
|
|
409
487
|
if (missing.length)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { closeSync, existsSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const STALE_MS = 15_000;
|
|
5
|
+
const WAIT_MS = 20_000;
|
|
6
|
+
const sleepSync = (ms) => { try {
|
|
7
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
8
|
+
}
|
|
9
|
+
catch { } };
|
|
10
|
+
const readLock = (lock) => { try {
|
|
11
|
+
return readStateFile(lock);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
} };
|
|
16
|
+
function alive(pid) {
|
|
17
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
return e.code === "EPERM";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function stale(lock, content) {
|
|
28
|
+
let age;
|
|
29
|
+
try {
|
|
30
|
+
age = Math.abs(Date.now() - statSync(lock).mtimeMs);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (age > STALE_MS)
|
|
36
|
+
return true;
|
|
37
|
+
const m = /^(\d+) [0-9a-f]{24}$/.exec(content ?? "");
|
|
38
|
+
if (!m)
|
|
39
|
+
return age > 1_000;
|
|
40
|
+
return !alive(Number(m[1])) && age > 50;
|
|
41
|
+
}
|
|
42
|
+
const clear = (p) => { try {
|
|
43
|
+
rmSync(p, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
catch { } };
|
|
46
|
+
function takeOver(lock, seen) {
|
|
47
|
+
const brk = `${lock}.break`;
|
|
48
|
+
const mine = `${brk}.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(mine, "", { flag: "wx", mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
try {
|
|
57
|
+
linkSync(mine, brk);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
try {
|
|
61
|
+
const st = statSync(brk);
|
|
62
|
+
if (!st.isFile() || Math.abs(Date.now() - st.mtimeMs) > 5_000)
|
|
63
|
+
clear(brk);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const now = readLock(lock);
|
|
70
|
+
if (now !== seen || !stale(lock, now))
|
|
71
|
+
return;
|
|
72
|
+
const aside = `${lock}.stale.${process.pid}.${randomBytes(4).toString("hex")}`;
|
|
73
|
+
try {
|
|
74
|
+
renameSync(lock, aside);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
clear(lock);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (readLock(aside) !== seen) {
|
|
81
|
+
try {
|
|
82
|
+
linkSync(aside, lock);
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
}
|
|
86
|
+
clear(aside);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
try {
|
|
90
|
+
rmSync(brk, { force: true });
|
|
91
|
+
}
|
|
92
|
+
catch { }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
try {
|
|
97
|
+
rmSync(mine, { force: true });
|
|
98
|
+
}
|
|
99
|
+
catch { }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function readStateFile(p) {
|
|
103
|
+
let st;
|
|
104
|
+
try {
|
|
105
|
+
st = statSync(p);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (!st.isFile())
|
|
111
|
+
throw new Error(`${p} is not a plain file`);
|
|
112
|
+
return readFileSync(p, "utf8");
|
|
113
|
+
}
|
|
114
|
+
export function brokenStateFiles(dir) {
|
|
115
|
+
const out = [];
|
|
116
|
+
if (existsSync(dir)) {
|
|
117
|
+
const probe = join(dir, `.write-check.${randomBytes(4).toString("hex")}`);
|
|
118
|
+
try {
|
|
119
|
+
writeFileSync(probe, "", { flag: "wx", mode: 0o600 });
|
|
120
|
+
rmSync(probe, { force: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
out.push("(the folder)");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const f of ["nonces.json", "continued.json", "keys.json"]) {
|
|
127
|
+
const p = join(dir, f);
|
|
128
|
+
try {
|
|
129
|
+
const text = readStateFile(p);
|
|
130
|
+
if (text === null)
|
|
131
|
+
continue;
|
|
132
|
+
JSON.parse(text);
|
|
133
|
+
closeSync(openSync(p, "r+"));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
out.push(f);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
export function brokenStateFix(broken) {
|
|
142
|
+
const files = broken.filter((b) => b !== "(the folder)");
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (broken.includes("(the folder)"))
|
|
145
|
+
parts.push("make the Pantheon folder writable again (its permissions) — do not delete it: it holds your key and memberships");
|
|
146
|
+
if (files.length)
|
|
147
|
+
parts.push(`repair or delete ${files.join(", ")} (one made read-only or locked: make it writable again; one that is a folder or not a file: delete it)`);
|
|
148
|
+
return parts.join("; ");
|
|
149
|
+
}
|
|
150
|
+
export function withFileLock(f, fn) {
|
|
151
|
+
mkdirSync(dirname(f), { recursive: true, mode: 0o700 });
|
|
152
|
+
const lock = `${f}.lock`;
|
|
153
|
+
const token = `${process.pid} ${randomBytes(12).toString("hex")}`;
|
|
154
|
+
const deadline = Date.now() + WAIT_MS;
|
|
155
|
+
const made = `${lock}.${randomBytes(6).toString("hex")}.new`;
|
|
156
|
+
writeFileSync(made, token, { mode: 0o600, flag: "wx" });
|
|
157
|
+
try {
|
|
158
|
+
for (;;) {
|
|
159
|
+
let got = false;
|
|
160
|
+
try {
|
|
161
|
+
linkSync(made, lock);
|
|
162
|
+
got = true;
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const code = e.code;
|
|
166
|
+
if (code !== "EEXIST" && !(process.platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBUSY")))
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
if (got)
|
|
170
|
+
break;
|
|
171
|
+
const content = readLock(lock);
|
|
172
|
+
if (stale(lock, content))
|
|
173
|
+
takeOver(lock, content);
|
|
174
|
+
if (Date.now() > deadline)
|
|
175
|
+
throw new Error(`${f} is locked by another pantheon process (${lock})`);
|
|
176
|
+
sleepSync(2 + Math.floor(Math.random() * 8));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
try {
|
|
181
|
+
rmSync(made, { force: true });
|
|
182
|
+
}
|
|
183
|
+
catch { }
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return fn();
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
if (readLock(lock) === token) {
|
|
190
|
+
try {
|
|
191
|
+
rmSync(lock, { force: true });
|
|
192
|
+
}
|
|
193
|
+
catch { }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=file-lock.js.map
|