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.
Files changed (3) hide show
  1. package/README.md +149 -0
  2. package/cli.js +332 -0
  3. 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
+ }