Persist agent deletion and support subdir worktree cleanup

This commit is contained in:
Mohamed Boudra
2026-01-24 23:34:09 +07:00
parent 504fb83fa9
commit 084177da16
7 changed files with 338 additions and 50 deletions
@@ -305,6 +305,25 @@ describe("paseo worktree manager", () => {
expect(remaining.map((worktree) => worktree.path)).toEqual([second.worktreePath]);
});
it("deletes a paseo worktree even when given a subdirectory path", async () => {
const created = await createWorktree({
branchName: "main",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "alpha",
paseoHome,
});
const nestedDir = join(created.worktreePath, "nested", "dir");
execSync(`mkdir -p ${nestedDir}`);
await deletePaseoWorktree({ cwd: repoDir, worktreePath: nestedDir, paseoHome });
expect(existsSync(created.worktreePath)).toBe(false);
const remaining = await listPaseoWorktrees({ cwd: repoDir, paseoHome });
expect(remaining.some((worktree) => worktree.path === created.worktreePath)).toBe(false);
});
it("ensures .paseo is ignored in .gitignore", async () => {
await ensurePaseoIgnored(repoDir);
await ensurePaseoIgnored(repoDir);
+60 -7
View File
@@ -514,6 +514,57 @@ export async function listPaseoWorktrees({
.filter((entry) => entry.path.startsWith(rootPrefix));
}
export async function resolvePaseoWorktreeRootForCwd(
cwd: string,
options?: { paseoHome?: string }
): Promise<{ repoRoot: string; worktreeRoot: string; worktreePath: string } | null> {
let repoInfo: RepoInfo;
try {
repoInfo = await detectRepoInfo(cwd);
} catch {
return null;
}
const worktreesRoot = await getPaseoWorktreesRoot(repoInfo.path, options?.paseoHome);
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
let worktreeRoot: string | null = null;
try {
const { stdout } = await execAsync(
"git rev-parse --path-format=absolute --show-toplevel",
{ cwd, env: READ_ONLY_GIT_ENV }
);
const trimmed = stdout.trim();
worktreeRoot = trimmed.length > 0 ? trimmed : null;
} catch {
worktreeRoot = null;
}
if (!worktreeRoot) {
return null;
}
const resolvedWorktreeRoot = normalizePathForOwnership(worktreeRoot);
if (!resolvedWorktreeRoot.startsWith(resolvedRoot)) {
return null;
}
const knownWorktrees = await listPaseoWorktrees({
cwd: repoInfo.path,
paseoHome: options?.paseoHome,
});
const match = knownWorktrees.find((entry) => entry.path === resolvedWorktreeRoot);
if (!match) {
return null;
}
return {
repoRoot: repoInfo.path,
worktreeRoot: worktreesRoot,
worktreePath: match.path,
};
}
export async function deletePaseoWorktree({
cwd,
worktreePath,
@@ -531,21 +582,23 @@ export async function deletePaseoWorktree({
const repoInfo = await detectRepoInfo(cwd);
const worktreesRoot = await getPaseoWorktreesRoot(repoInfo.path, paseoHome);
const targetPath = worktreePath ?? join(worktreesRoot, worktreeSlug!);
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
const resolvedTarget = normalizePathForOwnership(targetPath);
const requestedPath = worktreePath ?? join(worktreesRoot, worktreeSlug!);
const resolvedRequested = normalizePathForOwnership(requestedPath);
const resolvedWorktree =
(await resolvePaseoWorktreeRootForCwd(requestedPath, { paseoHome }))?.worktreePath ??
resolvedRequested;
if (!resolvedTarget.startsWith(resolvedRoot)) {
if (!resolvedWorktree.startsWith(resolvedRoot)) {
throw new Error("Refusing to delete non-Paseo worktree");
}
const canonicalTargetPath = normalizePathForOwnership(targetPath);
await execAsync(`git worktree remove "${canonicalTargetPath}" --force`, {
await execAsync(`git worktree remove "${resolvedWorktree}" --force`, {
cwd: repoInfo.path,
});
if (existsSync(canonicalTargetPath)) {
rmSync(canonicalTargetPath, { recursive: true, force: true });
if (existsSync(resolvedWorktree)) {
rmSync(resolvedWorktree, { recursive: true, force: true });
}
}