feat: add logging for worktree lifecycle and port assignments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-02-21 17:53:54 +00:00
parent 587142ddac
commit e9ac1ce9eb
2 changed files with 30 additions and 11 deletions
+10 -1
View File
@@ -146,25 +146,32 @@ async function handleApi(req: Request, url: URL): Promise<Response> {
}
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<Response> {
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<Response> {
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);
}
}
+20 -10
View File
@@ -70,7 +70,9 @@ async function runChecked(args: string[]): Promise<string> {
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<string> {
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<string> {