refactoring the api endpoints to be separate files to reduce context usage

This commit is contained in:
Cody Seibert
2025-12-14 17:53:21 -05:00
parent cdc8334d82
commit 6b30271441
121 changed files with 4281 additions and 2927 deletions

View File

@@ -0,0 +1,36 @@
/**
* Common utilities for worktree routes
*/
import { createLogger } from "../../lib/logger.js";
import { exec } from "child_process";
import { promisify } from "util";
const logger = createLogger("Worktree");
const execAsync = promisify(exec);
/**
* Check if a path is a git repo
*/
export async function isGitRepo(repoPath: string): Promise<boolean> {
try {
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
return true;
} catch {
return false;
}
}
/**
* Get error message from error object
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error";
}
/**
* Log error details consistently
*/
export function logError(error: unknown, context: string): void {
logger.error(`${context}:`, error);
}

View File

@@ -0,0 +1,26 @@
/**
* Worktree routes - HTTP API for git worktree operations
*/
import { Router } from "express";
import { createInfoHandler } from "./routes/info.js";
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";
export function createWorktreeRoutes(): Router {
const router = Router();
router.post("/info", createInfoHandler());
router.post("/status", createStatusHandler());
router.post("/list", createListHandler());
router.post("/diffs", createDiffsHandler());
router.post("/file-diff", createFileDiffHandler());
router.post("/revert", createRevertHandler());
router.post("/merge", createMergeHandler());
return router;
}

View File

@@ -0,0 +1,85 @@
/**
* POST /diffs endpoint - Get diffs for a worktree
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createDiffsHandler() {
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;
}
const worktreePath = path.join(
projectPath,
".automaker",
"worktrees",
featureId
);
try {
await fs.access(worktreePath);
const { stdout: diff } = await execAsync("git diff HEAD", {
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024,
});
const { stdout: status } = await execAsync("git status --porcelain", {
cwd: worktreePath,
});
const files = status
.split("\n")
.filter(Boolean)
.map((line) => {
const statusChar = line[0];
const filePath = line.slice(3);
const statusMap: Record<string, string> = {
M: "Modified",
A: "Added",
D: "Deleted",
R: "Renamed",
C: "Copied",
U: "Updated",
"?": "Untracked",
};
return {
status: statusChar,
path: filePath,
statusText: statusMap[statusChar] || "Unknown",
};
});
res.json({
success: true,
diff,
files,
hasChanges: files.length > 0,
});
} catch {
res.json({ success: true, diff: "", files: [], hasChanges: false });
}
} catch (error) {
logError(error, "Get worktree diffs failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,57 @@
/**
* POST /file-diff endpoint - Get diff for a specific file
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createFileDiffHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, filePath } = req.body as {
projectPath: string;
featureId: string;
filePath: string;
};
if (!projectPath || !featureId || !filePath) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and filePath required",
});
return;
}
const worktreePath = path.join(
projectPath,
".automaker",
"worktrees",
featureId
);
try {
await fs.access(worktreePath);
const { stdout: diff } = await execAsync(
`git diff HEAD -- "${filePath}"`,
{
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024,
}
);
res.json({ success: true, diff, filePath });
} catch {
res.json({ success: true, diff: "", filePath });
}
} catch (error) {
logError(error, "Get worktree file diff failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,57 @@
/**
* POST /info endpoint - Get worktree info
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createInfoHandler() {
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;
}
// Check if worktree exists
const worktreePath = path.join(
projectPath,
".automaker",
"worktrees",
featureId
);
try {
await fs.access(worktreePath);
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
cwd: worktreePath,
});
res.json({
success: true,
worktreePath,
branchName: stdout.trim(),
});
} catch {
res.json({ success: true, worktreePath: null, branchName: null });
}
} catch (error) {
logError(error, "Get worktree info failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,54 @@
/**
* POST /list endpoint - List all worktrees
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import { isGitRepo, getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createListHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (!(await isGitRepo(projectPath))) {
res.json({ success: true, worktrees: [] });
return;
}
const { stdout } = await execAsync("git worktree list --porcelain", {
cwd: projectPath,
});
const worktrees: Array<{ path: string; branch: string }> = [];
const lines = stdout.split("\n");
let current: { path?: string; branch?: string } = {};
for (const line of lines) {
if (line.startsWith("worktree ")) {
current.path = line.slice(9);
} else if (line.startsWith("branch ")) {
current.branch = line.slice(7).replace("refs/heads/", "");
} else if (line === "") {
if (current.path && current.branch) {
worktrees.push({ path: current.path, branch: current.branch });
}
current = {};
}
}
res.json({ success: true, worktrees });
} catch (error) {
logError(error, "List worktrees failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,81 @@
/**
* POST /merge endpoint - Merge feature (merge worktree branch into main)
*/
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 createMergeHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, options } = req.body as {
projectPath: string;
featureId: string;
options?: { squash?: boolean; message?: string };
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId required",
});
return;
}
const branchName = `feature/${featureId}`;
const worktreePath = path.join(
projectPath,
".automaker",
"worktrees",
featureId
);
// Get current branch
const { stdout: currentBranch } = await execAsync(
"git rev-parse --abbrev-ref HEAD",
{ cwd: projectPath }
);
// Merge the feature branch
const mergeCmd = options?.squash
? `git merge --squash ${branchName}`
: `git merge ${branchName} -m "${
options?.message || `Merge ${branchName}`
}"`;
await execAsync(mergeCmd, { cwd: projectPath });
// If squash merge, need to commit
if (options?.squash) {
await execAsync(
`git commit -m "${
options?.message || `Merge ${branchName} (squash)`
}"`,
{ cwd: projectPath }
);
}
// Clean up worktree and branch
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: projectPath,
});
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
} catch {
// Cleanup errors are non-fatal
}
res.json({ success: true, mergedBranch: branchName });
} catch (error) {
logError(error, "Merge worktree failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,58 @@
/**
* 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;
}
const worktreePath = path.join(
projectPath,
".automaker",
"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) });
}
};
}

View File

@@ -0,0 +1,77 @@
/**
* POST /status endpoint - Get worktree status
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
import { getErrorMessage, logError } from "../common.js";
const execAsync = promisify(exec);
export function createStatusHandler() {
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;
}
const worktreePath = path.join(
projectPath,
".automaker",
"worktrees",
featureId
);
try {
await fs.access(worktreePath);
const { stdout: status } = await execAsync("git status --porcelain", {
cwd: worktreePath,
});
const files = status
.split("\n")
.filter(Boolean)
.map((line) => line.slice(3));
const { stdout: diffStat } = await execAsync("git diff --stat", {
cwd: worktreePath,
});
const { stdout: logOutput } = await execAsync(
'git log --oneline -5 --format="%h %s"',
{ cwd: worktreePath }
);
res.json({
success: true,
modifiedFiles: files.length,
files,
diffStat: diffStat.trim(),
recentCommits: logOutput.trim().split("\n").filter(Boolean),
});
} catch {
res.json({
success: true,
modifiedFiles: 0,
files: [],
diffStat: "",
recentCommits: [],
});
}
} catch (error) {
logError(error, "Get worktree status failed");
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}