wip: local changes and task backlog
- Refactor agent-form-dropdowns component - Simplify app index routing - Update agent-input-area and message-input - Improve worktree utility with tests - Add task backlog for upcoming work
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { existsSync, readFileSync, rmSync } from "fs";
|
||||
import { join, basename, dirname } from "path";
|
||||
|
||||
interface PaseoConfig {
|
||||
worktree?: {
|
||||
setup?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const READ_ONLY_GIT_ENV: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
@@ -165,37 +171,6 @@ function sanitizeWorktreeSlug(input: string): string {
|
||||
return slug.length > 0 ? slug : "worktree";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a worktree already exists for a branch
|
||||
*/
|
||||
async function findExistingWorktree(
|
||||
branchName: string,
|
||||
repoPath: string
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
||||
cwd: repoPath,
|
||||
});
|
||||
const lines = stdout.split("\n");
|
||||
|
||||
let currentWorktree: string | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("worktree ")) {
|
||||
currentWorktree = line.substring("worktree ".length);
|
||||
} else if (
|
||||
line === `branch refs/heads/${branchName}` &&
|
||||
currentWorktree
|
||||
) {
|
||||
return currentWorktree;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a git worktree with proper naming conventions
|
||||
@@ -228,52 +203,98 @@ export async function createWorktree({
|
||||
}
|
||||
|
||||
// Check if branch already exists
|
||||
let branchExists = false;
|
||||
try {
|
||||
await execAsync(
|
||||
`git show-ref --verify --quiet refs/heads/${branchName}`,
|
||||
{ cwd: repoInfo.path }
|
||||
);
|
||||
|
||||
// Branch exists, check for existing worktree
|
||||
const existingWorktree = await findExistingWorktree(
|
||||
branchName,
|
||||
repoInfo.path
|
||||
);
|
||||
|
||||
if (existingWorktree) {
|
||||
if (existsSync(existingWorktree)) {
|
||||
throw new Error(
|
||||
`Worktree already exists at: ${existingWorktree}. Use 'git worktree remove ${existingWorktree}' to remove it first.`
|
||||
);
|
||||
} else {
|
||||
// Prune stale worktree reference
|
||||
await execAsync("git worktree prune", { cwd: repoInfo.path });
|
||||
}
|
||||
}
|
||||
|
||||
// Create worktree using existing branch
|
||||
await execAsync(`git worktree add "${worktreePath}" "${branchName}"`, {
|
||||
cwd: repoInfo.path,
|
||||
});
|
||||
} catch (error) {
|
||||
// Branch doesn't exist, create new branch and worktree
|
||||
const baseArg = baseBranch ? ` "${baseBranch}"` : "";
|
||||
const command = `git worktree add "${worktreePath}" -b "${branchName}"${baseArg}`;
|
||||
await execAsync(command, { cwd: repoInfo.path });
|
||||
branchExists = true;
|
||||
} catch {
|
||||
branchExists = false;
|
||||
}
|
||||
|
||||
// Copy .env file if it exists
|
||||
const envSource =
|
||||
repoInfo.type === "bare"
|
||||
? join(repoInfo.path, "main", ".env")
|
||||
: join(repoInfo.path, ".env");
|
||||
// Always create a new branch for the worktree
|
||||
// If branchName already exists, use it as base and create worktree-slug as branch name
|
||||
// If branchName doesn't exist, create it from baseBranch
|
||||
const base = branchExists ? branchName : (baseBranch ?? "HEAD");
|
||||
const candidateBranch = branchExists ? desiredSlug : branchName;
|
||||
|
||||
if (existsSync(envSource)) {
|
||||
await execAsync(`cp "${envSource}" "${worktreePath}/.env"`);
|
||||
// Find unique branch name if collision
|
||||
let newBranchName = candidateBranch;
|
||||
let suffix = 1;
|
||||
while (true) {
|
||||
try {
|
||||
await execAsync(
|
||||
`git show-ref --verify --quiet refs/heads/${newBranchName}`,
|
||||
{ cwd: repoInfo.path }
|
||||
);
|
||||
// Branch exists, try with suffix
|
||||
newBranchName = `${candidateBranch}-${suffix}`;
|
||||
suffix++;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Also handle worktree path collision
|
||||
let finalWorktreePath = worktreePath;
|
||||
let pathSuffix = 1;
|
||||
while (existsSync(finalWorktreePath)) {
|
||||
finalWorktreePath = `${worktreePath}-${pathSuffix}`;
|
||||
pathSuffix++;
|
||||
}
|
||||
|
||||
const command = `git worktree add "${finalWorktreePath}" -b "${newBranchName}" "${base}"`;
|
||||
await execAsync(command, { cwd: repoInfo.path });
|
||||
worktreePath = finalWorktreePath;
|
||||
|
||||
// Run setup commands from paseo.json if present
|
||||
const paseoConfigPath = join(repoInfo.path, "paseo.json");
|
||||
if (existsSync(paseoConfigPath)) {
|
||||
let config: PaseoConfig;
|
||||
try {
|
||||
config = JSON.parse(readFileSync(paseoConfigPath, "utf8"));
|
||||
} catch {
|
||||
throw new Error(`Failed to parse paseo.json`);
|
||||
}
|
||||
|
||||
const setupCommands = config.worktree?.setup;
|
||||
if (setupCommands && setupCommands.length > 0) {
|
||||
const setupEnv = {
|
||||
...process.env,
|
||||
PASEO_ROOT_PATH: repoInfo.path,
|
||||
PASEO_WORKTREE_PATH: worktreePath,
|
||||
PASEO_BRANCH_NAME: newBranchName,
|
||||
};
|
||||
|
||||
for (const cmd of setupCommands) {
|
||||
try {
|
||||
await execAsync(cmd, {
|
||||
cwd: worktreePath,
|
||||
env: setupEnv,
|
||||
shell: "/bin/bash",
|
||||
});
|
||||
} catch (error) {
|
||||
// Cleanup worktree on setup failure
|
||||
try {
|
||||
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||
cwd: repoInfo.path,
|
||||
});
|
||||
} catch {
|
||||
// If git worktree remove fails, try rmSync
|
||||
rmSync(worktreePath, { recursive: true, force: true });
|
||||
}
|
||||
throw new Error(
|
||||
`Worktree setup command failed: ${cmd}\n${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
branchName,
|
||||
branchName: newBranchName,
|
||||
worktreePath,
|
||||
repoType: repoInfo.type,
|
||||
repoPath: repoInfo.path,
|
||||
|
||||
Reference in New Issue
Block a user