killportall 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ SDODS License (Simple Do Or Don't Software License)
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ 1. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ 2. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10
+
11
+ 3. This license explicitly allows you to:
12
+ - Use the software for any purpose
13
+ - Modify the software
14
+ - Distribute the software
15
+ - Sublicense the software
16
+ - Sell the software
17
+
18
+ 4. This license explicitly prohibits you from:
19
+ - Holding the authors liable for damages
20
+ - Using the authors' names, trademarks, or logos without permission
21
+ - Claiming original authorship of the software
22
+
23
+ If you do not agree to these terms, you must not use, modify, or distribute the software.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # killportall
2
+
3
+ A powerful cross-platform Node.js CLI tool for efficiently killing processes on specified ports with detailed logging and interactive mode.
4
+
5
+ ## Features
6
+
7
+ - 🎯 Kill processes on single or multiple ports
8
+ - 📊 Interactive mode to select ports from a list
9
+ - 🔄 Configurable retry mechanism
10
+ - 📝 Detailed process information display
11
+ - ⚙️ Configuration file support
12
+ - 🖥️ Cross-platform support (Windows, macOS, Linux)
13
+ - 🎨 JSON output format for scripting
14
+ - 🚀 Fast and efficient port killing
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install -g killportall
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Basic Usage
25
+
26
+ Kill a single port:
27
+ ```bash
28
+ killportall 3000
29
+ ```
30
+
31
+ Kill multiple ports:
32
+ ```bash
33
+ killportall 3000 8080 5000
34
+ ```
35
+
36
+ Kill a range of ports:
37
+ ```bash
38
+ killportall 3000-3005
39
+ ```
40
+
41
+ ### Interactive Mode
42
+
43
+ Run in interactive mode to select ports from a list:
44
+ ```bash
45
+ killportall -i
46
+ # or
47
+ killportall --interactive
48
+ ```
49
+
50
+ ### Options
51
+
52
+ - `-r, --retries <number>`: Number of retry attempts (default: 3)
53
+ - `-t, --timeout <ms>`: Timeout between retries in milliseconds (default: 1000)
54
+ - `-i, --interactive`: Run in interactive mode
55
+ - `-j, --json`: Output results in JSON format
56
+ - `--config <key=value>`: Set a configuration value
57
+ - `-v, --version`: Display version number
58
+ - `-h, --help`: Display help information
59
+
60
+ ### Configuration
61
+
62
+ The tool supports configuration through a `.killportallrc.json` file in either the current directory or your home directory. Available configuration options:
63
+
64
+ ```json
65
+ {
66
+ "retries": 3,
67
+ "timeout": 1000,
68
+ "interactive": false,
69
+ "outputFormat": "text"
70
+ }
71
+ ```
72
+
73
+ Set configuration values using the CLI:
74
+ ```bash
75
+ killportall --config retries=5
76
+ killportall --config timeout=2000
77
+ killportall --config interactive=true
78
+ killportall --config outputFormat=json
79
+ ```
80
+
81
+ ## Process Information
82
+
83
+ Before killing a process, the tool displays detailed information:
84
+ - Process ID (PID)
85
+ - Process name/command
86
+ - User (on Unix-like systems)
87
+ - CPU usage
88
+ - Memory usage
89
+
90
+ ## Output Formats
91
+
92
+ ### Text Output (Default)
93
+ ```
94
+ ✓ Port 3000: Process killed successfully
95
+ ✗ Port 8080: No process found
96
+ ```
97
+
98
+ ### JSON Output
99
+ ```json
100
+ [
101
+ {
102
+ "port": 3000,
103
+ "success": true
104
+ },
105
+ {
106
+ "port": 8080,
107
+ "success": false,
108
+ "error": "No process found"
109
+ }
110
+ ]
111
+ ```
112
+
113
+ ## Error Handling
114
+
115
+ The tool includes robust error handling:
116
+ - Invalid port numbers
117
+ - Permission issues
118
+ - Process not found
119
+ - Failed kill attempts
120
+
121
+ ## Debug Mode
122
+
123
+ Enable debug logging:
124
+ ```bash
125
+ DEBUG=killportall:* killportall 3000
126
+ ```
127
+
128
+ ## Contributing
129
+
130
+ Contributions are welcome! Please feel free to submit a Pull Request.
131
+
132
+ ## Publication Steps
133
+
134
+ ### For Maintainers
135
+
136
+ 1. Prepare for publication:
137
+ ```bash
138
+ # Update version
139
+ npm version patch # or minor/major for bigger changes
140
+
141
+ # Run tests
142
+ npm test
143
+
144
+ # Login to npm (if not already logged in)
145
+ npm login
146
+ ```
147
+
148
+ 2. Test package locally:
149
+ ```bash
150
+ # Link package locally
151
+ npm link
152
+
153
+ # Test CLI
154
+ killportall --version
155
+ killportall --help
156
+
157
+ # Run some test commands
158
+ killportall 3000
159
+ killportall -i
160
+ ```
161
+
162
+ 3. Publish to npm:
163
+ ```bash
164
+ # Publish main version
165
+ npm publish
166
+
167
+ # For beta releases
168
+ npm publish --tag beta
169
+ ```
170
+
171
+ 4. Create GitHub Release:
172
+ ```bash
173
+ # Create and push git tag
174
+ git tag v1.0.0
175
+ git push origin v1.0.0
176
+
177
+ # Create release on GitHub with release notes
178
+ ```
179
+
180
+ 5. Post-publication verification:
181
+ ```bash
182
+ # Unlink local version
183
+ npm unlink killportall
184
+
185
+ # Install from npm
186
+ npm install -g killportall
187
+
188
+ # Verify installation
189
+ killportall --version
190
+ ```
191
+
192
+ ### Version Update Guidelines
193
+
194
+ - MAJOR version (1.x.x): Breaking changes
195
+ - MINOR version (x.1.x): New features, backward compatible
196
+ - PATCH version (x.x.1): Bug fixes, backward compatible
197
+
198
+ ## License
199
+
200
+ This project is licensed under the SDODS License - see the [LICENSE](LICENSE) file for details.
201
+
202
+ ## Badges
203
+
204
+ [![npm version](https://badge.fury.io/js/killportall.svg)](https://badge.fury.io/js/killportall)
205
+ [![License: SDODS](https://img.shields.io/badge/License-SDODS-blue.svg)](LICENSE)
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import debug from 'debug';
6
+ import { killPorts } from '../src/portKiller.js';
7
+ import { validatePorts } from '../src/utils.js';
8
+ import { runInteractiveMode } from '../src/interactive.js';
9
+ import { fileURLToPath } from 'url';
10
+ import { dirname, join } from 'path';
11
+ import { readFileSync } from 'fs';
12
+ import { loadConfig, saveConfig } from '../src/config.js';
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const packageJson = JSON.parse(readFileSync(join(__dirname, '..', 'package.json')));
16
+ const debugLog = debug('killportall:cli');
17
+
18
+ const program = new Command();
19
+ const config = loadConfig();
20
+
21
+ program
22
+ .version(packageJson.version)
23
+ .description('Kill processes running on specified ports')
24
+ .argument('[ports...]', 'Port numbers to kill (can be single port, range, or multiple ports)')
25
+ .option('-r, --retries <number>', 'Number of retry attempts', String(config.retries))
26
+ .option('-t, --timeout <ms>', 'Timeout between retries in milliseconds', String(config.timeout))
27
+ .option('-i, --interactive', 'Run in interactive mode to select ports from a list', config.interactive)
28
+ .option('-j, --json', 'Output results in JSON format')
29
+ .option('--config <key=value>', 'Set a configuration value (retries, timeout, interactive, outputFormat)')
30
+ .action(async (ports, options) => {
31
+ try {
32
+ // Handle config setting
33
+ if (options.config) {
34
+ const [key, value] = options.config.split('=');
35
+ if (!key || !value) {
36
+ console.error(chalk.red('Error: Invalid config format. Use key=value'));
37
+ process.exit(1);
38
+ }
39
+ const newConfig = {};
40
+ switch (key) {
41
+ case 'retries':
42
+ newConfig.retries = parseInt(value);
43
+ break;
44
+ case 'timeout':
45
+ newConfig.timeout = parseInt(value);
46
+ break;
47
+ case 'interactive':
48
+ newConfig.interactive = value === 'true';
49
+ break;
50
+ case 'outputFormat':
51
+ if (!['text', 'json'].includes(value)) {
52
+ console.error(chalk.red('Error: outputFormat must be either "text" or "json"'));
53
+ process.exit(1);
54
+ }
55
+ newConfig.outputFormat = value;
56
+ break;
57
+ default:
58
+ console.error(chalk.red(`Error: Unknown config key "${key}"`));
59
+ process.exit(1);
60
+ }
61
+ if (saveConfig({ ...config, ...newConfig })) {
62
+ console.log(chalk.green('Configuration saved successfully'));
63
+ process.exit(0);
64
+ } else {
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ debugLog('Starting port killing process with arguments:', { ports, options });
70
+
71
+ const outputFormat = options.json ? 'json' : config.outputFormat;
72
+ let results;
73
+
74
+ if (options.interactive || config.interactive || ports.length === 0) {
75
+ results = await runInteractiveMode({
76
+ retries: parseInt(options.retries),
77
+ timeout: parseInt(options.timeout)
78
+ });
79
+ } else {
80
+ const validatedPorts = validatePorts(ports);
81
+ if (!validatedPorts.length) {
82
+ console.error(chalk.red('Error: No valid ports specified'));
83
+ process.exit(1);
84
+ }
85
+
86
+ results = await killPorts(validatedPorts, {
87
+ retries: parseInt(options.retries),
88
+ timeout: parseInt(options.timeout)
89
+ });
90
+ }
91
+
92
+ if (outputFormat === 'json') {
93
+ console.log(JSON.stringify(results, null, 2));
94
+ } else {
95
+ results.forEach(result => {
96
+ if (result.success) {
97
+ console.log(chalk.green(`✓ Port ${result.port}: Process killed successfully`));
98
+ } else {
99
+ console.error(chalk.red(`✗ Port ${result.port}: ${result.error}`));
100
+ }
101
+ });
102
+ }
103
+
104
+ } catch (error) {
105
+ console.error(chalk.red('Error:', error.message));
106
+ debugLog('Error details:', error);
107
+ process.exit(1);
108
+ }
109
+ });
110
+
111
+ program.parse(process.argv);
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "killportall",
3
+ "version": "1.0.0",
4
+ "description": "A cross-platform Node.js CLI tool for efficiently killing processes on specified ports",
5
+ "type": "module",
6
+ "main": "bin/killportall.js",
7
+ "bin": {
8
+ "killportall": "./bin/killportall.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=14.0.0"
17
+ },
18
+ "directories": {
19
+ "test": "tests"
20
+ },
21
+ "scripts": {
22
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache",
23
+ "start": "node bin/killportall.js",
24
+ "prepublishOnly": "npm test",
25
+ "postinstall": "chmod +x bin/killportall.js"
26
+ },
27
+ "keywords": [],
28
+ "author": "contact@yarlis.com>",
29
+ "license": "SDODS",
30
+ "bugs": {
31
+ "url": "https://github.com/yarlis/killportall/issues"
32
+ },
33
+ "homepage": "https://github.com/yarlis/killportall#readme",
34
+ "dependencies": {
35
+ "chalk": "^5.3.0",
36
+ "commander": "^12.1.0",
37
+ "cross-spawn": "^7.0.3",
38
+ "debug": "^4.3.7",
39
+ "inquirer": "^12.0.1",
40
+ "jest": "^29.7.0"
41
+ }
42
+ }
package/src/config.js ADDED
@@ -0,0 +1,69 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { homedir } from 'os';
5
+ import { log } from './logger.js';
6
+
7
+ const CONFIG_FILE_NAME = '.killportallrc.json';
8
+ const DEFAULT_CONFIG = {
9
+ retries: 3,
10
+ timeout: 1000,
11
+ interactive: false,
12
+ outputFormat: 'text'
13
+ };
14
+
15
+ function getConfigPath() {
16
+ // First try current directory
17
+ const currentDirConfig = join(process.cwd(), CONFIG_FILE_NAME);
18
+ try {
19
+ readFileSync(currentDirConfig);
20
+ return currentDirConfig;
21
+ } catch (error) {
22
+ // If not found in current directory, use home directory
23
+ return join(homedir(), CONFIG_FILE_NAME);
24
+ }
25
+ }
26
+
27
+ function loadConfig() {
28
+ try {
29
+ const configPath = getConfigPath();
30
+ const configContent = readFileSync(configPath, 'utf8');
31
+ const config = JSON.parse(configContent);
32
+
33
+ // Validate and merge with defaults
34
+ return {
35
+ ...DEFAULT_CONFIG,
36
+ ...config,
37
+ retries: Number(config.retries) || DEFAULT_CONFIG.retries,
38
+ timeout: Number(config.timeout) || DEFAULT_CONFIG.timeout,
39
+ interactive: Boolean(config.interactive),
40
+ outputFormat: ['text', 'json'].includes(config.outputFormat)
41
+ ? config.outputFormat
42
+ : DEFAULT_CONFIG.outputFormat
43
+ };
44
+ } catch (error) {
45
+ // If config file doesn't exist or is invalid, return defaults
46
+ return DEFAULT_CONFIG;
47
+ }
48
+ }
49
+
50
+ function saveConfig(config) {
51
+ try {
52
+ const configPath = getConfigPath();
53
+ const mergedConfig = {
54
+ ...DEFAULT_CONFIG,
55
+ ...config
56
+ };
57
+ writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
58
+ return true;
59
+ } catch (error) {
60
+ log.error(`Failed to save config: ${error.message}`);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ export {
66
+ loadConfig,
67
+ saveConfig,
68
+ DEFAULT_CONFIG
69
+ };
@@ -0,0 +1,32 @@
1
+ export const COMMANDS = {
2
+ win32: {
3
+ find: {
4
+ command: 'netstat',
5
+ args: ['-ano', '|', 'findstr', ':']
6
+ },
7
+ kill: {
8
+ command: 'taskkill',
9
+ args: ['/F', '/PID']
10
+ }
11
+ },
12
+ darwin: {
13
+ find: {
14
+ command: 'lsof',
15
+ args: ['-i', ':']
16
+ },
17
+ kill: {
18
+ command: 'kill',
19
+ args: ['-9']
20
+ }
21
+ },
22
+ linux: {
23
+ find: {
24
+ command: 'lsof',
25
+ args: ['-i', ':']
26
+ },
27
+ kill: {
28
+ command: 'kill',
29
+ args: ['-9']
30
+ }
31
+ }
32
+ };
@@ -0,0 +1,126 @@
1
+ import inquirer from 'inquirer';
2
+ import { log } from './logger.js';
3
+ import { COMMANDS } from './constants.js';
4
+ import crossSpawn from 'cross-spawn';
5
+ import { killPorts, findProcessId, getProcessInfo } from './portKiller.js';
6
+ import { isValidPort } from './utils.js';
7
+
8
+ async function getAllActivePorts() {
9
+ return new Promise((resolve, reject) => {
10
+ const platform = process.platform;
11
+ let command, args;
12
+
13
+ if (platform === 'win32') {
14
+ command = 'netstat';
15
+ args = ['-ano'];
16
+ } else {
17
+ command = 'lsof';
18
+ args = ['-i', '-n', '-P']; // Removed -t to get full output
19
+ }
20
+
21
+ const child = crossSpawn(command, args);
22
+ let output = '';
23
+
24
+ child.stdout.on('data', (data) => {
25
+ output += data.toString();
26
+ });
27
+
28
+ child.on('close', (code) => {
29
+ if (code !== 0) {
30
+ log.warn(`Command failed with code ${code}`);
31
+ return resolve([]);
32
+ }
33
+
34
+ const ports = new Set();
35
+ const lines = output.split('\n');
36
+
37
+ lines.forEach(line => {
38
+ const port = extractPortFromLine(line, platform);
39
+ if (port && isValidPort(port)) {
40
+ ports.add(port);
41
+ }
42
+ });
43
+
44
+ resolve(Array.from(ports).sort((a, b) => a - b));
45
+ });
46
+
47
+ child.on('error', (err) => {
48
+ reject(new Error(`Failed to list active ports: ${err.message}`));
49
+ });
50
+ });
51
+ }
52
+
53
+ function extractPortFromLine(line, platform) {
54
+ if (platform === 'win32') {
55
+ const match = line.match(/:(\d+)\s/);
56
+ return match ? parseInt(match[1]) : null;
57
+ } else {
58
+ // Enhanced regex for Unix-like systems to match both IPv4 and IPv6
59
+ const matches = line.match(/[.:]*:(\d+)\s+\(LISTEN\)/);
60
+ return matches ? parseInt(matches[1]) : null;
61
+ }
62
+ }
63
+
64
+ async function getPortWithProcessInfo(port) {
65
+ const pid = await findProcessId(port);
66
+ if (!pid) return { port, description: 'Unknown process' };
67
+
68
+ const processInfo = await getProcessInfo(pid, process.platform);
69
+ if (!processInfo) return { port, description: `PID: ${pid}` };
70
+
71
+ const description = processInfo.user
72
+ ? `${processInfo.name || processInfo.command} (PID: ${pid}, User: ${processInfo.user})`
73
+ : `${processInfo.name || processInfo.command} (PID: ${pid})`;
74
+
75
+ return { port, description };
76
+ }
77
+
78
+ async function runInteractiveMode(options) {
79
+ try {
80
+ log.info('Scanning for active ports...');
81
+ const activePorts = await getAllActivePorts();
82
+
83
+ if (activePorts.length === 0) {
84
+ log.info('No active ports found');
85
+ return [];
86
+ }
87
+
88
+ // Get process information for all ports
89
+ const portsWithInfo = await Promise.all(
90
+ activePorts.map(port => getPortWithProcessInfo(port))
91
+ );
92
+
93
+ const choices = portsWithInfo.map(({ port, description }) => ({
94
+ name: `Port ${port} - ${description}`,
95
+ value: port
96
+ }));
97
+
98
+ const { selectedPorts } = await inquirer.prompt([
99
+ {
100
+ type: 'checkbox',
101
+ name: 'selectedPorts',
102
+ message: 'Select ports to kill (use space to select, enter to confirm):',
103
+ choices,
104
+ pageSize: 15,
105
+ validate: (answer) => {
106
+ if (answer.length === 0) {
107
+ return 'You must select at least one port';
108
+ }
109
+ return true;
110
+ }
111
+ }
112
+ ]);
113
+
114
+ if (selectedPorts.length === 0) {
115
+ log.warn('No ports selected');
116
+ return [];
117
+ }
118
+
119
+ return await killPorts(selectedPorts, options);
120
+ } catch (error) {
121
+ log.error(`Interactive mode failed: ${error.message}`);
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ export { runInteractiveMode };
package/src/logger.js ADDED
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk';
2
+ import debug from 'debug';
3
+
4
+ const debugLog = debug('killportall:logger');
5
+
6
+ class Logger {
7
+ info(message) {
8
+ console.log(chalk.blue('ℹ'), message);
9
+ debugLog('INFO:', message);
10
+ }
11
+
12
+ success(message) {
13
+ console.log(chalk.green('✓'), message);
14
+ debugLog('SUCCESS:', message);
15
+ }
16
+
17
+ warn(message) {
18
+ console.log(chalk.yellow('⚠'), message);
19
+ debugLog('WARN:', message);
20
+ }
21
+
22
+ error(message) {
23
+ console.error(chalk.red('✗'), message);
24
+ debugLog('ERROR:', message);
25
+ }
26
+ }
27
+
28
+ export const log = new Logger();
@@ -0,0 +1,197 @@
1
+ import crossSpawn from 'cross-spawn';
2
+ import debug from 'debug';
3
+ import { sleep } from './utils.js';
4
+ import { COMMANDS } from './constants.js';
5
+ import { log } from './logger.js';
6
+
7
+ const debugLog = debug('killportall:killer');
8
+
9
+ async function getProcessInfo(pid, platform) {
10
+ if (!pid) return null;
11
+
12
+ let command, args;
13
+ if (platform === 'win32') {
14
+ command = 'tasklist';
15
+ args = ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'];
16
+ } else {
17
+ command = 'ps';
18
+ args = ['-p', pid, '-o', 'pid,ppid,user,%cpu,%mem,command'];
19
+ }
20
+
21
+ return new Promise((resolve) => {
22
+ const child = crossSpawn(command, args);
23
+ let output = '';
24
+
25
+ child.stdout.on('data', (data) => {
26
+ output += data.toString();
27
+ });
28
+
29
+ child.on('close', (code) => {
30
+ if (code !== 0) return resolve(null);
31
+
32
+ const info = parseProcessInfo(output, platform);
33
+ debugLog(`Process info for PID ${pid}:`, info);
34
+ return resolve(info);
35
+ });
36
+
37
+ child.on('error', () => resolve(null));
38
+ });
39
+ }
40
+
41
+ function parseProcessInfo(output, platform) {
42
+ if (!output.trim()) return null;
43
+
44
+ if (platform === 'win32') {
45
+ // Windows CSV format: "Image Name","PID","Session Name","Session#","Mem Usage"
46
+ const [imageName, pid, , , memUsage] = output.trim()
47
+ .split(',')
48
+ .map(field => field.replace(/^"|"$/g, ''));
49
+
50
+ return {
51
+ pid: parseInt(pid),
52
+ name: imageName,
53
+ memory: memUsage,
54
+ command: imageName
55
+ };
56
+ } else {
57
+ // Unix format: PID PPID USER %CPU %MEM COMMAND
58
+ const lines = output.trim().split('\n');
59
+ const processLine = lines.length > 1 ? lines[1] : lines[0]; // Skip header if present
60
+ const [pid, ppid, user, cpu, mem, ...commandParts] = processLine.trim().split(/\s+/);
61
+ return {
62
+ pid: parseInt(pid),
63
+ ppid: parseInt(ppid),
64
+ user,
65
+ cpu: parseFloat(cpu),
66
+ memory: parseFloat(mem),
67
+ command: commandParts.join(' ')
68
+ };
69
+ }
70
+ }
71
+
72
+ async function findProcessId(port) {
73
+ debugLog(`Finding process on port ${port}`);
74
+
75
+ return new Promise((resolve, reject) => {
76
+ let command, args;
77
+ if (process.platform === 'win32') {
78
+ command = 'netstat';
79
+ args = ['-ano', '|', 'findstr', `:${port}`];
80
+ } else {
81
+ command = 'lsof';
82
+ args = ['-i', `:${port}`, '-t']; // -t to only get PID
83
+ }
84
+
85
+ const child = crossSpawn(command, args);
86
+ let output = '';
87
+
88
+ child.stdout.on('data', (data) => {
89
+ output += data.toString();
90
+ });
91
+
92
+ child.on('close', (code) => {
93
+ if (code !== 0) {
94
+ debugLog('Command failed with code:', code);
95
+ return resolve(null);
96
+ }
97
+
98
+ const pid = parsePidFromOutput(output, process.platform, port);
99
+ debugLog(`Found PID ${pid} for port ${port}`);
100
+ resolve(pid);
101
+ });
102
+
103
+ child.on('error', (err) => {
104
+ debugLog('Command error:', err);
105
+ reject(new Error(`Failed to execute find command: ${err.message}`));
106
+ });
107
+ });
108
+ }
109
+
110
+ function parsePidFromOutput(output, platform, port) {
111
+ if (!output.trim()) return null;
112
+
113
+ if (platform === 'win32') {
114
+ // Look for exact port match in Windows netstat output
115
+ const regex = new RegExp(`[:\\s]${port}\\s+.*?\\s+(\\d+)\\s*$`, 'm');
116
+ const match = output.match(regex);
117
+ return match ? match[1] : null;
118
+ } else {
119
+ // For Unix-like systems, lsof -t returns only the PID
120
+ return output.trim().split('\n')[0];
121
+ }
122
+ }
123
+
124
+ async function killProcess(pid) {
125
+ debugLog(`Killing process ${pid}`);
126
+ const { command, args } = COMMANDS[process.platform].kill;
127
+
128
+ return new Promise((resolve, reject) => {
129
+ const child = crossSpawn(command, [...args, pid]);
130
+
131
+ child.on('close', (code) => {
132
+ resolve(code === 0);
133
+ });
134
+
135
+ child.on('error', (err) => {
136
+ reject(new Error(`Failed to kill process: ${err.message}`));
137
+ });
138
+ });
139
+ }
140
+
141
+ async function killPort(port, options = {}) {
142
+ const { retries = 3, timeout = 1000 } = options;
143
+
144
+ for (let attempt = 1; attempt <= retries; attempt++) {
145
+ try {
146
+ const pid = await findProcessId(port);
147
+
148
+ if (!pid) {
149
+ log.info(`No process found on port ${port}`);
150
+ return { port, success: true };
151
+ }
152
+
153
+ // Get and display process information before killing
154
+ const processInfo = await getProcessInfo(pid, process.platform);
155
+ if (processInfo) {
156
+ log.info(`Process found on port ${port}:`);
157
+ log.info(` PID: ${processInfo.pid}`);
158
+ log.info(` Name: ${processInfo.name || processInfo.command}`);
159
+ if (processInfo.user) log.info(` User: ${processInfo.user}`);
160
+ if (processInfo.cpu) log.info(` CPU: ${processInfo.cpu}%`);
161
+ log.info(` Memory: ${processInfo.memory}`);
162
+ }
163
+
164
+ const killed = await killProcess(pid);
165
+ if (killed) {
166
+ log.success(`Successfully killed process ${pid} on port ${port}`);
167
+ return { port, success: true };
168
+ }
169
+
170
+ if (attempt < retries) {
171
+ log.warn(`Failed to kill port ${port}, attempt ${attempt}/${retries}`);
172
+ await sleep(timeout);
173
+ }
174
+ } catch (error) {
175
+ debugLog(`Error killing port ${port}:`, error);
176
+ if (attempt === retries) {
177
+ return { port, success: false, error: error.message };
178
+ }
179
+ await sleep(timeout);
180
+ }
181
+ }
182
+
183
+ return { port, success: false, error: 'Max retries reached' };
184
+ }
185
+
186
+ async function killPorts(ports, options) {
187
+ debugLog(`Killing ports: ${ports.join(', ')}`);
188
+ return Promise.all(ports.map(port => killPort(port, options)));
189
+ }
190
+
191
+ export {
192
+ killPorts,
193
+ killPort,
194
+ findProcessId,
195
+ killProcess,
196
+ getProcessInfo
197
+ };
package/src/utils.js ADDED
@@ -0,0 +1,40 @@
1
+ import debug from 'debug';
2
+
3
+ const debugLog = debug('killportall:utils');
4
+
5
+ function sleep(ms) {
6
+ return new Promise(resolve => setTimeout(resolve, ms));
7
+ }
8
+
9
+ function validatePorts(ports) {
10
+ debugLog('Validating ports:', ports);
11
+ const validPorts = new Set();
12
+
13
+ ports.forEach(port => {
14
+ if (port.includes('-')) {
15
+ // Handle port ranges
16
+ const [start, end] = port.split('-').map(Number);
17
+ if (!isNaN(start) && !isNaN(end) && start <= end) {
18
+ for (let i = start; i <= end; i++) {
19
+ if (isValidPort(i)) validPorts.add(i);
20
+ }
21
+ }
22
+ } else {
23
+ // Handle single ports
24
+ const numPort = parseInt(port);
25
+ if (isValidPort(numPort)) validPorts.add(numPort);
26
+ }
27
+ });
28
+
29
+ return Array.from(validPorts);
30
+ }
31
+
32
+ function isValidPort(port) {
33
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
34
+ }
35
+
36
+ export {
37
+ sleep,
38
+ validatePorts,
39
+ isValidPort
40
+ };