@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/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
+ }