portclean 1.0.1 → 1.1.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 +39 -10
  2. package/cli.js +105 -73
  3. package/package.json +10 -2
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g portclean
14
14
  portclean [ports...] [options]
15
15
 
16
16
  Arguments:
17
- ports Port number(s) to target
17
+ ports Port number(s) or ranges to target (e.g. 3000 or 3000-3010)
18
18
 
19
19
  Options:
20
20
  --force, -f Skip confirmation prompt
@@ -38,7 +38,7 @@ Process 12345 (node) is using port 3000. Kill it? (y/N) y
38
38
  ### Kill multiple ports
39
39
 
40
40
  ```bash
41
- $ portclean 3000 8080
41
+ $ portclean 3000 8080 9000
42
42
  Processes on port 3000:
43
43
  1. PID 12345 (node)
44
44
  Process 12345 (node) is using port 3000. Kill it? (y/N) y
@@ -48,6 +48,11 @@ Processes on port 8080:
48
48
  1. PID 54321 (python)
49
49
  Process 54321 (python) is using port 8080. Kill it? (y/N) y
50
50
  ✓ Killed process 54321 (python)
51
+
52
+ Processes on port 9000:
53
+ 1. PID 11111 (go)
54
+ Process 11111 (go) is using port 9000. Kill it? (y/N) y
55
+ ✓ Killed process 11111 (go)
51
56
  ```
52
57
 
53
58
  ### Kill without confirmation
@@ -59,6 +64,21 @@ Processes on port 3000:
59
64
  ✓ Killed process 12345 (node)
60
65
  ```
61
66
 
67
+ ### Kill a port range
68
+
69
+ ```bash
70
+ $ portclean 3000-3010 --force
71
+ Processes on port 3000:
72
+ 1. PID 12345 (node)
73
+ ✓ Killed process 12345 (node)
74
+
75
+ ...
76
+
77
+ Processes on port 3010:
78
+ 1. PID 12399 (go)
79
+ ✓ Killed process 12399 (go)
80
+ ```
81
+
62
82
  ### Kill all processes using a port
63
83
 
64
84
  When multiple processes are using the same port:
@@ -78,7 +98,7 @@ Kill all 3 process(es) on port 3000? (y/N) y
78
98
  ### Kill all processes without confirmation
79
99
 
80
100
  ```bash
81
- $ portclean 3000 8080 --all --force
101
+ $ portclean 3000 8080 9000 --all --force
82
102
  Processes on port 3000:
83
103
  1. PID 12345 (node)
84
104
  2. PID 12346 (node)
@@ -88,20 +108,28 @@ Processes on port 3000:
88
108
  Processes on port 8080:
89
109
  1. PID 54321 (python)
90
110
  ✓ Killed process 54321 (python)
111
+
112
+ Processes on port 9000:
113
+ 1. PID 11111 (go)
114
+ ✓ Killed process 11111 (go)
91
115
  ```
92
116
 
93
117
  ### Kill processes on ports without --all (prompts per process)
94
118
 
95
119
  ```bash
96
- $ portclean 3000
120
+ $ portclean 3000-3002
97
121
  Processes on port 3000:
98
122
  1. PID 12345 (node)
99
- 2. PID 12346 (node)
100
- 3. PID 12347 (node)
101
123
  Process 12345 (node) is using port 3000. Kill it? (y/N) y
102
124
  ✓ 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
125
+
126
+ Processes on port 3001:
127
+ 1. PID 12346 (node)
128
+ Process 12346 (node) is using port 3001. Kill it? (y/N) n
129
+
130
+ Processes on port 3002:
131
+ 1. PID 12347 (node)
132
+ Process 12347 (node) is using port 3002. Kill it? (y/N) y
105
133
  ✓ Killed process 12347 (node)
