Commit 09f5f6f66708
Changed files (2)
dots
pi
agent
extensions
git
dots/pi/agent/extensions/git/index.ts
@@ -441,7 +441,7 @@ export default function (pi: ExtensionAPI) {
};
}
- const output = execInWorktree(branch, execCmd, ctx.cwd);
+ const output = await execInWorktree(branch, execCmd, ctx.cwd);
return {
content: [{
dots/pi/agent/extensions/git/worktree.ts
@@ -3,7 +3,7 @@
// Uses lazyworktree when available, falls back to raw git commands.
// =============================================================================
-import { execSync } from "child_process";
+import { execSync, spawn } from "child_process";
import * as path from "path";
import * as fs from "fs";
import type { WorktreeInfo, WorktreeCreateOptions, WorktreeRemoveOptions, LazyworktreeInfo } from "./types.js";
@@ -276,48 +276,83 @@ function createWithGit(options: WorktreeCreateOptions, repoRoot: string, cwd: st
// Execute in Worktree
// =============================================================================
-export function execInWorktree(nameOrPath: string, command: string, cwd: string = process.cwd()): string {
+export async function execInWorktree(nameOrPath: string, command: string, cwd: string = process.cwd()): Promise<string> {
const repoRoot = findRepoRoot(cwd);
if (!repoRoot) {
throw new Error("Not in a git repository");
}
+ // Resolve worktree path
+ let worktreePath: string;
+
if (hasLazyworktree()) {
try {
- return execSync(
- `lazyworktree exec -w "${nameOrPath}" "${command.replace(/"/g, '\\"')}"`,
- {
- cwd: repoRoot,
- encoding: "utf-8",
- stdio: "pipe",
- }
- ).trim();
+ // Use lazyworktree to resolve the worktree path
+ const output = execSync(`lazyworktree list --json`, {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ stdio: "pipe",
+ }).trim();
+ const items: LazyworktreeInfo[] = JSON.parse(output);
+ const match = items.find(
+ (wt) => wt.name === nameOrPath || wt.branch === nameOrPath || wt.path === nameOrPath || wt.path.endsWith(`/${nameOrPath}`)
+ );
+ if (match) {
+ worktreePath = match.path;
+ } else {
+ throw new Error(`Worktree '${nameOrPath}' not found`);
+ }
} catch (error: any) {
- // If lazyworktree exec fails, try direct execution
- if (error.stdout) return error.stdout.toString().trim();
- throw new Error(`Failed to exec in worktree: ${error.message}`);
+ if (error.message?.includes("not found")) throw error;
+ // Fall through to git worktree list
+ const worktrees = listWorktrees(cwd);
+ const worktree = worktrees.find(
+ (wt) => wt.branch === nameOrPath || wt.path === nameOrPath || wt.path.endsWith(`/${nameOrPath}`)
+ );
+ if (!worktree) throw new Error(`Worktree '${nameOrPath}' not found`);
+ worktreePath = worktree.path;
}
+ } else {
+ const worktrees = listWorktrees(cwd);
+ const worktree = worktrees.find(
+ (wt) => wt.branch === nameOrPath || wt.path === nameOrPath || wt.path.endsWith(`/${nameOrPath}`)
+ );
+ if (!worktree) throw new Error(`Worktree '${nameOrPath}' not found`);
+ worktreePath = worktree.path;
}
- // Fallback: find worktree path and exec directly
- const worktrees = listWorktrees(cwd);
- const worktree = worktrees.find(
- (wt) => wt.branch === nameOrPath || wt.path === nameOrPath || wt.path.endsWith(`/${nameOrPath}`)
- );
+ // Execute command asynchronously to avoid blocking the event loop
+ return new Promise<string>((resolve, reject) => {
+ const child = spawn("sh", ["-c", command], {
+ cwd: worktreePath,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
- if (!worktree) {
- throw new Error(`Worktree '${nameOrPath}' not found`);
- }
+ const stdoutChunks: Buffer[] = [];
+ const stderrChunks: Buffer[] = [];
- try {
- return execSync(command, {
- cwd: worktree.path,
- encoding: "utf-8",
- stdio: "pipe",
- }).trim();
- } catch (error) {
- throw new Error(`Failed to exec in worktree: ${error}`);
- }
+ child.stdout.on("data", (chunk: Buffer) => stdoutChunks.push(chunk));
+ child.stderr.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
+
+ child.on("error", (err) => {
+ reject(new Error(`Failed to exec in worktree: ${err.message}`));
+ });
+
+ child.on("close", (code) => {
+ const stdout = Buffer.concat(stdoutChunks).toString("utf-8").trim();
+ const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
+
+ if (code !== 0) {
+ // Include both stdout and stderr in output for failed commands
+ const output = [stdout, stderr].filter(Boolean).join("\n");
+ reject(new Error(`Command failed (exit ${code}): ${output.substring(0, 2000)}`));
+ return;
+ }
+
+ // Return stdout if available, otherwise stderr (git writes to stderr)
+ resolve(stdout || stderr);
+ });
+ });
}
// =============================================================================