Skip to content

Instantly share code, notes, and snippets.

@gregberns
Created December 3, 2025 23:12
Show Gist options
  • Select an option

  • Save gregberns/abdfddd566eb65cef0e49fb7877027b0 to your computer and use it in GitHub Desktop.

Select an option

Save gregberns/abdfddd566eb65cef0e49fb7877027b0 to your computer and use it in GitHub Desktop.
Git Worktree Creator Script - Useful for running multiple coding agents locally
#!/usr/bin/env node
/**
* Git Worktree Creator Script
*
* This script creates new git worktrees for development, allowing you to work on multiple
* branches simultaneously without switching branches in your main repository.
*
* Key Features:
* - Creates worktrees from any base branch (not just current branch)
* - Automatically handles uncommitted work in current directory
* - Copies useful files (.env, .vscode, etc.) to new worktree
* - Installs dependencies automatically
* - Supports custom branch names and base branches
*
* Usage Examples:
* wtadd worktree-dir # Create worktree from current/main branch
* wtadd worktree-dir --base main # Explicitly use main as base branch
* wtadd worktree-dir --base develop # Use develop as base branch
* wtadd worktree-dir --branch feature/GOLD-1234 # Custom branch name
* wtadd worktree-dir --branch feature/GOLD-1234 --base main # Custom branch from main base
* wtadd worktree-dir --setup # Run setup.sh script after creation
* wtadd worktree-dir --exec "python -m venv venv" # Execute custom command in worktree
* wtadd worktree-dir --setup --exec "source venv/bin/activate && pip install -r requirements.txt"
*
* Installation:
* cp ./worktree.js $HOME/bin/wtadd
* chmod +x $HOME/bin/wtadd
*/
import { execSync } from "child_process";
import { existsSync, copyFileSync, mkdirSync, readdirSync, statSync } from "fs";
import { join, dirname, basename } from "path";
// Parse command line arguments
function parseArgs() {
const args = process.argv.slice(2);
let subPath = null;
let branchName = null;
let baseBranch = null;
let execCommand = null;
let runSetup = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--branch" && i + 1 < args.length) {
branchName = args[i + 1];
i++; // Skip the next argument since we consumed it
} else if (args[i] === "--base" && i + 1 < args.length) {
baseBranch = args[i + 1];
i++; // Skip the next argument since we consumed it
} else if (args[i] === "--exec" && i + 1 < args.length) {
execCommand = args[i + 1];
i++; // Skip the next argument since we consumed it
} else if (args[i] === "--setup") {
runSetup = true;
} else if (!args[i].startsWith("--")) {
// First non-flag argument is the sub-path
if (!subPath) {
subPath = args[i];
}
}
}
if (!subPath) {
console.error(
'Error: Please provide a sub-path argument (e.g., "INI-1234")',
);
console.error(
"Usage: wtadd [--branch <branch-name>] [--base <base-branch>] [--exec <command>] [--setup] <sub-path>",
);
process.exit(1);
}
// Default branch name to sub-path if not provided
if (!branchName) {
branchName = subPath;
}
return { subPath, branchName, baseBranch, execCommand, runSetup };
}
const {
subPath,
branchName,
baseBranch: userSpecifiedBaseBranch,
execCommand,
runSetup,
} = parseArgs();
// Allowed branch names
const ALLOWED_BRANCHES = ["main", "master", "dev", "development"];
// Get git repository root from current working directory
function getGitRoot() {
try {
const root = execSync("git rev-parse --show-toplevel", {
encoding: "utf-8",
cwd: process.cwd(),
}).trim();
return root;
} catch (error) {
console.error("Error: Not in a git repository or git command failed");
process.exit(1);
}
}
// Get current branch
function getCurrentBranch() {
const gitRoot = getGitRoot();
try {
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
encoding: "utf-8",
cwd: gitRoot,
}).trim();
return branch;
} catch (error) {
console.error("Error: Could not determine current branch");
process.exit(1);
}
}
// Check if branch exists
function branchExists(branchName) {
const gitRoot = getGitRoot();
try {
execSync(`git show-ref --verify --quiet refs/heads/${branchName}`, {
encoding: "utf-8",
cwd: gitRoot,
stdio: "ignore",
});
return true;
} catch (error) {
return false;
}
}
// Check if branch is allowed
function isBranchAllowed(branch) {
return ALLOWED_BRANCHES.includes(branch);
}
// Get current directory name
function getCurrentDirName() {
const gitRoot = getGitRoot();
return basename(gitRoot);
}
// Get parent directory
function getParentDir() {
const gitRoot = getGitRoot();
return dirname(gitRoot);
}
// Copy file or directory recursively
function copyRecursive(src, dest) {
const stat = statSync(src);
if (stat.isDirectory()) {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = readdirSync(src);
for (const entry of entries) {
const srcPath = join(src, entry);
const destPath = join(dest, entry);
copyRecursive(srcPath, destPath);
}
} else {
copyFileSync(src, dest);
}
}
// Check if file exists
function fileExists(filePath) {
const gitRoot = getGitRoot();
return existsSync(join(gitRoot, filePath));
}
// Execute command in worktree directory
function execInWorktree(worktreePath, command) {
try {
execSync(command, {
encoding: "utf-8",
cwd: worktreePath,
stdio: "inherit",
});
} catch (error) {
console.error(`Error executing command: ${command}`);
throw error;
}
}
// Main execution
async function main() {
// Check current branch
const currentBranch = getCurrentBranch();
console.log(`Current branch: ${currentBranch}`);
console.log(`Worktree branch: ${branchName}`);
// Determine the base branch for the worktree
let baseBranch;
if (userSpecifiedBaseBranch) {
// User explicitly specified a base branch
baseBranch = userSpecifiedBaseBranch;
console.log(`Using user-specified base branch: ${baseBranch}`);
// Verify the specified base branch exists
const gitRoot = getGitRoot();
try {
execSync(`git show-ref --verify --quiet refs/heads/${baseBranch}`, {
encoding: "utf-8",
cwd: gitRoot,
stdio: "ignore",
});
} catch (error) {
console.error(
`Error: Specified base branch "${baseBranch}" does not exist locally.`,
);
process.exit(1);
}
} else {
// No user-specified base branch, use automatic logic
baseBranch = currentBranch;
// If current branch is not allowed, try to use main instead
if (!isBranchAllowed(currentBranch)) {
console.log(`Current branch "${currentBranch}" is not in allowed list.`);
console.log(`Checking if 'main' branch exists and is up to date...`);
const gitRoot = getGitRoot();
try {
// Check if main branch exists
execSync(`git show-ref --verify --quiet refs/heads/main`, {
encoding: "utf-8",
cwd: gitRoot,
stdio: "ignore",
});
// Check if main is up to date with remote
execSync(`git fetch origin main`, {
encoding: "utf-8",
cwd: gitRoot,
stdio: "ignore",
});
const localMain = execSync(`git rev-parse main`, {
encoding: "utf-8",
cwd: gitRoot,
}).trim();
const remoteMain = execSync(`git rev-parse origin/main`, {
encoding: "utf-8",
cwd: gitRoot,
}).trim();
if (localMain !== remoteMain) {
console.log(
`Warning: Local main is not up to date with remote. Using local main anyway.`,
);
}
baseBranch = "main";
console.log(`Using 'main' as base branch for worktree creation.`);
} catch (error) {
console.error(
`Error: Current branch "${currentBranch}" is not allowed and 'main' branch is not available or up to date. ` +
`Must be on one of: ${ALLOWED_BRANCHES.join(", ")}`,
);
process.exit(1);
}
}
}
// Determine worktree path
const currentDirName = getCurrentDirName();
const parentDir = getParentDir();
const worktreeName = `${currentDirName}-${subPath}`;
const worktreePath = join(parentDir, worktreeName);
console.log(`Creating worktree: ${worktreePath}`);
// Check if worktree already exists
if (existsSync(worktreePath)) {
console.error(`Error: Directory already exists: ${worktreePath}`);
process.exit(1);
}
// Create worktree
const gitRoot = getGitRoot();
const branchAlreadyExists = branchExists(branchName);
try {
let worktreeCommand;
if (branchAlreadyExists) {
// Branch exists, checkout the existing branch
console.log(`Using existing branch: ${branchName}`);
worktreeCommand = `git worktree add "${worktreePath}" ${branchName}`;
} else {
// Branch doesn't exist, create new branch from base branch
console.log(`Creating new branch: ${branchName} from ${baseBranch}`);
worktreeCommand = `git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`;
}
execSync(worktreeCommand, {
encoding: "utf-8",
cwd: gitRoot,
stdio: "inherit",
});
console.log("Worktree created successfully");
} catch (error) {
console.error("Error creating worktree");
process.exit(1);
}
// Files and directories to copy
const filesToCopy = ["AGENTS.md", "AGENT.md", ".env", ".vscode", ".cursor"];
// Copy files/directories
console.log("\nCopying files and directories...");
for (const item of filesToCopy) {
const srcPath = join(gitRoot, item);
const destPath = join(worktreePath, item);
if (existsSync(srcPath)) {
console.log(` Copying ${item}...`);
copyRecursive(srcPath, destPath);
} else {
console.log(` Skipping ${item} (not found)`);
}
}
// Check for dependency files and install
console.log("\nChecking for dependencies...");
const hasRequirementsTxt = fileExists("requirements.txt");
const hasPackageJson = fileExists("package.json");
const hasYarnLock = fileExists("yarn.lock");
const hasPipfile = fileExists("Pipfile");
const hasPoetryLock = fileExists("poetry.lock");
if (hasRequirementsTxt || hasPipfile || hasPoetryLock) {
console.log(" Python dependencies detected");
if (hasRequirementsTxt) {
console.log(" Installing Python dependencies via pip...");
execInWorktree(worktreePath, "pip install -r requirements.txt");
} else if (hasPipfile) {
console.log(" Installing Python dependencies via pipenv...");
execInWorktree(worktreePath, "pipenv install");
} else if (hasPoetryLock) {
console.log(" Installing Python dependencies via poetry...");
execInWorktree(worktreePath, "poetry install");
}
}
if (hasPackageJson) {
console.log(" Node.js dependencies detected");
if (hasYarnLock) {
console.log(" Installing Node.js dependencies via yarn...");
execInWorktree(worktreePath, "yarn install");
} else {
console.log(" Installing Node.js dependencies via npm...");
execInWorktree(worktreePath, "npm install");
}
}
if (!hasRequirementsTxt && !hasPackageJson && !hasPipfile && !hasPoetryLock) {
console.log(" No dependency files found");
}
// Execute custom commands if specified
if (runSetup) {
console.log("\nRunning setup script...");
const setupScriptPath = join(gitRoot, "setup.sh");
if (existsSync(setupScriptPath)) {
console.log(" Executing setup.sh...");
try {
execInWorktree(worktreePath, `bash "${setupScriptPath}"`);
console.log(" Setup script completed successfully");
} catch (error) {
console.error(" Error executing setup script");
process.exit(1);
}
} else {
console.error(" Error: setup.sh not found in repository root");
process.exit(1);
}
}
if (execCommand) {
console.log(`\nExecuting custom command: ${execCommand}`);
try {
execInWorktree(worktreePath, execCommand);
console.log(" Custom command completed successfully");
} catch (error) {
console.error(" Error executing custom command");
process.exit(1);
}
}
console.log(`\n✅ Worktree setup complete: ${worktreePath}`);
}
main().catch((error) => {
console.error("Unexpected error:", error);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment