import { realpathSync } from "node:fs"; import { createConnection, createServer, type Server } from "node:net"; import { fileURLToPath } from "node:url"; export const worktreeRoot = realpathSync(fileURLToPath(new URL("..", import.meta.url))); export function worktreeId(root: string): string { let hash = 0xcbf29ce484222325n; for (const byte of Buffer.from(root)) { hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n); } return `ai.l0ng.openidea.dev.w${hash.toString(16).padStart(16, "0")}`; } export interface DevSession { worktree: string; identifier: string; pid: number; devUrl?: string; } export function preferredVitePort(identifier: string): number { return 15000 + (Number.parseInt(identifier.slice(-8), 16) % 20000); } function readSession(port: number): Promise { return new Promise((resolve) => { const socket = createConnection({ host: "127.0.0.1", port }); let data = ""; const finish = (session?: DevSession) => { socket.destroy(); resolve(session); }; socket.setTimeout(1000, () => finish()); socket.on("error", () => finish()); socket.on("data", (chunk) => { data += chunk.toString(); if (data.length > 16384) finish(); }); socket.on("end", () => { try { finish(JSON.parse(data)); } catch { finish(); } }); }); } // A live OS listener is the lock: no stale PID files after crashes or PID reuse. export async function claimSession( session: DevSession, firstPort = 43000 + (Number.parseInt(session.identifier.slice(-8), 16) % 10000), ): Promise<{ server: Server } | { existing: DevSession }> { for (let offset = 0; offset < 100; offset++) { const port = firstPort + offset; const server = createServer((socket) => { socket.on("error", () => {}); socket.end(JSON.stringify(session)); }); try { await new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", () => { server.removeListener("error", reject); resolve(); }); }); return { server }; } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== "EADDRINUSE" && code !== "EACCES") throw error; const existing = await readSession(port); if (existing?.worktree === session.worktree && existing.identifier === session.identifier) { return { existing }; } } } throw new Error("No available development session port"); } /** `tauri dev` options that take a value as the next argument. */ const VALUE_OPTIONS = new Set(["-c", "--config", "-t", "--target", "-f", "--features", "-r", "--runner", "--port"]); /** * `args` with the session's Vite URL as the last `--config`, placed before the first * positional argument: `tauri dev` passes everything from there on to the app, so a config * after `pnpm dev:app ` would be ignored. */ export function withDevConfig(args: string[], devUrl: string): string[] { let index = 1; while (index < args.length && args[index] !== "--" && args[index]!.startsWith("-")) { index += VALUE_OPTIONS.has(args[index]!) ? 2 : 1; } index = Math.min(index, args.length); return [ ...args.slice(0, index), "--config", JSON.stringify({ build: { beforeDevCommand: "", devUrl } }), ...args.slice(index), ]; }