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 +23 -0
- package/README.md +205 -0
- package/bin/killportall.js +111 -0
- package/package.json +42 -0
- package/src/config.js +69 -0
- package/src/constants.js +32 -0
- package/src/interactive.js +126 -0
- package/src/logger.js +28 -0
- package/src/portKiller.js +197 -0
- package/src/utils.js +40 -0
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
|
+
[](https://badge.fury.io/js/killportall)
|
|
205
|
+
[](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
|
+
};
|
package/src/constants.js
ADDED
|
@@ -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
|
+
};
|