portdog 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 ADDED
@@ -0,0 +1,43 @@
1
+ # portdog
2
+
3
+ Your port watchdog. Stop googling `lsof -i :3000`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g portdog
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # See all ports in use
15
+ portdog list
16
+
17
+ # Kill whatever is on port 3000
18
+ portdog kill 3000
19
+
20
+ # Force kill (SIGKILL)
21
+ portdog kill 3000 --force
22
+
23
+ # Check if ports are free
24
+ portdog scan 3000 8080 5432
25
+
26
+ # Get a free port
27
+ portdog free
28
+
29
+ # Get 5 free ports
30
+ portdog free -n 5
31
+
32
+ # Detailed info about what's on a port
33
+ portdog who 3000
34
+ ```
35
+
36
+ ## Aliases
37
+
38
+ - `portdog ls` = `portdog list`
39
+ - `portdog k <port>` = `portdog kill <port>`
40
+
41
+ ## License
42
+
43
+ MIT
package/bin/portdog.js ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { listPorts, killPort, scanPorts, freePort, whoisPort } = require('../src/commands');
5
+
6
+ program
7
+ .name('portdog')
8
+ .description('🐕 Your port watchdog. Stop googling lsof.')
9
+ .version('1.0.0');
10
+
11
+ program
12
+ .command('list')
13
+ .alias('ls')
14
+ .description('Show all ports in use with process info')
15
+ .option('-p, --port <port>', 'Filter by specific port')
16
+ .action(listPorts);
17
+
18
+ program
19
+ .command('kill <port>')
20
+ .alias('k')
21
+ .description('Kill whatever is running on a port')
22
+ .option('-f, --force', 'Force kill (SIGKILL)', false)
23
+ .action(killPort);
24
+
25
+ program
26
+ .command('scan <ports...>')
27
+ .description('Check if ports are free (e.g. portdog scan 3000 8080 5432)')
28
+ .action(scanPorts);
29
+
30
+ program
31
+ .command('free')
32
+ .description('Find and print a random free port')
33
+ .option('-n, --count <number>', 'Number of free ports to find', '1')
34
+ .action(freePort);
35
+
36
+ program
37
+ .command('who <port>')
38
+ .description('Detailed info about what is using a port')
39
+ .action(whoisPort);
40
+
41
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "portdog",
3
+ "version": "1.0.0",
4
+ "description": "Your port watchdog. Stop googling lsof.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "portdog": "./bin/portdog.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": ["port", "kill", "process", "cli", "devtools", "lsof", "watchdog"],
13
+ "author": "",
14
+ "license": "MIT",
15
+ "type": "commonjs",
16
+ "dependencies": {
17
+ "chalk": "^4.1.2",
18
+ "cli-table3": "^0.6.5",
19
+ "commander": "^14.0.3"
20
+ }
21
+ }
@@ -0,0 +1,195 @@
1
+ const chalk = require('chalk');
2
+ const Table = require('cli-table3');
3
+ const {
4
+ getProcessesOnPorts,
5
+ isPortFree,
6
+ findFreePorts,
7
+ killProcess,
8
+ getDetailedProcessInfo,
9
+ timeSince,
10
+ } = require('./portUtils');
11
+
12
+ async function listPorts(options) {
13
+ const processes = getProcessesOnPorts();
14
+
15
+ if (processes.length === 0) {
16
+ console.log(chalk.green('\n No ports in use. Your machine is squeaky clean.\n'));
17
+ return;
18
+ }
19
+
20
+ let filtered = processes;
21
+ if (options.port) {
22
+ const targetPort = parseInt(options.port, 10);
23
+ filtered = processes.filter((p) => p.port === targetPort);
24
+ if (filtered.length === 0) {
25
+ console.log(chalk.green(`\n Port ${targetPort} is free. Nothing there.\n`));
26
+ return;
27
+ }
28
+ }
29
+
30
+ // Sort by port number
31
+ filtered.sort((a, b) => a.port - b.port);
32
+
33
+ const table = new Table({
34
+ head: [
35
+ chalk.cyan('PORT'),
36
+ chalk.cyan('PID'),
37
+ chalk.cyan('PROCESS'),
38
+ chalk.cyan('USER'),
39
+ chalk.cyan('MEMORY'),
40
+ chalk.cyan('RUNNING SINCE'),
41
+ ],
42
+ style: { head: [], border: ['gray'] },
43
+ });
44
+
45
+ for (const p of filtered) {
46
+ const portColor = p.port < 1024 ? chalk.red : chalk.green;
47
+ table.push([
48
+ portColor(p.port),
49
+ chalk.yellow(p.pid),
50
+ chalk.white(p.command),
51
+ chalk.gray(p.user),
52
+ chalk.magenta(p.memory || '-'),
53
+ chalk.gray(timeSince(p.startTime) || '-'),
54
+ ]);
55
+ }
56
+
57
+ console.log(`\n ${chalk.bold(`${filtered.length} port(s) in use`)}\n`);
58
+ console.log(table.toString());
59
+ console.log(chalk.gray(`\n Kill with: ${chalk.white('portdog kill <port>')}\n`));
60
+ }
61
+
62
+ async function killPort(port, options) {
63
+ const portNum = parseInt(port, 10);
64
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
65
+ console.log(chalk.red(`\n Invalid port: ${port}\n`));
66
+ process.exit(1);
67
+ }
68
+
69
+ const processes = getProcessesOnPorts().filter((p) => p.port === portNum);
70
+
71
+ if (processes.length === 0) {
72
+ console.log(chalk.green(`\n Port ${portNum} is already free. Nothing to kill.\n`));
73
+ return;
74
+ }
75
+
76
+ for (const p of processes) {
77
+ const method = options.force ? 'SIGKILL' : 'SIGTERM';
78
+ console.log(
79
+ chalk.yellow(
80
+ `\n Killing ${chalk.white(p.command)} (PID ${p.pid}) on port ${portNum} with ${method}...`
81
+ )
82
+ );
83
+
84
+ const success = killProcess(p.pid, options.force);
85
+
86
+ if (success) {
87
+ // Verify it's actually dead
88
+ await new Promise((r) => setTimeout(r, 500));
89
+ const stillAlive = getProcessesOnPorts().some(
90
+ (proc) => proc.pid === p.pid && proc.port === portNum
91
+ );
92
+
93
+ if (stillAlive && !options.force) {
94
+ console.log(
95
+ chalk.yellow(
96
+ ` Process didn't die. Try: ${chalk.white(`portdog kill ${portNum} --force`)}`
97
+ )
98
+ );
99
+ } else if (stillAlive) {
100
+ console.log(chalk.red(` Process refused to die even with SIGKILL. Check permissions.`));
101
+ } else {
102
+ console.log(chalk.green(` Done. Port ${portNum} is now free.\n`));
103
+ }
104
+ } else {
105
+ console.log(
106
+ chalk.red(` Failed to kill PID ${p.pid}. You might need sudo:\n`)
107
+ );
108
+ console.log(chalk.gray(` sudo portdog kill ${portNum}\n`));
109
+ }
110
+ }
111
+ }
112
+
113
+ async function scanPorts(ports) {
114
+ console.log('');
115
+ const results = await Promise.all(
116
+ ports.map(async (p) => {
117
+ const portNum = parseInt(p, 10);
118
+ if (isNaN(portNum)) return { port: p, status: 'invalid' };
119
+ const free = await isPortFree(portNum);
120
+ return { port: portNum, status: free ? 'free' : 'in-use' };
121
+ })
122
+ );
123
+
124
+ for (const r of results) {
125
+ if (r.status === 'invalid') {
126
+ console.log(chalk.red(` ${r.port} invalid port number`));
127
+ } else if (r.status === 'free') {
128
+ console.log(chalk.green(` :${r.port} free`));
129
+ } else {
130
+ const proc = getProcessesOnPorts().find((p) => p.port === r.port);
131
+ const info = proc ? chalk.gray(` (${proc.command}, PID ${proc.pid})`) : '';
132
+ console.log(chalk.red(` :${r.port} in use${info}`));
133
+ }
134
+ }
135
+ console.log('');
136
+ }
137
+
138
+ async function freePort(options) {
139
+ const count = parseInt(options.count, 10) || 1;
140
+ const ports = await findFreePorts(count);
141
+
142
+ if (count === 1) {
143
+ console.log(`\n ${chalk.green(ports[0])}\n`);
144
+ } else {
145
+ console.log('');
146
+ for (const p of ports) {
147
+ console.log(` ${chalk.green(p)}`);
148
+ }
149
+ console.log('');
150
+ }
151
+ }
152
+
153
+ async function whoisPort(port) {
154
+ const portNum = parseInt(port, 10);
155
+ if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
156
+ console.log(chalk.red(`\n Invalid port: ${port}\n`));
157
+ process.exit(1);
158
+ }
159
+
160
+ const processes = getProcessesOnPorts().filter((p) => p.port === portNum);
161
+
162
+ if (processes.length === 0) {
163
+ console.log(chalk.green(`\n Port ${portNum} is free. Nobody home.\n`));
164
+ return;
165
+ }
166
+
167
+ for (const p of processes) {
168
+ const details = getDetailedProcessInfo(p.pid);
169
+
170
+ console.log(`\n ${chalk.bold.cyan(`Port ${portNum}`)}`);
171
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
172
+ console.log(` ${chalk.gray('Process:')} ${chalk.white(p.command)}`);
173
+ console.log(` ${chalk.gray('PID:')} ${chalk.yellow(p.pid)}`);
174
+ console.log(` ${chalk.gray('User:')} ${p.user}`);
175
+ console.log(` ${chalk.gray('Memory:')} ${chalk.magenta(p.memory || '-')}`);
176
+ console.log(` ${chalk.gray('Started:')} ${p.startTime || '-'} ${chalk.gray(`(${timeSince(p.startTime)})`)}`);
177
+
178
+ if (details) {
179
+ console.log(` ${chalk.gray('Full cmd:')} ${chalk.dim(details.fullCommand)}`);
180
+ console.log(` ${chalk.gray('Open files:')} ${details.openFiles}`);
181
+ console.log(
182
+ ` ${chalk.gray('Children:')} ${details.childCount}${
183
+ details.childPids.length > 0
184
+ ? chalk.gray(` (PIDs: ${details.childPids.join(', ')})`)
185
+ : ''
186
+ }`
187
+ );
188
+ }
189
+
190
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
191
+ console.log(chalk.gray(` Kill it: ${chalk.white(`portdog kill ${portNum}`)}\n`));
192
+ }
193
+ }
194
+
195
+ module.exports = { listPorts, killPort, scanPorts, freePort, whoisPort };
@@ -0,0 +1,229 @@
1
+ const { execSync } = require('child_process');
2
+ const net = require('net');
3
+ const os = require('os');
4
+
5
+ function getProcessesOnPorts() {
6
+ const platform = os.platform();
7
+ const results = [];
8
+
9
+ try {
10
+ let output;
11
+ if (platform === 'darwin' || platform === 'linux') {
12
+ // Get listening TCP ports
13
+ output = execSync('lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null || true', {
14
+ encoding: 'utf-8',
15
+ timeout: 5000,
16
+ });
17
+
18
+ const lines = output.trim().split('\n');
19
+ if (lines.length <= 1) return results;
20
+
21
+ // Skip header
22
+ for (let i = 1; i < lines.length; i++) {
23
+ const parts = lines[i].split(/\s+/);
24
+ if (parts.length < 9) continue;
25
+
26
+ const command = parts[0];
27
+ const pid = parseInt(parts[1], 10);
28
+ const user = parts[2];
29
+ const nameField = parts[8]; // e.g. *:3000 or 127.0.0.1:8080
30
+
31
+ const portMatch = nameField.match(/:(\d+)$/);
32
+ if (!portMatch) continue;
33
+
34
+ const port = parseInt(portMatch[1], 10);
35
+
36
+ // Get process start time and memory
37
+ let startTime = '';
38
+ let memory = '';
39
+ try {
40
+ const psOut = execSync(`ps -p ${pid} -o lstart=,rss= 2>/dev/null || true`, {
41
+ encoding: 'utf-8',
42
+ timeout: 3000,
43
+ }).trim();
44
+ if (psOut) {
45
+ const rssMatch = psOut.match(/(\d+)\s*$/);
46
+ if (rssMatch) {
47
+ memory = formatMemory(parseInt(rssMatch[1], 10));
48
+ startTime = psOut.replace(/\s*\d+\s*$/, '').trim();
49
+ }
50
+ }
51
+ } catch {}
52
+
53
+ results.push({ port, pid, command, user, startTime, memory });
54
+ }
55
+ } else if (platform === 'win32') {
56
+ output = execSync('netstat -ano -p TCP | findstr LISTENING', {
57
+ encoding: 'utf-8',
58
+ timeout: 5000,
59
+ });
60
+
61
+ const lines = output.trim().split('\n');
62
+ for (const line of lines) {
63
+ const parts = line.trim().split(/\s+/);
64
+ if (parts.length < 5) continue;
65
+
66
+ const localAddr = parts[1];
67
+ const pid = parseInt(parts[4], 10);
68
+ const portMatch = localAddr.match(/:(\d+)$/);
69
+ if (!portMatch) continue;
70
+
71
+ const port = parseInt(portMatch[1], 10);
72
+
73
+ let command = '';
74
+ try {
75
+ command = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH 2>nul`, {
76
+ encoding: 'utf-8',
77
+ timeout: 3000,
78
+ }).trim().split(',')[0]?.replace(/"/g, '') || '';
79
+ } catch {}
80
+
81
+ results.push({ port, pid, command, user: '', startTime: '', memory: '' });
82
+ }
83
+ }
84
+ } catch (err) {
85
+ // Silently handle - will return empty results
86
+ }
87
+
88
+ // Deduplicate by port+pid
89
+ const seen = new Set();
90
+ return results.filter((r) => {
91
+ const key = `${r.port}:${r.pid}`;
92
+ if (seen.has(key)) return false;
93
+ seen.add(key);
94
+ return true;
95
+ });
96
+ }
97
+
98
+ function isPortFree(port) {
99
+ return new Promise((resolve) => {
100
+ const server = net.createServer();
101
+ server.once('error', () => resolve(false));
102
+ server.once('listening', () => {
103
+ server.close(() => resolve(true));
104
+ });
105
+ server.listen(port, '0.0.0.0');
106
+ });
107
+ }
108
+
109
+ function findFreePorts(count = 1) {
110
+ return new Promise((resolve) => {
111
+ const ports = [];
112
+ let found = 0;
113
+
114
+ function findNext() {
115
+ if (found >= count) return resolve(ports);
116
+
117
+ const server = net.createServer();
118
+ server.listen(0, '0.0.0.0', () => {
119
+ const port = server.address().port;
120
+ server.close(() => {
121
+ ports.push(port);
122
+ found++;
123
+ findNext();
124
+ });
125
+ });
126
+ }
127
+ findNext();
128
+ });
129
+ }
130
+
131
+ function killProcess(pid, force = false) {
132
+ const platform = os.platform();
133
+ const signal = force ? 'SIGKILL' : 'SIGTERM';
134
+
135
+ try {
136
+ if (platform === 'win32') {
137
+ execSync(`taskkill ${force ? '/F' : ''} /PID ${pid} 2>nul`, { encoding: 'utf-8' });
138
+ } else {
139
+ process.kill(pid, signal);
140
+ }
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ function getDetailedProcessInfo(pid) {
148
+ const platform = os.platform();
149
+ if (platform === 'win32') return null;
150
+
151
+ try {
152
+ const psOut = execSync(
153
+ `ps -p ${pid} -o pid=,ppid=,user=,lstart=,%cpu=,%mem=,rss=,command= 2>/dev/null`,
154
+ { encoding: 'utf-8', timeout: 3000 }
155
+ ).trim();
156
+
157
+ if (!psOut) return null;
158
+
159
+ // Also get the full command with args
160
+ const cmdOut = execSync(`ps -p ${pid} -o args= 2>/dev/null`, {
161
+ encoding: 'utf-8',
162
+ timeout: 3000,
163
+ }).trim();
164
+
165
+ // Get open file count
166
+ let openFiles = 0;
167
+ try {
168
+ const lsofOut = execSync(`lsof -p ${pid} 2>/dev/null | wc -l`, {
169
+ encoding: 'utf-8',
170
+ timeout: 3000,
171
+ }).trim();
172
+ openFiles = parseInt(lsofOut, 10) || 0;
173
+ } catch {}
174
+
175
+ // Get child process count
176
+ let children = [];
177
+ try {
178
+ const childOut = execSync(`pgrep -P ${pid} 2>/dev/null || true`, {
179
+ encoding: 'utf-8',
180
+ timeout: 3000,
181
+ }).trim();
182
+ if (childOut) children = childOut.split('\n').filter(Boolean);
183
+ } catch {}
184
+
185
+ return {
186
+ fullCommand: cmdOut,
187
+ openFiles,
188
+ childCount: children.length,
189
+ childPids: children.map(Number),
190
+ };
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function formatMemory(rssKb) {
197
+ if (rssKb > 1024 * 1024) return `${(rssKb / (1024 * 1024)).toFixed(1)} GB`;
198
+ if (rssKb > 1024) return `${(rssKb / 1024).toFixed(1)} MB`;
199
+ return `${rssKb} KB`;
200
+ }
201
+
202
+ function timeSince(dateStr) {
203
+ if (!dateStr) return '';
204
+ try {
205
+ const start = new Date(dateStr);
206
+ const now = new Date();
207
+ const diffMs = now - start;
208
+ const diffSec = Math.floor(diffMs / 1000);
209
+ const diffMin = Math.floor(diffSec / 60);
210
+ const diffHr = Math.floor(diffMin / 60);
211
+ const diffDay = Math.floor(diffHr / 24);
212
+
213
+ if (diffDay > 0) return `${diffDay}d ${diffHr % 24}h ago`;
214
+ if (diffHr > 0) return `${diffHr}h ${diffMin % 60}m ago`;
215
+ if (diffMin > 0) return `${diffMin}m ago`;
216
+ return `${diffSec}s ago`;
217
+ } catch {
218
+ return '';
219
+ }
220
+ }
221
+
222
+ module.exports = {
223
+ getProcessesOnPorts,
224
+ isPortFree,
225
+ findFreePorts,
226
+ killProcess,
227
+ getDetailedProcessInfo,
228
+ timeSince,
229
+ };