UNPKG

@approximate/capable

Version:

The Swiss Army Knife for development servers - auto port resolution, browser opening, process management, and more

474 lines (393 loc) 12.6 kB
#!/usr/bin/env node const { spawn, exec, execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const net = require('net'); const args = process.argv.slice(2); const command = args[0]; // Command to execute const targetType = args[0]; // 'front', 'back', or undefined // Colors for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function error(message) { log(`❌ ${message}`, 'red'); process.exit(1); } function success(message) { log(`✓ ${message}`, 'green'); } function info(message) { log(`ℹ ${message}`, 'cyan'); } function warning(message) { log(`⚠ ${message}`, 'yellow'); } // Open browser to a URL function openBrowser(url) { const platform = process.platform; let cmd; if (platform === 'darwin') { cmd = `open ${url}`; } else if (platform === 'win32') { cmd = `start ${url}`; } else { cmd = `xdg-open ${url}`; } exec(cmd, (err) => { if (err) { warning(`Could not open browser automatically`); } }); } // Kill process on a specific port function killPort(port) { const platform = process.platform; let cmd; if (platform === 'win32') { cmd = `netstat -ano | findstr :${port} | awk '{print $5}' | xargs taskkill /PID /F`; } else { cmd = `lsof -ti:${port} | xargs kill -9 2>/dev/null`; } try { execSync(cmd, { stdio: 'ignore' }); return true; } catch (err) { return false; } } // Get running processes on common ports function getRunningProcesses() { const commonPorts = [3000, 3001, 3002, 4200, 5000, 5173, 8000, 8080, 9000]; const running = []; for (let port of commonPorts) { try { const platform = process.platform; let cmd; if (platform === 'win32') { cmd = `netstat -ano | findstr :${port}`; } else { cmd = `lsof -ti:${port}`; } const result = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }); if (result.trim()) { running.push(port); } } catch (err) { // Port is not in use } } return running; } // Check if dependencies are installed function checkDependencies() { const nodeModulesPath = path.join(process.cwd(), 'node_modules'); const packageLockPath = path.join(process.cwd(), 'package-lock.json'); const yarnLockPath = path.join(process.cwd(), 'yarn.lock'); if (!fs.existsSync(nodeModulesPath)) { return { installed: false, hasLock: fs.existsSync(packageLockPath) || fs.existsSync(yarnLockPath) }; } return { installed: true }; } // Auto-install dependencies function installDependencies() { info('Installing dependencies...'); const yarnLockPath = path.join(process.cwd(), 'yarn.lock'); const cmd = fs.existsSync(yarnLockPath) ? 'yarn install' : 'npm install'; try { execSync(cmd, { stdio: 'inherit' }); success('Dependencies installed successfully'); return true; } catch (err) { error('Failed to install dependencies'); return false; } } // Check if a port is available function isPortAvailable(port) { return new Promise((resolve) => { const server = net.createServer(); server.once('error', (err) => { if (err.code === 'EADDRINUSE') { resolve(false); } else { resolve(false); } }); server.once('listening', () => { server.close(); resolve(true); }); server.listen(port); }); } // Find an available port starting from a given port async function findAvailablePort(startPort) { let port = startPort; const maxAttempts = 100; for (let i = 0; i < maxAttempts; i++) { const available = await isPortAvailable(port); if (available) { return port; } port++; } return null; } // Detect default port from package.json or common patterns function detectDefaultPort(pkg, type) { // Check package.json for port configuration if (pkg.config && pkg.config.port) { return pkg.config.port; } // Check scripts for port hints const scripts = pkg.scripts || {}; const allScripts = Object.values(scripts).join(' '); // Look for PORT= in scripts const portMatch = allScripts.match(/PORT=(\d+)/); if (portMatch) { return parseInt(portMatch[1]); } // Look for --port in scripts const portFlagMatch = allScripts.match(/--port[=\s]+(\d+)/); if (portFlagMatch) { return parseInt(portFlagMatch[1]); } // Check for common framework defaults const dependencies = { ...pkg.dependencies, ...pkg.devDependencies }; if (dependencies['next']) return 3000; if (dependencies['react-scripts']) return 3000; if (dependencies['vite']) return 5173; if (dependencies['@vue/cli-service']) return 8080; if (dependencies['@angular/cli']) return 4200; if (dependencies['express']) return 3000; if (dependencies['fastify']) return 3000; if (dependencies['django']) return 8000; // Default fallback based on type if (type && type.includes('back')) { return 3001; // Backend default } return 3000; // Frontend default } // Check if package.json exists function getPackageJson() { const packagePath = path.join(process.cwd(), 'package.json'); if (!fs.existsSync(packagePath)) { error('No package.json found in current directory'); } try { return JSON.parse(fs.readFileSync(packagePath, 'utf8')); } catch (err) { error('Failed to parse package.json'); } } // Detect project type and return the best command to run function detectCommand(pkg, type) { const scripts = pkg.scripts || {}; // Frontend patterns const frontendPatterns = [ 'dev', 'start:dev', 'develop', 'start', 'serve', 'vite', 'next dev' ]; // Backend patterns const backendPatterns = [ 'server', 'start:server', 'dev:server', 'backend', 'start:backend', 'serve:api', 'api' ]; if (type === 'front' || type === 'frontend') { for (let pattern of frontendPatterns) { if (scripts[pattern]) { return { cmd: 'npm', args: ['run', pattern], type: 'frontend' }; } } error('No frontend development script found'); } if (type === 'back' || type === 'backend') { for (let pattern of backendPatterns) { if (scripts[pattern]) { return { cmd: 'npm', args: ['run', pattern], type: 'backend' }; } } error('No backend development script found'); } // Auto-detect (no type specified) // First check for common framework-specific commands if (scripts['dev']) { return { cmd: 'npm', args: ['run', 'dev'], type: 'auto (dev)' }; } if (scripts['start']) { return { cmd: 'npm', args: ['start'], type: 'auto (start)' }; } if (scripts['serve']) { return { cmd: 'npm', args: ['run', 'serve'], type: 'auto (serve)' }; } if (scripts['develop']) { return { cmd: 'npm', args: ['run', 'develop'], type: 'auto (develop)' }; } // Check for Django if (fs.existsSync('manage.py')) { return { cmd: 'python', args: ['manage.py', 'runserver'], type: 'Django' }; } // Check for other Python frameworks if (pkg.dependencies && (pkg.dependencies['flask'] || pkg.dependencies['fastapi'])) { if (scripts['start']) { return { cmd: 'npm', args: ['start'], type: 'Python backend' }; } } // List available scripts const availableScripts = Object.keys(scripts).join(', '); error(`Could not auto-detect dev command.\n\nAvailable scripts: ${availableScripts}\n\nTry: dev front | dev back`); } // Run the command with port conflict resolution async function runCommand(command, pkg, openBrowserFlag = true) { info(`Starting ${command.type} development server...`); // Detect and handle port conflicts const defaultPort = detectDefaultPort(pkg, command.type); const availablePort = await findAvailablePort(defaultPort); if (!availablePort) { error(`Could not find an available port starting from ${defaultPort}`); } const env = { ...process.env }; if (availablePort !== defaultPort) { warning(`Port ${defaultPort} is in use, using port ${availablePort} instead`); env.PORT = availablePort.toString(); } else { success(`Starting on port ${availablePort}`); env.PORT = availablePort.toString(); } info(`Running: ${command.cmd} ${command.args.join(' ')}`); console.log(''); const child = spawn(command.cmd, command.args, { stdio: 'inherit', cwd: process.cwd(), env: env }); // Auto-open browser after a delay if (openBrowserFlag) { setTimeout(() => { const url = `http://localhost:${availablePort}`; info(`Opening browser at ${url}`); openBrowser(url); }, 2000); } child.on('error', (err) => { error(`Failed to start: ${err.message}`); }); child.on('exit', (code) => { if (code !== 0 && code !== null) { error(`Process exited with code ${code}`); } }); } // Main execution async function main() { log('\n🚀 Capable - The Swiss Army Knife for Dev Servers\n', 'bright'); // Handle help if (args.includes('--help') || args.includes('-h') || command === 'help') { console.log('Usage:'); console.log(' capable - Auto-detect and start dev server'); console.log(' capable front - Start frontend development server'); console.log(' capable back - Start backend development server'); console.log(' capable kill [PORT] - Kill process on port (default: auto-detect)'); console.log(' capable ps - Show running dev servers on common ports'); console.log(' capable install - Install dependencies (npm/yarn)'); console.log('\nFeatures:'); console.log(' • Automatic port conflict resolution'); console.log(' • Smart framework detection'); console.log(' • Auto-open browser'); console.log(' • Dependency management'); console.log(' • No configuration needed'); console.log('\nExamples:'); console.log(' capable # Start dev server, auto-open browser'); console.log(' capable kill 3000 # Kill process on port 3000'); console.log(' capable ps # See what\'s running'); console.log(' capable install # Install dependencies'); process.exit(0); } // Handle 'kill' command if (command === 'kill') { let port = args[1]; if (!port) { // Try to detect port from package.json const pkg = getPackageJson(); port = detectDefaultPort(pkg, null); info(`No port specified, using default: ${port}`); } info(`Killing process on port ${port}...`); const killed = killPort(port); if (killed) { success(`Successfully killed process on port ${port}`); } else { warning(`No process found on port ${port}`); } process.exit(0); } // Handle 'ps' command if (command === 'ps' || command === 'status') { info('Scanning common ports...\n'); const running = getRunningProcesses(); if (running.length === 0) { log('No dev servers detected on common ports', 'yellow'); } else { success(`Found ${running.length} active port(s):\n`); running.forEach(port => { log(` • Port ${port} - http://localhost:${port}`, 'green'); }); } process.exit(0); } // Handle 'install' command if (command === 'install' || command === 'i') { installDependencies(); process.exit(0); } // Default: Start dev server const pkg = getPackageJson(); // Check dependencies before starting const depsStatus = checkDependencies(); if (!depsStatus.installed) { warning('node_modules not found!'); console.log(''); if (depsStatus.hasLock) { const answer = await new Promise((resolve) => { process.stdout.write('Install dependencies now? (y/n): '); process.stdin.once('data', (data) => { resolve(data.toString().trim().toLowerCase()); }); }); if (answer === 'y' || answer === 'yes') { const installed = installDependencies(); if (!installed) { error('Cannot start without dependencies'); } console.log(''); } else { error('Cannot start without dependencies'); } } else { error('No package-lock.json or yarn.lock found. Run npm install or yarn install first.'); } } const devCommand = detectCommand(pkg, targetType); await runCommand(devCommand, pkg); } main();