#!/usr/bin/env node
import { treehouse } from "./treehouse.js";
import * as logger from "./utils/logger.js";
import { colors } from "./utils/logger.js";
function showHelp(): void {
logger.log("treehouse - Git worktree manager for parallel AI agent workflows", "cyan");
console.log(`
Usage:
treehouse init Create treehouse.json with defaults
treehouse list Show all worktrees
treehouse create <name> [branch] Create a new worktree
Uses current branch as base
treehouse status [name] Show status (all or specific worktree)
treehouse complete <name> [options] Complete worktree work
--merge Merge into target branch
--squash Squash merge
--target <branch> Target branch (default: current)
--delete-branch Delete branch after merge
--message <msg> Commit message for squash
treehouse remove <name> [--force] Remove a worktree
treehouse lock <name> [options] Lock a worktree
--agent <id> Agent ID
--message <msg> Lock reason
--expiry <minutes> Lock expiry in minutes
treehouse unlock <name> Unlock a worktree
treehouse conflicts [name] Show merge conflicts
treehouse resolve [name] --ours|--theirs Resolve conflicts
treehouse abort [name] Abort merge operation
treehouse prune Clean up orphaned worktree entries
treehouse clean [--dry-run] Remove old worktrees per config
treehouse help Show this help message
Config files (in order of precedence):
.cursor/worktrees.json Cursor compatibility mode
treehouse.json Default configuration
Examples:
treehouse init
treehouse create feature-api # branches from current
treehouse create hotfix-bug fix/urgent # uses specific branch name
treehouse lock feature-api --agent claude-1
treehouse complete feature-api --merge
treehouse remove feature-api
`);
}
function parseArgs(args: string[]): {
command: string;
positional: string[];
flags: Record<string, string | boolean>;
} {
const command = args[0] || "help";
const positional: string[] = [];
const flags: Record<string, string | boolean> = {};
for (let i = 1; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
const nextArg = args[i + 1];
if (nextArg && !nextArg.startsWith("-")) {
flags[key] = nextArg;
i++;
} else {
flags[key] = true;
}
} else if (arg.startsWith("-")) {
const key = arg.slice(1);
flags[key] = true;
} else {
positional.push(arg);
}
}
return { command, positional, flags };
}
function formatWorktreeList(data: unknown): void {
const worktrees = data as Array<{
path: string;
branch?: string;
name?: string;
bare?: boolean;
locked?: boolean;
lockReason?: string;
lock?: { agentId: string; expiresAt?: string };
prunable?: boolean;
}>;
logger.log("\nGit Worktrees:", "cyan");
console.log("");
for (const wt of worktrees) {
const name = wt.name || wt.path.split("/").pop() || wt.path;
const branch = wt.branch || "detached";
const bareTag = wt.bare ? " (bare)" : "";
const lockTag = wt.locked || wt.lock ? " 🔒" : "";
const prunableTag = wt.prunable ? ` ${colors.yellow}(prunable)${colors.reset}` : "";
console.log(` ${colors.yellow}${name}${bareTag}${lockTag}${colors.reset}${prunableTag}`);
console.log(` Branch: ${colors.cyan}${branch}${colors.reset}`);
console.log(` Path: ${colors.gray}${wt.path}${colors.reset}`);
if (wt.lock) {
console.log(` Locked by: ${colors.magenta}${wt.lock.agentId}${colors.reset}`);
if (wt.lock.expiresAt) {
const expires = new Date(wt.lock.expiresAt);
console.log(` Expires: ${colors.gray}${expires.toLocaleString()}${colors.reset}`);
}
} else if (wt.locked && wt.lockReason) {
console.log(` Locked: ${colors.magenta}${wt.lockReason}${colors.reset}`);
}
console.log("");
}
}
function formatConflicts(data: unknown): void {
const { conflicts } = data as { conflicts: string[] };
if (conflicts.length === 0) {
logger.success("No merge conflicts detected.");
return;
}
logger.warn("Merge conflicts detected!");
console.log("");
logger.info("Conflicted files:");
for (const file of conflicts) {
console.log(` ${colors.red}⚠ ${file}${colors.reset}`);
}
console.log("");
logger.info("Resolution options:");
console.log(" 1. Resolve manually and commit:");
console.log(" - Edit conflicted files");
console.log(" - git add <file>");
console.log(" - git commit");
console.log("");
console.log(" 2. Accept all incoming changes (theirs):");
console.log(` ${colors.cyan}treehouse resolve --theirs${colors.reset}`);
console.log("");
console.log(" 3. Keep all current changes (ours):");
console.log(` ${colors.cyan}treehouse resolve --ours${colors.reset}`);
console.log("");
console.log(" 4. Abort merge:");
console.log(` ${colors.cyan}treehouse abort${colors.reset}`);
}
function formatStatus(data: unknown): void {
const status = data as {
name: string;
branch: string;
path: string;
currentBranch: string;
hasUncommittedChanges: boolean;
gitStatus: string;
recentCommits: string;
lock?: { agentId: string; expiresAt?: string };
};
logger.info(`Status for worktree '${status.name}':`);
console.log("");
console.log(` Branch: ${colors.cyan}${status.currentBranch}${colors.reset}`);
console.log(` Path: ${colors.gray}${status.path}${colors.reset}`);
console.log(` Changes: ${status.hasUncommittedChanges ? colors.yellow + "yes" : colors.green + "clean"}${colors.reset}`);
if (status.lock) {
console.log(` Locked by: ${colors.magenta}${status.lock.agentId}${colors.reset}`);
}
console.log("");
logger.info("Recent commits:");
console.log(colors.gray + status.recentCommits + colors.reset);
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
const { command, positional, flags } = parseArgs(args);
try {
switch (command) {
case "init": {
const result = await treehouse.init();
if (result.success) {
logger.success(result.message);
if (result.data) {
console.log("");
logger.info("Default configuration:");
console.log(colors.gray + JSON.stringify(result.data, null, 2) + colors.reset);
}
} else {
logger.warn(result.message);
}
break;
}
case "list":
case "ls": {
const result = await treehouse.list();
if (result.success && result.data) {
formatWorktreeList(result.data);
} else {
logger.error(result.message);
}
break;
}
case "create":
case "new": {
const name = positional[0];
const branch = positional[1];
const agentId = flags.agent as string | undefined;
const lockMessage = flags.message as string | undefined;
const result = await treehouse.create({
name,
branch,
agentId,
lockMessage,
});
if (result.success) {
logger.success(result.message);
const data = result.data as { path: string };
console.log(` ${colors.gray}cd ${data.path}${colors.reset}`);
} else {
logger.warn(result.message);
process.exit(1);
}
break;
}
case "status": {
const name = positional[0];
const result = await treehouse.status(name);
if (result.success && result.data) {
if (name) {
formatStatus(result.data);
} else {
formatWorktreeList(result.data);
}
} else {
logger.error(result.message);
}
break;
}
case "complete": {
const name = positional[0];
const result = await treehouse.complete({
name,
merge: !!flags.merge,
squash: !!flags.squash,
targetBranch: flags.target as string | undefined,
deleteBranch: !!flags["delete-branch"],
message: flags.message as string | undefined,
});
if (result.success) {
logger.success(result.message);
} else {
if (result.data) {
const data = result.data as { conflicts?: string[] };
if (data.conflicts) {
formatConflicts(result.data);
}
}
logger.error(result.message);
process.exit(1);
}
break;
}
case "remove":
case "rm":
case "close": {
const name = positional[0];
const force = !!flags.force || !!flags.f;
const result = await treehouse.remove(name, force);
if (result.success) {
logger.success(result.message);
} else {
logger.error(result.message);
process.exit(1);
}
break;
}
case "lock": {
const name = positional[0];
const result = await treehouse.lock({
name,
agentId: flags.agent as string | undefined,
message: flags.message as string | undefined,
expiryMinutes: flags.expiry ? parseInt(flags.expiry as string, 10) : undefined,
});
if (result.success) {
logger.success(result.message);
if (result.data) {
const lock = result.data as { agentId: string; expiresAt?: string };
console.log(` Agent: ${colors.cyan}${lock.agentId}${colors.reset}`);
if (lock.expiresAt) {
console.log(` Expires: ${colors.gray}${new Date(lock.expiresAt).toLocaleString()}${colors.reset}`);
}
}
} else {
logger.error(result.message);
process.exit(1);
}
break;
}
case "unlock": {
const name = positional[0];
const result = await treehouse.unlock(name);
if (result.success) {
logger.success(result.message);
} else {
logger.error(result.message);
process.exit(1);
}
break;
}
case "conflicts": {
const name = positional[0];
const result = await treehouse.conflicts(name);
if (result.success && result.data) {
formatConflicts(result.data);
} else {
logger.error(result.message);
}
break;
}
case "resolve": {
const name = positional[0];
let strategy: "ours" | "theirs";
if (flags.ours) {
strategy = "ours";
} else if (flags.theirs) {
strategy = "theirs";
} else {
logger.error("Must specify --ours or --theirs");
process.exit(1);
}
const result = await treehouse.resolveConflicts({ strategy, name });
if (result.success) {
logger.success(result.message);
} else {
logger.error(result.message);
process.exit(1);
}
break;
}
case "abort": {
const name = positional[0];
const result = await treehouse.abort(name);
if (result.success) {
logger.success(result.message);
} else {
logger.error(result.message);
}
break;
}
case "prune": {
const result = await treehouse.prune();
if (result.success) {
logger.success(result.message);
if (result.data) {
const data = result.data as { output: string };
if (data.output.trim()) {
console.log(colors.gray + data.output + colors.reset);
}
}
} else {
logger.error(result.message);
}
break;
}
case "clean": {
const dryRun = !!flags["dry-run"] || !!flags.n;
const result = await treehouse.clean({ dryRun });
if (result.success) {
if (dryRun && result.data) {
const data = result.data as { wouldRemove?: Array<{ name: string; lastAccessed: string }> };
if (data.wouldRemove && data.wouldRemove.length > 0) {
logger.info("Would remove:");
for (const wt of data.wouldRemove) {
console.log(` - ${wt.name} (last accessed: ${new Date(wt.lastAccessed).toLocaleDateString()})`);
}
} else {
logger.success(result.message);
}
} else {
logger.success(result.message);
}
} else {
logger.warn(result.message);
}
break;
}
case "help":
case "--help":
case "-h":
default:
showHelp();
break;
}
} catch (e: unknown) {
const error = e as Error;
logger.error(error.message);
process.exit(1);
}
}
main();