@dprrwt/ports 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/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +165 -0
- package/dist/display.d.ts +32 -0
- package/dist/display.js +205 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/scanner.d.ts +40 -0
- package/dist/scanner.js +251 -0
- package/package.json +46 -0
- package/src/cli.ts +218 -0
- package/src/display.ts +228 -0
- package/src/index.ts +16 -0
- package/src/scanner.ts +282 -0
- package/tsconfig.json +17 -0
package/src/display.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display Layer — Making port data beautiful
|
|
3
|
+
*
|
|
4
|
+
* netstat is powerful but ugly. The information is there — it just needs
|
|
5
|
+
* a human-friendly presentation. Like Da Vinci's anatomical drawings:
|
|
6
|
+
* the same body everyone could see, but rendered with clarity and beauty.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import Table from "cli-table3";
|
|
11
|
+
import type { PortInfo } from "./scanner.js";
|
|
12
|
+
|
|
13
|
+
// Well-known ports that developers care about
|
|
14
|
+
const KNOWN_PORTS: Record<number, string> = {
|
|
15
|
+
80: "HTTP",
|
|
16
|
+
443: "HTTPS",
|
|
17
|
+
3000: "Dev Server",
|
|
18
|
+
3001: "Dev Server",
|
|
19
|
+
3333: "Dev Server",
|
|
20
|
+
4000: "Dev Server",
|
|
21
|
+
4200: "Angular",
|
|
22
|
+
5000: "Flask/Vite",
|
|
23
|
+
5173: "Vite",
|
|
24
|
+
5174: "Vite",
|
|
25
|
+
5432: "PostgreSQL",
|
|
26
|
+
5500: "Live Server",
|
|
27
|
+
6379: "Redis",
|
|
28
|
+
8000: "Django/FastAPI",
|
|
29
|
+
8080: "HTTP Alt",
|
|
30
|
+
8443: "HTTPS Alt",
|
|
31
|
+
8888: "Jupyter",
|
|
32
|
+
9000: "PHP-FPM",
|
|
33
|
+
9090: "Prometheus",
|
|
34
|
+
9229: "Node Debug",
|
|
35
|
+
18791: "OpenClaw",
|
|
36
|
+
18800: "OpenClaw Browser",
|
|
37
|
+
27017: "MongoDB",
|
|
38
|
+
2960: "Mission Control",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Process name to color category
|
|
42
|
+
function getProcessColor(name: string): (text: string) => string {
|
|
43
|
+
const lower = name.toLowerCase();
|
|
44
|
+
if (lower.includes("node") || lower.includes("deno") || lower.includes("bun")) {
|
|
45
|
+
return chalk.green; // JS runtimes — green (your dev servers)
|
|
46
|
+
}
|
|
47
|
+
if (lower.includes("python") || lower.includes("flask") || lower.includes("django")) {
|
|
48
|
+
return chalk.blue; // Python
|
|
49
|
+
}
|
|
50
|
+
if (lower.includes("java") || lower.includes("gradle") || lower.includes("maven")) {
|
|
51
|
+
return chalk.yellow; // Java
|
|
52
|
+
}
|
|
53
|
+
if (lower.includes("docker") || lower.includes("containerd")) {
|
|
54
|
+
return chalk.cyan; // Containers
|
|
55
|
+
}
|
|
56
|
+
if (lower.includes("postgres") || lower.includes("mysql") || lower.includes("mongo") || lower.includes("redis")) {
|
|
57
|
+
return chalk.magenta; // Databases
|
|
58
|
+
}
|
|
59
|
+
if (lower === "system" || lower === "svchost" || lower.includes("system")) {
|
|
60
|
+
return chalk.gray; // System processes
|
|
61
|
+
}
|
|
62
|
+
if (lower === "unknown") {
|
|
63
|
+
return chalk.red; // Unknown — potential concern
|
|
64
|
+
}
|
|
65
|
+
return chalk.white; // Everything else
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getStateColor(state: string): string {
|
|
69
|
+
switch (state) {
|
|
70
|
+
case "LISTENING":
|
|
71
|
+
return chalk.green(state);
|
|
72
|
+
case "ESTABLISHED":
|
|
73
|
+
return chalk.cyan(state);
|
|
74
|
+
case "TIME_WAIT":
|
|
75
|
+
return chalk.gray(state);
|
|
76
|
+
case "CLOSE_WAIT":
|
|
77
|
+
return chalk.yellow(state);
|
|
78
|
+
default:
|
|
79
|
+
return chalk.white(state);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getPortLabel(port: number): string {
|
|
84
|
+
const label = KNOWN_PORTS[port];
|
|
85
|
+
return label ? chalk.dim(` (${label})`) : "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Display a full port table
|
|
90
|
+
*/
|
|
91
|
+
export function displayPortTable(ports: PortInfo[], title?: string): void {
|
|
92
|
+
if (ports.length === 0) {
|
|
93
|
+
console.log(chalk.yellow("\n No ports found.\n"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (title) {
|
|
98
|
+
console.log(chalk.bold(`\n ${title}`));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const table = new Table({
|
|
102
|
+
head: [
|
|
103
|
+
chalk.bold.white("Port"),
|
|
104
|
+
chalk.bold.white("Proto"),
|
|
105
|
+
chalk.bold.white("PID"),
|
|
106
|
+
chalk.bold.white("Process"),
|
|
107
|
+
chalk.bold.white("State"),
|
|
108
|
+
chalk.bold.white("Address"),
|
|
109
|
+
],
|
|
110
|
+
style: {
|
|
111
|
+
head: [],
|
|
112
|
+
border: ["gray"],
|
|
113
|
+
},
|
|
114
|
+
chars: {
|
|
115
|
+
top: "─",
|
|
116
|
+
"top-mid": "┬",
|
|
117
|
+
"top-left": "┌",
|
|
118
|
+
"top-right": "┐",
|
|
119
|
+
bottom: "─",
|
|
120
|
+
"bottom-mid": "┴",
|
|
121
|
+
"bottom-left": "└",
|
|
122
|
+
"bottom-right": "┘",
|
|
123
|
+
left: "│",
|
|
124
|
+
"left-mid": "├",
|
|
125
|
+
mid: "─",
|
|
126
|
+
"mid-mid": "┼",
|
|
127
|
+
right: "│",
|
|
128
|
+
"right-mid": "┤",
|
|
129
|
+
middle: "│",
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
for (const p of ports) {
|
|
134
|
+
const colorFn = getProcessColor(p.processName);
|
|
135
|
+
table.push([
|
|
136
|
+
chalk.bold.white(String(p.port)) + getPortLabel(p.port),
|
|
137
|
+
p.protocol === "UDP" ? chalk.yellow(p.protocol) : chalk.cyan(p.protocol),
|
|
138
|
+
chalk.dim(String(p.pid)),
|
|
139
|
+
colorFn(p.processName),
|
|
140
|
+
getStateColor(p.state),
|
|
141
|
+
chalk.dim(p.localAddress),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(table.toString());
|
|
146
|
+
console.log(chalk.dim(` ${ports.length} port${ports.length === 1 ? "" : "s"} found\n`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Display compact port info (for single port lookup)
|
|
151
|
+
*/
|
|
152
|
+
export function displayPortDetail(port: number, infos: PortInfo[]): void {
|
|
153
|
+
if (infos.length === 0) {
|
|
154
|
+
console.log(chalk.green(`\n ✓ Port ${port} is free\n`));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log(chalk.bold(`\n Port ${port}`) + getPortLabel(port));
|
|
159
|
+
console.log();
|
|
160
|
+
|
|
161
|
+
for (const info of infos) {
|
|
162
|
+
const colorFn = getProcessColor(info.processName);
|
|
163
|
+
console.log(` ${chalk.dim("Process:")} ${colorFn(info.processName)} ${chalk.dim(`(PID ${info.pid})`)}`);
|
|
164
|
+
console.log(` ${chalk.dim("Proto:")} ${info.protocol}`);
|
|
165
|
+
console.log(` ${chalk.dim("State:")} ${getStateColor(info.state)}`);
|
|
166
|
+
console.log(` ${chalk.dim("Address:")} ${info.localAddress}:${port}`);
|
|
167
|
+
if (infos.indexOf(info) < infos.length - 1) {
|
|
168
|
+
console.log(chalk.dim(" ─────────────────────────"));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Display scan range results
|
|
176
|
+
*/
|
|
177
|
+
export function displayScanRange(startPort: number, endPort: number, ports: PortInfo[]): void {
|
|
178
|
+
const total = endPort - startPort + 1;
|
|
179
|
+
const used = new Set(ports.map((p) => p.port)).size;
|
|
180
|
+
const free = total - used;
|
|
181
|
+
|
|
182
|
+
console.log(chalk.bold(`\n Scanning ports ${startPort}-${endPort}`));
|
|
183
|
+
console.log(
|
|
184
|
+
` ${chalk.red(String(used))} in use · ${chalk.green(String(free))} free · ${chalk.dim(String(total) + " total")}`
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (ports.length > 0) {
|
|
188
|
+
displayPortTable(ports);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(chalk.green(`\n ✓ All ${total} ports are free\n`));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Display free port result
|
|
196
|
+
*/
|
|
197
|
+
export function displayFreePort(port: number | null, startPort: number): void {
|
|
198
|
+
if (port === null) {
|
|
199
|
+
console.log(chalk.red(`\n ✗ No free ports found starting from ${startPort}\n`));
|
|
200
|
+
} else {
|
|
201
|
+
console.log(chalk.green(`\n ✓ Next free port: ${chalk.bold.white(String(port))}\n`));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Display kill result
|
|
207
|
+
*/
|
|
208
|
+
export function displayKillResult(port: number, pid: number, processName: string, success: boolean): void {
|
|
209
|
+
if (success) {
|
|
210
|
+
console.log(
|
|
211
|
+
chalk.green(`\n ✓ Killed ${processName} (PID ${pid}) on port ${port}\n`)
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
console.log(
|
|
215
|
+
chalk.red(`\n ✗ Failed to kill ${processName} (PID ${pid}) on port ${port}`)
|
|
216
|
+
);
|
|
217
|
+
console.log(chalk.dim(" Try running with --force or as administrator\n"));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Display legend
|
|
223
|
+
*/
|
|
224
|
+
export function displayLegend(): void {
|
|
225
|
+
console.log(chalk.dim(" Legend:"));
|
|
226
|
+
console.log(` ${chalk.green("■")} JS Runtime ${chalk.blue("■")} Python ${chalk.yellow("■")} Java ${chalk.cyan("■")} Container ${chalk.magenta("■")} Database ${chalk.gray("■")} System ${chalk.red("■")} Unknown`);
|
|
227
|
+
console.log();
|
|
228
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dprrwt/ports — Programmatic API
|
|
3
|
+
*
|
|
4
|
+
* Use the CLI for interactive use, or import these functions
|
|
5
|
+
* for programmatic port management in your Node.js scripts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
getListeningPorts,
|
|
10
|
+
getPortInfo,
|
|
11
|
+
isPortInUse,
|
|
12
|
+
findFreePort,
|
|
13
|
+
scanRange,
|
|
14
|
+
killProcess,
|
|
15
|
+
type PortInfo,
|
|
16
|
+
} from "./scanner.js";
|
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port Scanner — Cross-platform port detection
|
|
3
|
+
*
|
|
4
|
+
* Da Vinci's curiosity: How do operating systems track network connections?
|
|
5
|
+
* Windows uses netstat, macOS/Linux use ss/lsof. Different tools, same truth.
|
|
6
|
+
* The abstraction layer here unifies them — like translating between languages
|
|
7
|
+
* to find the same underlying meaning.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from "node:child_process";
|
|
11
|
+
import { platform } from "node:os";
|
|
12
|
+
|
|
13
|
+
export interface PortInfo {
|
|
14
|
+
port: number;
|
|
15
|
+
pid: number;
|
|
16
|
+
protocol: "TCP" | "UDP";
|
|
17
|
+
state: string;
|
|
18
|
+
localAddress: string;
|
|
19
|
+
processName: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get all listening ports on the system
|
|
24
|
+
*/
|
|
25
|
+
export function getListeningPorts(): PortInfo[] {
|
|
26
|
+
const os = platform();
|
|
27
|
+
if (os === "win32") {
|
|
28
|
+
return getPortsWindows();
|
|
29
|
+
}
|
|
30
|
+
return getPortsUnix();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get info for a specific port
|
|
35
|
+
*/
|
|
36
|
+
export function getPortInfo(port: number): PortInfo[] {
|
|
37
|
+
return getListeningPorts().filter((p) => p.port === port);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if a port is in use
|
|
42
|
+
*/
|
|
43
|
+
export function isPortInUse(port: number): boolean {
|
|
44
|
+
return getPortInfo(port).length > 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find the next free port starting from a given port
|
|
49
|
+
*/
|
|
50
|
+
export function findFreePort(startPort: number = 3000, endPort: number = 65535): number | null {
|
|
51
|
+
const usedPorts = new Set(getListeningPorts().map((p) => p.port));
|
|
52
|
+
for (let port = startPort; port <= endPort; port++) {
|
|
53
|
+
if (!usedPorts.has(port)) {
|
|
54
|
+
return port;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Scan a range of ports and return which are in use
|
|
62
|
+
*/
|
|
63
|
+
export function scanRange(startPort: number, endPort: number): PortInfo[] {
|
|
64
|
+
const allPorts = getListeningPorts();
|
|
65
|
+
return allPorts.filter((p) => p.port >= startPort && p.port <= endPort);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Kill a process by PID
|
|
70
|
+
*/
|
|
71
|
+
export function killProcess(pid: number, force: boolean = false): boolean {
|
|
72
|
+
try {
|
|
73
|
+
const os = platform();
|
|
74
|
+
if (os === "win32") {
|
|
75
|
+
execSync(`taskkill ${force ? "/F" : ""} /PID ${pid}`, { stdio: "pipe" });
|
|
76
|
+
} else {
|
|
77
|
+
execSync(`kill ${force ? "-9" : "-15"} ${pid}`, { stdio: "pipe" });
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get process name by PID
|
|
87
|
+
*/
|
|
88
|
+
function getProcessName(pid: number): string {
|
|
89
|
+
if (pid === 0) return "System";
|
|
90
|
+
try {
|
|
91
|
+
const os = platform();
|
|
92
|
+
if (os === "win32") {
|
|
93
|
+
const result = execSync(
|
|
94
|
+
`powershell -Command "(Get-Process -Id ${pid} -ErrorAction SilentlyContinue).ProcessName"`,
|
|
95
|
+
{ stdio: "pipe", encoding: "utf-8", timeout: 5000 }
|
|
96
|
+
).trim();
|
|
97
|
+
return result || "unknown";
|
|
98
|
+
} else {
|
|
99
|
+
const result = execSync(`ps -p ${pid} -o comm= 2>/dev/null`, {
|
|
100
|
+
stdio: "pipe",
|
|
101
|
+
encoding: "utf-8",
|
|
102
|
+
timeout: 5000,
|
|
103
|
+
}).trim();
|
|
104
|
+
return result || "unknown";
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
return "unknown";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Cache process names to avoid repeated lookups
|
|
112
|
+
const processNameCache = new Map<number, string>();
|
|
113
|
+
|
|
114
|
+
function getCachedProcessName(pid: number): string {
|
|
115
|
+
if (processNameCache.has(pid)) {
|
|
116
|
+
return processNameCache.get(pid)!;
|
|
117
|
+
}
|
|
118
|
+
const name = getProcessName(pid);
|
|
119
|
+
processNameCache.set(pid, name);
|
|
120
|
+
return name;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Windows: Parse netstat -ano output
|
|
125
|
+
*
|
|
126
|
+
* Interesting: Windows netstat shows ALL connection states by default,
|
|
127
|
+
* while Unix ss needs flags. Different philosophies — Windows shows everything,
|
|
128
|
+
* Unix shows what you ask for.
|
|
129
|
+
*/
|
|
130
|
+
function getPortsWindows(): PortInfo[] {
|
|
131
|
+
try {
|
|
132
|
+
const output = execSync("netstat -ano -p TCP", {
|
|
133
|
+
stdio: "pipe",
|
|
134
|
+
encoding: "utf-8",
|
|
135
|
+
timeout: 10000,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const ports: PortInfo[] = [];
|
|
139
|
+
const lines = output.split("\n");
|
|
140
|
+
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const trimmed = line.trim();
|
|
143
|
+
if (!trimmed || trimmed.startsWith("Active") || trimmed.startsWith("Proto")) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 1234
|
|
148
|
+
// TCP 127.0.0.1:8080 0.0.0.0:0 LISTENING 5678
|
|
149
|
+
const parts = trimmed.split(/\s+/);
|
|
150
|
+
if (parts.length < 5) continue;
|
|
151
|
+
|
|
152
|
+
const [proto, localAddr, , state, pidStr] = parts;
|
|
153
|
+
if (!proto || !localAddr || !state) continue;
|
|
154
|
+
|
|
155
|
+
// Only care about LISTENING and ESTABLISHED
|
|
156
|
+
if (state !== "LISTENING" && state !== "ESTABLISHED") continue;
|
|
157
|
+
|
|
158
|
+
const lastColon = localAddr.lastIndexOf(":");
|
|
159
|
+
if (lastColon === -1) continue;
|
|
160
|
+
|
|
161
|
+
const port = parseInt(localAddr.substring(lastColon + 1), 10);
|
|
162
|
+
const pid = parseInt(pidStr, 10);
|
|
163
|
+
|
|
164
|
+
if (isNaN(port) || isNaN(pid)) continue;
|
|
165
|
+
|
|
166
|
+
ports.push({
|
|
167
|
+
port,
|
|
168
|
+
pid,
|
|
169
|
+
protocol: proto.toUpperCase() as "TCP" | "UDP",
|
|
170
|
+
state,
|
|
171
|
+
localAddress: localAddr.substring(0, lastColon),
|
|
172
|
+
processName: getCachedProcessName(pid),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Also grab UDP
|
|
177
|
+
try {
|
|
178
|
+
const udpOutput = execSync("netstat -ano -p UDP", {
|
|
179
|
+
stdio: "pipe",
|
|
180
|
+
encoding: "utf-8",
|
|
181
|
+
timeout: 10000,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
for (const line of udpOutput.split("\n")) {
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
if (!trimmed || trimmed.startsWith("Active") || trimmed.startsWith("Proto")) continue;
|
|
187
|
+
|
|
188
|
+
const parts = trimmed.split(/\s+/);
|
|
189
|
+
if (parts.length < 4) continue;
|
|
190
|
+
|
|
191
|
+
const [proto, localAddr] = parts;
|
|
192
|
+
const pidStr = parts[parts.length - 1];
|
|
193
|
+
|
|
194
|
+
if (!proto?.toUpperCase().startsWith("UDP")) continue;
|
|
195
|
+
|
|
196
|
+
const lastColon = localAddr.lastIndexOf(":");
|
|
197
|
+
if (lastColon === -1) continue;
|
|
198
|
+
|
|
199
|
+
const port = parseInt(localAddr.substring(lastColon + 1), 10);
|
|
200
|
+
const pid = parseInt(pidStr, 10);
|
|
201
|
+
|
|
202
|
+
if (isNaN(port) || isNaN(pid)) continue;
|
|
203
|
+
|
|
204
|
+
ports.push({
|
|
205
|
+
port,
|
|
206
|
+
pid,
|
|
207
|
+
protocol: "UDP",
|
|
208
|
+
state: "*",
|
|
209
|
+
localAddress: localAddr.substring(0, lastColon),
|
|
210
|
+
processName: getCachedProcessName(pid),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// UDP scan failed — not critical
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return deduplicatePorts(ports);
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Unix: Parse ss or lsof output
|
|
225
|
+
*/
|
|
226
|
+
function getPortsUnix(): PortInfo[] {
|
|
227
|
+
try {
|
|
228
|
+
// Try ss first (faster, more modern)
|
|
229
|
+
const output = execSync("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null", {
|
|
230
|
+
stdio: "pipe",
|
|
231
|
+
encoding: "utf-8",
|
|
232
|
+
timeout: 10000,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const ports: PortInfo[] = [];
|
|
236
|
+
const lines = output.split("\n");
|
|
237
|
+
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
const trimmed = line.trim();
|
|
240
|
+
if (!trimmed || trimmed.startsWith("State") || trimmed.startsWith("Proto")) continue;
|
|
241
|
+
|
|
242
|
+
// ss format: LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("node",pid=1234,fd=18))
|
|
243
|
+
const pidMatch = trimmed.match(/pid=(\d+)/);
|
|
244
|
+
const addrMatch = trimmed.match(/\s([\d.*:[\]]+):(\d+)\s/);
|
|
245
|
+
|
|
246
|
+
if (addrMatch && pidMatch) {
|
|
247
|
+
const port = parseInt(addrMatch[2], 10);
|
|
248
|
+
const pid = parseInt(pidMatch[1], 10);
|
|
249
|
+
|
|
250
|
+
if (!isNaN(port) && !isNaN(pid)) {
|
|
251
|
+
ports.push({
|
|
252
|
+
port,
|
|
253
|
+
pid,
|
|
254
|
+
protocol: "TCP",
|
|
255
|
+
state: "LISTEN",
|
|
256
|
+
localAddress: addrMatch[1],
|
|
257
|
+
processName: getCachedProcessName(pid),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return deduplicatePorts(ports);
|
|
264
|
+
} catch {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Remove duplicate port entries (keep LISTENING over ESTABLISHED)
|
|
271
|
+
*/
|
|
272
|
+
function deduplicatePorts(ports: PortInfo[]): PortInfo[] {
|
|
273
|
+
const seen = new Map<string, PortInfo>();
|
|
274
|
+
for (const p of ports) {
|
|
275
|
+
const key = `${p.protocol}:${p.port}:${p.pid}`;
|
|
276
|
+
const existing = seen.get(key);
|
|
277
|
+
if (!existing || (p.state === "LISTENING" && existing.state !== "LISTENING")) {
|
|
278
|
+
seen.set(key, p);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return Array.from(seen.values()).sort((a, b) => a.port - b.port);
|
|
282
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|