@join-pantheon/cli 0.33.5 → 0.33.6
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 +34 -2
- package/dist/agents.js +114 -13
- package/dist/board.js +4 -0
- package/dist/claude-confine.js +46 -2
- package/dist/cli.js +194 -23
- package/dist/client.js +143 -6
- package/dist/doctor.js +49 -7
- package/dist/file-lock.js +2 -2
- package/dist/git-safe.js +5 -1
- package/dist/home-guard.js +57 -2
- package/dist/home-safety.js +2 -0
- package/dist/linux-sandbox.js +71 -0
- package/dist/machine.js +18 -0
- package/dist/mcp.js +13 -5
- package/dist/notifier.js +57 -7
- package/dist/persona.js +34 -10
- package/dist/pickup.js +82 -0
- package/dist/profile.js +24 -1
- package/dist/protocol.js +19 -2
- package/dist/relay.js +128 -20
- package/dist/removed.js +132 -0
- package/dist/run-failure.js +13 -0
- package/dist/setup-guide.js +27 -4
- package/dist/store.js +59 -0
- package/dist/tray.js +45 -7
- package/dist/usage.js +97 -0
- package/dist/which.js +3 -2
- package/dist/worker-fleet.js +33 -3
- package/dist/worker-policy.js +58 -0
- package/dist/worker.js +215 -29
- package/helper/macos/prebuilt/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -406,12 +406,30 @@ peer's machine was answered in **31 seconds with no human involved**, and:
|
|
|
406
406
|
2. …unless that report is over **30 minutes** old with nothing after it. Claude Code runs no
|
|
407
407
|
Stop hook on an interrupt, and no hook at all when a window is closed mid-turn, so a busy
|
|
408
408
|
report can outlive its turn; past 30 minutes it is read as "ended when reported"
|
|
409
|
-
3. your last turn ended under **2 minutes** ago
|
|
409
|
+
3. your last turn ended under **2 minutes** ago → the worker waits. Set the wait on your machine
|
|
410
|
+
with `pantheon policy set idle-seconds <0-900>` (read each tick, so the background worker needs
|
|
411
|
+
no restart; `--idle` sets it for one worker). The teammate who asked is told when to expect it:
|
|
412
|
+
the `pantheon_send` result for an ASK, `pantheon status` and the board say "Rohit's agent will
|
|
413
|
+
pick this up in about 2 minutes", "Rohit is at his keyboard…", that his worker is offline, or
|
|
414
|
+
that his machine can't take requests yet (no agent CLI installed or signed in)
|
|
410
415
|
4. nothing has ever reported and no hook is installed here → any open window wins (the
|
|
411
416
|
conservative fallback); with the hook installed, silence reads as idle
|
|
412
417
|
5. otherwise the worker claims the work — after a 20-second grace while a window is open,
|
|
413
418
|
~2 seconds when none is. `pantheon ps` shows which of these applies right now
|
|
419
|
+
- an agent CLI it can run unattended (Claude Code, Codex, Cursor's `cursor-agent`, or GitHub's
|
|
420
|
+
`copilot`), installed and signed in. The Claude desktop app alone is not enough. `pantheon join`,
|
|
421
|
+
`start` and `setup` check this and say what to install for your OS before anyone sends you work
|
|
422
|
+
- on Linux, with commands on: Claude Code's sandbox (bubblewrap, plus socat) must be able to start.
|
|
423
|
+
Some containers and Ubuntu 23.10+/24.04 (AppArmor's restriction on unprivileged user namespaces)
|
|
424
|
+
stop it even when both are installed; `pantheon doctor` starts it once and says what failed and the
|
|
425
|
+
likely fix, and teammates are told "its Linux sandbox can't start". Pantheon never runs commands
|
|
426
|
+
outside the sandbox instead
|
|
414
427
|
- one run at a time, capped at 12/hour by default
|
|
428
|
+
- each run may take **60 minutes** by default; one still going then is stopped (with everything it
|
|
429
|
+
started), tried once more in the same worktree, then handed to you with the limit named. Set it
|
|
430
|
+
on your machine with `pantheon policy set run-minutes <5-180>` (read at the next run, so the
|
|
431
|
+
background worker needs no reinstall); `pantheon worker --run-minutes <n>` sets it for one worker,
|
|
432
|
+
and a persona's `max_run_minutes` can only lower it
|
|
415
433
|
- the full transcript is left in `.pantheon-run.log` on the branch, and the run's scratch files
|
|
416
434
|
(brief, transcript, MCP config) are git-excluded so they cannot ride along into a commit —
|
|
417
435
|
**no credential is ever written into the worktree**
|
|
@@ -595,6 +613,7 @@ pantheon worker --persona reviewer # pin one; or let each request match by its r
|
|
|
595
613
|
---
|
|
596
614
|
allow_bash: false # never shell for runs this persona handles, even with --allow-bash
|
|
597
615
|
max_runs_per_hour: 6
|
|
616
|
+
max_run_minutes: 30 # a run it handles is stopped after 30 minutes (only lowers the machine's limit)
|
|
598
617
|
wake_by: alice, bob # only these peers
|
|
599
618
|
kinds: ANSWER, ACCEPT # asks and proposal verdicts, not revisions
|
|
600
619
|
goals: launch-* # only matching goals
|
|
@@ -613,6 +632,10 @@ The rules that make this safe to commit into a shared repo:
|
|
|
613
632
|
the skip is logged with the reason. No personas at all = open mode, as before.
|
|
614
633
|
- Several personas match different work (by peer, kind, goal) — one worker, many hats. `default`
|
|
615
634
|
is always tried last, so it never shadows a named persona.
|
|
635
|
+
- **`max_run_minutes` is new in 0.33.6.** A Pantheon before 0.33.6 does not know the key and reads
|
|
636
|
+
a persona that uses it as broken, so a teammate's worker on an older version skips a *committed*
|
|
637
|
+
persona with it until they upgrade. `pantheon doctor` and `pantheon persona show` say so. Put it
|
|
638
|
+
in a persona under `~/.pantheon/personas` (yours alone), or commit it once the team is on 0.33.6.
|
|
616
639
|
|
|
617
640
|
## Guardrails
|
|
618
641
|
|
|
@@ -634,7 +657,7 @@ never in a prompt: a prompt can be argued out of its instructions.
|
|
|
634
657
|
| Invites | single-use, expiring | a leaked link is inert once redeemed or timed out |
|
|
635
658
|
| Claims | one session per obligation | parallel Conductor workspaces cannot duplicate work |
|
|
636
659
|
| Wake rights | `pantheon wake allow`, per machine; `--may-wake`, per member for the team | under "anyone" (the default): every peer the team lets wake; under "approved": only those you also approved here. Checked by the worker before a run starts, and by the relay at its claim |
|
|
637
|
-
| Run budget | 12/hour, 1 concurrent | a peer cannot drain your tokens |
|
|
660
|
+
| Run budget | 12/hour, 1 concurrent, 60 minutes each | a peer cannot drain your tokens; `pantheon policy set run-minutes` (5–180) |
|
|
638
661
|
| Personas | tighten-only | a persona file — even one a teammate committed — can lower caps and turn bash off, never the reverse |
|
|
639
662
|
| Signed acts | every act and goal, ed25519, since 0.31 | checked on each receiving machine against the key it pinned for the sender: the relay cannot forge, alter, re-file or replay a teammate's request without that machine noticing, and a worker runs only requests that check out. A teammate's changed key is held back until you confirm it (`pantheon trust`) |
|
|
640
663
|
| Worktree hygiene | no secrets on disk | credentials reach a woken run through its environment only; run scratch files are git-excluded |
|
|
@@ -731,6 +754,15 @@ shape of a common secret).
|
|
|
731
754
|
Whoever operates the relay can read those acts — the operator dashboard shows each space's
|
|
732
755
|
message history — so treat a space like a shared team channel, not a private notebook.
|
|
733
756
|
|
|
757
|
+
The relay also keeps **daily usage counts per team** (from 0.33.6), so pricing can follow use
|
|
758
|
+
later; nothing is charged from them. Per team and per UTC day it counts: messages by type (ASK,
|
|
759
|
+
ANSWER, PROPOSE, DELIVERED, …), how many of those an unattended worker run sent and how many a
|
|
760
|
+
person's own session sent, and goals opened and closed. That is all: no count per person or per
|
|
761
|
+
member, no bandwidth, no message content. The counts are kept for 400 days in a file beside the
|
|
762
|
+
team's ledger, survive restarts and ledger compaction, and are deleted with the team. The team's
|
|
763
|
+
founder reads them with `pantheon usage` (last 30 days; other members are refused), and the relay's
|
|
764
|
+
operator through the admin report.
|
|
765
|
+
|
|
734
766
|
- `pantheon sessions` shows every device and session on your account; `revoke` signs one out.
|
|
735
767
|
- `pantheon audit` is your own trail; `pantheon account delete` removes your account, devices,
|
|
736
768
|
sessions, and memberships.
|
package/dist/agents.js
CHANGED
|
@@ -6,7 +6,8 @@ import { join, dirname, isAbsolute, relative } from "node:path";
|
|
|
6
6
|
import { copyInside, mkdirInside, writeInside } from "./safe-write.js";
|
|
7
7
|
import { gitCallEnv, safeGitArgs } from "./git-safe.js";
|
|
8
8
|
import { launcher, kiroDir } from "./install.js";
|
|
9
|
-
import { agentSpawn, which } from "./which.js";
|
|
9
|
+
import { agentSpawn, loginShellLookup, loginShellRunner, which } from "./which.js";
|
|
10
|
+
import { probeLinuxSandbox, sandboxAdvice } from "./linux-sandbox.js";
|
|
10
11
|
import { earliest, whenAgain } from "./agent-outage.js";
|
|
11
12
|
import { PANTHEON_TOOL_NAMES, PANTHEON_TOOLS } from "./tools.js";
|
|
12
13
|
import { claudeRunSettings, NO_CLAUDE_FEATURES, runTmpDir, sandboxCacheEnv, versionAtLeast } from "./claude-confine.js";
|
|
@@ -40,18 +41,19 @@ export function hasCopilotExtension(home = homedir()) {
|
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
const sh = (cmd) => {
|
|
44
|
+
const sh = (cmd, timeoutMs = 5_000) => {
|
|
44
45
|
try {
|
|
45
|
-
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || null;
|
|
46
|
+
return execFileSync("sh", ["-lc", cmd], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: timeoutMs, killSignal: "SIGKILL" }).trim() || null;
|
|
46
47
|
}
|
|
47
48
|
catch {
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
};
|
|
51
52
|
export function cliOnPath(name) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const k = `path:${name}`, c = binaryCache.get(k);
|
|
54
|
+
if (!c || (!c.path && Date.now() - c.at > NOT_FOUND_TTL_MS))
|
|
55
|
+
binaryCache.set(k, { path: which(name), at: Date.now() });
|
|
56
|
+
return binaryCache.get(k).path;
|
|
55
57
|
}
|
|
56
58
|
export function agentById(id) {
|
|
57
59
|
return AGENTS.find((a) => a.id === id || a.binaries.includes(id));
|
|
@@ -84,6 +86,9 @@ export function detectForHooks(projectDir = process.cwd()) {
|
|
|
84
86
|
}
|
|
85
87
|
return out;
|
|
86
88
|
}
|
|
89
|
+
export function anyUnattendedAgentOnPath(lookupTimeoutMs = READINESS_LOOKUP_MS) {
|
|
90
|
+
return AGENTS.some((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.id) && !!resolveBinary(a, { lookupTimeoutMs }));
|
|
91
|
+
}
|
|
87
92
|
export function detectAvailable() {
|
|
88
93
|
const out = [];
|
|
89
94
|
for (const backend of AGENTS) {
|
|
@@ -124,19 +129,36 @@ export function detectOnPath() {
|
|
|
124
129
|
return null;
|
|
125
130
|
}
|
|
126
131
|
const binaryCache = new Map();
|
|
127
|
-
|
|
132
|
+
const NOT_FOUND_TTL_MS = 30_000;
|
|
133
|
+
const notCursorAgent = new Set();
|
|
134
|
+
export const READINESS_LOOKUP_MS = 3_000;
|
|
135
|
+
export function forgetMissingBinaries() {
|
|
136
|
+
for (const [b, v] of binaryCache)
|
|
137
|
+
if (!v.path)
|
|
138
|
+
binaryCache.delete(b);
|
|
139
|
+
}
|
|
140
|
+
export function resolveBinary(backend, o = {}) {
|
|
128
141
|
for (const b of backend.binaries) {
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
const cached = binaryCache.get(b);
|
|
143
|
+
if (!cached || (!cached.path && Date.now() - cached.at > NOT_FOUND_TTL_MS)) {
|
|
144
|
+
const hit = o.lookupTimeoutMs !== undefined
|
|
145
|
+
? which(b, platform(), process.env, { login: (n) => loginShellLookup(n, loginShellRunner(o.lookupTimeoutMs)) })
|
|
146
|
+
: which(b);
|
|
131
147
|
let found = hit ? (platform() === "win32" ? hit : b) : null;
|
|
132
148
|
if (found && b === "agent") {
|
|
133
|
-
|
|
134
|
-
if (!/cursor/i.test(v))
|
|
149
|
+
if (notCursorAgent.has(hit))
|
|
135
150
|
found = null;
|
|
151
|
+
else {
|
|
152
|
+
const v = `${sh("agent --version 2>/dev/null | head -1", 3_000) ?? ""} ${sh("agent --help 2>/dev/null | head -3", 3_000) ?? ""}`;
|
|
153
|
+
if (!/cursor/i.test(v)) {
|
|
154
|
+
found = null;
|
|
155
|
+
notCursorAgent.add(hit);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
136
158
|
}
|
|
137
|
-
binaryCache.set(b, found);
|
|
159
|
+
binaryCache.set(b, { path: found, at: Date.now() });
|
|
138
160
|
}
|
|
139
|
-
const hit = binaryCache.get(b);
|
|
161
|
+
const hit = binaryCache.get(b).path;
|
|
140
162
|
if (hit)
|
|
141
163
|
return hit;
|
|
142
164
|
}
|
|
@@ -315,6 +337,85 @@ export function unattendedRefusal(c, o = {}) {
|
|
|
315
337
|
return null;
|
|
316
338
|
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}`;
|
|
317
339
|
}
|
|
340
|
+
export function claudeInstallCommand(os = platform()) {
|
|
341
|
+
return os === "win32" ? "irm https://claude.ai/install.ps1 | iex (in PowerShell)" : "curl -fsSL https://claude.ai/install.sh | bash";
|
|
342
|
+
}
|
|
343
|
+
export function unattendedReadiness(o = {}) {
|
|
344
|
+
if (o.available)
|
|
345
|
+
return readinessFrom(o.available, o);
|
|
346
|
+
const resolve = o.resolve ?? ((b) => resolveBinary(b, { lookupTimeoutMs: READINESS_LOOKUP_MS }));
|
|
347
|
+
let probed = null;
|
|
348
|
+
const probe = o.sandboxProbe ?? probeLinuxSandbox;
|
|
349
|
+
const probeOnce = () => (probed ??= { p: probe() }).p;
|
|
350
|
+
o = { ...o, sandboxProbe: probeOnce };
|
|
351
|
+
const seen = new Map();
|
|
352
|
+
const base = o.check === null ? null : (o.check ?? ((b, bin) => signedIn(b, bin, undefined, o.home)));
|
|
353
|
+
const check = base === null ? null : (b, bin) => { const k = `${b.id}|${bin}`; let r = seen.get(k); if (!r) {
|
|
354
|
+
r = base(b, bin);
|
|
355
|
+
seen.set(k, r);
|
|
356
|
+
} return r; };
|
|
357
|
+
const pinned = o.pinned && o.pinned !== "auto" ? agentById(o.pinned) : undefined;
|
|
358
|
+
const wanted = o.prefer ? agentById(o.prefer) : undefined;
|
|
359
|
+
const order = pinned ? [pinned] : [
|
|
360
|
+
...(o.unsandboxedOk ? [] : AGENTS.filter((a) => SANDBOXED_FOR_UNATTENDED.has(a.id))),
|
|
361
|
+
...(wanted ? [wanted] : []),
|
|
362
|
+
...AGENTS.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.id)),
|
|
363
|
+
...AGENTS.filter((a) => NOT_FOR_UNATTENDED_RUNS.has(a.id)),
|
|
364
|
+
].filter((a, i, all) => all.findIndex((x) => x.id === a.id) === i);
|
|
365
|
+
const found = [];
|
|
366
|
+
let last = null;
|
|
367
|
+
for (const b of order) {
|
|
368
|
+
const bin = resolve(b);
|
|
369
|
+
if (!bin)
|
|
370
|
+
continue;
|
|
371
|
+
found.push({ backend: b, binary: bin });
|
|
372
|
+
if (NOT_FOR_UNATTENDED_RUNS.has(b.id))
|
|
373
|
+
continue;
|
|
374
|
+
const r = readinessFrom(found, { ...o, check });
|
|
375
|
+
if (r.ok)
|
|
376
|
+
return r;
|
|
377
|
+
last = { r, n: found.length };
|
|
378
|
+
}
|
|
379
|
+
return last && last.n === found.length ? last.r : readinessFrom(found, { ...o, check });
|
|
380
|
+
}
|
|
381
|
+
function readinessFrom(given, o) {
|
|
382
|
+
const os = o.os ?? platform();
|
|
383
|
+
const available = given.filter((a) => !NOT_FOR_UNATTENDED_RUNS.has(a.backend.id));
|
|
384
|
+
if (!available.length && (!o.pinned || o.pinned === "auto")) {
|
|
385
|
+
const kiro = given.some((a) => a.backend.id === "kiro");
|
|
386
|
+
return {
|
|
387
|
+
ok: false, code: kiro ? "kiro_only" : "no_agent", short: "Claude Code (or another agent CLI) isn't installed",
|
|
388
|
+
says: `No agent on this machine can take teammates' requests while you're away: none of claude, codex, cursor-agent or copilot is installed${kiro ? " (Kiro answers only in your live sessions)" : ""}. ` +
|
|
389
|
+
`The Claude desktop app alone is not enough — it puts no command-line tool here.`,
|
|
390
|
+
fix: `Install Claude Code: ${claudeInstallCommand(os)} — then run \`claude\` once and sign in (/login). Codex (npm install -g @openai/codex), Cursor's cursor-agent or GitHub's copilot CLI work too.`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
let choice;
|
|
394
|
+
try {
|
|
395
|
+
choice = (o.choose ?? chooseAgent)(o.pinned ?? "auto", { ...o, available });
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
const m = e.message;
|
|
399
|
+
return { ok: false, code: /no agent CLI on PATH|is not on PATH/.test(m) ? "no_agent" : "no_agent_choice", short: m.replace(/ \(looked for .*\)$/, "").slice(0, 120), says: m, fix: "pantheon doctor names what is missing" };
|
|
400
|
+
}
|
|
401
|
+
const refused = unattendedRefusal(choice, { ...(o.unsandboxedOk !== undefined ? { unsandboxedOk: o.unsandboxedOk } : {}) });
|
|
402
|
+
if (refused) {
|
|
403
|
+
return { ok: false, code: NOT_FOR_UNATTENDED_RUNS.has(choice.backend.id) ? "kiro_only" : "unsandboxed_off", short: `${choice.backend.label} is set to leave requests for them`, says: `Teammates' requests wait for your next session here: ${refused}`,
|
|
404
|
+
fix: `install Claude Code (${claudeInstallCommand(os)}) to have requests answered in its sandbox, or: pantheon policy grant unsandboxed-runs` };
|
|
405
|
+
}
|
|
406
|
+
if (choice.signIn && !choice.signIn.ok && !choice.signIn.unknown) {
|
|
407
|
+
return { ok: false, code: "not_signed_in", short: `${choice.backend.label} isn't signed in`, says: `${choice.backend.label} is installed but not signed in, so a request while you're away would fail.`,
|
|
408
|
+
fix: SIGN_IN_CHECK[choice.backend.id].fix };
|
|
409
|
+
}
|
|
410
|
+
if (o.commandsOn && choice.backend.id === "claude-code" && os === "linux") {
|
|
411
|
+
const p = (o.sandboxProbe ?? probeLinuxSandbox)();
|
|
412
|
+
if (p && !p.ok) {
|
|
413
|
+
const a = sandboxAdvice(p, { container: !!o.inContainer });
|
|
414
|
+
return { ok: false, code: "sandbox_unavailable", short: "its Linux sandbox can't start", says: a.says, fix: a.fix };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return { ok: true, short: "", says: `${choice.backend.label} takes teammates' requests while you're away.`, fix: "" };
|
|
418
|
+
}
|
|
318
419
|
export function describeChoice(c) {
|
|
319
420
|
const head = `${c.backend.label}${c.signIn ? ` — ${c.signIn.detail}` : ""} (${c.because})`;
|
|
320
421
|
return c.skipped.length ? `${head}; skipped ${c.skipped.map((s) => `${s.backend.label} — ${s.why}`).join("; ")}` : head;
|
package/dist/board.js
CHANGED
|
@@ -899,6 +899,8 @@ export function renderWatch(s, o = { now: Date.now() }) {
|
|
|
899
899
|
lines.push(D(fit(` ${meta} ${i.id}`)));
|
|
900
900
|
if (i.context)
|
|
901
901
|
lines.push(D(fit(` for: ${termSafe(i.context)}`)));
|
|
902
|
+
if (i.pickup)
|
|
903
|
+
lines.push(D(fit(` ${termSafe(i.pickup)}`)));
|
|
902
904
|
if (lane.id === "now")
|
|
903
905
|
for (const cmd of watchCommands({ ...i, ...(i.space ? { space: termSafe(i.space) } : {}) }, multi))
|
|
904
906
|
lines.push(` ${cmd}`);
|
|
@@ -1216,6 +1218,8 @@ function row(item, card) {
|
|
|
1216
1218
|
else meta.append(el("span", "chip", item.decider ? item.decider + " decides" : "waiting on " + item.who));
|
|
1217
1219
|
li.append(meta);
|
|
1218
1220
|
if (item.context) li.append(el("p", "ctx", "For: " + item.context));
|
|
1221
|
+
// When a teammate's agent will pick it up (#69): so a wait does not read as broken.
|
|
1222
|
+
if (item.pickup) li.append(el("p", "ctx", item.pickup));
|
|
1219
1223
|
const acts = el("div", "actions");
|
|
1220
1224
|
if (card && item.wake) {
|
|
1221
1225
|
// A teammate waiting on this person's approval: Approve / Just this once /
|
package/dist/claude-confine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { lstatSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync } from "node:fs";
|
|
4
4
|
import { writeInside } from "./safe-write.js";
|
|
5
5
|
import { homedir, platform, tmpdir } from "node:os";
|
|
6
6
|
import { isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
|
|
@@ -110,6 +110,50 @@ export function gitDataDeny(common) {
|
|
|
110
110
|
catch { }
|
|
111
111
|
return { files, dirs: [join(c, "hooks"), join(c, "info")] };
|
|
112
112
|
}
|
|
113
|
+
export function absentDeniedPaths(common) {
|
|
114
|
+
const d = gitDataDeny(common);
|
|
115
|
+
const out = new Set();
|
|
116
|
+
const there = (p) => { try {
|
|
117
|
+
lstatSync(p);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
} };
|
|
123
|
+
const root = resolve(common);
|
|
124
|
+
for (const p of [...d.files, ...d.dirs]) {
|
|
125
|
+
let q = resolve(p);
|
|
126
|
+
while (!there(q) && q.startsWith(root + sep)) {
|
|
127
|
+
out.add(q);
|
|
128
|
+
q = resolve(q, "..");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...out].sort((a, b) => b.length - a.length);
|
|
132
|
+
}
|
|
133
|
+
export function removeSandboxLeftovers(absent) {
|
|
134
|
+
const removed = [];
|
|
135
|
+
for (const p of absent) {
|
|
136
|
+
let st;
|
|
137
|
+
try {
|
|
138
|
+
st = lstatSync(p);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (st.isFile() && st.size === 0 && st.nlink === 1) {
|
|
145
|
+
rmSync(p, { force: true });
|
|
146
|
+
removed.push(p);
|
|
147
|
+
}
|
|
148
|
+
else if (st.isDirectory() && readdirSync(p).length === 0) {
|
|
149
|
+
rmdirSync(p);
|
|
150
|
+
removed.push(p);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch { }
|
|
154
|
+
}
|
|
155
|
+
return removed;
|
|
156
|
+
}
|
|
113
157
|
const inside = (child, parent) => {
|
|
114
158
|
const r = relative(parent, child);
|
|
115
159
|
return r === "" || (!!r && !r.startsWith("..") && !isAbsolute(r));
|
|
@@ -140,7 +184,7 @@ function userExcludedCommands(file) {
|
|
|
140
184
|
}
|
|
141
185
|
}
|
|
142
186
|
export const SECRET_FILE_GLOBS = [".env", ".env.*", "*.pem", "*.key", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*", ".npmrc", ".netrc", "credentials*", "*.keystore", "service-account*.json"];
|
|
143
|
-
const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "bin", "runtime", "ops", "run-homes", "logs", "mcp", "data"];
|
|
187
|
+
const PANTHEON_HOME_ENTRIES = ["config.json", "config.json.bak", "config.json.lock", "worker-policy.json", "profile.json", "continued.json", "removed.json", "removed.json.lock", "readiness-recheck.stamp", "bin", "runtime", "ops", "run-homes", "logs", "mcp", "data"];
|
|
144
188
|
function aroundWorktree(top, keeps, named) {
|
|
145
189
|
const keep = (typeof keeps === "string" ? [keeps] : [...keeps]).map((k) => resolve(k));
|
|
146
190
|
const out = [];
|