github-router 0.3.31 → 0.3.33

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.
@@ -0,0 +1,24 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "github-router browser bridge",
4
+ "short_name": "gh-router-browser",
5
+ "version": "0.0.1",
6
+ "description": "Bridge between Claude (via github-router /mcp) and the browser. Implements tab control, navigation, clicks, form fill, downloads, screenshots, devtools eval. Blocks navigation to chrome://settings.",
7
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqJElxuBlonBS3TVW9FJN0mGTtShB3L1hoaYf6k39SOr1ogGYmF90EjRxy1i21k9wQQjPf26bcBu/9X67KrQjQV0uB38CaNukgiSeoLjfptN811u+PJHx6BP+jx3Qa6/3VenNPxHC8WEU0GXql8QSjIHEyCwKb6fMASXOK94JyB5Ywov2x8mt/+9ncqBBBMVzf6r5Sagy4PL1XnryLsuADD/vOEkPet8wXgH/Oj7v5tTsQQZ7U1JT51PoDs2BFnXc5v3TkVgZwd32k3ONh+nkDw1Hof+4zwUGOyJE6eMrlYzRlKM4Qxdf9JpavQvqfieAbTRWcyKeclnHeoIfE7cDBQIDAQAB",
8
+ "background": {
9
+ "service_worker": "background.js",
10
+ "type": "module"
11
+ },
12
+ "permissions": [
13
+ "nativeMessaging",
14
+ "tabs",
15
+ "activeTab",
16
+ "scripting",
17
+ "downloads",
18
+ "webNavigation",
19
+ "debugger",
20
+ "storage",
21
+ "alarms"
22
+ ],
23
+ "host_permissions": ["<all_urls>"]
24
+ }
@@ -0,0 +1,292 @@
1
+ import { c as writeRuntimeFileSecure, t as PATHS } from "./paths-Cr2gfGiA.js";
2
+ import { randomBytes, randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ //#region src/lib/worker-agent/lifecycle.ts
9
+ /**
10
+ * Same regex worktree.ts uses for its per-call age sweep — kept in
11
+ * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.
12
+ */
13
+ const WORKTREE_DIR_NAME_RE = /^(\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/;
14
+ /**
15
+ * Cap on the ledger: how many repos we remember across boots, and how
16
+ * old an entry may be before it's pruned. Both are belt-and-suspenders
17
+ * — the per-call age sweep is the primary guard against accumulation
18
+ * inside any single repo.
19
+ */
20
+ const LEDGER_MAX_ENTRIES = 100;
21
+ const LEDGER_MAX_AGE_MS = 720 * 60 * 60 * 1e3;
22
+ /**
23
+ * Set-like in-memory registry of worktrees this proxy created. Engine
24
+ * passes it to `createWorktree` so per-call cleanup deletes the entry
25
+ * on success; the signal handlers walk what's left at shutdown.
26
+ *
27
+ * Not a bare `Set` because we want to expose only the operations we
28
+ * actually use, and we want a stable testable surface.
29
+ */
30
+ var WorktreeRegistry = class {
31
+ entries = /* @__PURE__ */ new Set();
32
+ add(entry) {
33
+ this.entries.add(entry);
34
+ }
35
+ delete(entry) {
36
+ this.entries.delete(entry);
37
+ }
38
+ has(entry) {
39
+ return this.entries.has(entry);
40
+ }
41
+ values() {
42
+ return this.entries.values();
43
+ }
44
+ get size() {
45
+ return this.entries.size;
46
+ }
47
+ clear() {
48
+ this.entries.clear();
49
+ }
50
+ };
51
+ let _instanceUuid = null;
52
+ /**
53
+ * Stable UUID4 generated once per proxy process. Used in worktree
54
+ * dir/branch names so the boot sweep can reliably distinguish "this
55
+ * proxy's still-live worktrees" from "stranded dirs from a prior
56
+ * proxy that happens to have a recycled PID" — Docker PID-1 across
57
+ * container restarts is the classic case (peer-review HIGH finding).
58
+ */
59
+ function getInstanceUuid() {
60
+ if (_instanceUuid === null) _instanceUuid = randomUUID();
61
+ return _instanceUuid;
62
+ }
63
+ let _registered = false;
64
+ let _activeRegistry = null;
65
+ let _exitHandler = null;
66
+ let _sigintHandler = null;
67
+ let _sigtermHandler = null;
68
+ /**
69
+ * Synchronous cleanup of every registry entry. Best-effort:
70
+ * `execFileSync` failures are swallowed (the dir may have been
71
+ * removed already, or git may not be on PATH any more in some
72
+ * environments). After a successful removal we drop the entry from
73
+ * the registry so a second call is a true no-op.
74
+ *
75
+ * Synchronous on purpose — exit handlers can't reliably await async
76
+ * work; the process would die before the promise settled.
77
+ */
78
+ function sweepRegistry() {
79
+ if (!_activeRegistry) return;
80
+ const snapshot = [..._activeRegistry.values()];
81
+ for (const entry of snapshot) {
82
+ try {
83
+ execFileSync("git", [
84
+ "-C",
85
+ entry.repoRoot,
86
+ "worktree",
87
+ "remove",
88
+ "--force",
89
+ entry.dir
90
+ ], {
91
+ stdio: "ignore",
92
+ timeout: 1e4,
93
+ windowsHide: true
94
+ });
95
+ } catch {}
96
+ try {
97
+ execFileSync("git", [
98
+ "-C",
99
+ entry.repoRoot,
100
+ "branch",
101
+ "-D",
102
+ entry.branch
103
+ ], {
104
+ stdio: "ignore",
105
+ timeout: 5e3,
106
+ windowsHide: true
107
+ });
108
+ } catch {}
109
+ _activeRegistry.delete(entry);
110
+ }
111
+ }
112
+ /**
113
+ * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and
114
+ * remove every entry. Idempotent: subsequent calls swap the registry
115
+ * pointer but do NOT register additional process listeners (otherwise
116
+ * we'd leak listeners on every `runWorkerAgent`).
117
+ *
118
+ * Signal handlers re-raise the signal after sweeping. Naively running
119
+ * the sweep on SIGINT/SIGTERM and returning would *suppress* the
120
+ * signal: Node defaults to terminating the process on these, but only
121
+ * if no user listener is attached. Once we attach a listener, the
122
+ * default action is cancelled and the process keeps running — which
123
+ * means Ctrl-C would clean worktrees but not actually exit, leaving
124
+ * orphan processes in dev. The `process.kill(pid, sig)` re-raise
125
+ * after removing our own listener restores the default behaviour
126
+ * (the second delivery now hits an empty listener list, so Node
127
+ * terminates with the conventional `128 + signum` exit code).
128
+ */
129
+ function registerExitHandlers(registry) {
130
+ _activeRegistry = registry;
131
+ if (_registered) return;
132
+ _registered = true;
133
+ _exitHandler = () => sweepRegistry();
134
+ _sigintHandler = () => {
135
+ sweepRegistry();
136
+ if (_sigintHandler) process.off("SIGINT", _sigintHandler);
137
+ process.kill(process.pid, "SIGINT");
138
+ };
139
+ _sigtermHandler = () => {
140
+ sweepRegistry();
141
+ if (_sigtermHandler) process.off("SIGTERM", _sigtermHandler);
142
+ process.kill(process.pid, "SIGTERM");
143
+ };
144
+ process.on("SIGINT", _sigintHandler);
145
+ process.on("SIGTERM", _sigtermHandler);
146
+ process.on("exit", _exitHandler);
147
+ }
148
+ function ledgerPath() {
149
+ return path.join(PATHS.APP_DIR, "worker-repos.json");
150
+ }
151
+ async function readLedger() {
152
+ let raw;
153
+ try {
154
+ raw = await fs.readFile(ledgerPath(), "utf8");
155
+ } catch (err) {
156
+ if (err.code === "ENOENT") return { entries: [] };
157
+ return { entries: [] };
158
+ }
159
+ try {
160
+ const parsed = JSON.parse(raw);
161
+ if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] };
162
+ const cleaned = [];
163
+ for (const e of parsed.entries) if (e && typeof e === "object" && typeof e.repoRoot === "string" && typeof e.lastSeenMs === "number") cleaned.push({
164
+ repoRoot: e.repoRoot,
165
+ lastSeenMs: e.lastSeenMs
166
+ });
167
+ return { entries: cleaned };
168
+ } catch {
169
+ return { entries: [] };
170
+ }
171
+ }
172
+ /**
173
+ * Per-process serializer for ledger writes. Multiple concurrent
174
+ * `recordWorkerRepo` calls (legitimate: several workers may start at
175
+ * once) would otherwise race read-modify-write on the JSON file. Each
176
+ * call chains onto the previous so the on-disk sequence is
177
+ * deterministic from this process's perspective.
178
+ *
179
+ * Cross-process safety is provided by the atomic temp+rename below,
180
+ * which makes the final state of the file always be a well-formed
181
+ * full snapshot from ONE writer — never a partial write or
182
+ * interleaved JSON.
183
+ */
184
+ let _ledgerChain = Promise.resolve();
185
+ /**
186
+ * Append `repoRoot` to the ledger (or update its `lastSeenMs`).
187
+ * Atomic temp+rename per peer review.
188
+ */
189
+ function recordWorkerRepo(repoRoot) {
190
+ const next = _ledgerChain.then(async () => {
191
+ await fs.mkdir(PATHS.APP_DIR, { recursive: true });
192
+ const filtered = (await readLedger()).entries.filter((e) => e.repoRoot !== repoRoot);
193
+ filtered.push({
194
+ repoRoot,
195
+ lastSeenMs: Date.now()
196
+ });
197
+ const now = Date.now();
198
+ const ledger = { entries: filtered.filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS).slice(-LEDGER_MAX_ENTRIES) };
199
+ const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString("hex")}`;
200
+ try {
201
+ await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2));
202
+ await fs.rename(tmp, ledgerPath());
203
+ } catch (err) {
204
+ await fs.unlink(tmp).catch(() => {});
205
+ throw err;
206
+ }
207
+ });
208
+ _ledgerChain = next.catch(() => void 0);
209
+ return next;
210
+ }
211
+ function isPidAlive(pid) {
212
+ if (!Number.isInteger(pid) || pid <= 0) return false;
213
+ try {
214
+ process.kill(pid, 0);
215
+ return true;
216
+ } catch (err) {
217
+ if (err.code === "EPERM") return true;
218
+ return false;
219
+ }
220
+ }
221
+ /**
222
+ * Boot-time sweep. For every repo we recorded in the ledger,
223
+ * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional
224
+ * location — for repos already inside a worktree, the actual
225
+ * `git-common-dir` may differ, in which case we'll miss this batch
226
+ * and the per-call age sweep will catch them within 7 days) and
227
+ * remove dirs that aren't owned by THIS proxy.
228
+ *
229
+ * Ownership rule: dir is "ours" iff its embedded PID is alive AND
230
+ * its embedded UUID equals `getInstanceUuid()`. Either condition
231
+ * failing → remove.
232
+ */
233
+ async function sweepStaleWorktreesAtBoot() {
234
+ const ledger = await readLedger();
235
+ if (ledger.entries.length === 0) return;
236
+ const currentUuid = getInstanceUuid();
237
+ for (const entry of ledger.entries) {
238
+ const parent = path.join(entry.repoRoot, ".git", "worker-worktrees");
239
+ let names;
240
+ try {
241
+ names = await fs.readdir(parent);
242
+ } catch {
243
+ continue;
244
+ }
245
+ for (const name of names) {
246
+ const m = WORKTREE_DIR_NAME_RE.exec(name);
247
+ if (!m) continue;
248
+ const pid = Number.parseInt(m[1], 10);
249
+ const uuid = m[2];
250
+ if (isPidAlive(pid) && uuid === currentUuid) continue;
251
+ const fullDir = path.join(parent, name);
252
+ const branch = `worker/${pid}-${uuid}-${m[3]}`;
253
+ try {
254
+ execFileSync("git", [
255
+ "-C",
256
+ entry.repoRoot,
257
+ "worktree",
258
+ "remove",
259
+ "--force",
260
+ fullDir
261
+ ], {
262
+ stdio: "ignore",
263
+ timeout: 1e4,
264
+ windowsHide: true
265
+ });
266
+ } catch {}
267
+ try {
268
+ execFileSync("git", [
269
+ "-C",
270
+ entry.repoRoot,
271
+ "branch",
272
+ "-D",
273
+ branch
274
+ ], {
275
+ stdio: "ignore",
276
+ timeout: 5e3,
277
+ windowsHide: true
278
+ });
279
+ } catch {}
280
+ try {
281
+ await fs.rm(fullDir, {
282
+ recursive: true,
283
+ force: true
284
+ });
285
+ } catch {}
286
+ }
287
+ }
288
+ }
289
+
290
+ //#endregion
291
+ export { sweepRegistry as a, registerExitHandlers as i, getInstanceUuid as n, sweepStaleWorktreesAtBoot as o, recordWorkerRepo as r, WorktreeRegistry as t };
292
+ //# sourceMappingURL=lifecycle-3OXRVrtQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle-3OXRVrtQ.js","names":["_instanceUuid: string | null","_activeRegistry: WorktreeRegistry | null","_exitHandler: (() => void) | null","_sigintHandler: (() => void) | null","_sigtermHandler: (() => void) | null","raw: string","cleaned: Array<LedgerEntry>","_ledgerChain: Promise<void>","ledger: LedgerFile","names: Array<string>"],"sources":["../src/lib/worker-agent/lifecycle.ts"],"sourcesContent":["/**\n * Lifecycle plumbing for worker worktrees: in-memory registry, signal\n * handlers, ledger of repos touched, and the boot-time PID+instance\n * safety net.\n *\n * Plan: see `plans/we-have-added-a-dreamy-tide.md` (\"Worktree mode\" →\n * \"Cleanup paths\"). Three layers cooperate, none of them sufficient\n * alone:\n *\n * 1. Per-call cleanup (`engine.ts` finally block invoking\n * `WorktreeHandle.remove()`) — covers the happy path.\n *\n * 2. Session-end signal sweep (this file, registered via\n * `registerExitHandlers`) — covers Ctrl+C, service-manager stop,\n * and (in `github-router claude` mode) the spawned child's exit.\n * Synchronous `execFileSync` is intentional: exit handlers can't\n * reliably await async work.\n *\n * 3. Boot-time PID+instance sweep (`sweepStaleWorktreesAtBoot`) —\n * covers SIGKILL, OOM, container restart. Walks the ledger of\n * repos this proxy has touched and removes worktree dirs whose\n * `<pid>` is dead OR whose `<instance>` UUID doesn't match the\n * current proxy's UUID.\n *\n * Ledger writes are ATOMIC (temp + rename) per peer review — a\n * concurrent-RMW corruption would silently strand worktrees because\n * the boot sweep can't find their repo roots.\n */\n\nimport { execFileSync } from \"node:child_process\"\nimport { randomBytes, randomUUID } from \"node:crypto\"\nimport fs from \"node:fs/promises\"\nimport path from \"node:path\"\nimport process from \"node:process\"\n\nimport { PATHS, writeRuntimeFileSecure } from \"../paths\"\n\n/**\n * Same regex worktree.ts uses for its per-call age sweep — kept in\n * sync intentionally. `<pid>-<uuid>-<8hex>` strictly.\n */\nconst WORKTREE_DIR_NAME_RE =\n /^(\\d+)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-([0-9a-f]{8})$/\n\n/**\n * Cap on the ledger: how many repos we remember across boots, and how\n * old an entry may be before it's pruned. Both are belt-and-suspenders\n * — the per-call age sweep is the primary guard against accumulation\n * inside any single repo.\n */\nconst LEDGER_MAX_ENTRIES = 100\nconst LEDGER_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000\n\nexport interface WorktreeRegistryEntry {\n repoRoot: string\n dir: string\n branch: string\n}\n\n/**\n * Set-like in-memory registry of worktrees this proxy created. Engine\n * passes it to `createWorktree` so per-call cleanup deletes the entry\n * on success; the signal handlers walk what's left at shutdown.\n *\n * Not a bare `Set` because we want to expose only the operations we\n * actually use, and we want a stable testable surface.\n */\nexport class WorktreeRegistry {\n private readonly entries = new Set<WorktreeRegistryEntry>()\n\n add(entry: WorktreeRegistryEntry): void {\n this.entries.add(entry)\n }\n delete(entry: WorktreeRegistryEntry): void {\n this.entries.delete(entry)\n }\n has(entry: WorktreeRegistryEntry): boolean {\n return this.entries.has(entry)\n }\n values(): IterableIterator<WorktreeRegistryEntry> {\n return this.entries.values()\n }\n get size(): number {\n return this.entries.size\n }\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------\n// Per-launch instance UUID\n// ---------------------------------------------------------------------\n\nlet _instanceUuid: string | null = null\n\n/**\n * Stable UUID4 generated once per proxy process. Used in worktree\n * dir/branch names so the boot sweep can reliably distinguish \"this\n * proxy's still-live worktrees\" from \"stranded dirs from a prior\n * proxy that happens to have a recycled PID\" — Docker PID-1 across\n * container restarts is the classic case (peer-review HIGH finding).\n */\nexport function getInstanceUuid(): string {\n if (_instanceUuid === null) {\n _instanceUuid = randomUUID()\n }\n return _instanceUuid\n}\n\n/** Test-only: reset the cached UUID. */\nexport function __resetInstanceUuidForTests(): void {\n _instanceUuid = null\n}\n\n// ---------------------------------------------------------------------\n// Signal handlers + sweepRegistry\n// ---------------------------------------------------------------------\n\nlet _registered = false\nlet _activeRegistry: WorktreeRegistry | null = null\nlet _exitHandler: (() => void) | null = null\nlet _sigintHandler: (() => void) | null = null\nlet _sigtermHandler: (() => void) | null = null\n\n/**\n * Synchronous cleanup of every registry entry. Best-effort:\n * `execFileSync` failures are swallowed (the dir may have been\n * removed already, or git may not be on PATH any more in some\n * environments). After a successful removal we drop the entry from\n * the registry so a second call is a true no-op.\n *\n * Synchronous on purpose — exit handlers can't reliably await async\n * work; the process would die before the promise settled.\n */\nexport function sweepRegistry(): void {\n if (!_activeRegistry) return\n // Snapshot the values first so we can mutate the underlying set\n // during iteration without skipping entries.\n const snapshot = [..._activeRegistry.values()]\n for (const entry of snapshot) {\n try {\n // `-C entry.repoRoot` is load-bearing: without it git resolves\n // the worktree path relative to the proxy's cwd (which is the\n // user's launch dir, typically NOT inside the target repo), and\n // fails with `fatal: '<path>' is not a working tree`. The E2E\n // boot-sweep test (worker-agent-boot-sweep.test.ts) is what\n // caught the missing flag.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", entry.dir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // Already gone, EBUSY, or git not on PATH — best effort.\n }\n try {\n execFileSync(\"git\", [\"-C\", entry.repoRoot, \"branch\", \"-D\", entry.branch], {\n stdio: \"ignore\",\n timeout: 5_000,\n windowsHide: true,\n })\n } catch {\n // Same as above.\n }\n _activeRegistry.delete(entry)\n }\n}\n\n/**\n * Wire up SIGINT/SIGTERM/exit handlers that walk the registry and\n * remove every entry. Idempotent: subsequent calls swap the registry\n * pointer but do NOT register additional process listeners (otherwise\n * we'd leak listeners on every `runWorkerAgent`).\n *\n * Signal handlers re-raise the signal after sweeping. Naively running\n * the sweep on SIGINT/SIGTERM and returning would *suppress* the\n * signal: Node defaults to terminating the process on these, but only\n * if no user listener is attached. Once we attach a listener, the\n * default action is cancelled and the process keeps running — which\n * means Ctrl-C would clean worktrees but not actually exit, leaving\n * orphan processes in dev. The `process.kill(pid, sig)` re-raise\n * after removing our own listener restores the default behaviour\n * (the second delivery now hits an empty listener list, so Node\n * terminates with the conventional `128 + signum` exit code).\n */\nexport function registerExitHandlers(registry: WorktreeRegistry): void {\n _activeRegistry = registry\n if (_registered) return\n _registered = true\n _exitHandler = () => sweepRegistry()\n _sigintHandler = () => {\n sweepRegistry()\n if (_sigintHandler) process.off(\"SIGINT\", _sigintHandler)\n process.kill(process.pid, \"SIGINT\")\n }\n _sigtermHandler = () => {\n sweepRegistry()\n if (_sigtermHandler) process.off(\"SIGTERM\", _sigtermHandler)\n process.kill(process.pid, \"SIGTERM\")\n }\n process.on(\"SIGINT\", _sigintHandler)\n process.on(\"SIGTERM\", _sigtermHandler)\n // `exit` handlers can only run synchronous code — exactly what\n // sweepRegistry does. Async work here would never complete.\n process.on(\"exit\", _exitHandler)\n}\n\n/**\n * Test-only: unregister the handlers and reset module state. Tests\n * that want to verify `registerExitHandlers` semantics must clean up\n * after themselves or future tests in the same process inherit the\n * (now stale) registry pointer.\n */\nexport function __unregisterExitHandlersForTests(): void {\n if (_sigintHandler) {\n process.off(\"SIGINT\", _sigintHandler)\n _sigintHandler = null\n }\n if (_sigtermHandler) {\n process.off(\"SIGTERM\", _sigtermHandler)\n _sigtermHandler = null\n }\n if (_exitHandler) {\n process.off(\"exit\", _exitHandler)\n _exitHandler = null\n }\n _registered = false\n _activeRegistry = null\n}\n\n// ---------------------------------------------------------------------\n// Ledger: which repos has this proxy touched?\n// ---------------------------------------------------------------------\n\ninterface LedgerEntry {\n repoRoot: string\n lastSeenMs: number\n}\n\ninterface LedgerFile {\n entries: Array<LedgerEntry>\n}\n\nfunction ledgerPath(): string {\n return path.join(PATHS.APP_DIR, \"worker-repos.json\")\n}\n\nasync function readLedger(): Promise<LedgerFile> {\n let raw: string\n try {\n raw = await fs.readFile(ledgerPath(), \"utf8\")\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return { entries: [] }\n }\n return { entries: [] }\n }\n try {\n const parsed = JSON.parse(raw) as Partial<LedgerFile>\n if (!parsed || !Array.isArray(parsed.entries)) return { entries: [] }\n const cleaned: Array<LedgerEntry> = []\n for (const e of parsed.entries) {\n if (\n e &&\n typeof e === \"object\" &&\n typeof (e as LedgerEntry).repoRoot === \"string\" &&\n typeof (e as LedgerEntry).lastSeenMs === \"number\"\n ) {\n cleaned.push({\n repoRoot: (e as LedgerEntry).repoRoot,\n lastSeenMs: (e as LedgerEntry).lastSeenMs,\n })\n }\n }\n return { entries: cleaned }\n } catch {\n // Corrupted JSON — start fresh rather than crashing the proxy.\n return { entries: [] }\n }\n}\n\n/**\n * Per-process serializer for ledger writes. Multiple concurrent\n * `recordWorkerRepo` calls (legitimate: several workers may start at\n * once) would otherwise race read-modify-write on the JSON file. Each\n * call chains onto the previous so the on-disk sequence is\n * deterministic from this process's perspective.\n *\n * Cross-process safety is provided by the atomic temp+rename below,\n * which makes the final state of the file always be a well-formed\n * full snapshot from ONE writer — never a partial write or\n * interleaved JSON.\n */\nlet _ledgerChain: Promise<void> = Promise.resolve()\n\n/**\n * Append `repoRoot` to the ledger (or update its `lastSeenMs`).\n * Atomic temp+rename per peer review.\n */\nexport function recordWorkerRepo(repoRoot: string): Promise<void> {\n const next = _ledgerChain.then(async () => {\n await fs.mkdir(PATHS.APP_DIR, { recursive: true })\n const current = await readLedger()\n // Dedup: drop any existing entry for this root before appending\n // the fresh one so the array doesn't grow unbounded with repeats.\n const filtered = current.entries.filter((e) => e.repoRoot !== repoRoot)\n filtered.push({ repoRoot, lastSeenMs: Date.now() })\n // Prune by age and cap entry count (newest wins).\n const now = Date.now()\n const pruned = filtered\n .filter((e) => now - e.lastSeenMs < LEDGER_MAX_AGE_MS)\n .slice(-LEDGER_MAX_ENTRIES)\n const ledger: LedgerFile = { entries: pruned }\n\n // Atomic temp+rename. The temp filename is unique per call\n // (PID + 8 random hex chars) so concurrent processes don't\n // collide on the temp name; the final `rename` is atomic on\n // POSIX and on Windows (both with same filesystem).\n const tmp = `${ledgerPath()}.tmp.${process.pid}.${randomBytes(4).toString(\n \"hex\",\n )}`\n try {\n await writeRuntimeFileSecure(tmp, JSON.stringify(ledger, null, 2))\n await fs.rename(tmp, ledgerPath())\n } catch (err) {\n // Clean up the temp file if rename failed midway.\n await fs.unlink(tmp).catch(() => {})\n throw err\n }\n })\n // Swallow chain-internal errors so one failed write doesn't poison\n // the chain for every subsequent caller. Each call still sees its\n // own rejection (we return `next`, not the catch-handler chain).\n _ledgerChain = next.catch(() => undefined)\n return next\n}\n\nfunction isPidAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) return false\n try {\n process.kill(pid, 0)\n return true\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code\n // EPERM = process exists but we can't signal it — still alive\n // for our purposes (we just need to know whether to clean up).\n if (code === \"EPERM\") return true\n return false\n }\n}\n\n/**\n * Boot-time sweep. For every repo we recorded in the ledger,\n * enumerate `<repoRoot>/.git/worker-worktrees/` (the conventional\n * location — for repos already inside a worktree, the actual\n * `git-common-dir` may differ, in which case we'll miss this batch\n * and the per-call age sweep will catch them within 7 days) and\n * remove dirs that aren't owned by THIS proxy.\n *\n * Ownership rule: dir is \"ours\" iff its embedded PID is alive AND\n * its embedded UUID equals `getInstanceUuid()`. Either condition\n * failing → remove.\n */\nexport async function sweepStaleWorktreesAtBoot(): Promise<void> {\n const ledger = await readLedger()\n if (ledger.entries.length === 0) return\n const currentUuid = getInstanceUuid()\n for (const entry of ledger.entries) {\n const parent = path.join(entry.repoRoot, \".git\", \"worker-worktrees\")\n let names: Array<string>\n try {\n names = await fs.readdir(parent)\n } catch {\n continue\n }\n for (const name of names) {\n const m = WORKTREE_DIR_NAME_RE.exec(name)\n if (!m) continue\n const pid = Number.parseInt(m[1], 10)\n const uuid = m[2]\n const isOurs = isPidAlive(pid) && uuid === currentUuid\n if (isOurs) continue\n\n const fullDir = path.join(parent, name)\n const branch = `worker/${pid}-${uuid}-${m[3]}`\n try {\n // `-C entry.repoRoot` is load-bearing here too — see the\n // matching comment in `sweepRegistry`. The boot sweep runs\n // BEFORE any worker tool has set cwd, so the proxy's cwd is\n // the user's launch dir, which is almost never inside the\n // target repo.\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"worktree\", \"remove\", \"--force\", fullDir],\n { stdio: \"ignore\", timeout: 10_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n execFileSync(\n \"git\",\n [\"-C\", entry.repoRoot, \"branch\", \"-D\", branch],\n { stdio: \"ignore\", timeout: 5_000, windowsHide: true },\n )\n } catch {\n // ignore\n }\n try {\n await fs.rm(fullDir, { recursive: true, force: true })\n } catch {\n // ignore — git may have removed it already\n }\n }\n }\n}\n\n/** Test-only: clear the ledger file (does NOT remove on-disk worktrees). */\nexport async function __clearLedgerForTests(): Promise<void> {\n await fs.unlink(ledgerPath()).catch(() => {})\n}\n\n/** Test-only: read the ledger as a plain array (no side effects). */\nexport async function __readLedgerForTests(): Promise<Array<LedgerEntry>> {\n return (await readLedger()).entries\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
- import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-BrNqqJZH.js";
1
+ import "./paths-Cr2gfGiA.js";
2
+ import { a as sweepRegistry, i as registerExitHandlers, n as getInstanceUuid, o as sweepStaleWorktreesAtBoot, r as recordWorkerRepo, t as WorktreeRegistry } from "./lifecycle-3OXRVrtQ.js";
2
3
 
3
4
  export { WorktreeRegistry, getInstanceUuid, recordWorkerRepo, registerExitHandlers, sweepRegistry, sweepStaleWorktreesAtBoot };