Skip to content

Instantly share code, notes, and snippets.

@Innei
Created December 24, 2025 05:37
Show Gist options
  • Select an option

  • Save Innei/b980052436fcf2067ebd730b1f9558e2 to your computer and use it in GitHub Desktop.

Select an option

Save Innei/b980052436fcf2067ebd730b1f9558e2 to your computer and use it in GitHub Desktop.
worktree.ts
#!/usr/bin/env -S npx tsx
import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { mkdir, readdir, symlink } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const ROOT_DIR = join(__dirname, '..');
const WORKFLOW_DIR = join(ROOT_DIR, '.workflow');
const RANDOM_NAMES = [
'aurora',
'breeze',
'cascade',
'dawn',
'echo',
'flame',
'galaxy',
'horizon',
'iris',
'jasmine',
'kite',
'luna',
'mercury',
'nebula',
'orbit',
'prism',
'quartz',
'river',
'stellar',
'titan',
'unity',
'violet',
'whisper',
'zenith',
'azimuth',
] as const;
async function findAllNodeModules(baseDir: string): Promise<string[]> {
const nodeModulesPaths: string[] = [];
async function scanDirectory(dir: string): Promise<void> {
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.name === 'node_modules' && entry.isDirectory()) {
nodeModulesPaths.push(fullPath);
} else if (
entry.isDirectory() &&
entry.name !== '.git' &&
entry.name !== '.workflow' &&
!entry.name.startsWith('.')
) {
await scanDirectory(fullPath);
}
}
} catch (error) {
// Ignore permission errors or inaccessible directories
}
}
await scanDirectory(baseDir);
return nodeModulesPaths;
}
function getCurrentBranch(): string {
try {
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: ROOT_DIR,
encoding: 'utf-8',
}).trim();
return branch === 'HEAD' ? 'main' : branch;
} catch {
return 'main';
}
}
function branchExists(branch: string): boolean {
try {
execSync(`git show-ref --verify refs/heads/${branch}`, { cwd: ROOT_DIR, stdio: 'pipe' });
return true;
} catch {
return false;
}
}
async function createWorkflow(baseBranch?: string, name?: string, force = false): Promise<string> {
const workflowName = name || RANDOM_NAMES[Math.floor(Math.random() * RANDOM_NAMES.length)];
const workflowPath = join(WORKFLOW_DIR, workflowName);
const branch = baseBranch || getCurrentBranch();
console.log(`Creating workflow: ${workflowName}`);
console.log(` Base branch: ${branch}`);
await mkdir(WORKFLOW_DIR, { recursive: true });
if (branchExists(workflowName)) {
if (force) {
console.log(` Branch '${workflowName}' already exists, deleting...`);
execSync(`git branch -D ${workflowName}`, { cwd: ROOT_DIR, stdio: 'inherit' });
} else {
console.error(`❌ Branch '${workflowName}' already exists`);
console.log(` Use --force to delete existing branch and recreate`);
console.log(` Or remove with: bun run scripts/createGitWorkflow.ts remove ${workflowName}`);
process.exit(1);
}
}
console.log(` Adding git worktree...`);
execSync(`git worktree add -b ${workflowName} ${workflowPath} ${branch}`, {
cwd: ROOT_DIR,
stdio: 'inherit',
});
console.log(' Linking node_modules from parent monorepo...');
try {
const rootModulesPath = join(ROOT_DIR, 'node_modules');
const workflowModulesPath = join(workflowPath, 'node_modules');
if (!existsSync(workflowModulesPath)) {
const relativeTarget = relative(workflowPath, rootModulesPath);
await symlink(relativeTarget, workflowModulesPath, 'dir');
console.log(` ✅ node_modules linked: ${relativeTarget}`);
}
} catch (error) {
console.log(` ⚠️ Failed to link node_modules: ${error}`);
}
const allNodeModules = await findAllNodeModules(ROOT_DIR);
for (const nodeModulesPath of allNodeModules) {
if (nodeModulesPath === join(ROOT_DIR, 'node_modules')) continue;
const relativePath = relative(ROOT_DIR, nodeModulesPath);
const symlinkPath = join(workflowPath, relativePath);
await mkdir(dirname(symlinkPath), { recursive: true });
if (!existsSync(symlinkPath)) {
const relativeTarget = relative(workflowPath, nodeModulesPath);
await symlink(relativeTarget, symlinkPath, 'dir');
console.log(` Symlink: ${relativePath}`);
}
}
console.log(`\n✅ Workflow created at: ${workflowPath}`);
console.log(` Branch: ${workflowName}`);
return workflowPath;
}
function listWorkflows(): void {
console.log('Listing workflows:');
const result = execSync('git worktree list --porcelain', { cwd: ROOT_DIR, encoding: 'utf-8' });
console.log(result);
}
function removeWorkflow(name: string, deleteBranch = true): void {
const workflowPath = join(WORKFLOW_DIR, name);
console.log(`Removing workflow: ${name}`);
try {
if (existsSync(workflowPath)) {
execSync(`git worktree remove ${workflowPath}`, { cwd: ROOT_DIR, stdio: 'inherit' });
console.log(`✅ Workflow removed: ${name}`);
} else {
console.log(`⚠️ Workflow directory not found: ${workflowPath}`);
}
if (deleteBranch) {
console.log(` Deleting branch: ${name}`);
try {
execSync(`git branch -D ${name}`, { cwd: ROOT_DIR, stdio: 'inherit' });
console.log(`✅ Branch deleted: ${name}`);
} catch (error) {
console.warn(`⚠️ Branch ${name} may not exist or is still in use`);
}
}
} catch (error) {
console.error(`❌ Failed to remove workflow: ${name}`);
console.error(error);
}
}
function pruneWorkflows(): void {
console.log('Pruning workflows...');
execSync('git worktree prune', { cwd: ROOT_DIR, stdio: 'inherit' });
console.log('✅ Workflows pruned');
}
async function forcePurgeAll(): Promise<void> {
console.log('Force purging all workflows in .workflow...');
try {
if (!existsSync(WORKFLOW_DIR)) {
console.log('ℹ️ .workflow directory does not exist');
return;
}
const entries = await readdir(WORKFLOW_DIR, { withFileTypes: true });
const workflows = entries
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
.map((entry) => entry.name);
if (workflows.length === 0) {
console.log('ℹ️ No workflows to purge');
return;
}
console.log(`Found ${workflows.length} workflow(s): ${workflows.join(', ')}`);
for (const workflowName of workflows) {
removeWorkflow(workflowName, true);
}
console.log('✅ All workflows purged');
} catch (error) {
console.error('❌ Failed to purge workflows');
console.error(error);
}
}
function submitPR(name: string, title?: string, body?: string): void {
const workflowPath = join(WORKFLOW_DIR, name);
console.log(`Submitting PR for workflow: ${name}`);
try {
console.log(' Checking for changes...');
const status = execSync('git status --porcelain', {
cwd: workflowPath,
encoding: 'utf-8',
}).trim();
if (!status) {
console.warn('⚠️ No changes to commit');
return;
}
console.log(' Adding all changes...');
execSync('git add -A', { cwd: workflowPath, stdio: 'inherit' });
console.log(' Committing changes...');
const commitMessage = title || `feat: ${name} implementation`;
execSync(`git commit -m "${commitMessage}"`, { cwd: workflowPath, stdio: 'inherit' });
console.log(' Pushing branch...');
execSync(`git push -u origin ${name}`, { cwd: workflowPath, stdio: 'inherit' });
console.log(' Creating pull request...');
const baseBranch = execSync('git rev-parse --abbrev-ref @{u}', {
cwd: workflowPath,
encoding: 'utf-8',
})
.trim()
.split('/')[1];
let prCommand = `gh pr create --base ${baseBranch} --head ${name}`;
if (title) prCommand += ` --title "${title}"`;
if (body) prCommand += ` --body "${body}"`;
execSync(prCommand, { cwd: workflowPath, stdio: 'inherit' });
console.log(`✅ PR created for branch: ${name}`);
} catch (error) {
console.error(`❌ Failed to submit PR: ${name}`);
console.error(error);
}
}
function listPRs(name?: string): void {
console.log('Listing pull requests...');
try {
let command = 'gh pr list --state open';
if (name) command += ` --head ${name}`;
execSync(command, { cwd: ROOT_DIR, stdio: 'inherit' });
} catch (error) {
console.error('❌ Failed to list PRs');
console.error(error);
}
}
function mergePR(name: string, mergeMethod: 'merge' | 'squash' | 'rebase' = 'squash'): void {
console.log(`Merging PR for branch: ${name}`);
console.log(` Merge method: ${mergeMethod}`);
try {
execSync(`gh pr merge ${name} --${mergeMethod} --delete-branch`, {
cwd: ROOT_DIR,
stdio: 'inherit',
});
console.log(`✅ PR merged: ${name}`);
} catch (error) {
console.error(`❌ Failed to merge PR: ${name}`);
console.error(error);
}
}
function closePR(name: string): void {
console.log(`Closing PR for branch: ${name}`);
try {
execSync(`gh pr close ${name} --delete-branch`, { cwd: ROOT_DIR, stdio: 'inherit' });
console.log(`✅ PR closed: ${name}`);
} catch (error) {
console.error(`❌ Failed to close PR: ${name}`);
console.error(error);
}
}
async function main() {
const command = process.argv[2];
const args = process.argv.slice(3);
switch (command) {
case 'create':
const force = args.includes('--force');
const cleanArgs = args.filter((arg) => arg !== '--force');
await createWorkflow(cleanArgs[0], cleanArgs[1], force);
break;
case 'list':
listWorkflows();
break;
case 'remove':
case 'rm':
if (!args[0]) {
console.error('Usage: bun run scripts/createGitWorkflow.ts remove <name> [--keep-branch]');
process.exit(1);
}
const keepBranch = args.includes('--keep-branch');
removeWorkflow(args[0], !keepBranch);
break;
case 'prune':
pruneWorkflows();
break;
case 'force-purge-all':
await forcePurgeAll();
break;
case 'pr':
const prCommand = args[0];
const prArgs = args.slice(1);
switch (prCommand) {
case 'submit':
if (!prArgs[0]) {
console.error(
'Usage: bun run scripts/createGitWorkflow.ts pr submit <name> [title] [body]',
);
process.exit(1);
}
submitPR(prArgs[0], prArgs[1], prArgs[2]);
break;
case 'list':
listPRs(prArgs[0]);
break;
case 'merge':
if (!prArgs[0]) {
console.error('Usage: bun run scripts/createGitWorkflow.ts pr merge <name> [method]');
console.error(' method: merge | squash | rebase (default: squash)');
process.exit(1);
}
mergePR(prArgs[0], (prArgs[1] as 'merge' | 'squash' | 'rebase') || 'squash');
break;
case 'close':
if (!prArgs[0]) {
console.error('Usage: bun run scripts/createGitWorkflow.ts pr close <name>');
process.exit(1);
}
closePR(prArgs[0]);
break;
default:
console.error('Unknown PR command:', prCommand);
console.log(`
Available PR commands:
submit <name> [title] [body] Commit, push and create PR
list [name] List open PRs (optionally filter by branch)
merge <name> [method] Merge PR (default: squash)
close <name> Close PR and delete branch
`);
}
break;
default:
console.log(`
Usage: bun run scripts/createGitWorkflow.ts <command> [options]
Commands:
create [baseBranch] [name] [--force] Create a new workflow worktree
list List all worktrees
remove <name> Remove a worktree and its branch
remove <name> --keep-branch Remove worktree but keep the branch
prune Prune stale worktrees
force-purge-all Force delete all workflows and branches in .workflow
pr Pull request management
Examples:
bun run scripts/createGitWorkflow.ts create origin/main aurora
bun run scripts/createGitWorkflow.ts create origin/main aurora --force
bun run scripts/createGitWorkflow.ts list
bun run scripts/createGitWorkflow.ts remove aurora
bun run scripts/createGitWorkflow.ts remove aurora --keep-branch
bun run scripts/createGitWorkflow.ts prune
bun run scripts/createGitWorkflow.ts force-purge-all
bun run scripts/createGitWorkflow.ts pr submit aurora "Fix bug" "Description"
bun run scripts/createGitWorkflow.ts pr merge aurora squash
bun run scripts/createGitWorkflow.ts pr close aurora
`);
}
}
main().catch(console.error);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment