@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
JavaScript
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();