106
134
  ```
107
135
 
@@ -110,8 +138,9 @@ Process 12347 (node) is using port 3000. Kill it? (y/N) y
110
138
  ### macOS/Linux
111
139
 
112
140
  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
141
+ 2. **Fallback (Linux only)**: If `lsof` is not available, uses `netstat -anp` and parses the output (stderr suppressed)
142
+ 3. **macOS fallback**: macOS `netstat` lacks PID data, so we skip it and return no processes instead of printing errors
143
+ 4. **Process names**: Extracted from the command output or via `ps` command
115
144
 
116
145
  ### Windows
117
146
 
package/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execSync, exec } from 'child_process';
3
+ import { execSync } from 'child_process';
4
4
  import { kill } from 'process';
5
- import { stdin, stdout, stderr } from 'process';
5
+ import { stdin, stdout } from 'process';
6
6
  import parseArgs from 'mri';
7
7
  import colors from 'picocolors';
8
8
  import * as readline from 'readline';
@@ -10,26 +10,63 @@ import * as readline from 'readline';
10
10
  const VERSION = '1.0.0';
11
11
  const PLATFORM = process.platform;
12
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(`
13
+ // Parse ports (single numbers or inclusive ranges like 3000-3010)
14
+ function parsePorts(inputs) {
15
+ const ports = new Set();
16
+ const errors = [];
17
+
18
+ for (const input of inputs) {
19
+ if (typeof input !== 'string') {
20
+ errors.push(`Invalid port input: ${input}`);
21
+ continue;
22
+ }
23
+
24
+ if (input.includes('-')) {
25
+ const [startStr, endStr] = input.split('-', 2);
26
+ const start = parseInt(startStr, 10);
27
+ const end = parseInt(endStr, 10);
28
+
29
+ if (Number.isNaN(start) || Number.isNaN(end) || start < 1 || end > 65535 || start > end) {
30
+ errors.push(`Error: Invalid port range ${input}`);
31
+ continue;
32
+ }
33
+
34
+ for (let p = start; p <= end; p++) {
35
+ ports.add(p);
36
+ }
37
+ continue;
38
+ }
39
+
40
+ const port = parseInt(input, 10);
41
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
42
+ errors.push(`Error: Invalid port ${input}`);
43
+ continue;
44
+ }
45
+ ports.add(port);
46
+ }
47
+
48
+ return { ports: Array.from(ports), errors };
49
+ }
50
+
51
+ async function main() {
52
+ const args = parseArgs(process.argv.slice(2), {
53
+ alias: {
54
+ h: 'help',
55
+ v: 'version',
56
+ f: 'force',
57
+ a: 'all',
58
+ },
59
+ });
60
+
61
+ if (args.help) {
62
+ console.log(`
26
63
  ${colors.bold('portclean')} - Kill processes using specific ports
27
64
 
28
65
  ${colors.bold('Usage:')}
29
66
  portclean [ports...] [options]
30
67
 
31
68
  ${colors.bold('Arguments:')}
32
- ports Port number(s) to target
69
+ ports Port number(s) or ranges to target (e.g. 3000 or 3000-3010)
33
70
 
34
71
  ${colors.bold('Options:')}
35
72
  --force, -f Skip confirmation prompt
@@ -39,59 +76,50 @@ ${colors.bold('Options:')}
39
76
 
40
77
  ${colors.bold('Examples:')}
41
78
  portclean 3000 Kill process on port 3000
42
- portclean 3000 8080 Kill processes on ports 3000 and 8080
79
+ portclean 3000 8080 9000 Kill processes on multiple ports
80
+ portclean 3000-3010 Kill processes on ports 3000 through 3010
43
81
  portclean 3000 --force Kill port 3000 without confirmation
44
82
  portclean 3000 --all Kill all processes using port 3000
45
83
  portclean 3000 8080 --force --all Kill all processes on both ports without confirmation
46
84
  `);
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
- }
85
+ process.exit(0);
86
+ }
55
87
 
56
- // Get ports from positional arguments
57
- const ports = args._;
88
+ if (args.version) {
89
+ console.log(`portkill v${VERSION}`);
90
+ process.exit(0);
91
+ }
58
92
 
