feat: enhance worktree management and UI integration

- Refactored BoardView and WorktreeSelector components for improved readability and maintainability, including consistent formatting and structure.
- Updated feature handling to ensure correct worktree assignment and reset logic when worktrees are deleted, enhancing user experience.
- Enhanced KanbanCard to display priority badges with improved styling and layout.
- Removed deprecated revert feature logic from the server and client, streamlining the codebase.
- Introduced new tests for feature lifecycle and worktree integration, ensuring robust functionality and error handling.
This commit is contained in:
Cody Seibert
2025-12-16 21:49:33 -05:00
parent f9ec7222f2
commit 58d6ae02a5
20 changed files with 2316 additions and 504 deletions

View File

@@ -22,3 +22,4 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
return router;
}

View File

@@ -102,3 +102,4 @@ export function createDeleteApiKeyHandler() {
};
}

View File

@@ -8,7 +8,6 @@ import { createStatusHandler } from "./routes/status.js";
import { createListHandler } from "./routes/list.js";
import { createDiffsHandler } from "./routes/diffs.js";
import { createFileDiffHandler } from "./routes/file-diff.js";
import { createRevertHandler } from "./routes/revert.js";
import { createMergeHandler } from "./routes/merge.js";
import { createCreateHandler } from "./routes/create.js";
import { createDeleteHandler } from "./routes/delete.js";
@@ -19,9 +18,11 @@ import { createPullHandler } from "./routes/pull.js";
import { createCheckoutBranchHandler } from "./routes/checkout-branch.js";
import { createListBranchesHandler } from "./routes/list-branches.js";
import { createSwitchBranchHandler } from "./routes/switch-branch.js";
import { createOpenInEditorHandler, createGetDefaultEditorHandler } from "./routes/open-in-editor.js";
import {
createOpenInEditorHandler,
createGetDefaultEditorHandler,
} from "./routes/open-in-editor.js";
import { createInitGitHandler } from "./routes/init-git.js";
import { createActivateHandler } from "./routes/activate.js";
import { createMigrateHandler } from "./routes/migrate.js";
import { createStartDevHandler } from "./routes/start-dev.js";
import { createStopDevHandler } from "./routes/stop-dev.js";
@@ -35,7 +36,6 @@ export function createWorktreeRoutes(): Router {
router.post("/list", createListHandler());
router.post("/diffs", createDiffsHandler());
router.post("/file-diff", createFileDiffHandler());
router.post("/revert", createRevertHandler());
router.post("/merge", createMergeHandler());
router.post("/create", createCreateHandler());
router.post("/delete", createDeleteHandler());
@@ -49,7 +49,6 @@ export function createWorktreeRoutes(): Router {
router.post("/open-in-editor", createOpenInEditorHandler());
router.get("/default-editor", createGetDefaultEditorHandler());
router.post("/init-git", createInitGitHandler());
router.post("/activate", createActivateHandler());
router.post("/migrate", createMigrateHandler());
router.post("/start-dev", createStartDevHandler());
router.post("/stop-dev", createStopDevHandler());

View File

@@ -1,149 +0,0 @@
/**
* POST /activate endpoint - Switch main project to a worktree's branch
*
* This allows users to "activate" a worktree so their running dev server
* (like Vite) shows the worktree's files. It does this by:
* 1. Checking for uncommitted changes (fails if found)
* 2. Removing the worktree (unlocks the branch)
* 3. Checking out that branch in the main directory
*
* Users should commit their changes before activating a worktree.
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
async function hasUncommittedChanges(cwd: string): Promise<boolean> {
try {
const { stdout } = await execAsync("git status --porcelain", { cwd });
// Filter out our own .worktrees directory from the check
const lines = stdout.trim().split("\n").filter((line) => {
if (!line.trim()) return false;
// Exclude .worktrees/ directory (created by automaker)
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
return true;
});
return lines.length > 0;
} catch {
return false;
}
}
async function getCurrentBranch(cwd: string): Promise<string> {
const { stdout } = await execAsync("git branch --show-current", { cwd });
return stdout.trim();
}
async function getWorktreeBranch(worktreePath: string): Promise<string> {
const { stdout } = await execAsync("git branch --show-current", {
cwd: worktreePath,
});
return stdout.trim();
}
async function getChangesSummary(cwd: string): Promise<string> {
try {
const { stdout } = await execAsync("git status --short", { cwd });
const lines = stdout.trim().split("\n").filter((line) => {
if (!line.trim()) return false;
// Exclude .worktrees/ directory
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
return true;
});
if (lines.length === 0) return "";
if (lines.length <= 5) return lines.join(", ");
return `${lines.slice(0, 5).join(", ")} and ${lines.length - 5} more files`;
} catch {
return "unknown changes";
}
}
export function createActivateHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, worktreePath } = req.body as {
projectPath: string;
worktreePath: string | null; // null means switch back to main branch
};
if (!projectPath) {
res.status(400).json({
success: false,
error: "projectPath is required",
});
return;
}
const currentBranch = await getCurrentBranch(projectPath);
let targetBranch: string;
// Check for uncommitted changes in main directory
if (await hasUncommittedChanges(projectPath)) {
const summary = await getChangesSummary(projectPath);
res.status(400).json({
success: false,
error: `Cannot switch: you have uncommitted changes in the main directory (${summary}). Please commit your changes first.`,
code: "UNCOMMITTED_CHANGES",
});
return;
}
if (worktreePath) {
// Switching to a worktree's branch
targetBranch = await getWorktreeBranch(worktreePath);
// Check for uncommitted changes in the worktree
if (await hasUncommittedChanges(worktreePath)) {
const summary = await getChangesSummary(worktreePath);
res.status(400).json({
success: false,
error: `Cannot switch: you have uncommitted changes in the worktree (${summary}). Please commit your changes first.`,
code: "UNCOMMITTED_CHANGES",
});
return;
}
// Remove the worktree (unlocks the branch)
console.log(`[activate] Removing worktree at ${worktreePath}...`);
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: projectPath,
});
// Checkout the branch in main directory
console.log(`[activate] Checking out branch ${targetBranch}...`);
await execAsync(`git checkout "${targetBranch}"`, { cwd: projectPath });
} else {
// Switching back to main branch
try {
const { stdout: mainBranch } = await execAsync(
"git symbolic-ref refs/remotes/origin/HEAD --short 2>/dev/null | sed 's@origin/@@' || echo 'main'",
{ cwd: projectPath }
);
targetBranch = mainBranch.trim() || "main";
} catch {
targetBranch = "main";
}
// Checkout main branch
console.log(`[activate] Checking out main branch ${targetBranch}...`);
await execAsync(`git checkout "${targetBranch}"`, { cwd: projectPath });
}
res.json({
success: true,
result: {
previousBranch: currentBranch,
currentBranch: targetBranch,
message: `Switched from ${currentBranch} to ${targetBranch}`,
},
});
} catch (error) {
logError(error, "Activate worktree failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -1,54 +0,0 @@
/**
* POST /revert endpoint - Revert feature (remove worktree)
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createRevertHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId required",
});
return;
}
// Git worktrees are stored in project directory
const worktreePath = path.join(projectPath, ".worktrees", featureId);
try {
// Remove worktree
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: projectPath,
});
// Delete branch
await execAsync(`git branch -D feature/${featureId}`, {
cwd: projectPath,
});
res.json({ success: true, removedPath: worktreePath });
} catch (error) {
// Worktree might not exist
res.json({ success: true, removedPath: null });
}
} catch (error) {
logError(error, "Revert worktree failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}