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.
- package/CONTRIBUTING.md +142 -0
- package/LICENSE +21 -0
- package/README.md +202 -0
- package/bin/portguard.js +270 -0
- package/lib/portguard.js +319 -0
- package/package.json +51 -0
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+
[](https://www.npmjs.com/package/portguard)
|
|
6
|
+
[](https://www.npmjs.com/package/portguard)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://nodejs.org)
|
|
9
|
+
[](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.*
|
package/bin/portguard.js
ADDED
|
@@ -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();
|
package/lib/portguard.js
ADDED
|
@@ -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
|
+
}
|