| import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises' |
| import { dirname, join } from 'path' |
| import { z } from 'zod/v4' |
| import { logForDebugging } from '../utils/debug.js' |
| import { isENOENT } from '../utils/errors.js' |
| import { getWorktreePathsPortable } from '../utils/getWorktreePathsPortable.js' |
| import { lazySchema } from '../utils/lazySchema.js' |
| import { |
| getProjectsDir, |
| sanitizePath, |
| } from '../utils/sessionStoragePortable.js' |
| import { jsonParse, jsonStringify } from '../utils/slowOperations.js' |
|
|
| |
| |
| |
| |
| |
| const MAX_WORKTREE_FANOUT = 50 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000 |
|
|
| const BridgePointerSchema = lazySchema(() => |
| z.object({ |
| sessionId: z.string(), |
| environmentId: z.string(), |
| source: z.enum(['standalone', 'repl']), |
| }), |
| ) |
|
|
| export type BridgePointer = z.infer<ReturnType<typeof BridgePointerSchema>> |
|
|
| export function getBridgePointerPath(dir: string): string { |
| return join(getProjectsDir(), sanitizePath(dir), 'bridge-pointer.json') |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export async function writeBridgePointer( |
| dir: string, |
| pointer: BridgePointer, |
| ): Promise<void> { |
| const path = getBridgePointerPath(dir) |
| try { |
| await mkdir(dirname(path), { recursive: true }) |
| await writeFile(path, jsonStringify(pointer), 'utf8') |
| logForDebugging(`[bridge:pointer] wrote ${path}`) |
| } catch (err: unknown) { |
| logForDebugging(`[bridge:pointer] write failed: ${err}`, { level: 'warn' }) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function readBridgePointer( |
| dir: string, |
| ): Promise<(BridgePointer & { ageMs: number }) | null> { |
| const path = getBridgePointerPath(dir) |
| let raw: string |
| let mtimeMs: number |
| try { |
| |
| |
| mtimeMs = (await stat(path)).mtimeMs |
| raw = await readFile(path, 'utf8') |
| } catch { |
| return null |
| } |
|
|
| const parsed = BridgePointerSchema().safeParse(safeJsonParse(raw)) |
| if (!parsed.success) { |
| logForDebugging(`[bridge:pointer] invalid schema, clearing: ${path}`) |
| await clearBridgePointer(dir) |
| return null |
| } |
|
|
| const ageMs = Math.max(0, Date.now() - mtimeMs) |
| if (ageMs > BRIDGE_POINTER_TTL_MS) { |
| logForDebugging(`[bridge:pointer] stale (>4h mtime), clearing: ${path}`) |
| await clearBridgePointer(dir) |
| return null |
| } |
|
|
| return { ...parsed.data, ageMs } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function readBridgePointerAcrossWorktrees( |
| dir: string, |
| ): Promise<{ pointer: BridgePointer & { ageMs: number }; dir: string } | null> { |
| |
| |
| const here = await readBridgePointer(dir) |
| if (here) { |
| return { pointer: here, dir } |
| } |
|
|
| |
| |
| const worktrees = await getWorktreePathsPortable(dir) |
| if (worktrees.length <= 1) return null |
| if (worktrees.length > MAX_WORKTREE_FANOUT) { |
| logForDebugging( |
| `[bridge:pointer] ${worktrees.length} worktrees exceeds fanout cap ${MAX_WORKTREE_FANOUT}, skipping`, |
| ) |
| return null |
| } |
|
|
| |
| |
| |
| const dirKey = sanitizePath(dir) |
| const candidates = worktrees.filter(wt => sanitizePath(wt) !== dirKey) |
|
|
| |
| |
| |
| const results = await Promise.all( |
| candidates.map(async wt => { |
| const p = await readBridgePointer(wt) |
| return p ? { pointer: p, dir: wt } : null |
| }), |
| ) |
|
|
| |
| |
| |
| let freshest: { |
| pointer: BridgePointer & { ageMs: number } |
| dir: string |
| } | null = null |
| for (const r of results) { |
| if (r && (!freshest || r.pointer.ageMs < freshest.pointer.ageMs)) { |
| freshest = r |
| } |
| } |
| if (freshest) { |
| logForDebugging( |
| `[bridge:pointer] fanout found pointer in worktree ${freshest.dir} (ageMs=${freshest.pointer.ageMs})`, |
| ) |
| } |
| return freshest |
| } |
|
|
| |
| |
| |
| |
| export async function clearBridgePointer(dir: string): Promise<void> { |
| const path = getBridgePointerPath(dir) |
| try { |
| await unlink(path) |
| logForDebugging(`[bridge:pointer] cleared ${path}`) |
| } catch (err: unknown) { |
| if (!isENOENT(err)) { |
| logForDebugging(`[bridge:pointer] clear failed: ${err}`, { |
| level: 'warn', |
| }) |
| } |
| } |
| } |
|
|
| function safeJsonParse(raw: string): unknown { |
| try { |
| return jsonParse(raw) |
| } catch { |
| return null |
| } |
| } |
|
|