#!/usr/bin/env node /** * Git post-checkout hook for auto-configuring worktree environments. * This runs after 'git checkout' or 'git worktree add'. * * Args from git: * process.argv[2] - previous HEAD ref * process.argv[3] - new HEAD ref * process.argv[4] - flag (1 = branch checkout, 0 = file checkout) */ import fs from 'fs'; import path from 'path'; import { execSync, execFileSync } from 'child_process'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const isBranchCheckout = process.argv[4] === '1'; if (!isBranchCheckout) { process.exit(0); } // Check if we're in a worktree by looking for .git file (not directory) const gitPath = path.join(process.cwd(), '.git'); const isWorktree = fs.existsSync(gitPath) && fs.statSync(gitPath).isFile(); if (!isWorktree) { process.exit(0); } const envLocalPath = path.join(process.cwd(), '.env.local'); // Don't overwrite existing .env.local if (fs.existsSync(envLocalPath)) { process.exit(0); } console.log('Detected new worktree - configuring ports in .env.local'); // Find the highest ports in use across all worktrees // Main worktree (first in list) is assumed to use default ports 1420/64343 let maxMcpPort = 64343; let maxDevPort = 1420; try { const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); const worktreePaths = worktreeList .split('\n') .filter(line => line.startsWith('worktree ')) .map(line => line.replace('worktree ', '').trim()); // Skip the first worktree (main) since it uses default ports for (let i = 1; i < worktreePaths.length; i++) { const worktreePath = worktreePaths[i]; const envPath = path.join(worktreePath, '.env.local'); if (fs.existsSync(envPath)) { const content = fs.readFileSync(envPath, 'utf8'); const mcpMatch = content.match(/^YAAK_PLUGIN_MCP_SERVER_PORT=(\d+)/m); if (mcpMatch) { const port = parseInt(mcpMatch[1], 10); if (port > maxMcpPort) { maxMcpPort = port; } } const devMatch = content.match(/^YAAK_DEV_PORT=(\d+)/m); if (devMatch) { const port = parseInt(devMatch[1], 10); if (port > maxDevPort) { maxDevPort = port; } } } } // Increment to get the next available port maxDevPort++; maxMcpPort++; } catch (err) { console.error('Warning: Could not check other worktrees for port conflicts:', err.message); // Continue with default ports } // Get worktree name from current directory const worktreeName = path.basename(process.cwd()); // Create .env.local with unique ports const envContent = `# Auto-generated by git post-checkout hook # This file configures ports for this worktree to avoid conflicts # Vite dev server port (main worktree uses 1420) YAAK_DEV_PORT=${maxDevPort} # MCP Server port (main worktree uses 64343) YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort} `; fs.writeFileSync(envLocalPath, envContent, 'utf8'); console.log(`Created .env.local with YAAK_DEV_PORT=${maxDevPort} and YAAK_PLUGIN_MCP_SERVER_PORT=${maxMcpPort}`); // Create tauri.worktree.conf.json with unique app identifier for complete isolation // This gives each worktree its own app data directory, avoiding the need for DB path prefixes const tauriWorktreeConfig = { identifier: `app.yaak.desktop.dev.${worktreeName}`, productName: `Daak (${worktreeName})` }; const tauriConfigPath = path.join(process.cwd(), 'crates-tauri', 'yaak-app', 'tauri.worktree.conf.json'); fs.writeFileSync(tauriConfigPath, JSON.stringify(tauriWorktreeConfig, null, 2) + '\n', 'utf8'); console.log(`Created tauri.worktree.conf.json with identifier: ${tauriWorktreeConfig.identifier}`); // Copy gitignored editor config folders from main worktree (.zed, .vscode, .claude, etc.) // This ensures your editor settings, tasks, and configurations are available in the new worktree // without needing to manually copy them or commit them to git. try { const worktreeList = execSync('git worktree list --porcelain', { encoding: 'utf8' }); const mainWorktreePath = worktreeList .split('\n') .find(line => line.startsWith('worktree ')) ?.replace('worktree ', '') .trim(); if (mainWorktreePath) { // Find all .* folders in main worktree root that are gitignored const entries = fs.readdirSync(mainWorktreePath, { withFileTypes: true }); const dotFolders = entries .filter(entry => entry.isDirectory() && entry.name.startsWith('.')) .map(entry => entry.name); for (const folder of dotFolders) { const sourcePath = path.join(mainWorktreePath, folder); const destPath = path.join(process.cwd(), folder); try { // Check if it's gitignored - run from main worktree directory execFileSync('git', ['check-ignore', '-q', folder], { stdio: 'pipe', cwd: mainWorktreePath }); // It's gitignored, copy it fs.cpSync(sourcePath, destPath, { recursive: true }); console.log(`Copied ${folder} from main worktree`); } catch { // Not gitignored or doesn't exist, skip } } } } catch (err) { console.warn('Warning: Could not copy files from main worktree:', err.message); // Continue anyway } console.log('\n✓ Worktree setup complete! Run `npm run init` to install dependencies.');