UNPKG

veas

Version:

Veas CLI - Command-line interface for Veas platform

511 lines (506 loc) 23.5 kB
import { exec } from 'node:child_process'; import { unlinkSync, writeFileSync } from 'node:fs'; import { platform, tmpdir } from 'node:os'; import { join } from 'node:path'; import chalk from 'chalk'; export class TerminalSpawner { platform; constructor() { this.platform = platform(); } isInteractiveCommand(command) { const interactivePatterns = [ /^claude\b/i, /^ssh\b/i, /^vim?\b/i, /^nano\b/i, /^emacs\b/i, /^less\b/i, /^more\b/i, /^top\b/i, /^htop\b/i, /^python\b(?!\s+\S+\.py)/i, /^node\b(?!\s+\S+\.js)/i, /^irb\b/i, /^pry\b/i, /^mysql\b/i, /^psql\b/i, /^redis-cli\b/i, /^mongo\b/i, /^sqlite3\b/i, /^bash\b(?!\s+\S+\.sh)/i, /^zsh\b(?!\s+\S+\.sh)/i, /^sh\b(?!\s+\S+\.sh)/i, /docker\s+(exec|run)\s+.*-it/i, /^telnet\b/i, /^ftp\b/i, /^sftp\b/i, /^screen\b/i, /^tmux\b/i, /^watch\b/i, /^tail\s+-f/i, /^git\s+rebase\s+-i/i, /^npm\s+init\b/i, /^yarn\s+init\b/i, /^npx\s+create-/i, ]; return interactivePatterns.some(pattern => pattern.test(command)); } async spawnInNewTerminal(options) { console.log(chalk.cyan('🖥️ Opening new terminal window...')); switch (this.platform) { case 'darwin': return this.spawnMacTerminal(options); case 'win32': return this.spawnWindowsTerminal(options); case 'linux': return this.spawnLinuxTerminal(options); default: throw new Error(`Unsupported platform: ${this.platform}`); } } async spawnMacTerminal(options) { const { command, cwd, env, title, keepOpen, autoResponses, terminalApp = 'terminal' } = options; const scriptPath = join(tmpdir(), `veas-task-${Date.now()}.sh`); let scriptContent = '#!/bin/bash\n'; if (env) { for (const [key, value] of Object.entries(env)) { scriptContent += `export ${key}="${value}"\n`; } } if (cwd) { scriptContent += `cd "${cwd}"\n`; } scriptContent += `echo -e "\\033]0;${title || 'Veas Task Execution'}\\007"\n`; scriptContent += `echo ""\n`; scriptContent += `echo "════════════════════════════════════════════════════════════════"\n`; scriptContent += `echo " 🚀 VEAS TASK EXECUTION"\n`; scriptContent += `echo " 📋 Command: ${command}"\n`; if (autoResponses && autoResponses.length > 0) { scriptContent += `echo " 🤖 Auto-responses configured: ${autoResponses.length}"\n`; } scriptContent += `echo "════════════════════════════════════════════════════════════════"\n`; scriptContent += `echo ""\n`; const isInteractiveCommand = this.isInteractiveCommand(command); if (autoResponses && autoResponses.length > 0) { scriptContent += this.generateExpectScript(command, autoResponses); } else if (isInteractiveCommand) { scriptContent += `${command}\n`; } else { scriptContent += `${command}\n`; } if (keepOpen && !isInteractiveCommand) { scriptContent += `echo ""\n`; scriptContent += `echo "════════════════════════════════════════════════════════════════"\n`; scriptContent += `echo " ✅ Task completed. Press any key to close this window..."\n`; scriptContent += `echo "════════════════════════════════════════════════════════════════"\n`; scriptContent += `read -n 1 -s\n`; } else if (!keepOpen && !isInteractiveCommand) { scriptContent += `exit\n`; } writeFileSync(scriptPath, scriptContent, { mode: 0o755 }); const shouldUseScript = (autoResponses && autoResponses.length > 0) || !isInteractiveCommand; const appleScript = this.generateMacTerminalScript(terminalApp.toLowerCase(), scriptPath, title || 'Veas Task', command, isInteractiveCommand && !shouldUseScript, cwd || process.cwd()); return new Promise((resolve, reject) => { if (terminalApp.toLowerCase() === 'iterm' || terminalApp.toLowerCase() === 'iterm2') { exec(`osascript -e '${appleScript}'`, error => { if (error) { console.error(chalk.red('Failed to open iTerm:'), error); reject(error); return; } console.log(chalk.green('✅ iTerm window opened')); if (!isInteractiveCommand) { this.monitorScriptCompletion(scriptPath, resolve); } else { resolve({ pid: 0, exitCode: 0 }); } }); } else if (terminalApp.toLowerCase() === 'warp') { exec(`open -a Warp ${scriptPath}`, error => { if (error) { console.error(chalk.red('Failed to open Warp:'), error); reject(error); return; } console.log(chalk.green('✅ Warp window opened')); this.monitorScriptCompletion(scriptPath, resolve); }); } else if (terminalApp.toLowerCase() === 'alacritty') { exec(`open -na Alacritty --args -e bash ${scriptPath}`, error => { if (error) { console.error(chalk.red('Failed to open Alacritty:'), error); reject(error); return; } console.log(chalk.green('✅ Alacritty window opened')); this.monitorScriptCompletion(scriptPath, resolve); }); } else if (terminalApp.toLowerCase() === 'kitty') { exec(`open -na kitty --args bash ${scriptPath}`, error => { if (error) { console.error(chalk.red('Failed to open Kitty:'), error); reject(error); return; } console.log(chalk.green('✅ Kitty window opened')); this.monitorScriptCompletion(scriptPath, resolve); }); } else if (terminalApp.toLowerCase() === 'hyper') { exec(`open -na Hyper --args bash ${scriptPath}`, error => { if (error) { console.error(chalk.red('Failed to open Hyper:'), error); reject(error); return; } console.log(chalk.green('✅ Hyper window opened')); this.monitorScriptCompletion(scriptPath, resolve); }); } else { exec(`osascript -e '${appleScript}'`, (error, stdout) => { if (error) { console.error(chalk.red('Failed to open terminal:'), error); reject(error); return; } const windowId = parseInt(stdout.trim(), 10); console.log(chalk.green(`✅ Terminal window opened (ID: ${windowId})`)); this.monitorScriptCompletion(scriptPath, resolve, windowId); }); } }); } generateMacTerminalScript(app, scriptPath, title, command, isInteractive, cwd) { switch (app) { case 'iterm': case 'iterm2': { let executeCommands = ''; if (isInteractive && command && cwd) { const combinedCommand = `cd ${JSON.stringify(cwd)} && ${command}`; const escapedCommand = combinedCommand.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); executeCommands += `write text "${escapedCommand}"`; } else if (isInteractive && command) { const escapedCommand = command.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); executeCommands += `write text "${escapedCommand}"`; } else { executeCommands += `write text "bash ${scriptPath}"`; } return ` tell application "iTerm" activate create window with default profile tell current session of current window ${executeCommands} set name to "${title}" end tell return id of current window end tell `; } default: return ` tell application "Terminal" activate set newWindow to do script "bash ${scriptPath}" set current settings of newWindow to settings set "Pro" delay 0.5 return id of front window end tell `; } } monitorScriptCompletion(scriptPath, resolve, pid = 0) { const checkInterval = setInterval(() => { exec(`ps aux | grep -v grep | grep "${scriptPath}"`, (_err, out) => { if (!out.trim()) { clearInterval(checkInterval); try { unlinkSync(scriptPath); } catch (_e) { } resolve({ pid, exitCode: 0 }); } }); }, 1000); } async spawnWindowsTerminal(options) { const { command, cwd, env, title, keepOpen, terminalApp = 'cmd' } = options; const scriptPath = join(tmpdir(), `veas-task-${Date.now()}.bat`); let scriptContent = '@echo off\n'; scriptContent += `title ${title || 'Veas Task Execution'}\n`; if (env) { for (const [key, value] of Object.entries(env)) { scriptContent += `set ${key}=${value}\n`; } } if (cwd) { scriptContent += `cd /d "${cwd}"\n`; } scriptContent += 'echo.\n'; scriptContent += 'echo ================================================================\n'; scriptContent += 'echo VEAS TASK EXECUTION\n'; scriptContent += `echo Command: ${command}\n`; scriptContent += 'echo ================================================================\n'; scriptContent += 'echo.\n'; scriptContent += `${command}\n`; if (keepOpen) { scriptContent += 'echo.\n'; scriptContent += 'echo ================================================================\n'; scriptContent += 'echo Task completed. Press any key to close this window...\n'; scriptContent += 'echo ================================================================\n'; scriptContent += 'pause > nul\n'; } writeFileSync(scriptPath, scriptContent); let terminalCmd; switch (terminalApp.toLowerCase()) { case 'wt': case 'windowsterminal': terminalCmd = `wt new-tab --title "${title || 'Veas Task'}" cmd /c "${scriptPath}"`; break; case 'powershell': terminalCmd = `start powershell -NoExit -Command "& '${scriptPath}'"`; break; default: terminalCmd = `start "Veas Task" cmd /c "${scriptPath}"`; break; } return new Promise((resolve, reject) => { exec(terminalCmd, error => { if (error) { console.error(chalk.red('Failed to open terminal:'), error); reject(error); return; } console.log(chalk.green('✅ Terminal window opened')); const checkInterval = setInterval(() => { exec(`tasklist | findstr "${scriptPath}"`, (_err, out) => { if (!out.trim()) { clearInterval(checkInterval); try { unlinkSync(scriptPath); } catch (_e) { } resolve({ pid: 0, exitCode: 0 }); } }); }, 1000); }); }); } async spawnLinuxTerminal(options) { const { command, cwd, env, title, keepOpen, autoResponses, terminalApp } = options; const scriptPath = join(tmpdir(), `veas-task-${Date.now()}.sh`); let scriptContent = '#!/bin/bash\n'; if (env) { for (const [key, value] of Object.entries(env)) { scriptContent += `export ${key}="${value}"\n`; } } if (cwd) { scriptContent += `cd "${cwd}"\n`; } scriptContent += 'echo ""\n'; scriptContent += 'echo "════════════════════════════════════════════════════════════════"\n'; scriptContent += 'echo " 🚀 VEAS TASK EXECUTION"\n'; scriptContent += `echo " 📋 Command: ${command}"\n`; if (autoResponses && autoResponses.length > 0) { scriptContent += `echo " 🤖 Auto-responses configured: ${autoResponses.length}"\n`; } scriptContent += 'echo "════════════════════════════════════════════════════════════════"\n'; scriptContent += 'echo ""\n'; if (autoResponses && autoResponses.length > 0) { scriptContent += this.generateExpectScript(command, autoResponses); } else { scriptContent += `${command}\n`; } if (keepOpen) { scriptContent += 'echo ""\n'; scriptContent += 'echo "════════════════════════════════════════════════════════════════"\n'; scriptContent += 'echo " ✅ Task completed. Press any key to close this window..."\n'; scriptContent += 'echo "════════════════════════════════════════════════════════════════"\n'; scriptContent += 'read -n 1 -s\n'; } writeFileSync(scriptPath, scriptContent, { mode: 0o755 }); let terminals; if (terminalApp) { switch (terminalApp.toLowerCase()) { case 'gnome-terminal': terminals = [`gnome-terminal --title="${title || 'Veas Task'}" -- bash ${scriptPath}`]; break; case 'konsole': terminals = [`konsole --title "${title || 'Veas Task'}" -e bash ${scriptPath}`]; break; case 'xterm': terminals = [`xterm -title "${title || 'Veas Task'}" -e bash ${scriptPath}`]; break; case 'terminator': terminals = [`terminator -T "${title || 'Veas Task'}" -x bash ${scriptPath}`]; break; case 'alacritty': terminals = [`alacritty --title "${title || 'Veas Task'}" -e bash ${scriptPath}`]; break; case 'kitty': terminals = [`kitty --title "${title || 'Veas Task'}" bash ${scriptPath}`]; break; default: terminals = [`${terminalApp} -e bash ${scriptPath}`]; } } else { terminals = [ `gnome-terminal --title="${title || 'Veas Task'}" -- bash ${scriptPath}`, `konsole --title "${title || 'Veas Task'}" -e bash ${scriptPath}`, `xterm -title "${title || 'Veas Task'}" -e bash ${scriptPath}`, `x-terminal-emulator -e bash ${scriptPath}`, ]; } for (const termCmd of terminals) { try { return await new Promise((resolve, reject) => { exec(termCmd, error => { if (error) { reject(error); return; } console.log(chalk.green('✅ Terminal window opened')); const checkInterval = setInterval(() => { exec(`ps aux | grep -v grep | grep "${scriptPath}"`, (_err, out) => { if (!out.trim()) { clearInterval(checkInterval); try { unlinkSync(scriptPath); } catch (_e) { } resolve({ pid: 0, exitCode: 0 }); } }); }, 1000); }); }); } catch (_e) { } } throw new Error('No suitable terminal emulator found'); } generateExpectScript(command, autoResponses) { let script = ` # Check if expect is available if ! command -v expect &> /dev/null; then echo "⚠️ 'expect' not installed - running without auto-responses" ${command} exit $? fi echo "🤖 Starting with auto-responses..." echo "" # Run with expect for auto-responses expect -c ' set timeout -1 spawn ${command} # Give the program time to start sleep 1 # Handle auto-responses `; autoResponses.forEach((response, index) => { const escapedInput = response.input ? response.input.replace(/\n/g, '\\r').replace(/"/g, '\\"').replace(/'/g, "\\'") : '\\r'; if (response.immediate) { script += `puts "\\n>>> Sending message immediately..."\n`; script += `after ${response.delay || 100}\n`; script += `send "${escapedInput}"\n`; } else if (response.trigger) { script += `expect {\n`; script += ` -re "${response.trigger}" {\n`; script += ` puts "\\n>>> Trigger matched: ${response.trigger}"\n`; script += ` after ${response.delay || 0}\n`; script += ` send "${escapedInput}"\n`; if (response.closeAfter) { script += ` send "\\003"\n`; script += ` expect eof\n`; script += ` exit 0\n`; } else { script += ` exp_continue\n`; } script += ` }\n`; script += ` timeout {\n`; script += ` exp_continue\n`; script += ` }\n`; script += ` eof {\n`; script += ` exit 0\n`; script += ` }\n`; script += `}\n`; } else if (response.delay) { script += `puts "\\n>>> Waiting ${response.delay}ms before sending message ${index + 1}..."\n`; script += `after ${response.delay}\n`; script += `send "${escapedInput}"\n`; if (response.closeAfter) { script += `send "\\003"\n`; script += `expect eof\n`; script += `exit 0\n`; } } }); script += ` puts "\\n>>> Auto-responses complete. Handing over control..." # Hand over control to user interact '`; return script; } async spawnWithCompanion(options) { console.log(chalk.cyan('🖥️ Opening companion terminal for monitoring...')); const companionScript = join(tmpdir(), `veas-companion-${Date.now()}.sh`); let companionContent = '#!/bin/bash\n'; companionContent += 'echo "════════════════════════════════════════════════════════════════"\n'; companionContent += 'echo " 📊 VEAS TASK MONITOR"\n'; companionContent += `echo " 📋 Monitoring: ${options.command}"\n`; companionContent += 'echo " 🔄 Status: Running..."\n'; companionContent += 'echo "════════════════════════════════════════════════════════════════"\n'; companionContent += 'echo ""\n'; companionContent += 'echo "Auto-responses will be sent to the main terminal:"\n'; if (options.autoResponses) { options.autoResponses.forEach((r, i) => { companionContent += `echo " ${i + 1}. ${r.trigger ? `On '${r.trigger}' → ` : ''}Send '${r.input?.replace(/\n/g, '\\n')}' (delay: ${r.delay}ms)"\n`; }); } companionContent += 'echo ""\n'; companionContent += 'echo "Monitoring output..."\n'; companionContent += 'echo "────────────────────────────────────────────────────────────────"\n'; const logFile = join(tmpdir(), `veas-task-${Date.now()}.log`); companionContent += `tail -f ${logFile}\n`; writeFileSync(companionScript, companionContent, { mode: 0o755 }); const companionPid = await this.spawnInNewTerminal({ command: `bash ${companionScript}`, title: 'Veas Task Monitor', keepOpen: true, }).then(r => r.pid); const mainPid = await this.spawnInNewTerminal({ ...options, title: options.title || 'Veas Task Execution', env: { ...options.env, VEAS_LOG_FILE: logFile, }, }).then(r => r.pid); return { mainPid, companionPid }; } } //# sourceMappingURL=terminal-spawner.js.map