#!/usr/bin/env node /** * Auto-Tmux Dev Hook - Start dev servers in tmux/cmd automatically * * macOS/Linux: Runs dev server in a named tmux session (non-blocking). * Falls back to original command if tmux is not installed. * Windows: Opens dev server in a new cmd window (non-blocking). * * Runs before Bash tool use. If command is a dev server (npm run dev, pnpm dev, yarn dev, bun run dev), * transforms it to run in a detached session. * * Benefits: * - Dev server runs detached (doesn't block Claude Code) * - Session persists (can run `tmux capture-pane -t -p` to see logs on Unix) * - Session name matches project directory (allows multiple projects simultaneously) * * Session management (Unix): * - Checks tmux availability before transforming * - Kills any existing session with the same name (clean restart) * - Creates new detached session * - Reports session name and how to view logs * * Session management (Windows): * - Opens new cmd window with descriptive title * - Allows multiple dev servers to run simultaneously */ const path = require('path'); const { spawnSync } = require('child_process'); const MAX_STDIN = 1024 * 1024; // 1MB limit let data = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (data.length < MAX_STDIN) { const remaining = MAX_STDIN - data.length; data += chunk.substring(0, remaining); } }); process.stdin.on('end', () => { let input; try { input = JSON.parse(data); const cmd = input.tool_input?.command || ''; // Detect dev server commands: npm run dev, pnpm dev, yarn dev, bun run dev // Use word boundary (\b) to avoid matching partial commands const devServerRegex = /(npm run dev\b|pnpm( run)? dev\b|yarn dev\b|bun run dev\b)/; if (devServerRegex.test(cmd)) { // Get session name from current directory basename, sanitize for shell safety // e.g., /home/user/Portfolio → "Portfolio", /home/user/my-app-v2 → "my-app-v2" const rawName = path.basename(process.cwd()); // Replace non-alphanumeric characters (except - and _) with underscore to prevent shell injection const sessionName = rawName.replace(/[^a-zA-Z0-9_-]/g, '_') || 'dev'; if (process.platform === 'win32') { // Windows: open in a new cmd window (non-blocking) // Escape double quotes in cmd for cmd /k syntax const escapedCmd = cmd.replace(/"/g, '""'); input.tool_input.command = `start "DevServer-${sessionName}" cmd /k "${escapedCmd}"`; } else { // Unix (macOS/Linux): Check tmux is available before transforming const tmuxCheck = spawnSync('which', ['tmux'], { encoding: 'utf8' }); if (tmuxCheck.status === 0) { // Escape single quotes for shell safety: 'text' -> 'text'\''text' const escapedCmd = cmd.replace(/'/g, "'\\''"); // Build the transformed command: // 1. Kill existing session (silent if doesn't exist) // 2. Create new detached session with the dev command // 3. Echo confirmation message with instructions for viewing logs const transformedCmd = `SESSION="${sessionName}"; tmux kill-session -t "$SESSION" 2>/dev/null || true; tmux new-session -d -s "$SESSION" '${escapedCmd}' && echo "[Hook] Dev server started in tmux session '${sessionName}'. View logs: tmux capture-pane -t ${sessionName} -p -S -100"`; input.tool_input.command = transformedCmd; } // else: tmux not found, pass through original command unchanged } } process.stdout.write(JSON.stringify(input)); } catch { // Invalid input — pass through original data unchanged process.stdout.write(data); } process.exit(0); });