portguard 0.1.1

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.
@@ -0,0 +1,142 @@
1
+ # Contributing to portguard
2
+
3
+ Thanks for your interest in contributing! šŸŽ‰
4
+
5
+ ## Quick Start
6
+
7
+ 1. Fork this repo
8
+ 2. Create a branch: `git checkout -b feature/amazing-feature`
9
+ 3. Make your changes
10
+ 4. Write tests if applicable
11
+ 5. Run tests: `npm test`
12
+ 6. Commit: `git commit -m 'feat: add amazing feature'`
13
+ 7. Push: `git push origin feature/amazing-feature`
14
+ 8. Open a Pull Request
15
+
16
+ ## Development Setup
17
+
18
+ ```bash
19
+ # Clone your fork
20
+ git clone https://github.com/YOUR_USERNAME/portguard.git
21
+
22
+ # Install dependencies
23
+ npm install
24
+
25
+ # Run in development mode
26
+ npm run dev
27
+
28
+ # Run tests
29
+ npm test
30
+ ```
31
+
32
+ ## Code Style
33
+
34
+ We use:
35
+ - ESLint for linting
36
+ - Prettier for formatting
37
+ - Conventional Commits for commit messages
38
+
39
+ ### Commit Message Format
40
+
41
+ ```
42
+ type(scope): subject
43
+
44
+ body (optional)
45
+
46
+ footer (optional)
47
+ ```
48
+
49
+ **Types:**
50
+ - `feat`: New feature
51
+ - `fix`: Bug fix
52
+ - `docs`: Documentation only
53
+ - `style`: Code style (formatting, semicolons, etc.)
54
+ - `refactor`: Code change that neither fixes a bug nor adds a feature
55
+ - `perf`: Performance improvement
56
+ - `test`: Adding or updating tests
57
+ - `chore`: Maintenance tasks
58
+
59
+ **Example:**
60
+ ```
61
+ feat(cli): add --verbose flag for detailed output
62
+
63
+ Adds a new --verbose flag that shows debug information.
64
+ Useful for troubleshooting issues.
65
+
66
+ Closes #42
67
+ ```
68
+
69
+ ## Testing
70
+
71
+ Please add tests for new features. We aim for >80% coverage.
72
+
73
+ ```bash
74
+ # Run tests
75
+ npm test
76
+
77
+ # Run tests with coverage
78
+ npm run test:coverage
79
+
80
+ # Run tests in watch mode
81
+ npm run test:watch
82
+ ```
83
+
84
+ ## Reporting Bugs
85
+
86
+ Use GitHub Issues with the bug report template.
87
+
88
+ **Before reporting:**
89
+ 1. Check existing issues
90
+ 2. Try the latest version
91
+ 3. Include reproduction steps
92
+
93
+ ## Feature Requests
94
+
95
+ Use GitHub Issues with the feature request template.
96
+
97
+ **Before requesting:**
98
+ 1. Check existing issues and discussions
99
+ 2. Explain the use case, not just the solution
100
+ 3. Be open to alternative approaches
101
+
102
+ ## Pull Request Guidelines
103
+
104
+ **Before submitting:**
105
+ - [ ] Tests pass locally
106
+ - [ ] Code follows style guide (run `npm run lint`)
107
+ - [ ] Commit messages follow convention
108
+ - [ ] PR description explains what/why, not just how
109
+ - [ ] Breaking changes are clearly marked
110
+
111
+ **PR Checklist:**
112
+ - [ ] Updated documentation if needed
113
+ - [ ] Added tests for new functionality
114
+ - [ ] Updated CHANGELOG.md (if applicable)
115
+ - [ ] Linked related issues
116
+
117
+ ## Questions?
118
+
119
+ Open a discussion or reach out on [Twitter @muin_kr](https://twitter.com/muin_kr).
120
+
121
+ ---
122
+
123
+ ## Code of Conduct
124
+
125
+ Be respectful, inclusive, and constructive.
126
+
127
+ **Expected behavior:**
128
+ - Use welcoming and inclusive language
129
+ - Respect differing viewpoints and experiences
130
+ - Accept constructive criticism gracefully
131
+ - Focus on what's best for the community
132
+
133
+ **Unacceptable behavior:**
134
+ - Harassment, trolling, or discriminatory language
135
+ - Publishing others' private information
136
+ - Other conduct which could reasonably be considered inappropriate
137
+
138
+ ---
139
+
140
+ Thank you for contributing! šŸ™
141
+
142
+ **Run by AI, for humans** - [MUIN Company](https://muin.company)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MUIN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # portguard
2
+
3
+ **Who's stealing your ports? portguard knows.**
4
+
5
+ [![npm version](https://badge.fury.io/js/portguard.svg)](https://www.npmjs.com/package/portguard)
6
+ [![npm downloads](https://img.shields.io/npm/dm/portguard.svg)](https://www.npmjs.com/package/portguard)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js Version](https://img.shields.io/node/v/portguard.svg)](https://nodejs.org)
9
+ [![GitHub stars](https://img.shields.io/github/stars/muin-company/portguard.svg?style=social)](https://github.com/muin-company/portguard)
10
+
11
+ ## The Problem
12
+
13
+ ```
14
+ Error: listen EADDRINUSE: address already in use :::3000
15
+ ```
16
+
17
+ You've seen this error a thousand times. You run `lsof -ti:3000 | xargs kill -9` from muscle memory. But can you even remember that command? And which of your 8 abandoned dev servers is hogging port 8080?
18
+
19
+ ## The Solution
20
+
21
+ ```bash
22
+ $ portguard
23
+
24
+ šŸ”’ Active Ports:
25
+
26
+ PORT PID PROCESS ADDRESS UPTIME
27
+ 3000 45231 node *:3000 2h 15m
28
+ 5432 2341 postgres 127.0.0.1:5432 5d 3h
29
+ 8080 46123 node *:8080 1h 30m
30
+
31
+ $ portguard kill 3000 -y
32
+ āœ“ Killed process 45231 (node) on port 3000
33
+ ```
34
+
35
+ No memorizing `lsof` flags. No piping through `xargs`. One command.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ npm install -g portguard
41
+ ```
42
+
43
+ Or run without installing:
44
+
45
+ ```bash
46
+ npx portguard
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```bash
52
+ # What's using my ports?
53
+ portguard
54
+
55
+ # What's on port 3000?
56
+ portguard 3000
57
+
58
+ # Kill it
59
+ portguard kill 3000
60
+
61
+ # Kill without asking
62
+ portguard kill 3000 -y
63
+
64
+ # Nuke zombie dev servers
65
+ portguard clean
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ ### "EADDRINUSE" — the classic
71
+
72
+ ```bash
73
+ $ npm run dev
74
+ Error: listen EADDRINUSE: address already in use :::3000
75
+
76
+ $ portguard 3000
77
+ šŸ” Port 3000: node (PID 45231) — /Users/me/old-project/index.js
78
+ Running for 2h 15m
79
+
80
+ $ portguard kill 3000 -y
81
+ āœ“ Killed process 45231 (node) on port 3000
82
+
83
+ $ npm run dev
84
+ āœ“ Server started on http://localhost:3000
85
+ ```
86
+
87
+ ### End of day cleanup
88
+
89
+ ```bash
90
+ $ portguard
91
+
92
+ šŸ”’ Active Ports:
93
+
94
+ PORT PID PROCESS ADDRESS UPTIME
95
+ 3000 45231 node *:3000 4h
96
+ 5000 45892 python3 127.0.0.1:5000 2h
97
+ 5432 2341 postgres 127.0.0.1:5432 5d
98
+ 8080 46123 node *:8080 3h
99
+
100
+ # Kill dev servers, keep database
101
+ $ portguard kill 3000 -y
102
+ $ portguard kill 5000 -y
103
+ $ portguard kill 8080 -y
104
+ ```
105
+
106
+ ### Post-crash zombie hunt
107
+
108
+ ```bash
109
+ $ portguard clean
110
+
111
+ 🧟 Found 3 zombie processes:
112
+
113
+ PID 98123 - node (port 3001) - idle 2h
114
+ PID 98124 - node (port 3002) - idle 2h
115
+ PID 98125 - node (port 3003) - idle 2h
116
+
117
+ Kill all? (y/N): y
118
+
119
+ āœ“ Killed 3 zombie processes
120
+ ```
121
+
122
+ ### Finding free ports in a range
123
+
124
+ ```bash
125
+ $ portguard -r 3000-3010
126
+
127
+ šŸ“Š Port Range 3000-3010:
128
+ Used: 3000, 3003, 3007
129
+ Free: 3001, 3002, 3004, 3005, 3006, 3008, 3009, 3010
130
+ ```
131
+
132
+ ### Watch mode
133
+
134
+ ```bash
135
+ $ portguard watch -i 2
136
+
137
+ šŸ‘ļø Watching ports (refresh every 2s, Ctrl+C to stop)
138
+
139
+ PORT PID PROCESS ADDRESS
140
+ 5000 12341 node *:5000
141
+ 5001 12389 node *:5001 NEW!
142
+ ```
143
+
144
+ ## Commands
145
+
146
+ | Command | Description |
147
+ |---------|-------------|
148
+ | `portguard` | List all active ports |
149
+ | `portguard <port>` | Check specific port |
150
+ | `portguard kill <port>` | Kill process on port |
151
+ | `portguard kill <port> -f` | Force kill (SIGKILL) |
152
+ | `portguard kill <port> -y` | Skip confirmation |
153
+ | `portguard watch` | Continuous monitoring |
154
+ | `portguard watch -i <sec>` | Custom refresh interval |
155
+ | `portguard clean` | Kill zombie dev servers |
156
+ | `portguard -r 3000-4000` | Scan port range |
157
+
158
+ ## Integration
159
+
160
+ ### npm scripts
161
+
162
+ ```json
163
+ {
164
+ "scripts": {
165
+ "predev": "portguard kill 3000 -y || true",
166
+ "dev": "next dev"
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### Shell aliases
172
+
173
+ ```bash
174
+ alias ports='portguard'
175
+ alias killport='portguard kill'
176
+ alias kill3000='portguard kill 3000 -y'
177
+ ```
178
+
179
+ ### CI cleanup
180
+
181
+ ```yaml
182
+ - name: Clean ports
183
+ run: npx portguard clean -y || true
184
+ ```
185
+
186
+ ## Platform Support
187
+
188
+ | Platform | Method |
189
+ |----------|--------|
190
+ | macOS | `lsof` |
191
+ | Linux | `lsof` |
192
+ | Windows | `netstat` |
193
+
194
+ System processes (root-owned) require `sudo portguard kill <port>`.
195
+
196
+ ## License
197
+
198
+ MIT Ā© [muin](https://github.com/muin-company)
199
+
200
+ ---
201
+
202
+ *Stop fighting with ports. Start guarding them.*
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import readline from 'readline';
5
+ import {
6
+ getActivePorts,
7
+ getPortInfo,
8
+ getPortRange,
9
+ killPort,
10
+ cleanZombies,
11
+ displayPorts,
12
+ displayPortInfo,
13
+ displayPortRange
14
+ } from '../lib/portguard.js';
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('portguard')
20
+ .description('Monitor and manage localhost ports')
21
+ .version('0.1.0');
22
+
23
+ // Main command - show all ports
24
+ program
25
+ .argument('[port]', 'specific port to check')
26
+ .option('-r, --range <range>', 'scan port range (e.g., 3000-4000)')
27
+ .action(async (port, options) => {
28
+ if (options.range) {
29
+ // Scan port range
30
+ await handleRange(options.range);
31
+ } else if (port) {
32
+ // Show specific port
33
+ await handlePortCheck(port);
34
+ } else {
35
+ // Show all ports
36
+ await handleListAll();
37
+ }
38
+ });
39
+
40
+ // Kill command
41
+ program
42
+ .command('kill <port>')
43
+ .description('Kill process on specific port')
44
+ .option('-f, --force', 'force kill (SIGKILL)')
45
+ .option('-y, --yes', 'skip confirmation')
46
+ .action(async (port, options) => {
47
+ await handleKill(port, options);
48
+ });
49
+
50
+ // Watch command
51
+ program
52
+ .command('watch')
53
+ .description('Continuous monitoring mode')
54
+ .option('-i, --interval <seconds>', 'refresh interval in seconds', '3')
55
+ .action(async (options) => {
56
+ await handleWatch(options);
57
+ });
58
+
59
+ // Clean command
60
+ program
61
+ .command('clean')
62
+ .description('Kill common zombie processes (node, python, etc.)')
63
+ .option('-f, --force', 'force kill (SIGKILL)')
64
+ .option('-y, --yes', 'skip confirmation')
65
+ .action(async (options) => {
66
+ await handleClean(options);
67
+ });
68
+
69
+ /**
70
+ * Handle listing all ports
71
+ */
72
+ async function handleListAll() {
73
+ try {
74
+ const ports = await getActivePorts();
75
+ displayPorts(ports);
76
+ } catch (error) {
77
+ console.error(chalk.red('Error: ') + error.message);
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Handle checking specific port
84
+ */
85
+ async function handlePortCheck(port) {
86
+ try {
87
+ const processes = await getPortInfo(port);
88
+ displayPortInfo(port, processes);
89
+ } catch (error) {
90
+ console.error(chalk.red('Error: ') + error.message);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Handle port range scanning
97
+ */
98
+ async function handleRange(range) {
99
+ try {
100
+ // Parse range (e.g., "3000-4000")
101
+ const match = range.match(/^(\d+)-(\d+)$/);
102
+
103
+ if (!match) {
104
+ console.error(chalk.red('Error: Invalid range format. Use: portguard --range 3000-4000'));
105
+ process.exit(1);
106
+ }
107
+
108
+ const [, start, end] = match;
109
+ const rangeInfo = await getPortRange(start, end);
110
+ displayPortRange(rangeInfo);
111
+ } catch (error) {
112
+ console.error(chalk.red('Error: ') + error.message);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Handle kill command
119
+ */
120
+ async function handleKill(port, options) {
121
+ try {
122
+ const processes = await getPortInfo(port);
123
+
124
+ if (processes.length === 0) {
125
+ console.log(chalk.yellow(`Port ${port} is not in use`));
126
+ return;
127
+ }
128
+
129
+ // Show what will be killed
130
+ console.log(chalk.bold(`\nāš ļø About to kill:\n`));
131
+ for (const proc of processes) {
132
+ console.log(chalk.yellow(` PID ${proc.pid}: ${proc.process} on port ${proc.port}`));
133
+ }
134
+ console.log('');
135
+
136
+ // Confirm unless --yes
137
+ if (!options.yes) {
138
+ const confirmed = await confirm('Continue?');
139
+ if (!confirmed) {
140
+ console.log(chalk.gray('Cancelled'));
141
+ return;
142
+ }
143
+ }
144
+
145
+ // Kill processes
146
+ const results = await killPort(port, options.force);
147
+
148
+ for (const result of results) {
149
+ if (result.success) {
150
+ console.log(chalk.green(`āœ“ Killed PID ${result.pid} (${result.process})`));
151
+ } else {
152
+ console.log(chalk.red(`āœ— Failed to kill PID ${result.pid}: ${result.error}`));
153
+ }
154
+ }
155
+ } catch (error) {
156
+ console.error(chalk.red('Error: ') + error.message);
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Handle watch mode
163
+ */
164
+ async function handleWatch(options) {
165
+ const interval = parseInt(options.interval) * 1000;
166
+
167
+ console.log(chalk.bold(`šŸ‘ļø Watching ports (refresh every ${options.interval}s, Ctrl+C to stop)\n`));
168
+
169
+ const refresh = async () => {
170
+ // Clear screen
171
+ console.clear();
172
+ console.log(chalk.bold(`šŸ‘ļø Watching ports (refresh every ${options.interval}s, Ctrl+C to stop)\n`));
173
+
174
+ try {
175
+ const ports = await getActivePorts();
176
+ displayPorts(ports);
177
+ } catch (error) {
178
+ console.error(chalk.red('Error: ') + error.message);
179
+ }
180
+ };
181
+
182
+ // Initial display
183
+ await refresh();
184
+
185
+ // Refresh interval
186
+ const intervalId = setInterval(refresh, interval);
187
+
188
+ // Handle Ctrl+C
189
+ process.on('SIGINT', () => {
190
+ clearInterval(intervalId);
191
+ console.log(chalk.gray('\n\nStopped watching'));
192
+ process.exit(0);
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Handle clean command
198
+ */
199
+ async function handleClean(options) {
200
+ try {
201
+ const zombies = await cleanZombies();
202
+
203
+ if (zombies.length === 0) {
204
+ console.log(chalk.green('āœ“ No zombie processes found'));
205
+ return;
206
+ }
207
+
208
+ // Show what will be killed
209
+ console.log(chalk.bold(`\n🧟 Found ${zombies.length} zombie process${zombies.length > 1 ? 'es' : ''}:\n`));
210
+ for (const proc of zombies) {
211
+ console.log(chalk.yellow(` PID ${proc.pid}: ${proc.process} on port ${proc.port}`));
212
+ }
213
+ console.log('');
214
+
215
+ // Confirm unless --yes
216
+ if (!options.yes) {
217
+ const confirmed = await confirm('Kill all?');
218
+ if (!confirmed) {
219
+ console.log(chalk.gray('Cancelled'));
220
+ return;
221
+ }
222
+ }
223
+
224
+ // Kill each zombie
225
+ let killed = 0;
226
+ let failed = 0;
227
+
228
+ for (const zombie of zombies) {
229
+ try {
230
+ const results = await killPort(zombie.port, options.force);
231
+ for (const result of results) {
232
+ if (result.success) {
233
+ console.log(chalk.green(`āœ“ Killed PID ${result.pid} (${result.process})`));
234
+ killed++;
235
+ } else {
236
+ console.log(chalk.red(`āœ— Failed to kill PID ${result.pid}: ${result.error}`));
237
+ failed++;
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.log(chalk.red(`āœ— Failed: ${error.message}`));
242
+ failed++;
243
+ }
244
+ }
245
+
246
+ console.log(chalk.bold(`\n${killed} killed, ${failed} failed`));
247
+ } catch (error) {
248
+ console.error(chalk.red('Error: ') + error.message);
249
+ process.exit(1);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Prompt for confirmation
255
+ */
256
+ function confirm(question) {
257
+ const rl = readline.createInterface({
258
+ input: process.stdin,
259
+ output: process.stdout
260
+ });
261
+
262
+ return new Promise((resolve) => {
263
+ rl.question(chalk.yellow(`${question} (y/N) `), (answer) => {
264
+ rl.close();
265
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
266
+ });
267
+ });
268
+ }
269
+
270
+ program.parse();
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env node
2
+ import { exec } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import chalk from 'chalk';
5
+
6
+ const execAsync = promisify(exec);
7
+
8
+ const PLATFORM = process.platform;
9
+
10
+ /**
11
+ * Get all active ports and their processes
12
+ */
13
+ export async function getActivePorts() {
14
+ try {
15
+ if (PLATFORM === 'win32') {
16
+ return await getPortsWindows();
17
+ } else {
18
+ return await getPortsUnix();
19
+ }
20
+ } catch (error) {
21
+ throw new Error(`Failed to get active ports: ${error.message}`);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Get ports on Unix-like systems (macOS, Linux)
27
+ */
28
+ async function getPortsUnix() {
29
+ // Try different lsof locations
30
+ const lsofPaths = [
31
+ 'lsof', // In PATH
32
+ '/usr/sbin/lsof', // macOS default
33
+ '/usr/bin/lsof' // Linux default
34
+ ];
35
+
36
+ let lastError;
37
+
38
+ for (const lsofPath of lsofPaths) {
39
+ try {
40
+ const { stdout } = await execAsync(`${lsofPath} -iTCP -sTCP:LISTEN -n -P`);
41
+ return parseUnixOutput(stdout);
42
+ } catch (error) {
43
+ // lsof exits with code 1 when no ports are listening
44
+ if (error.code === 1 && error.stdout) {
45
+ return parseUnixOutput(error.stdout || '');
46
+ }
47
+ lastError = error;
48
+ continue;
49
+ }
50
+ }
51
+
52
+ // All paths failed
53
+ if (lastError.message.includes('not found') || lastError.code === 'ENOENT') {
54
+ throw new Error('lsof not found. Please install lsof to use portguard.');
55
+ }
56
+ throw lastError;
57
+ }
58
+
59
+ /**
60
+ * Get ports on Windows
61
+ */
62
+ async function getPortsWindows() {
63
+ const { stdout } = await execAsync('netstat -ano | findstr LISTENING');
64
+ return parseWindowsOutput(stdout);
65
+ }
66
+
67
+ /**
68
+ * Parse lsof output
69
+ */
70
+ function parseUnixOutput(output) {
71
+ const lines = output.trim().split('\n').slice(1); // Skip header
72
+ const ports = [];
73
+ const seen = new Set();
74
+
75
+ for (const line of lines) {
76
+ const parts = line.split(/\s+/);
77
+ if (parts.length < 9) continue;
78
+
79
+ const command = parts[0];
80
+ const pid = parts[1];
81
+ const address = parts[8];
82
+
83
+ // Extract port from address (format: *:PORT or IP:PORT)
84
+ const portMatch = address.match(/:(\d+)$/);
85
+ if (!portMatch) continue;
86
+
87
+ const port = portMatch[1];
88
+ const key = `${pid}-${port}`;
89
+
90
+ if (!seen.has(key)) {
91
+ seen.add(key);
92
+ ports.push({
93
+ port: parseInt(port),
94
+ pid: parseInt(pid),
95
+ process: command,
96
+ address: address
97
+ });
98
+ }
99
+ }
100
+
101
+ return ports.sort((a, b) => a.port - b.port);
102
+ }
103
+
104
+ /**
105
+ * Parse netstat output (Windows)
106
+ */
107
+ function parseWindowsOutput(output) {
108
+ const lines = output.trim().split('\n');
109
+ const ports = [];
110
+ const seen = new Set();
111
+
112
+ for (const line of lines) {
113
+ const parts = line.trim().split(/\s+/);
114
+ if (parts.length < 5) continue;
115
+
116
+ const address = parts[1];
117
+ const pid = parts[4];
118
+
119
+ // Extract port from address (format: IP:PORT)
120
+ const portMatch = address.match(/:(\d+)$/);
121
+ if (!portMatch) continue;
122
+
123
+ const port = portMatch[1];
124
+ const key = `${pid}-${port}`;
125
+
126
+ if (!seen.has(key)) {
127
+ seen.add(key);
128
+ ports.push({
129
+ port: parseInt(port),
130
+ pid: parseInt(pid),
131
+ process: 'unknown', // Windows netstat doesn't show process name
132
+ address: address
133
+ });
134
+ }
135
+ }
136
+
137
+ return ports.sort((a, b) => a.port - b.port);
138
+ }
139
+
140
+ /**
141
+ * Get process info for a specific port
142
+ */
143
+ export async function getPortInfo(port) {
144
+ const ports = await getActivePorts();
145
+ return ports.filter(p => p.port === parseInt(port));
146
+ }
147
+
148
+ /**
149
+ * Get process info for a range of ports
150
+ */
151
+ export async function getPortRange(startPort, endPort) {
152
+ const start = parseInt(startPort);
153
+ const end = parseInt(endPort);
154
+
155
+ if (isNaN(start) || isNaN(end)) {
156
+ throw new Error('Invalid port range: ports must be numbers');
157
+ }
158
+
159
+ if (start > end) {
160
+ throw new Error('Invalid port range: start port must be less than end port');
161
+ }
162
+
163
+ if (start < 1 || end > 65535) {
164
+ throw new Error('Invalid port range: ports must be between 1 and 65535');
165
+ }
166
+
167
+ const allPorts = await getActivePorts();
168
+ const rangePorts = allPorts.filter(p => p.port >= start && p.port <= end);
169
+
170
+ return {
171
+ start,
172
+ end,
173
+ total: end - start + 1,
174
+ used: rangePorts.length,
175
+ free: (end - start + 1) - rangePorts.length,
176
+ ports: rangePorts
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Kill process by PID
182
+ */
183
+ export async function killProcess(pid, force = false) {
184
+ try {
185
+ if (PLATFORM === 'win32') {
186
+ await execAsync(`taskkill ${force ? '/F' : ''} /PID ${pid}`);
187
+ } else {
188
+ await execAsync(`kill ${force ? '-9' : ''} ${pid}`);
189
+ }
190
+ return true;
191
+ } catch (error) {
192
+ if (error.message.includes('No such process') || error.message.includes('not found')) {
193
+ throw new Error(`Process ${pid} not found`);
194
+ }
195
+ if (error.message.includes('permission') || error.message.includes('denied')) {
196
+ throw new Error(`Permission denied. Try running with sudo/administrator privileges.`);
197
+ }
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Kill process on a specific port
204
+ */
205
+ export async function killPort(port, force = false) {
206
+ const processes = await getPortInfo(port);
207
+
208
+ if (processes.length === 0) {
209
+ throw new Error(`No process found on port ${port}`);
210
+ }
211
+
212
+ const results = [];
213
+ for (const proc of processes) {
214
+ try {
215
+ await killProcess(proc.pid, force);
216
+ results.push({ success: true, ...proc });
217
+ } catch (error) {
218
+ results.push({ success: false, error: error.message, ...proc });
219
+ }
220
+ }
221
+
222
+ return results;
223
+ }
224
+
225
+ /**
226
+ * Find and kill zombie processes
227
+ */
228
+ export async function cleanZombies() {
229
+ const ports = await getActivePorts();
230
+ const zombieProcesses = ['node', 'python', 'python3', 'ruby', 'java', 'deno'];
231
+
232
+ const zombies = ports.filter(p =>
233
+ zombieProcesses.some(name => p.process.toLowerCase().includes(name))
234
+ );
235
+
236
+ return zombies;
237
+ }
238
+
239
+ /**
240
+ * Format and display ports
241
+ */
242
+ export function displayPorts(ports) {
243
+ if (ports.length === 0) {
244
+ console.log(chalk.yellow('No active ports found'));
245
+ return;
246
+ }
247
+
248
+ console.log(chalk.bold('\nšŸ”’ Active Ports:\n'));
249
+
250
+ const header = `${chalk.bold('PORT').padEnd(10)} ${chalk.bold('PID').padEnd(10)} ${chalk.bold('PROCESS').padEnd(20)} ${chalk.bold('ADDRESS')}`;
251
+ console.log(header);
252
+ console.log('─'.repeat(70));
253
+
254
+ for (const port of ports) {
255
+ const portStr = chalk.cyan(port.port.toString().padEnd(10));
256
+ const pidStr = chalk.yellow(port.pid.toString().padEnd(10));
257
+ const processStr = chalk.green(port.process.padEnd(20));
258
+ const addressStr = chalk.gray(port.address);
259
+
260
+ console.log(`${portStr}${pidStr}${processStr}${addressStr}`);
261
+ }
262
+
263
+ console.log('');
264
+ }
265
+
266
+ /**
267
+ * Display single port info
268
+ */
269
+ export function displayPortInfo(port, processes) {
270
+ if (processes.length === 0) {
271
+ console.log(chalk.yellow(`\nPort ${port} is not in use`));
272
+ return;
273
+ }
274
+
275
+ console.log(chalk.bold(`\nšŸ” Port ${port} details:\n`));
276
+
277
+ for (const proc of processes) {
278
+ console.log(chalk.cyan(' Port: ') + proc.port);
279
+ console.log(chalk.yellow(' PID: ') + proc.pid);
280
+ console.log(chalk.green(' Process: ') + proc.process);
281
+ console.log(chalk.gray(' Address: ') + proc.address);
282
+ console.log('');
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Display port range info
288
+ */
289
+ export function displayPortRange(rangeInfo) {
290
+ const { start, end, total, used, free, ports } = rangeInfo;
291
+
292
+ console.log(chalk.bold(`\nšŸ“Š Port Range ${start}-${end} Analysis:\n`));
293
+
294
+ console.log(chalk.cyan(' Range: ') + `${start} - ${end}`);
295
+ console.log(chalk.yellow(' Total ports: ') + total);
296
+ console.log(chalk.green(' Free ports: ') + free);
297
+ console.log(chalk.red(' Used ports: ') + used);
298
+
299
+ if (used > 0) {
300
+ console.log(chalk.bold('\nšŸ”’ Active Ports in Range:\n'));
301
+
302
+ const header = `${chalk.bold('PORT').padEnd(10)} ${chalk.bold('PID').padEnd(10)} ${chalk.bold('PROCESS').padEnd(20)} ${chalk.bold('ADDRESS')}`;
303
+ console.log(header);
304
+ console.log('─'.repeat(70));
305
+
306
+ for (const port of ports) {
307
+ const portStr = chalk.cyan(port.port.toString().padEnd(10));
308
+ const pidStr = chalk.yellow(port.pid.toString().padEnd(10));
309
+ const processStr = chalk.green(port.process.padEnd(20));
310
+ const addressStr = chalk.gray(port.address);
311
+
312
+ console.log(`${portStr}${pidStr}${processStr}${addressStr}`);
313
+ }
314
+ } else {
315
+ console.log(chalk.green('\nāœ“ All ports in this range are free!'));
316
+ }
317
+
318
+ console.log('');
319
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "portguard",
3
+ "version": "0.1.1",
4
+ "description": "Monitor and manage localhost ports - kill zombie processes and prevent port conflicts",
5
+ "main": "lib/portguard.js",
6
+ "bin": {
7
+ "portguard": "bin/portguard.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node test/test.js"
11
+ },
12
+ "keywords": [
13
+ "port",
14
+ "localhost",
15
+ "process",
16
+ "monitor",
17
+ "cli",
18
+ "developer-tools",
19
+ "kill-port",
20
+ "eaddrinuse",
21
+ "lsof",
22
+ "zombie",
23
+ "devtools",
24
+ "port-scanner",
25
+ "port-killer",
26
+ "network",
27
+ "tcp",
28
+ "port-conflict",
29
+ "port-management",
30
+ "process-manager",
31
+ "cleanup"
32
+ ],
33
+ "homepage": "https://github.com/muin-company/portguard#readme",
34
+ "author": "muin",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/muin-company/portguard.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/muin-company/portguard/issues"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "chalk": "^5.3.0",
48
+ "commander": "^12.0.0"
49
+ },
50
+ "type": "module"
51
+ }