UNPKG

dmux

Version:

Tmux pane manager with AI agent integration for parallel development workflows

149 lines 7.4 kB
import { execSync } from 'child_process'; import fs from 'fs/promises'; import { applySmartLayout } from '../utils/tmux.js'; export default function usePaneRunner({ panes, savePanes, projectSettings, setStatusMessage, setRunningCommand }) { const copyNonGitFiles = async (worktreePath) => { try { setStatusMessage('Copying non-git files from main...'); const projectRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: 'pipe' }).trim(); const rsyncCmd = `rsync -avz --exclude='.git' --exclude='.dmux' --exclude='node_modules' --exclude='dist' --exclude='build' --exclude='.next' --exclude='.turbo' "${projectRoot}/" "${worktreePath}/"`; execSync(rsyncCmd, { stdio: 'pipe' }); setStatusMessage('Non-git files copied successfully'); setTimeout(() => setStatusMessage(''), 2000); } catch { setStatusMessage('Failed to copy non-git files'); setTimeout(() => setStatusMessage(''), 2000); } }; const runCommandInternal = async (type, pane) => { if (!pane.worktreePath) { setStatusMessage('No worktree path for this pane'); setTimeout(() => setStatusMessage(''), 2000); return; } const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand; if (!command) { setStatusMessage('No command configured'); setTimeout(() => setStatusMessage(''), 2000); return; } try { setRunningCommand(true); setStatusMessage(`Starting ${type} in background window...`); const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId; if (existingWindowId) { try { execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' }); } catch { } } const windowName = `${pane.slug}-${type}`; const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim(); const logFile = `/tmp/dmux-${pane.id}-${type}.log`; const fullCommand = `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`; execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`, { stdio: 'pipe' }); const updatedPane = { ...pane, [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId, [type === 'test' ? 'testStatus' : 'devStatus']: 'running' }; const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p); await savePanes(updatedPanes); if (type === 'test') setTimeout(() => monitorTestOutput(pane.id, logFile), 2000); else setTimeout(() => monitorDevOutput(pane.id, logFile), 2000); setRunningCommand(false); setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`); setTimeout(() => setStatusMessage(''), 3000); } catch { setRunningCommand(false); setStatusMessage(`Failed to run ${type} command`); setTimeout(() => setStatusMessage(''), 3000); } }; const monitorTestOutput = async (paneId, logFile) => { try { const content = await fs.readFile(logFile, 'utf-8'); let status = 'running'; if (content.match(/(?:tests?|specs?) (?:passed|✓|succeeded)/i) || content.match(/\b0 fail(?:ing|ed|ures?)\b/i)) { status = 'passed'; } else if (content.match(/(?:tests?|specs?) (?:failed|✗|✖)/i) || content.match(/\d+ fail(?:ing|ed|ures?)/i) || content.match(/error:/i)) { status = 'failed'; } const pane = panes.find(p => p.id === paneId); if (pane?.testWindowId) { try { execSync(`tmux list-windows -F '#{window_id}' | rg -q '${pane.testWindowId}'`, { stdio: 'pipe' }); const paneOutput = execSync(`tmux capture-pane -t '${pane.testWindowId}' -p | tail -5`, { encoding: 'utf-8' }); if (paneOutput.includes('$') || paneOutput.includes('#')) { if (status === 'running') status = 'passed'; } } catch { if (status === 'running') status = 'failed'; } } const updatedPanes = panes.map(p => p.id === paneId ? { ...p, testStatus: status, testOutput: content.slice(-5000) } : p); await savePanes(updatedPanes); if (status === 'running') setTimeout(() => monitorTestOutput(paneId, logFile), 2000); } catch { } }; const monitorDevOutput = async (paneId, logFile) => { try { const content = await fs.readFile(logFile, 'utf-8'); const urlMatch = content.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d+/i) || content.match(/Local:\s+(https?:\/\/[^\s]+)/i) || content.match(/listening on port (\d+)/i); let devUrl = ''; if (urlMatch) { if (urlMatch[0].startsWith('http')) devUrl = urlMatch[0]; else if (urlMatch[1]) devUrl = `http://localhost:${urlMatch[1]}`; } const pane = panes.find(p => p.id === paneId); let status = 'running'; if (pane?.devWindowId) { try { execSync(`tmux list-windows -F '#{window_id}' | rg -q '${pane.devWindowId}'`, { stdio: 'pipe' }); } catch { status = 'stopped'; } } const updatedPanes = panes.map(p => p.id === paneId ? { ...p, devStatus: status, devUrl: devUrl || p.devUrl } : p); await savePanes(updatedPanes); if (status === 'running') setTimeout(() => monitorDevOutput(paneId, logFile), 2000); } catch { } }; const attachBackgroundWindow = async (pane, type) => { const windowId = type === 'test' ? pane.testWindowId : pane.devWindowId; if (!windowId) { setStatusMessage(`No ${type} window to attach`); setTimeout(() => setStatusMessage(''), 2000); return; } try { execSync(`tmux join-pane -h -s '${windowId}'`, { stdio: 'pipe' }); const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim()); applySmartLayout(paneCount); execSync(`tmux select-pane -t '{last}'`, { stdio: 'pipe' }); setStatusMessage(`Attached ${type} window`); setTimeout(() => setStatusMessage(''), 2000); } catch { setStatusMessage(`Failed to attach ${type} window`); setTimeout(() => setStatusMessage(''), 2000); } }; return { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow }; } //# sourceMappingURL=usePaneRunner.js.map