303 lines
11 KiB
JavaScript
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);
|
|
}); |