59
- // Validate ports
60
- if (ports.length === 0) {
61
- console.error(colors.red('Error: No ports specified'));
62
- process.exit(1);
63
- }
93
+ const rawPorts = args._;
64
94
 
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;
95
+ if (rawPorts.length === 0) {
96
+ console.error(colors.red('Error: No ports specified'));
97
+ process.exit(1);
70
98
  }
71
- return true;
72
- });
73
99
 
74
- if (validPorts.length === 0) {
75
- process.exit(1);
76
- }
100
+ const { ports, errors: portErrors } = parsePorts(rawPorts);
101
+ portErrors.forEach((msg) => console.error(colors.red(msg)));
77
102
 
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}`));
103
+ if (ports.length === 0) {
87
104
  process.exit(1);
88
105
  }
89
- })();
106
+
107
+ for (const port of ports) {
108
+ await handlePort(port, args);
109
+ }
110
+
111
+ process.exit(0);
112
+ }
90
113
 
91
114
  /**
92
115
  * Handle killing processes on a specific port
93
116
  */
94
- async function handlePort(port) {
117
+ async function handlePort(port, args) {
118
+ if (isNaN(port) || port < 1 || port > 65535) {
119
+ console.error(colors.red(`Error: Invalid port ${port}`));
120
+ return;
121
+ }
122
+
95
123
  try {
96
124
  const processes = await getProcessesOnPort(port);
97
125
 
@@ -108,12 +136,10 @@ async function handlePort(port) {
108
136
  if (args.force || args.all) {
109
137
  // If --force or --all, show single confirmation per port (or none with --force)
110
138
  if (args.force) {
111
- // Kill without confirmation
112
139
  for (const proc of processes) {
113
140
  await killProcess(proc.pid, proc.command);
114
141
  }
115
142
  } else if (args.all) {
116
- // --all without --force: single confirmation
117
143
  const confirmed = await prompt(
118
144
  `Kill all ${processes.length} process(es) on port ${port}? (Y/n) `
119
145
  );
@@ -124,7 +150,6 @@ async function handlePort(port) {
124
150
  }
125
151
  }
126
152
  } else {
127
- // Without --all: prompt per process
128
153
  for (const proc of processes) {
129
154
  const confirmed = await prompt(
130
155
  `Process ${proc.pid} (${proc.command}) is using port ${port}. Kill it? (Y/n) `
@@ -156,18 +181,21 @@ async function getProcessesOnPort(port) {
156
181
  * Get processes on macOS/Linux using lsof or netstat
157
182
  */
158
183
  async function getProcessesPosix(port) {
184
+ // Suppress stderr from system commands to avoid leaking raw tool logs
185
+ const execOpts = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
186
+
159
187
  try {
160
- // Try lsof first
161
188
  try {
162
- const output = execSync(`lsof -i :${port} -n -P`, { encoding: 'utf8' });
189
+ const output = execSync(`lsof -i :${port} -n -P`, execOpts);
163
190
  return parseLsofOutput(output);
164
191
  } catch {
165
- // Fallback to netstat
166
- const output = execSync('netstat -anp', { encoding: 'utf8' });
167
- return parseNetstatOutput(output, port);
192
+ if (PLATFORM === 'linux') {
193
+ const output = execSync('netstat -anp', execOpts);
194
+ return parseNetstatOutput(output, port);
195
+ }
196
+ return [];
168
197
  }
169
- } catch (error) {
170
- console.error(colors.red(`Failed to get processes on port ${port}: ${error.message}`));
198
+ } catch {
171
199
  return [];
172
200
  }
173
201
  }
@@ -176,7 +204,7 @@ async function getProcessesPosix(port) {
176
204
  * Parse lsof output
177
205
  */
178
206
  function parseLsofOutput(output) {
179
- const lines = output.trim().split('\n').slice(1); // Skip header
207
+ const lines = output.trim().split('\n').slice(1);
180
208
  const processes = [];
181
209
 
182
210
  for (const line of lines) {
@@ -186,7 +214,6 @@ function parseLsofOutput(output) {
186
214
  const command = parts[0];
187
215
 
188
216
  if (!isNaN(pid) && pid > 0) {
189
- // Check if already added
190
217
  if (!processes.find((p) => p.pid === pid)) {
191
218
  processes.push({ pid, command });
192
219
  }
@@ -201,7 +228,7 @@ function parseLsofOutput(output) {
201
228
  * Parse netstat output for a specific port
202
229
  */
203
230
  function parseNetstatOutput(output, port) {
204
- const lines = output.trim().split('\n').slice(1); // Skip header
231
+ const lines = output.trim().split('\n').slice(1);
205
232
  const processes = [];
206
233
 
207
234
  for (const line of lines) {
@@ -213,19 +240,17 @@ function parseNetstatOutput(output, port) {
213
240
 
214
241
  if (match && state === 'LISTEN') {
215
242
  const pid = parseInt(match[1], 10);
216
- const proto = parts[0];
217
243
  const addr = parts[3];
218
244
  const addrParts = addr.split(':');
219
245
  const addrPort = addrParts[addrParts.length - 1];
220
246
 
221
247
  if (parseInt(addrPort, 10) === port && !isNaN(pid) && pid > 0) {
222
- // Get command name
223
248
  let command = 'unknown';
224
249
  try {
225
250
  const psOutput = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8' });
226
251
  command = psOutput.trim();
227
252
  } catch {
228
- // Use default
253
+ // ignore
229
254
  }
230
255
 
231
256
  if (!processes.find((p) => p.pid === pid)) {
@@ -247,7 +272,7 @@ async function getProcessesWindows(port) {
247
272
  const netstatOutput = execSync('netstat -ano', { encoding: 'utf8' });
248
273
  const pids = new Set();
249
274
 
250
- const lines = netstatOutput.trim().split('\n').slice(4); // Skip header
275
+ const lines = netstatOutput.trim().split('\n').slice(4);
251
276
  for (const line of lines) {
252
277
  const parts = line.trim().split(/\s+/);
253
278
  if (parts.length >= 5) {
@@ -279,7 +304,7 @@ async function getProcessesWindows(port) {
279
304
  command = taskLine;
280
305
  }
281
306
  } catch {
282
- // Use default
307
+ // ignore
283
308
  }
284
309
 
285
310
  processes.push({ pid, command });
@@ -300,11 +325,9 @@ async function killProcess(pid, command) {
300
325
  if (PLATFORM === 'win32') {
301
326
  execSync(`taskkill /PID ${pid} /F`, { encoding: 'utf8' });
302
327
  } else {
303
- // Try using process.kill first
304
328
  try {
305
329
  kill(pid, 'SIGKILL');
306
330
  } catch {
307
- // Fallback to shell command
308
331
  execSync(`kill -9 ${pid}`);
309
332
  }
310
333
  }
@@ -331,3 +354,12 @@ function prompt(question) {
331
354
  });
332
355
  });
333
356
  }
357
+
358
+ if (import.meta.url === new URL(process.argv[1], 'file:').href) {
359
+ main().catch((error) => {
360
+ console.error(colors.red(`Error: ${error.message}`));
361
+ process.exit(1);
362
+ });
363
+ }
364
+
365
+ export { parsePorts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portclean",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Kill processes using specific ports on macOS, Linux, and Windows",
5
5
  "type": "module",
6
6
  "main": "cli.js",
@@ -17,8 +17,16 @@
17
17
  "kill",
18
18
  "process"
19
19
  ],
20
- "author": "",
20
+ "author": "Tejaswan Kalluri",
21
21
  "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/tejaswankalluri/portclean.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/tejaswankalluri/portclean/issues"
28
+ },
29
+ "homepage": "https://github.com/tejaswankalluri/portclean#readme",
22
30
  "dependencies": {
23
31
  "mri": "^1.2.0",
24
32
  "picocolors": "^1.0.0"