snowballtools-base/scripts/mono-start.js
2025-02-20 09:43:07 -08:00

303 lines
11 KiB
JavaScript

#!/usr/bin/env node
const { spawn, exec } = require('child_process');
const chalk = require('chalk');
const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const os = require('os');
const execAsync = promisify(exec);
// Configuration
const config = {
rootDir: process.cwd(),
backendDir: path.join(process.cwd(), 'packages/backend'),
frontendDir: path.join(process.cwd(), 'packages/frontend'),
ports: {
backend: 8000,
frontend: 3000,
alternativeFrontend: 3001
}
};
// Helper to check if a port is in use and kill the process if needed
async function ensurePortAvailable(port) {
try {
console.log(chalk.blue(`🔍 Checking if port ${port} is in use...`));
// Different commands for different operating systems
let command;
if (process.platform === 'win32') {
command = `netstat -ano | findstr :${port}`;
} else {
command = `lsof -i :${port} -t`;
}
const { stdout } = await execAsync(command);
if (stdout.trim()) {
console.log(chalk.yellow(`⚠️ Port ${port} is in use. Attempting to terminate processes...`));
let pids;
if (process.platform === 'win32') {
// Extract PIDs from Windows netstat output
pids = stdout.split('\n')
.filter(line => line.includes(`:${port}`))
.map(line => line.trim().split(/\s+/).pop() || '')
.filter(Boolean);
} else {
// Unix lsof already returns PIDs directly
pids = stdout.trim().split('\n').filter(Boolean);
}
// Kill each process
for (const pid of pids) {
try {
const killCommand = process.platform === 'win32'
? `taskkill /F /PID ${pid}`
: `kill -9 ${pid}`;
await execAsync(killCommand);
console.log(chalk.green(`✅ Terminated process ${pid} using port ${port}`));
} catch (error) {
console.error(chalk.red(`Failed to kill process ${pid}: ${error}`));
}
}
} else {
console.log(chalk.green(`✅ Port ${port} is available`));
}
} catch (error) {
// If the command fails (e.g., lsof not found), assume port is available
console.log(chalk.yellow(`⚠️ Could not check port ${port}: ${error}`));
}
}
// Helper to run commands with proper output handling
function runCommand(command, args, cwd, label) {
return new Promise((resolve, reject) => {
console.log(chalk.blue(`🚀 Starting: ${label}...`));
const childProcess = spawn(command, args, {
cwd,
stdio: 'pipe',
shell: true
});
let stdout = '';
let stderr = '';
childProcess.stdout && childProcess.stdout.on('data', (data) => {
const output = data.toString();
stdout += output;
process.stdout.write(chalk.gray(`[${label}] `) + output);
});
childProcess.stderr && childProcess.stderr.on('data', (data) => {
const output = data.toString();
stderr += output;
process.stderr.write(chalk.yellow(`[${label}] `) + output);
});
childProcess.on('error', (error) => {
console.error(chalk.red(`❌ Error in ${label}: ${error.message}`));
reject(error);
});
childProcess.on('close', (code) => {
if (code === 0) {
console.log(chalk.green(`✅ Completed: ${label}`));
resolve();
} else {
console.error(chalk.red(`❌ Failed: ${label} (exit code: ${code})`));
reject(new Error(`Command failed with exit code ${code}: ${stderr}`));
}
});
});
}
// Check if iTerm2 is available (macOS only)
async function isITerm2Available() {
if (process.platform !== 'darwin') return false;
try {
const { stdout } = await execAsync('osascript -e "exists application \\"iTerm2\\""');
return stdout.trim() === 'true';
} catch (error) {
return false;
}
}
// Open split panes in iTerm2
async function openITerm2SplitPanes(backendCommand, frontendCommand) {
const script = `
tell application "iTerm2"
create window with default profile
tell current window
tell current session
set backendSession to (split horizontally with default profile)
set frontendSession to (split vertically with default profile)
select
write text "cd ${config.backendDir} && clear && echo '🚀 BACKEND SERVER' && ${backendCommand}"
set name to "Backend Server"
tell backendSession
select
write text "cd ${config.frontendDir} && clear && echo '🚀 FRONTEND DEV SERVER' && ${frontendCommand}"
set name to "Frontend Dev Server"
end tell
tell frontendSession
select
write text "cd ${config.rootDir} && clear && echo '📊 MONOREPO ROOT'"
set name to "Monorepo Root"
end tell
end tell
end tell
end tell
`;
try {
await execAsync(`osascript -e '${script}'`);
return true;
} catch (error) {
console.error(chalk.yellow(`⚠️ Failed to open iTerm2 with split panes: ${error.message}`));
return false;
}
}
// Open a new terminal based on platform
async function openTerminals(backendCommand, frontendCommand) {
// First check if we can use iTerm2 on macOS
if (process.platform === 'darwin') {
const iTerm2Available = await isITerm2Available();
if (iTerm2Available) {
console.log(chalk.blue('🖥️ Opening iTerm2 with split panes...'));
const success = await openITerm2SplitPanes(backendCommand, frontendCommand);
if (success) return true;
}
}
// Fallback to separate terminal windows
const escapedBackendCommand = backendCommand.replace(/"/g, '\\"');
const escapedFrontendCommand = frontendCommand.replace(/"/g, '\\"');
const escapedBackendDir = config.backendDir.replace(/"/g, '\\"');
const escapedFrontendDir = config.frontendDir.replace(/"/g, '\\"');
// Platform-specific terminal opening commands
if (process.platform === 'darwin') {
// macOS - Terminal.app
const backendScript = `
tell application "Terminal"
do script "cd \\"${escapedBackendDir}\\" && clear && echo '🚀 BACKEND SERVER' && ${escapedBackendCommand}"
set custom title of front window to "Backend Server"
end tell
`;
const frontendScript = `
tell application "Terminal"
do script "cd \\"${escapedFrontendDir}\\" && clear && echo '🚀 FRONTEND DEV SERVER' && ${escapedFrontendCommand}"
set custom title of front window to "Frontend Dev Server"
end tell
`;
await execAsync(`osascript -e '${backendScript}'`);
await execAsync(`osascript -e '${frontendScript}'`);
} else if (process.platform === 'win32') {
// Windows - try Windows Terminal first, fall back to cmd
try {
// Windows Terminal (supports multiple tabs)
await execAsync(`wt -w 0 -d "${escapedBackendDir}" cmd /k "title Backend Server && ${escapedBackendCommand}" ; split-pane -d "${escapedFrontendDir}" cmd /k "title Frontend Dev Server && ${escapedFrontendCommand}"`);
} catch (error) {
// Fallback to regular cmd windows
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K',
`cd /d "${escapedBackendDir}" && title Backend Server && ${escapedBackendCommand}`]);
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/K',
`cd /d "${escapedFrontendDir}" && title Frontend Dev Server && ${escapedFrontendCommand}`]);
}
} else {
// Linux terminals with split support
try {
// Try Tilix (supports split screen)
await execAsync(`tilix --window-style=disable-csd-hide-toolbar --maximize --session-file=<(echo '[{"command":"cd ${escapedBackendDir} && ${escapedBackendCommand}","title":"Backend Server"},{"command":"cd ${escapedFrontendDir} && ${escapedFrontendCommand}","title":"Frontend Dev Server"}]')`);
} catch (error) {
try {
// Try Terminator (supports split screen)
await execAsync(`terminator --maximize -e "bash -c 'cd ${escapedBackendDir} && ${escapedBackendCommand}'" -e "bash -c 'cd ${escapedFrontendDir} && ${escapedFrontendCommand}'"`);
} catch (error) {
// Fallback to separate gnome-terminal windows
try {
spawn('gnome-terminal', ['--', 'bash', '-c',
`cd "${escapedBackendDir}" && echo -e "\\033]0;Backend Server\\007" && ${escapedBackendCommand}; exec bash`]);
spawn('gnome-terminal', ['--', 'bash', '-c',
`cd "${escapedFrontendDir}" && echo -e "\\033]0;Frontend Dev Server\\007" && ${escapedFrontendCommand}; exec bash`]);
} catch (error) {
// Last resort: try xterm
spawn('xterm', ['-T', 'Backend Server', '-e',
`cd "${escapedBackendDir}" && ${escapedBackendCommand}; exec bash`]);
spawn('xterm', ['-T', 'Frontend Dev Server', '-e',
`cd "${escapedFrontendDir}" && ${escapedFrontendCommand}; exec bash`]);
}
}
}
}
return true;
}
// Check if directory exists
function checkDirectoryExists(dir, name) {
if (!fs.existsSync(dir)) {
console.error(chalk.red(`❌ Error: ${name} directory not found at ${dir}`));
throw new Error(`Directory not found: ${dir}`);
}
}
// Main execution function
async function runMonorepoWorkflow() {
console.log(chalk.cyan.bold('📦 Monorepo Workflow Script'));
console.log(chalk.cyan('---------------------------'));
try {
// Validate directories
checkDirectoryExists(config.rootDir, 'Root');
checkDirectoryExists(config.backendDir, 'Backend');
checkDirectoryExists(config.frontendDir, 'Frontend');
// Ensure all required ports are available
await ensurePortAvailable(config.ports.backend);
await ensurePortAvailable(config.ports.frontend);
await ensurePortAvailable(config.ports.alternativeFrontend);
// Step 1: Install dependencies at root
await runCommand('yarn', [], config.rootDir, 'Root dependency installation');
// Step 2: Build packages (ignoring frontend)
await runCommand('yarn', ['build', '--ignore', 'frontend'], config.rootDir, 'Building packages');
// Step 3: Start services in split terminal
console.log(chalk.blue('🚀 Opening terminal with services...'));
await openTerminals('yarn start', 'yarn dev');
console.log(chalk.green.bold('✅ Development environment started!'));
console.log(chalk.cyan('Backend running at:') + chalk.yellow(` http://localhost:${config.ports.backend}`));
console.log(chalk.cyan('Frontend running at:') + chalk.yellow(` http://localhost:${config.ports.frontend}`));
console.log(chalk.gray('Check the opened terminal windows for detailed logs.'));
// Exit successfully
process.exit(0);
} catch (error) {
console.error(chalk.red.bold('❌ Workflow failed:'));
console.error(chalk.red(error.message));
process.exit(1);
}
}
// Execute the workflow
runMonorepoWorkflow().catch(error => {
console.error(chalk.red.bold('❌ Unhandled error:'));
console.error(error);
process.exit(1);
});