branchyard
Version:
branchyard — Your shipyard for parallel development workflows. Maintain your digital yard with clean branches, productive workflows, and AI-era readiness.
218 lines (190 loc) • 7.51 kB
text/typescript
import { $ } from "bun";
import path from "node:path";
import fs, { existsSync, writeFileSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { ask, choose, closePrompts } from "../utils/prompts";
import { getExistingWorktrees, getExistingBranches } from "../utils/git";
import { getConfig, saveConfig, editorCommands } from "../utils/config";
import { saveSession, getLastSession, getSession } from "../utils/sessions";
import { preflightCheck } from "../utils/preflight";
import { commandExists } from "../utils/system";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export async function runCreate(args: string[]) {
await preflightCheck(true, args.includes("--allow-subdir"));
const baseBranch =
args.includes("--base") && args[args.indexOf("--base") + 1]
? args[args.indexOf("--base") + 1]
: "main";
const noCreate = args.includes("--no-create");
const openEditor = args.includes("--open");
const dryRun = args.includes("--dry-run");
const sequential = args.includes("--sequential");
const cwd =
args.includes("--cwd") && args[args.indexOf("--cwd") + 1]
? args[args.indexOf("--cwd") + 1]!
: process.cwd();
const worktrees = args.filter(
(a) =>
!a.startsWith("--") &&
a !== baseBranch &&
a !== cwd &&
a !== "main" &&
a !== "develop"
);
if (worktrees.length === 0) {
console.error("❌ No worktree names provided.");
process.exit(1);
}
process.chdir(cwd);
const existingWorktrees = await getExistingWorktrees();
const existingBranches = await getExistingBranches();
// Check for worktrees in old location (parent directory)
const oldLocationWorktrees = worktrees.filter(name => existsSync(`../${name}`));
if (oldLocationWorktrees.length > 0) {
console.log(`\n⚠️ Found worktrees in old location (parent directory):`);
oldLocationWorktrees.forEach(name => console.log(` ../${name}`));
console.log(`\n💡 These should be removed with 'git worktree remove' or branchyard v1.2.x`);
console.log(` The new location will be: .worktrees/${oldLocationWorktrees.join(', ')}\n`);
const proceed = await ask("Continue with creating worktrees in new location? (y/n): ");
if (proceed.toLowerCase() !== 'y') {
console.log("Aborted.");
closePrompts();
return;
}
}
// Ensure .worktrees directory exists
if (!dryRun && !existsSync('.worktrees')) {
await $`mkdir -p .worktrees`;
}
async function createWorktree(name: string) {
const dir = `.worktrees/${name}`;
if (existingWorktrees.some((wt: any) => wt.branch === name)) {
console.log(`❌ Branch '${name}' is already checked out in another worktree. Skipping.`);
return;
}
if (existingBranches.includes(name)) {
const reuse = await ask(
`⚠️ Branch '${name}' already exists. Reuse it in a new worktree? (y/n): `
);
if (reuse.toLowerCase() !== "y") {
console.log(`⏩ Skipping '${name}'`);
return;
}
}
if (existsSync(dir)) {
console.log(`⚠️ Folder '${dir}' already exists. Skipping creation.`);
return;
}
if (dryRun) {
console.log(`[DRY-RUN] Would create worktree: ${name} from ${baseBranch}`);
} else {
console.log(`📂 Creating worktree: ${name} from ${baseBranch}`);
await $`git worktree add ${dir} -b ${name} ${baseBranch}`;
}
}
if (!noCreate) {
if (!sequential && worktrees.length > 1) {
console.log(`⏳ Creating ${worktrees.length} worktrees in parallel...`);
}
if (sequential) {
for (const name of worktrees) {
await createWorktree(name);
}
} else {
await Promise.all(worktrees.map(name => createWorktree(name)));
}
}
// Oprah Easter egg
if (worktrees.length > 1) {
const oprahPath = path.join(__dirname, "../assets/oprah.txt");
if (fs.existsSync(oprahPath)) {
console.log(fs.readFileSync(oprahPath, "utf-8"));
}
console.log("🌳 YOU GET A TREE! YOU GET A TREE! EVERYBODY GETS A TREE!");
}
let config = getConfig();
let editor: string = config.defaultEditor || "";
if (!editor) {
editor = await choose("Select editor:", Object.keys(editorCommands));
const remember = await ask("Set this as default editor? (y/n): ");
if (remember.toLowerCase() === "y") {
config.defaultEditor = editor;
saveConfig(config);
}
}
// Workspace generation with template merge
const workspace = {
folders: worktrees.map(w => ({ path: `.worktrees/${w}` })),
...(config.workspaceTemplate || {})
};
// Check if workspace file exists and might have old paths
if (existsSync("parallel-dev.code-workspace")) {
const existingWorkspace = JSON.parse(readFileSync("parallel-dev.code-workspace", "utf-8"));
const hasOldPaths = existingWorkspace.folders?.some((f: any) => f.path?.startsWith('../'));
if (hasOldPaths) {
console.log(`📝 Updating workspace file with new paths (was using ../worktree, now .worktrees/worktree)`);
}
}
writeFileSync("parallel-dev.code-workspace", JSON.stringify(workspace, null, 2));
console.log(`📝 Created workspace file: parallel-dev.code-workspace`);
// Ask to open workspace if not in dry-run mode and not already specified
let shouldOpen = openEditor;
if (!dryRun && !openEditor) {
const openNow = await ask("Open workspace in editor now? (Y/n): ");
shouldOpen = openNow.toLowerCase() !== 'n';
}
if (shouldOpen && !dryRun) {
const editorCommand = editorCommands[editor];
if (!editorCommand) {
console.error(`❌ Unknown editor: ${editor}`);
process.exit(1);
}
if (!(await commandExists(editorCommand))) {
console.error(`❌ The editor CLI '${editorCommand}' is not installed or not in your PATH.`);
process.exit(1);
}
console.log(`🚀 Opening workspace in ${editor}...`);
await $`${editorCommand} parallel-dev.code-workspace`;
} else if (!dryRun) {
console.log(`\n💡 To open your workspace later, run:`);
console.log(` ${editorCommands[editor] || 'code'} parallel-dev.code-workspace`);
}
const save = await ask("Do you want to save this setup as a named session? (y/n): ");
if (save.toLowerCase() === "y") {
const sessionName = await ask("Enter session name: ");
// Check if session already exists
const existingSession = getSession(sessionName);
if (existingSession) {
const overwrite = await ask(`⚠️ Session '${sessionName}' already exists. Overwrite? (y/n): `);
if (overwrite.toLowerCase() !== 'y') {
console.log("Session not saved.");
} else {
saveSession(sessionName, {
worktrees,
baseBranch,
editor
});
console.log(`💾 Session '${sessionName}' updated.`);
}
} else {
saveSession(sessionName, {
worktrees,
baseBranch,
editor
});
console.log(`💾 Session '${sessionName}' saved.`);
}
}
if (dryRun) {
console.log("✅ Dry run complete!");
} else {
console.log("\n✅ Setup complete!");
console.log(`📁 Worktrees created in: .worktrees/`);
console.log(`📝 Workspace file: parallel-dev.code-workspace`);
if (!shouldOpen) {
console.log(`\n💡 To start working, open the workspace:`);
console.log(` ${editorCommands[editor] || 'code'} parallel-dev.code-workspace`);
}
}
closePrompts();
}