veas
Version:
Veas CLI - Command-line interface for Veas platform
511 lines (506 loc) • 23.5 kB
JavaScript
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