portclean 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/README.md +149 -0
- package/cli.js +332 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# portclean
|
|
2
|
+
|
|
3
|
+
A fast, cross-platform CLI tool to kill processes using specific ports. Supports macOS, Linux, and Windows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g portclean
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
portclean [ports...] [options]
|
|
15
|
+
|
|
16
|
+
Arguments:
|
|
17
|
+
ports Port number(s) to target
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--force, -f Skip confirmation prompt
|
|
21
|
+
--all, -a Kill all processes using each port
|
|
22
|
+
--help, -h Show help message
|
|
23
|
+
--version, -v Show version number
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
### Kill a single port with confirmation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ portclean 3000
|
|
32
|
+
Processes on port 3000:
|
|
33
|
+
1. PID 12345 (node)
|
|
34
|
+
Process 12345 (node) is using port 3000. Kill it? (y/N) y
|
|
35
|
+
✓ Killed process 12345 (node)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Kill multiple ports
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
$ portclean 3000 8080
|
|
42
|
+
Processes on port 3000:
|
|
43
|
+
1. PID 12345 (node)
|
|
44
|
+
Process 12345 (node) is using port 3000. Kill it? (y/N) y
|
|
45
|
+
✓ Killed process 12345 (node)
|
|
46
|
+
|
|
47
|
+
Processes on port 8080:
|
|
48
|
+
1. PID 54321 (python)
|
|
49
|
+
Process 54321 (python) is using port 8080. Kill it? (y/N) y
|
|
50
|
+
✓ Killed process 54321 (python)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Kill without confirmation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ portclean 3000 --force
|
|
57
|
+
Processes on port 3000:
|
|
58
|
+
1. PID 12345 (node)
|
|
59
|
+
✓ Killed process 12345 (node)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Kill all processes using a port
|
|
63
|
+
|
|
64
|
+
When multiple processes are using the same port:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
$ portclean 3000 --all
|
|
68
|
+
Processes on port 3000:
|
|
69
|
+
1. PID 12345 (node)
|
|
70
|
+
2. PID 12346 (node)
|
|
71
|
+
3. PID 12347 (node)
|
|
72
|
+
Kill all 3 process(es) on port 3000? (y/N) y
|
|
73
|
+
✓ Killed process 12345 (node)
|
|
74
|
+
✓ Killed process 12346 (node)
|
|
75
|
+
✓ Killed process 12347 (node)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Kill all processes without confirmation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
$ portclean 3000 8080 --all --force
|
|
82
|
+
Processes on port 3000:
|
|
83
|
+
1. PID 12345 (node)
|
|
84
|
+
2. PID 12346 (node)
|
|
85
|
+
✓ Killed process 12345 (node)
|
|
86
|
+
✓ Killed process 12346 (node)
|
|
87
|
+
|
|
88
|
+
Processes on port 8080:
|
|
89
|
+
1. PID 54321 (python)
|
|
90
|
+
✓ Killed process 54321 (python)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Kill processes on ports without --all (prompts per process)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
$ portclean 3000
|
|
97
|
+
Processes on port 3000:
|
|
98
|
+
1. PID 12345 (node)
|
|
99
|
+
2. PID 12346 (node)
|
|
100
|
+
3. PID 12347 (node)
|
|
101
|
+
Process 12345 (node) is using port 3000. Kill it? (y/N) y
|
|
102
|
+
✓ Killed process 12345 (node)
|
|
103
|
+
Process 12346 (node) is using port 3000. Kill it? (y/N) n
|
|
104
|
+
Process 12347 (node) is using port 3000. Kill it? (y/N) y
|
|
105
|
+
✓ Killed process 12347 (node)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## How it works
|
|
109
|
+
|
|
110
|
+
### macOS/Linux
|
|
111
|
+
|
|
112
|
+
1. **Primary method**: Uses `lsof -i :<port>` to find processes
|
|
113
|
+
2. **Fallback**: If `lsof` is not available, uses `netstat -anp` and parses the output
|
|
114
|
+
3. **Process names**: Extracted from the command output or via `ps` command
|
|
115
|
+
|
|
116
|
+
### Windows
|
|
117
|
+
|
|
118
|
+
1. Uses `netstat -ano` to list all listening ports and their PIDs
|
|
119
|
+
2. Uses `tasklist /FI "PID eq <pid>"` to get the process image name
|
|
120
|
+
3. Sends SIGKILL equivalent via `taskkill /PID <pid> /F`
|
|
121
|
+
|
|
122
|
+
## Exit Codes
|
|
123
|
+
|
|
124
|
+
- `0`: Successfully handled (killed or no process found)
|
|
125
|
+
- `1`: Invalid arguments, unsupported platform, or system errors
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
### Running tests
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npm test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Smoke test
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run smoke
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Requirements
|
|
142
|
+
|
|
143
|
+
- Node.js >= 14
|
|
144
|
+
- macOS, Linux, or Windows
|
|
145
|
+
- No additional system dependencies required
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync, exec } from 'child_process';
|
|
4
|
+
import { kill } from 'process';
|
|
5
|
+
import { stdin, stdout, stderr } from 'process';
|
|
6
|
+
import parseArgs from 'mri';
|
|
7
|
+
import colors from 'picocolors';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
|
|
10
|
+
const VERSION = '1.0.0';
|
|
11
|
+
const PLATFORM = process.platform;
|
|
12
|
+
|
|
13
|
+
// Parse CLI arguments
|
|
14
|
+
const args = parseArgs(process.argv.slice(2), {
|
|
15
|
+
alias: {
|
|
16
|
+
h: 'help',
|
|
17
|
+
v: 'version',
|
|
18
|
+
f: 'force',
|
|
19
|
+
a: 'all',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Handle help flag
|
|
24
|
+
if (args.help) {
|
|
25
|
+
console.log(`
|
|
26
|
+
${colors.bold('portclean')} - Kill processes using specific ports
|
|
27
|
+
|
|
28
|
+
${colors.bold('Usage:')}
|
|
29
|
+
portclean [ports...] [options]
|
|
30
|
+
|
|
31
|
+
${colors.bold('Arguments:')}
|
|
32
|
+
ports Port number(s) to target
|
|
33
|
+
|
|
34
|
+
${colors.bold('Options:')}
|
|
35
|
+
--force, -f Skip confirmation prompt
|
|
36
|
+
--all, -a Kill all processes using each port
|
|
37
|
+
--help, -h Show this help message
|
|
38
|
+
--version, -v Show version number
|
|
39
|
+
|
|
40
|
+
${colors.bold('Examples:')}
|
|
41
|
+
portclean 3000 Kill process on port 3000
|
|
42
|
+
portclean 3000 8080 Kill processes on ports 3000 and 8080
|
|
43
|
+
portclean 3000 --force Kill port 3000 without confirmation
|
|
44
|
+
portclean 3000 --all Kill all processes using port 3000
|
|
45
|
+
portclean 3000 8080 --force --all Kill all processes on both ports without confirmation
|
|
46
|
+
`);
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle version flag
|
|
51
|
+
if (args.version) {
|
|
52
|
+
console.log(`portkill v${VERSION}`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get ports from positional arguments
|
|
57
|
+
const ports = args._;
|
|
58
|
+
|
|
59
|
+
// Validate ports
|
|
60
|
+
if (ports.length === 0) {
|
|
61
|
+
console.error(colors.red('Error: No ports specified'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const validPorts = ports.filter((p) => {
|
|
66
|
+
const port = parseInt(p, 10);
|
|
67
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
68
|
+
console.error(colors.red(`Error: Invalid port ${p}`));
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (validPorts.length === 0) {
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Main execution
|
|
79
|
+
(async () => {
|
|
80
|
+
try {
|
|
81
|
+
for (const port of validPorts) {
|
|
82
|
+
await handlePort(parseInt(port, 10));
|
|
83
|
+
}
|
|
84
|
+
process.exit(0);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(colors.red(`Error: ${error.message}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle killing processes on a specific port
|
|
93
|
+
*/
|
|
94
|
+
async function handlePort(port) {
|
|
95
|
+
try {
|
|
96
|
+
const processes = await getProcessesOnPort(port);
|
|
97
|
+
|
|
98
|
+
if (processes.length === 0) {
|
|
99
|
+
console.log(colors.yellow(`No process found on port ${port}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(colors.cyan(`\nProcesses on port ${port}:`));
|
|
104
|
+
processes.forEach((proc, idx) => {
|
|
105
|
+
console.log(` ${idx + 1}. PID ${proc.pid} (${proc.command})`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (args.force || args.all) {
|
|
109
|
+
// If --force or --all, show single confirmation per port (or none with --force)
|
|
110
|
+
if (args.force) {
|
|
111
|
+
// Kill without confirmation
|
|
112
|
+
for (const proc of processes) {
|
|
113
|
+
await killProcess(proc.pid, proc.command);
|
|
114
|
+
}
|
|
115
|
+
} else if (args.all) {
|
|
116
|
+
// --all without --force: single confirmation
|
|
117
|
+
const confirmed = await prompt(
|
|
118
|
+
`Kill all ${processes.length} process(es) on port ${port}? (y/N) `
|
|
119
|
+
);
|
|
120
|
+
if (confirmed) {
|
|
121
|
+
for (const proc of processes) {
|
|
122
|
+
await killProcess(proc.pid, proc.command);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Without --all: prompt per process
|
|
128
|
+
for (const proc of processes) {
|
|
129
|
+
const confirmed = await prompt(
|
|
130
|
+
`Process ${proc.pid} (${proc.command}) is using port ${port}. Kill it? (y/N) `
|
|
131
|
+
);
|
|
132
|
+
if (confirmed) {
|
|
133
|
+
await killProcess(proc.pid, proc.command);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(colors.red(`Failed to handle port ${port}: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get processes using a specific port
|
|
144
|
+
*/
|
|
145
|
+
async function getProcessesOnPort(port) {
|
|
146
|
+
if (PLATFORM === 'darwin' || PLATFORM === 'linux') {
|
|
147
|
+
return await getProcessesPosix(port);
|
|
148
|
+
} else if (PLATFORM === 'win32') {
|
|
149
|
+
return await getProcessesWindows(port);
|
|
150
|
+
} else {
|
|
151
|
+
throw new Error(`Unsupported platform: ${PLATFORM}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get processes on macOS/Linux using lsof or netstat
|
|
157
|
+
*/
|
|
158
|
+
async function getProcessesPosix(port) {
|
|
159
|
+
try {
|
|
160
|
+
// Try lsof first
|
|
161
|
+
try {
|
|
162
|
+
const output = execSync(`lsof -i :${port} -n -P`, { encoding: 'utf8' });
|
|
163
|
+
return parseLsofOutput(output);
|
|
164
|
+
} catch {
|
|
165
|
+
// Fallback to netstat
|
|
166
|
+
const output = execSync('netstat -anp', { encoding: 'utf8' });
|
|
167
|
+
return parseNetstatOutput(output, port);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(colors.red(`Failed to get processes on port ${port}: ${error.message}`));
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Parse lsof output
|
|
177
|
+
*/
|
|
178
|
+
function parseLsofOutput(output) {
|
|
179
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
180
|
+
const processes = [];
|
|
181
|
+
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const parts = line.split(/\s+/);
|
|
184
|
+
if (parts.length >= 2) {
|
|
185
|
+
const pid = parseInt(parts[1], 10);
|
|
186
|
+
const command = parts[0];
|
|
187
|
+
|
|
188
|
+
if (!isNaN(pid) && pid > 0) {
|
|
189
|
+
// Check if already added
|
|
190
|
+
if (!processes.find((p) => p.pid === pid)) {
|
|
191
|
+
processes.push({ pid, command });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return processes;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parse netstat output for a specific port
|
|
202
|
+
*/
|
|
203
|
+
function parseNetstatOutput(output, port) {
|
|
204
|
+
const lines = output.trim().split('\n').slice(1); // Skip header
|
|
205
|
+
const processes = [];
|
|
206
|
+
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const parts = line.split(/\s+/).filter((p) => p);
|
|
209
|
+
if (parts.length >= 7) {
|
|
210
|
+
const state = parts[5];
|
|
211
|
+
const pidField = parts[6];
|
|
212
|
+
const match = pidField.match(/^(\d+)\//);
|
|
213
|
+
|
|
214
|
+
if (match && state === 'LISTEN') {
|
|
215
|
+
const pid = parseInt(match[1], 10);
|
|
216
|
+
const proto = parts[0];
|
|
217
|
+
const addr = parts[3];
|
|
218
|
+
const addrParts = addr.split(':');
|
|
219
|
+
const addrPort = addrParts[addrParts.length - 1];
|
|
220
|
+
|
|
221
|
+
if (parseInt(addrPort, 10) === port && !isNaN(pid) && pid > 0) {
|
|
222
|
+
// Get command name
|
|
223
|
+
let command = 'unknown';
|
|
224
|
+
try {
|
|
225
|
+
const psOutput = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8' });
|
|
226
|
+
command = psOutput.trim();
|
|
227
|
+
} catch {
|
|
228
|
+
// Use default
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!processes.find((p) => p.pid === pid)) {
|
|
232
|
+
processes.push({ pid, command });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return processes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get processes on Windows using netstat and tasklist
|
|
244
|
+
*/
|
|
245
|
+
async function getProcessesWindows(port) {
|
|
246
|
+
try {
|
|
247
|
+
const netstatOutput = execSync('netstat -ano', { encoding: 'utf8' });
|
|
248
|
+
const pids = new Set();
|
|
249
|
+
|
|
250
|
+
const lines = netstatOutput.trim().split('\n').slice(4); // Skip header
|
|
251
|
+
for (const line of lines) {
|
|
252
|
+
const parts = line.trim().split(/\s+/);
|
|
253
|
+
if (parts.length >= 5) {
|
|
254
|
+
const state = parts[3];
|
|
255
|
+
const pidStr = parts[4];
|
|
256
|
+
const localAddr = parts[1];
|
|
257
|
+
const addrParts = localAddr.split(':');
|
|
258
|
+
const addrPort = parseInt(addrParts[addrParts.length - 1], 10);
|
|
259
|
+
|
|
260
|
+
if (state === 'LISTENING' && addrPort === port) {
|
|
261
|
+
const pid = parseInt(pidStr, 10);
|
|
262
|
+
if (!isNaN(pid) && pid > 0) {
|
|
263
|
+
pids.add(pid);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const processes = [];
|
|
270
|
+
for (const pid of pids) {
|
|
271
|
+
let command = 'unknown';
|
|
272
|
+
try {
|
|
273
|
+
const tasklistOutput = execSync(`tasklist /FI "PID eq ${pid}"`, {
|
|
274
|
+
encoding: 'utf8',
|
|
275
|
+
});
|
|
276
|
+
const taskLines = tasklistOutput.trim().split('\n');
|
|
277
|
+
if (taskLines.length > 1) {
|
|
278
|
+
const taskLine = taskLines[1].split(/\s+/)[0];
|
|
279
|
+
command = taskLine;
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// Use default
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
processes.push({ pid, command });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return processes;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error(colors.red(`Failed to get processes on port ${port}: ${error.message}`));
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Kill a process
|
|
297
|
+
*/
|
|
298
|
+
async function killProcess(pid, command) {
|
|
299
|
+
try {
|
|
300
|
+
if (PLATFORM === 'win32') {
|
|
301
|
+
execSync(`taskkill /PID ${pid} /F`, { encoding: 'utf8' });
|
|
302
|
+
} else {
|
|
303
|
+
// Try using process.kill first
|
|
304
|
+
try {
|
|
305
|
+
kill(pid, 'SIGKILL');
|
|
306
|
+
} catch {
|
|
307
|
+
// Fallback to shell command
|
|
308
|
+
execSync(`kill -9 ${pid}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log(colors.green(`✓ Killed process ${pid} (${command})`));
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error(colors.red(`✗ Failed to kill process ${pid}: ${error.message}`));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Prompt user for confirmation
|
|
319
|
+
*/
|
|
320
|
+
function prompt(question) {
|
|
321
|
+
return new Promise((resolve) => {
|
|
322
|
+
const rl = readline.createInterface({
|
|
323
|
+
input: stdin,
|
|
324
|
+
output: stdout,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
rl.question(question, (answer) => {
|
|
328
|
+
rl.close();
|
|
329
|
+
resolve(answer.toLowerCase() === 'y');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "portclean",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Kill processes using specific ports on macOS, Linux, and Windows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"portclean": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js",
|
|
12
|
+
"smoke": "node cli.js --help"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"cli",
|
|
16
|
+
"port",
|
|
17
|
+
"kill",
|
|
18
|
+
"process"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"mri": "^1.2.0",
|
|
24
|
+
"picocolors": "^1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {}
|
|
27
|
+
}
|