UNPKG

portio

Version:

A beautiful terminal UI for managing processes on network ports (Windows only)

274 lines (269 loc) 10.4 kB
#!/usr/bin/env node import React from 'react'; import { render } from 'ink'; import meow from 'meow'; import chalk from 'chalk'; import { InteractiveUI } from './components/InteractiveUI.js'; import { CheckMode } from './components/CheckMode.js'; import { KillMode } from './components/KillMode.js'; import { getProcessesOnPorts, getProcessOnPort } from './utils/portDetector.js'; /** * Preprocesses command line arguments to make CLI more flexible: * - Normalizes single dashes to double dashes for long flags * - Splits joined short flags (e.g., -kf -> -k -f) * - Preserves existing double-dash behavior */ function preprocessArgs(args) { const processed = []; // Map of long flag names to their short equivalents const longToShort = { 'dev': 'd', 'check': 'c', 'kill': 'k', 'mine': 'm', 'force': 'f', 'list': 'l', 'help': 'h', 'version': 'v' }; // Map of short flags to long names const shortToLong = { 'd': 'dev', 'c': 'check', 'k': 'kill', 'm': 'mine', 'f': 'force', 'l': 'list', 'h': 'help', 'v': 'version' }; for (let i = 0; i < args.length; i++) { const arg = args[i]; // Skip undefined arguments if (!arg) { continue; } // Handle arguments that start with a single dash if (arg.startsWith('-') && !arg.startsWith('--')) { const flagPart = arg.slice(1); // Remove the leading dash // Check if it's a single long flag with single dash (e.g., -kill) if (longToShort[flagPart]) { processed.push(`--${flagPart}`); } // Check if it's a joined short flags scenario (e.g., -kf, -dk) else if (flagPart.length > 1) { const flags = flagPart.split(''); let allValidShortFlags = true; // Check if all characters are valid short flags for (const flag of flags) { if (!shortToLong[flag]) { allValidShortFlags = false; break; } } if (allValidShortFlags) { // Split into individual short flags for (const flag of flags) { processed.push(`-${flag}`); } } else { // Not all are valid short flags, treat as single argument processed.push(arg); } } // Single character short flag (e.g., -k, -d) else { processed.push(arg); } } // Handle arguments that already start with double dash or no dash else { processed.push(arg); } } return processed; } // Preprocess arguments to support flexible CLI parsing const processedArgs = preprocessArgs(process.argv.slice(2)); const cli = meow(` ${chalk.cyan.bold('PORTIO')} - THE port pal you've been waiting for ${chalk.bold('Usage')} $ portio ${chalk.dim('# Interactive mode showing all ports (default)')} $ portio <filter> ${chalk.dim('# Interactive mode with initial filter (e.g., "node", "3000", "chrome")')} $ portio --dev ${chalk.dim('# Show only development ports')} $ portio --check <port> ${chalk.dim('# Check what\'s running on a specific port')} $ portio --wtf <port> ${chalk.dim('# Same as --check (for the frustrated)')} $ portio --kill <port> ${chalk.dim('# Kill process on a specific port')} $ portio --mine <port> ${chalk.dim('# Force kill process on port (no confirmation)')} $ portio --list [port] ${chalk.dim('# JSON output of processes')} ${chalk.bold('Options')} --dev, -d Show only development ports (3000-10000, etc) --check, -c Check what's running on a specific port --wtf Alias for --check (when you're frustrated) --kill, -k Kill process on a specific port --mine, -m Kill process on port without confirmation --force, -f Skip confirmation when killing --list, -l Output process list as JSON --help Show this help message --version Show version ${chalk.bold('Interactive Controls')} ↑/↓ Navigate through processes Enter Kill selected process / Filter processes r Refresh list d Toggle dev/all ports v Toggle verbose mode p Toggle full paths/filenames c Clear filter q Quit A Admin kill (when normal kill fails) ${chalk.bold('Examples')} $ portio -m 3000 && npm run dev ${chalk.dim('# Ensure port 3000 is free before starting dev server')} $ portio --mine 8080 && python -m http.server 8080 ${chalk.dim('# Chain with any server')} $ portio ${chalk.dim('# Show interactive UI with all ports')} $ portio node ${chalk.dim('# Show only processes containing "node"')} $ portio 3000 ${chalk.dim('# Show processes on port 3000')} $ portio chrome ${chalk.dim('# Show chrome processes')} $ portio -dev ${chalk.dim('# Show only dev ports (flexible syntax)')} $ portio -check 3000 ${chalk.dim('# See what\'s on port 3000 (single dash)')} $ portio -kf 3000 ${chalk.dim('# Kill with force (joined flags)')} $ portio -dk ${chalk.dim('# Dev mode + interactive kill (joined flags)')} $ portio --list ${chalk.dim('# Get JSON of all processes')} `, { importMeta: import.meta, argv: processedArgs, flags: { dev: { type: 'boolean', shortFlag: 'd', description: 'Show only development ports' }, check: { type: 'string', shortFlag: 'c' }, wtf: { type: 'string', description: 'Alias for --check (for the frustrated)' }, kill: { type: 'string', shortFlag: 'k' }, mine: { type: 'string', shortFlag: 'm', description: 'Kill process on port without confirmation' }, force: { type: 'boolean', shortFlag: 'f' }, list: { type: 'string', shortFlag: 'l', isMultiple: false }, help: { type: 'boolean' }, version: { type: 'boolean' } } }); async function main() { const { flags } = cli; if (flags.help) { cli.showHelp(); return; } if (flags.version) { cli.showVersion(); return; } if (flags.list !== undefined) { const port = flags.list ? parseInt(flags.list, 10) : undefined; if (port && isNaN(port)) { console.error(chalk.red('Error: Invalid port number')); process.exit(1); } try { if (port) { const process = await getProcessOnPort(port); console.log(JSON.stringify(process, null, 2)); } else { const processes = await getProcessesOnPorts(!flags.dev); // Show all by default, dev only if flag set console.log(JSON.stringify(processes, null, 2)); } } catch (error) { console.error(chalk.red('Error getting process information:'), error); process.exit(1); } return; } // Handle both --check and --wtf const checkPort = flags.check || flags.wtf; if (checkPort) { const port = parseInt(checkPort, 10); if (isNaN(port)) { console.error(chalk.red('Error: Invalid port number')); process.exit(1); } render(React.createElement(CheckMode, { port: port })); return; } if (flags.mine) { const port = parseInt(flags.mine, 10); if (isNaN(port)) { console.error(chalk.red('Error: Invalid port number')); process.exit(1); } render(React.createElement(KillMode, { port: port, force: true })); return; } if (flags.kill) { const port = parseInt(flags.kill, 10); if (isNaN(port)) { console.error(chalk.red('Error: Invalid port number')); process.exit(1); } render(React.createElement(KillMode, { port: port, force: flags.force })); return; } // Check if we can use interactive mode (raw mode support) if (!process.stdin.isTTY || !process.stdin.setRawMode) { console.error(chalk.yellow('⚠️ Interactive mode requires a TTY terminal with raw mode support.')); console.error(chalk.dim('Try using Windows Terminal, Git Bash, or PowerShell instead of CMD.')); console.error(''); console.error(chalk.bold('Alternative options:')); console.error(' portio --list # Get JSON output of all processes'); console.error(' portio --check <port> # Check what\'s on a specific port'); console.error(' portio --kill <port> # Kill process on a specific port'); process.exit(1); } // Check for non-flag arguments to use as initial filter const initialFilter = cli.input.length > 0 ? cli.input.join(' ') : ''; // Don't use alternate screen buffer to prevent terminal issues on Windows // This keeps the output visible after exit const app = render(React.createElement(InteractiveUI, { initialShowAll: !flags.dev, initialFilter: initialFilter }), { exitOnCtrlC: false // We handle this in the component }); // Wait for app to finish app.waitUntilExit().then(() => { // Just clear and unmount without switching screen buffers app.clear(); app.unmount(); // Add a small delay to ensure clean exit setTimeout(() => { process.exit(0); }, 50); }); } main().catch(error => { console.error(chalk.red('Fatal error:'), error); process.exit(1); });