From e9ac1ce9eb7edbdae8ecae2b2f6f79269adffa62 Mon Sep 17 00:00:00 2001 From: centdix Date: Sat, 21 Feb 2026 17:53:54 +0000 Subject: [PATCH] feat: add logging for worktree lifecycle and port assignments Co-Authored-By: Claude Opus 4.5 --- dev-dashboard/backend/src/server.ts | 11 +++++++++- dev-dashboard/backend/src/workmux.ts | 30 ++++++++++++++++++---------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/dev-dashboard/backend/src/server.ts b/dev-dashboard/backend/src/server.ts index b76d27442b..f4fe0f7cc1 100644 --- a/dev-dashboard/backend/src/server.ts +++ b/dev-dashboard/backend/src/server.ts @@ -146,25 +146,32 @@ async function handleApi(req: Request, url: URL): Promise { } const validProfiles = ["full", "agent-only", "agent-yolo"] as const; const profile = validProfiles.includes(body.profile as any) ? body.profile as Profile : "agent-only"; + console.log(`[worktree:add] branch=${body.branch} profile=${profile}${body.prompt ? ` prompt="${body.prompt.slice(0, 80)}"` : ""}`); const result = await addWorktree(body.branch, { prompt: body.prompt, profile }); + console.log(`[worktree:add] done branch=${body.branch}: ${result}`); return jsonResponse({ message: result }, 201); } // DELETE /api/worktrees/:name if (parts[0] === "worktrees" && parts.length === 2 && method === "DELETE") { const name = decodeURIComponent(parts[1]); - return jsonResponse({ message: await removeWorktree(name) }); + console.log(`[worktree:rm] name=${name}`); + const result = await removeWorktree(name); + console.log(`[worktree:rm] done name=${name}: ${result}`); + return jsonResponse({ message: result }); } // POST /api/worktrees/:name/open if (parts[0] === "worktrees" && parts.length === 3 && parts[2] === "open" && method === "POST") { const name = decodeURIComponent(parts[1]); + console.log(`[worktree:open] name=${name}`); return jsonResponse({ message: await openWorktree(name) }); } // POST /api/worktrees/:name/close if (parts[0] === "worktrees" && parts.length === 3 && parts[2] === "close" && method === "POST") { const name = decodeURIComponent(parts[1]); + console.log(`[worktree:close] name=${name}`); return jsonResponse({ message: await closeWorktree(name) }); } @@ -175,6 +182,7 @@ async function handleApi(req: Request, url: URL): Promise { if (!body.prompt) { return errorResponse("prompt is required", 400); } + console.log(`[worktree:send] name=${name} prompt="${body.prompt.slice(0, 80)}"`); return jsonResponse({ message: await sendPrompt(name, body.prompt) }); } @@ -189,6 +197,7 @@ async function handleApi(req: Request, url: URL): Promise { return errorResponse("Not Found", 404); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); + console.error(`[api:error] ${method} ${url.pathname}: ${message}`); return errorResponse(message); } } diff --git a/dev-dashboard/backend/src/workmux.ts b/dev-dashboard/backend/src/workmux.ts index fde79b644b..7ac187cf44 100644 --- a/dev-dashboard/backend/src/workmux.ts +++ b/dev-dashboard/backend/src/workmux.ts @@ -70,7 +70,9 @@ async function runChecked(args: string[]): Promise { const exitCode = await proc.exited; if (exitCode !== 0) { - throw new Error(`${args.join(" ")} failed: ${stderr || stdout}`); + const msg = `${args.join(" ")} failed (exit ${exitCode}): ${stderr || stdout}`; + console.error(`[workmux:exec] ${msg}`); + throw new Error(msg); } return stdout.trim(); } @@ -141,11 +143,23 @@ export async function addWorktree( if (opts?.prompt) args.push("-p", opts.prompt); args.push(branch); + console.log(`[workmux:add] running: ${args.join(" ")}`); const result = await runChecked(args); + console.log(`[workmux:add] result: ${result}`); + + const windowTarget = `wm-${branch}`; + + // Read worktree dir and log assigned ports + const wtDirResult = Bun.spawnSync( + ["tmux", "display-message", "-t", `${windowTarget}.0`, "-p", "#{pane_current_path}"], + { stdout: "pipe" } + ); + const wtDir = new TextDecoder().decode(wtDirResult.stdout).trim(); + const env = readEnvLocal(wtDir); + console.log(`[workmux:add] branch=${branch} dir=${wtDir} ports: backend=${env.BACKEND_PORT || "8000"} frontend=${env.FRONTEND_PORT || "3000"}`); // For non-full profiles, kill extra panes and send commands if (profile !== "full") { - const windowTarget = `wm-${branch}`; // Kill extra panes (highest index first to avoid shifting) const paneCountResult = Bun.spawnSync( ["tmux", "list-panes", "-t", windowTarget, "-F", "#{pane_index}"], @@ -156,14 +170,7 @@ export async function addWorktree( for (let i = paneIds.length - 1; i >= 1; i--) { Bun.spawnSync(["tmux", "kill-pane", "-t", `${windowTarget}.${paneIds[i]}`]); } - // Get the worktree directory from pane 0 - const cwdResult = Bun.spawnSync( - ["tmux", "display-message", "-t", `${windowTarget}.0`, "-p", "#{pane_current_path}"], - { stdout: "pipe" } - ); - const wtDir = new TextDecoder().decode(cwdResult.stdout).trim(); // Build and send claude command with environment-aware system prompt - const env = readEnvLocal(wtDir); const claudeCmd = buildClaudeCmd(profile, env); console.log(`[workmux] sending command to ${windowTarget}.0:\n${claudeCmd}`); Bun.spawnSync(["tmux", "send-keys", "-t", `${windowTarget}.0`, claudeCmd, "Enter"]); @@ -177,7 +184,10 @@ export async function addWorktree( } export async function removeWorktree(name: string): Promise { - return runChecked(["workmux", "rm", "--force", name]); + console.log(`[workmux:rm] running: workmux rm --force ${name}`); + const result = await runChecked(["workmux", "rm", "--force", name]); + console.log(`[workmux:rm] result: ${result}`); + return result; } export async function openWorktree(name: string): Promise {