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:
Mohamed Boudra
2026-01-09 08:40:16 +07:00
parent cc74d6d0d2
commit 3ba778eea7
37 changed files with 935 additions and 607 deletions
+156
View File
@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { createWorktree } from "./worktree";
import { execSync } from "child_process";
import { mkdtempSync, rmSync, existsSync, realpathSync, writeFileSync, readFileSync } from "fs";
import { join } from "path";
import { tmpdir } from "os";
describe("createWorktree", () => {
let tempDir: string;
let repoDir: string;
beforeEach(() => {
// Use realpathSync to resolve symlinks (e.g., /var -> /private/var on macOS)
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "worktree-test-")));
repoDir = join(tempDir, "test-repo");
// Create a git repo with an initial commit
execSync(`mkdir -p ${repoDir}`);
execSync("git init", { cwd: repoDir });
execSync("git config user.email 'test@test.com'", { cwd: repoDir });
execSync("git config user.name 'Test'", { cwd: repoDir });
execSync("echo 'hello' > file.txt", { cwd: repoDir });
execSync("git add .", { cwd: repoDir });
execSync("git commit -m 'initial'", { cwd: repoDir });
});
afterEach(() => {
rmSync(tempDir, { recursive: true, force: true });
});
it("creates a worktree for the current branch (main)", async () => {
const result = await createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "hello-world",
});
expect(result.worktreePath).toBe(join(tempDir, "test-repo-hello-world"));
expect(existsSync(result.worktreePath)).toBe(true);
expect(existsSync(join(result.worktreePath, "file.txt"))).toBe(true);
});
it("creates a worktree with a new branch", async () => {
const result = await createWorktree({
branchName: "feature-branch",
cwd: repoDir,
baseBranch: "main",
worktreeSlug: "my-feature",
});
expect(result.worktreePath).toBe(join(tempDir, "test-repo-my-feature"));
expect(existsSync(result.worktreePath)).toBe(true);
// Verify branch was created
const branches = execSync("git branch", { cwd: repoDir }).toString();
expect(branches).toContain("feature-branch");
});
it("fails with invalid branch name", async () => {
await expect(
createWorktree({
branchName: "INVALID_UPPERCASE",
cwd: repoDir,
worktreeSlug: "test",
})
).rejects.toThrow("Invalid branch name");
});
it("handles branch name collision by adding suffix", async () => {
// Create a branch named "hello" first
execSync("git branch hello", { cwd: repoDir });
const result = await createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "hello",
});
// Should create branch "hello-1" since "hello" exists
expect(result.worktreePath).toBe(join(tempDir, "test-repo-hello"));
expect(existsSync(result.worktreePath)).toBe(true);
const branches = execSync("git branch", { cwd: repoDir }).toString();
expect(branches).toContain("hello-1");
});
it("handles multiple collisions", async () => {
// Create branches "hello" and "hello-1"
execSync("git branch hello", { cwd: repoDir });
execSync("git branch hello-1", { cwd: repoDir });
const result = await createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "hello",
});
expect(existsSync(result.worktreePath)).toBe(true);
const branches = execSync("git branch", { cwd: repoDir }).toString();
expect(branches).toContain("hello-2");
});
it("runs setup commands from paseo.json", async () => {
// Create paseo.json with setup commands
const paseoConfig = {
worktree: {
setup: [
'echo "root=$PASEO_ROOT_PATH" > setup.log',
'echo "worktree=$PASEO_WORKTREE_PATH" >> setup.log',
'echo "branch=$PASEO_BRANCH_NAME" >> setup.log',
],
},
};
writeFileSync(join(repoDir, "paseo.json"), JSON.stringify(paseoConfig));
execSync("git add paseo.json && git commit -m 'add paseo.json'", { cwd: repoDir });
const result = await createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "setup-test",
});
expect(existsSync(result.worktreePath)).toBe(true);
// Verify setup ran and env vars were available
const setupLog = readFileSync(join(result.worktreePath, "setup.log"), "utf8");
expect(setupLog).toContain(`root=${repoDir}`);
expect(setupLog).toContain(`worktree=${result.worktreePath}`);
expect(setupLog).toContain("branch=setup-test");
});
it("cleans up worktree if setup command fails", async () => {
// Create paseo.json with failing setup command
const paseoConfig = {
worktree: {
setup: ["exit 1"],
},
};
writeFileSync(join(repoDir, "paseo.json"), JSON.stringify(paseoConfig));
execSync("git add paseo.json && git commit -m 'add paseo.json'", { cwd: repoDir });
const expectedWorktreePath = join(tempDir, "test-repo-fail-test");
await expect(
createWorktree({
branchName: "main",
cwd: repoDir,
worktreeSlug: "fail-test",
})
).rejects.toThrow("Worktree setup command failed");
// Verify worktree was cleaned up
expect(existsSync(expectedWorktreePath)).toBe(false);
});
});
+88 -67
View File
@@ -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,