@join-pantheon/cli 0.28.0 → 0.29.0
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 +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +377 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +273 -54
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +258 -57
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +180 -21
- package/dist/refusal.js +29 -0
- package/dist/relay.js +686 -440
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +173 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -168,6 +168,8 @@ export class Accounts {
|
|
|
168
168
|
const existing = this.accountByEmail(e);
|
|
169
169
|
if (existing && existing.status === "active")
|
|
170
170
|
return { ok: false, reason: "email_taken" };
|
|
171
|
+
if (existing && existing.status === "suspended")
|
|
172
|
+
return { ok: false, reason: "blocked" };
|
|
171
173
|
const byName = this.accountByUsername(u);
|
|
172
174
|
if (byName && byName.email !== e)
|
|
173
175
|
return { ok: false, reason: "username_taken" };
|
|
@@ -177,9 +179,12 @@ export class Accounts {
|
|
|
177
179
|
if (!acct || acct.status !== "active")
|
|
178
180
|
return { ok: false, reason: "no_such_account" };
|
|
179
181
|
}
|
|
180
|
-
const prev = this.db.prepare("SELECT created_at FROM verifications WHERE email = ?").get(e);
|
|
182
|
+
const prev = this.db.prepare("SELECT created_at, expires_at, pending_pubkey FROM verifications WHERE email = ?").get(e);
|
|
181
183
|
if (prev && now() - prev.created_at < CODE_RESEND_COOLDOWN_MS)
|
|
182
184
|
return { ok: false, reason: "too_soon" };
|
|
185
|
+
if (prev && now() < prev.expires_at && prev.pending_pubkey && prev.pending_pubkey !== opts.pubkey) {
|
|
186
|
+
return { ok: false, reason: "in_progress_elsewhere" };
|
|
187
|
+
}
|
|
183
188
|
const code = String(randomInt(0, 1_000_000)).padStart(6, "0");
|
|
184
189
|
this.db.prepare(`
|
|
185
190
|
INSERT INTO verifications (email, code_hash, purpose, pending_username, pending_pubkey, pending_label, expires_at, attempts, created_at)
|
|
@@ -192,7 +197,7 @@ export class Accounts {
|
|
|
192
197
|
`).run(e, sha(code), purpose, purpose === "signup" ? normalizeUsername(opts.username) : null, opts.pubkey, opts.label, now() + CODE_TTL_MS, now());
|
|
193
198
|
return { ok: true, code };
|
|
194
199
|
}
|
|
195
|
-
verify(email, code, ip) {
|
|
200
|
+
verify(email, code, ip, pubkey) {
|
|
196
201
|
const e = normalizeEmail(email);
|
|
197
202
|
const v = this.db.prepare("SELECT * FROM verifications WHERE email = ?").get(e);
|
|
198
203
|
if (!v)
|
|
@@ -203,6 +208,8 @@ export class Accounts {
|
|
|
203
208
|
}
|
|
204
209
|
if (v.attempts >= MAX_ATTEMPTS)
|
|
205
210
|
return { ok: false, reason: "too_many_attempts" };
|
|
211
|
+
if (pubkey && v.pending_pubkey && pubkey !== v.pending_pubkey)
|
|
212
|
+
return { ok: false, reason: "other_device" };
|
|
206
213
|
const want = Buffer.from(v.code_hash, "hex");
|
|
207
214
|
const got = Buffer.from(sha(code), "hex");
|
|
208
215
|
const good = want.length === got.length && timingSafeEqual(want, got);
|
|
@@ -216,6 +223,10 @@ export class Accounts {
|
|
|
216
223
|
let account;
|
|
217
224
|
if (v.purpose === "signup") {
|
|
218
225
|
const existing = this.accountByEmail(e);
|
|
226
|
+
if (existing?.status === "suspended") {
|
|
227
|
+
this.db.exec("ROLLBACK");
|
|
228
|
+
return { ok: false, reason: "suspended" };
|
|
229
|
+
}
|
|
219
230
|
if (existing) {
|
|
220
231
|
account = existing;
|
|
221
232
|
run("UPDATE accounts SET status = 'active', username = ?, tos_at = COALESCE(tos_at, ?) WHERE id = ?").run(v.pending_username, now(), account.id);
|
package/dist/agents.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { homedir, platform } from "node:os";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { constants as osConstants, homedir, platform } from "node:os";
|
|
5
5
|
import { join, dirname, isAbsolute } from "node:path";
|
|
6
6
|
import { launcher, kiroDir } from "./install.js";
|
|
7
7
|
import { agentSpawn, which } from "./which.js";
|
|
@@ -122,6 +122,8 @@ export const SIGN_IN_CHECK = {
|
|
|
122
122
|
kiro: { args: ["whoami", "--format", "json"], fix: "run `kiro-cli login` (or set KIRO_API_KEY)" },
|
|
123
123
|
};
|
|
124
124
|
export const NEEDS_SIGN_IN_BEFORE_SPAWN = new Set(["kiro"]);
|
|
125
|
+
export const NOT_FOR_UNATTENDED_RUNS = new Set(["kiro"]);
|
|
126
|
+
export const KIRO_UNATTENDED_REFUSAL = "Kiro cannot yet be fenced to its worktree for unattended runs; the worker uses another installed agent, or none";
|
|
125
127
|
export function kiroSignedIn(binary, run = runStatus, env = process.env, home = homedir(), os = platform()) {
|
|
126
128
|
if (env.KIRO_API_KEY)
|
|
127
129
|
return { ok: true, detail: "signed in (KIRO_API_KEY in the environment)" };
|
|
@@ -211,6 +213,12 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
211
213
|
if (!order.some((o) => o.backend.id === a.backend.id))
|
|
212
214
|
order.push({ ...a, because: available.length === 1 ? "only one on PATH" : "first signed in on PATH" });
|
|
213
215
|
const skipped = [];
|
|
216
|
+
const usable = order.filter((c) => !NOT_FOR_UNATTENDED_RUNS.has(c.backend.id));
|
|
217
|
+
for (const c of order)
|
|
218
|
+
if (NOT_FOR_UNATTENDED_RUNS.has(c.backend.id) && usable.length)
|
|
219
|
+
skipped.push({ backend: c.backend, why: KIRO_UNATTENDED_REFUSAL });
|
|
220
|
+
if (usable.length)
|
|
221
|
+
order.splice(0, order.length, ...usable);
|
|
214
222
|
for (const c of order) {
|
|
215
223
|
const s = state(c.backend, c.binary);
|
|
216
224
|
if (!s || s.ok || s.unknown)
|
|
@@ -221,6 +229,8 @@ export function chooseAgent(preferred, opts = {}) {
|
|
|
221
229
|
return { backend: first.backend, binary: first.binary, signIn: state(first.backend, first.binary), because: `${first.because} (none signed in)`, skipped };
|
|
222
230
|
}
|
|
223
231
|
export function unattendedRefusal(c) {
|
|
232
|
+
if (NOT_FOR_UNATTENDED_RUNS.has(c.backend.id))
|
|
233
|
+
return KIRO_UNATTENDED_REFUSAL;
|
|
224
234
|
if (!NEEDS_SIGN_IN_BEFORE_SPAWN.has(c.backend.id) || c.signIn?.ok)
|
|
225
235
|
return null;
|
|
226
236
|
return `${c.backend.label} is not signed in (${c.signIn?.detail ?? "not checked"}), and signed out it opens a browser to log in, so it is not started while you are away — ${SIGN_IN_CHECK[c.backend.id].fix}`;
|
|
@@ -229,6 +239,12 @@ export function describeChoice(c) {
|
|
|
229
239
|
const head = `${c.backend.label}${c.signIn ? ` — ${c.signIn.detail}` : ""} (${c.because})`;
|
|
230
240
|
return c.skipped.length ? `${head}; skipped ${c.skipped.map((s) => `${s.backend.label} — ${s.why}`).join("; ")}` : head;
|
|
231
241
|
}
|
|
242
|
+
export function skippedSummary(c) {
|
|
243
|
+
if (!c.skipped.length)
|
|
244
|
+
return "";
|
|
245
|
+
const why = [...new Set(c.skipped.map((s) => (s.why === KIRO_UNATTENDED_REFUSAL ? `${s.backend.label} cannot run unattended yet` : "not signed in")))];
|
|
246
|
+
return `${c.skipped.length} skipped (${why.join("; ")})`;
|
|
247
|
+
}
|
|
232
248
|
export function resolveAgent(preferred) {
|
|
233
249
|
const c = chooseAgent(preferred, { check: null });
|
|
234
250
|
return { backend: c.backend, binary: c.binary };
|
|
@@ -279,6 +295,9 @@ export function headlessSpawn(opts) {
|
|
|
279
295
|
}
|
|
280
296
|
if (opts.backend.id === "codex") {
|
|
281
297
|
const entry = { command: l.command, args: l.args };
|
|
298
|
+
const win = (opts.os ?? platform()) === "win32";
|
|
299
|
+
const str = (v) => (win ? tomlLiteral(v) : JSON.stringify(v));
|
|
300
|
+
const list = (vs) => (win ? `[${vs.map(tomlLiteral).join(", ")}]` : JSON.stringify(vs));
|
|
282
301
|
return {
|
|
283
302
|
command: opts.binary,
|
|
284
303
|
args: [
|
|
@@ -288,10 +307,10 @@ export function headlessSpawn(opts) {
|
|
|
288
307
|
: ["--sandbox", "read-only"]),
|
|
289
308
|
"--skip-git-repo-check",
|
|
290
309
|
"--ignore-user-config",
|
|
291
|
-
"-c", `mcp_servers.pantheon.command=${
|
|
292
|
-
"-c", `mcp_servers.pantheon.args=${
|
|
293
|
-
"-c", `mcp_servers.pantheon.env=${tomlInlineTable(fileEnv)}`,
|
|
294
|
-
"-c", `mcp_servers.pantheon.default_tools_approval_mode
|
|
310
|
+
"-c", `mcp_servers.pantheon.command=${str(entry.command)}`,
|
|
311
|
+
"-c", `mcp_servers.pantheon.args=${list(entry.args)}`,
|
|
312
|
+
"-c", `mcp_servers.pantheon.env=${tomlInlineTable(fileEnv, win)}`,
|
|
313
|
+
"-c", `mcp_servers.pantheon.default_tools_approval_mode=${str("approve")}`,
|
|
295
314
|
opts.prompt,
|
|
296
315
|
],
|
|
297
316
|
env: procEnv,
|
|
@@ -397,8 +416,11 @@ function readCopilotConfig() {
|
|
|
397
416
|
return {};
|
|
398
417
|
}
|
|
399
418
|
}
|
|
400
|
-
export function tomlInlineTable(kv) {
|
|
401
|
-
return `{${Object.entries(kv).map(([k, v]) => `${k} = ${JSON.stringify(v)}`).join(", ")}}`;
|
|
419
|
+
export function tomlInlineTable(kv, literal = false) {
|
|
420
|
+
return `{${Object.entries(kv).map(([k, v]) => `${k} = ${literal ? tomlLiteral(v) : JSON.stringify(v)}`).join(", ")}}`;
|
|
421
|
+
}
|
|
422
|
+
export function tomlLiteral(v) {
|
|
423
|
+
return /['\r\n]/.test(v) ? JSON.stringify(v) : `'${v}'`;
|
|
402
424
|
}
|
|
403
425
|
export function materializeSpawnFiles(worktree, spawn) {
|
|
404
426
|
for (const f of spawn.writeFiles ?? []) {
|
|
@@ -419,4 +441,350 @@ export function backendFromLabel(label) {
|
|
|
419
441
|
return undefined;
|
|
420
442
|
return agentById(label);
|
|
421
443
|
}
|
|
444
|
+
export const REPO_AGENT_CONFIG = [
|
|
445
|
+
".claude/settings.json",
|
|
446
|
+
".claude/settings.local.json",
|
|
447
|
+
".cursor/hooks.json",
|
|
448
|
+
".cursor/mcp.json",
|
|
449
|
+
".cursor/cli.json",
|
|
450
|
+
".github/hooks",
|
|
451
|
+
".mcp.json",
|
|
452
|
+
".vscode/mcp.json",
|
|
453
|
+
".kiro",
|
|
454
|
+
".codex",
|
|
455
|
+
];
|
|
456
|
+
export const AGENT_EDIT_SUFFIX = ".pantheon-agent-edit";
|
|
457
|
+
const quarantineDir = (stashRoot, worktree) => join(stashRoot, "quarantine", createHash("sha256").update(worktree).digest("hex").slice(0, 16));
|
|
458
|
+
const isLink = (p) => { try {
|
|
459
|
+
return lstatSync(p).isSymbolicLink();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return false;
|
|
463
|
+
} };
|
|
464
|
+
export function hashPath(p) {
|
|
465
|
+
let st;
|
|
466
|
+
try {
|
|
467
|
+
st = lstatSync(p);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const h = createHash("sha256");
|
|
473
|
+
if (st.isSymbolicLink())
|
|
474
|
+
h.update(`l:${readlinkSync(p)}`);
|
|
475
|
+
else if (st.isDirectory()) {
|
|
476
|
+
h.update("d:");
|
|
477
|
+
for (const name of readdirSync(p).sort())
|
|
478
|
+
h.update(`${name}\0${hashPath(join(p, name)) ?? ""}\0`);
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
h.update("f:").update(readFileSync(p));
|
|
482
|
+
return h.digest("hex");
|
|
483
|
+
}
|
|
484
|
+
function headOf(worktree) {
|
|
485
|
+
try {
|
|
486
|
+
return execFileSync("git", ["-C", worktree, "rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10_000 }).trim();
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const pause = (ms) => { try {
|
|
493
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
494
|
+
}
|
|
495
|
+
catch { } };
|
|
496
|
+
export function moveSync(from, to, rename = renameSync) {
|
|
497
|
+
for (let attempt = 0;; attempt++) {
|
|
498
|
+
try {
|
|
499
|
+
rename(from, to);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
catch (e) {
|
|
503
|
+
const code = e.code;
|
|
504
|
+
if (code === "EXDEV") {
|
|
505
|
+
cpSync(from, to, { recursive: true, verbatimSymlinks: true, errorOnExist: true, force: false });
|
|
506
|
+
if (hashPath(to) !== hashPath(from)) {
|
|
507
|
+
rmSync(to, { recursive: true, force: true });
|
|
508
|
+
throw e;
|
|
509
|
+
}
|
|
510
|
+
rmSync(from, { recursive: true, force: true });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if ((code === "EPERM" || code === "EBUSY" || code === "EACCES") && attempt < 5) {
|
|
514
|
+
pause(100 * (attempt + 1));
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
throw e;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function movableUnit(root, rel) {
|
|
522
|
+
const parts = rel.split("/");
|
|
523
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
524
|
+
const sub = parts.slice(0, i).join("/");
|
|
525
|
+
let st;
|
|
526
|
+
try {
|
|
527
|
+
st = lstatSync(join(root, sub));
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
if (st.isSymbolicLink())
|
|
533
|
+
return sub;
|
|
534
|
+
}
|
|
535
|
+
return rel;
|
|
536
|
+
}
|
|
537
|
+
function realParents(root, rel) {
|
|
538
|
+
const parts = rel.split("/").slice(0, -1);
|
|
539
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
540
|
+
const p = join(root, parts.slice(0, i).join("/"));
|
|
541
|
+
try {
|
|
542
|
+
if (lstatSync(p).isDirectory())
|
|
543
|
+
continue;
|
|
544
|
+
moveSync(p, freeName(`${p}${AGENT_EDIT_SUFFIX}`));
|
|
545
|
+
}
|
|
546
|
+
catch { }
|
|
547
|
+
mkdirSync(p);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function freeName(base) {
|
|
551
|
+
const taken = (p) => { try {
|
|
552
|
+
lstatSync(p);
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return false;
|
|
557
|
+
} };
|
|
558
|
+
if (!taken(base))
|
|
559
|
+
return base;
|
|
560
|
+
for (let i = 2;; i++)
|
|
561
|
+
if (!taken(`${base}-${i}`))
|
|
562
|
+
return `${base}-${i}`;
|
|
563
|
+
}
|
|
564
|
+
const writeManifest = (dir, m) => writeFileSync(join(dir, "manifest.json"), JSON.stringify(m), { mode: 0o600 });
|
|
565
|
+
function keepDiscarded(stashRoot, dir, rel) {
|
|
566
|
+
const to = join(stashRoot, "quarantine-kept", `${Date.now()}-${randomBytes(3).toString("hex")}`, rel);
|
|
567
|
+
try {
|
|
568
|
+
mkdirSync(dirname(to), { recursive: true, mode: 0o700 });
|
|
569
|
+
moveSync(join(dir, "files", rel), to);
|
|
570
|
+
return to;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return join(dir, "files", rel);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function restoreFrom(stashRoot, dir, m, mode) {
|
|
577
|
+
const r = { restored: [], setAside: [], discarded: [], lines: [] };
|
|
578
|
+
const left = [];
|
|
579
|
+
const headNow = mode === "recover" ? headOf(m.worktree) : m.head;
|
|
580
|
+
for (const e of m.moved) {
|
|
581
|
+
const from = join(dir, "files", e.rel);
|
|
582
|
+
if (!existsSync(from) && !isLink(from))
|
|
583
|
+
continue;
|
|
584
|
+
const to = join(m.worktree, e.rel);
|
|
585
|
+
try {
|
|
586
|
+
if (headNow !== m.head) {
|
|
587
|
+
const kept = keepDiscarded(stashRoot, dir, e.rel);
|
|
588
|
+
r.discarded.push(e.rel);
|
|
589
|
+
r.lines.push(`${e.rel}: not put back — the worktree moved from ${m.head.slice(0, 7) || "(no commit)"} to ${headNow.slice(0, 7) || "(no commit)"} since it was held; the held copy is at ${kept}`);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const now = hashPath(to);
|
|
593
|
+
if (now === e.hash) {
|
|
594
|
+
rmSync(from, { recursive: true, force: true });
|
|
595
|
+
r.restored.push(e.rel);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (now !== null && m.wrote[e.rel] !== now) {
|
|
599
|
+
if (mode === "recover") {
|
|
600
|
+
const kept = keepDiscarded(stashRoot, dir, e.rel);
|
|
601
|
+
r.discarded.push(e.rel);
|
|
602
|
+
r.lines.push(`${e.rel}: not put back — what is there now differs from what was held, and is kept; the held copy is at ${kept}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const aside = freeName(`${to}${AGENT_EDIT_SUFFIX}`);
|
|
606
|
+
moveSync(to, aside);
|
|
607
|
+
r.setAside.push(e.rel);
|
|
608
|
+
r.lines.push(`${e.rel}: the run wrote here while it was held; its version is kept at ${aside}, and the repo's is back`);
|
|
609
|
+
}
|
|
610
|
+
else if (now !== null) {
|
|
611
|
+
rmSync(to, { recursive: true, force: true });
|
|
612
|
+
}
|
|
613
|
+
realParents(m.worktree, e.rel);
|
|
614
|
+
moveSync(from, to);
|
|
615
|
+
r.restored.push(e.rel);
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
left.push(e);
|
|
619
|
+
r.lines.push(`${e.rel}: could not be put back (${err.message.split("\n")[0]}); it stays held for the next run`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (left.length)
|
|
623
|
+
writeManifest(dir, { ...m, moved: left });
|
|
624
|
+
else
|
|
625
|
+
rmSync(dir, { recursive: true, force: true });
|
|
626
|
+
return r;
|
|
627
|
+
}
|
|
628
|
+
function readManifest(file) {
|
|
629
|
+
try {
|
|
630
|
+
const m = JSON.parse(readFileSync(file, "utf8"));
|
|
631
|
+
if (m.v !== 2 || typeof m.worktree !== "string" || !Array.isArray(m.moved))
|
|
632
|
+
return null;
|
|
633
|
+
return { v: 2, worktree: m.worktree, head: typeof m.head === "string" ? m.head : "", moved: m.moved, wrote: m.wrote ?? {} };
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
export function recoverRepoAgentConfig(worktree, stashRoot) {
|
|
640
|
+
const dir = quarantineDir(stashRoot, worktree);
|
|
641
|
+
const m = readManifest(join(dir, "manifest.json"));
|
|
642
|
+
if (!m) {
|
|
643
|
+
if (existsSync(join(dir, "manifest.json"))) {
|
|
644
|
+
const kept = join(stashRoot, "quarantine-kept", `${Date.now()}-legacy`);
|
|
645
|
+
try {
|
|
646
|
+
mkdirSync(dirname(kept), { recursive: true });
|
|
647
|
+
moveSync(dir, kept);
|
|
648
|
+
}
|
|
649
|
+
catch { }
|
|
650
|
+
return { restored: [], setAside: [], discarded: ["*"], lines: [`an older held set for ${worktree} was not put back (it predates the checks that make that safe); it is kept at ${kept}`] };
|
|
651
|
+
}
|
|
652
|
+
return { restored: [], setAside: [], discarded: [], lines: [] };
|
|
653
|
+
}
|
|
654
|
+
return restoreFrom(stashRoot, dir, m, "recover");
|
|
655
|
+
}
|
|
656
|
+
export function pendingQuarantines(runHomesRoot) {
|
|
657
|
+
const out = [];
|
|
658
|
+
let homes = [];
|
|
659
|
+
try {
|
|
660
|
+
homes = readdirSync(runHomesRoot);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
for (const h of homes) {
|
|
666
|
+
const q = join(runHomesRoot, h, "quarantine");
|
|
667
|
+
let sets = [];
|
|
668
|
+
try {
|
|
669
|
+
sets = readdirSync(q);
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
for (const s of sets) {
|
|
675
|
+
const m = readManifest(join(q, s, "manifest.json"));
|
|
676
|
+
if (m)
|
|
677
|
+
out.push({ runHome: join(runHomesRoot, h), worktree: m.worktree, head: m.head, paths: m.moved.map((e) => e.rel), dir: join(q, s) });
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return out;
|
|
681
|
+
}
|
|
682
|
+
export function restorePendingQuarantines(runHomesRoot) {
|
|
683
|
+
const lines = [];
|
|
684
|
+
for (const p of pendingQuarantines(runHomesRoot)) {
|
|
685
|
+
if (!existsSync(p.worktree)) {
|
|
686
|
+
lines.push(`held agent config for ${p.worktree} (${p.paths.join(", ")}): that worktree is gone, so there is nothing to put it back into`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const r = recoverRepoAgentConfig(p.worktree, p.runHome);
|
|
690
|
+
if (r.restored.length)
|
|
691
|
+
lines.push(`put back ${r.restored.join(", ")} in ${p.worktree}`);
|
|
692
|
+
lines.push(...r.lines);
|
|
693
|
+
}
|
|
694
|
+
return lines;
|
|
695
|
+
}
|
|
696
|
+
const openFences = new Set();
|
|
697
|
+
const SIGNALS = ["SIGTERM", "SIGINT", "SIGHUP"];
|
|
698
|
+
const onSignal = (sig) => {
|
|
699
|
+
const onlyOurs = process.listenerCount(sig) === 1;
|
|
700
|
+
for (const f of [...openFences]) {
|
|
701
|
+
try {
|
|
702
|
+
f();
|
|
703
|
+
}
|
|
704
|
+
catch { }
|
|
705
|
+
}
|
|
706
|
+
if (onlyOurs) {
|
|
707
|
+
untrap();
|
|
708
|
+
process.exit(128 + (osConstants.signals[sig] ?? 15));
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const onExit = () => { for (const f of [...openFences]) {
|
|
712
|
+
try {
|
|
713
|
+
f();
|
|
714
|
+
}
|
|
715
|
+
catch { }
|
|
716
|
+
} };
|
|
717
|
+
let trapped = false;
|
|
718
|
+
function trap() {
|
|
719
|
+
if (trapped)
|
|
720
|
+
return;
|
|
721
|
+
trapped = true;
|
|
722
|
+
for (const s of SIGNALS)
|
|
723
|
+
process.on(s, onSignal);
|
|
724
|
+
process.on("exit", onExit);
|
|
725
|
+
}
|
|
726
|
+
function untrap() {
|
|
727
|
+
if (!trapped)
|
|
728
|
+
return;
|
|
729
|
+
trapped = false;
|
|
730
|
+
for (const s of SIGNALS)
|
|
731
|
+
process.removeListener(s, onSignal);
|
|
732
|
+
process.removeListener("exit", onExit);
|
|
733
|
+
}
|
|
734
|
+
export function quarantineRepoAgentConfig(worktree, stashRoot) {
|
|
735
|
+
const recovered = recoverRepoAgentConfig(worktree, stashRoot);
|
|
736
|
+
const dir = quarantineDir(stashRoot, worktree);
|
|
737
|
+
const m = { v: 2, worktree, head: headOf(worktree), moved: [], wrote: {} };
|
|
738
|
+
mkdirSync(join(dir, "files"), { recursive: true, mode: 0o700 });
|
|
739
|
+
let done = null;
|
|
740
|
+
const restore = () => {
|
|
741
|
+
if (done)
|
|
742
|
+
return { restored: [], setAside: [], discarded: [], lines: [] };
|
|
743
|
+
openFences.delete(restore);
|
|
744
|
+
if (!openFences.size)
|
|
745
|
+
untrap();
|
|
746
|
+
done = m.moved.length ? restoreFrom(stashRoot, dir, m, "run") : { restored: [], setAside: [], discarded: [], lines: [] };
|
|
747
|
+
if (!m.moved.length)
|
|
748
|
+
rmSync(dir, { recursive: true, force: true });
|
|
749
|
+
return done;
|
|
750
|
+
};
|
|
751
|
+
try {
|
|
752
|
+
for (const rel of REPO_AGENT_CONFIG) {
|
|
753
|
+
const unit = movableUnit(worktree, rel);
|
|
754
|
+
if (!unit || m.moved.some((u) => unit === u.rel || unit.startsWith(`${u.rel}/`)))
|
|
755
|
+
continue;
|
|
756
|
+
const hash = hashPath(join(worktree, unit));
|
|
757
|
+
const to = join(dir, "files", unit);
|
|
758
|
+
mkdirSync(dirname(to), { recursive: true, mode: 0o700 });
|
|
759
|
+
moveSync(join(worktree, unit), to);
|
|
760
|
+
m.moved.push({ rel: unit, hash });
|
|
761
|
+
writeManifest(dir, m);
|
|
762
|
+
if (m.moved.length === 1) {
|
|
763
|
+
openFences.add(restore);
|
|
764
|
+
trap();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (e) {
|
|
769
|
+
restore();
|
|
770
|
+
throw e;
|
|
771
|
+
}
|
|
772
|
+
if (!m.moved.length)
|
|
773
|
+
rmSync(dir, { recursive: true, force: true });
|
|
774
|
+
return {
|
|
775
|
+
moved: m.moved.map((e) => e.rel),
|
|
776
|
+
wrote: (rel) => {
|
|
777
|
+
const unit = m.moved.find((e) => rel === e.rel || rel.startsWith(`${e.rel}/`));
|
|
778
|
+
if (!unit || unit.rel !== rel)
|
|
779
|
+
return;
|
|
780
|
+
const h = hashPath(join(worktree, rel));
|
|
781
|
+
if (h) {
|
|
782
|
+
m.wrote[rel] = h;
|
|
783
|
+
writeManifest(dir, m);
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
restore,
|
|
787
|
+
recovered,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
422
790
|
//# sourceMappingURL=agents.js.map
|