#!/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"); const CLIENT_PORT_BASE = 1420; const PROXY_PORT_BASE = 2420; // Keep proxy +1000 from client to avoid cross-app conflicts // Find the highest worktree index and MCP port in use across all worktrees. // Main worktree (first in list) is assumed to use index 0: // client=1420, proxy=2420, mcp=64343. let maxMcpPort = 64343; let maxWorktreeIndex = 0; 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 clientDevMatch = content.match(/^(?:YAAK_CLIENT_DEV_PORT|YAAK_DEV_PORT)=(\d+)/m); if (clientDevMatch) { const port = parseInt(clientDevMatch[1], 10); const index = port - CLIENT_PORT_BASE; if (index > maxWorktreeIndex) { maxWorktreeIndex = index; } } const proxyDevMatch = content.match(/^YAAK_PROXY_DEV_PORT=(\d+)/m); if (proxyDevMatch) { const port = parseInt(proxyDevMatch[1], 10); const index = port - PROXY_PORT_BASE; if (index > maxWorktreeIndex) { maxWorktreeIndex = index; } } } } // Increment MCP to get the next available port maxMcpPort++; } catch (err) { console.error("Warning: Could not check other worktrees for port conflicts:", err.message); // Continue with default ports } const nextWorktreeIndex = maxWorktreeIndex + 1; const nextClientDevPort = CLIENT_PORT_BASE + nextWorktreeIndex; const nextProxyDevPort = PROXY_PORT_BASE + nextWorktreeIndex; // 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 for the client app (main worktree uses ${CLIENT_PORT_BASE}) YAAK_CLIENT_DEV_PORT=${nextClientDevPort} # Vite dev server port for the proxy app (main worktree uses ${PROXY_PORT_BASE}) YAAK_PROXY_DEV_PORT=${nextProxyDevPort} # 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_CLIENT_DEV_PORT=${nextClientDevPort}, YAAK_PROXY_DEV_PORT=${nextProxyDevPort}, 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 clientTauriWorktreeConfig = { identifier: `app.yaak.desktop.dev.${worktreeName}`, productName: `Daak (${worktreeName})`, }; const clientTauriConfigPath = path.join( process.cwd(), "crates-tauri", "yaak-app-client", "tauri.worktree.conf.json", ); fs.writeFileSync( clientTauriConfigPath, JSON.stringify(clientTauriWorktreeConfig, null, 2) + "\n", "utf8", ); console.log( `Created client tauri.worktree.conf.json with identifier: ${clientTauriWorktreeConfig.identifier}`, ); const proxyTauriWorktreeConfig = { identifier: `app.yaak.proxy.dev.${worktreeName}`, productName: `Yaak Proxy (${worktreeName})`, }; const proxyTauriConfigPath = path.join( process.cwd(), "crates-tauri", "yaak-app-proxy", "tauri.worktree.conf.json", ); fs.writeFileSync( proxyTauriConfigPath, JSON.stringify(proxyTauriWorktreeConfig, null, 2) + "\n", "utf8", ); console.log( `Created proxy tauri.worktree.conf.json with identifier: ${proxyTauriWorktreeConfig.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.");