zport 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dogita
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,99 @@
1
+ # zport
2
+
3
+ Kill processes running on specified ports. Fast, zero-dependency, cross-platform.
4
+
5
+ ## Why zport?
6
+
7
+ Existing tools like `kill-port` shell out to `lsof -i -P` and parse the entire connection table with a loose regex — which means they miss processes, false-match adjacent port numbers, and fail silently on Linux when `lsof` isn't installed.
8
+
9
+ **zport** fixes all of that:
10
+
11
+ - **Linux** — uses `fuser` (fast, exact match) → falls back to `ss` → then `lsof`
12
+ - **macOS** — uses `lsof -ti` with exact port + LISTEN filter
13
+ - **Windows** — uses `netstat -ano` with exact port + LISTENING match → `taskkill`
14
+ - **Zero dependencies** — just Node.js `child_process.execFile`, no shell spawning
15
+ - **TypeScript** — fully typed, ships with declarations
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm i -g zport
21
+ ```
22
+
23
+ Or run directly without installing:
24
+
25
+ ```bash
26
+ npx zport 3000
27
+ bunx zport 3000
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### CLI
33
+
34
+ ```bash
35
+ # Kill a single port
36
+ zport 3000
37
+
38
+ # Kill multiple ports
39
+ zport 3000 3001 8080
40
+
41
+ # Comma-separated
42
+ zport 3000,3001,8080
43
+
44
+ # UDP instead of TCP
45
+ zport 5353 --method udp
46
+ zport 5353 -m udp
47
+ ```
48
+
49
+ Output:
50
+
51
+ ```
52
+ ✔ :3000 — killed pid 12345
53
+ ✔ :3001 — killed pids 12346, 12347
54
+ ✘ :8080 — No process on this port
55
+ ```
56
+
57
+ ### Programmatic
58
+
59
+ ```ts
60
+ import { kill } from "zport";
61
+
62
+ const result = await kill(3000);
63
+ // { port: 3000, pids: [12345], killed: true }
64
+
65
+ const result2 = await kill(9999);
66
+ // { port: 9999, pids: [], killed: false, error: "No process on this port" }
67
+ ```
68
+
69
+ #### `kill(port, method?)`
70
+
71
+ | Param | Type | Default | Description |
72
+ | -------- | ---------------- | ------- | ---------------------- |
73
+ | `port` | `number` | — | Port number (1–65535) |
74
+ | `method` | `"tcp" \| "udp"` | `"tcp"` | Protocol to match |
75
+
76
+ Returns `Promise<KillResult>`:
77
+
78
+ ```ts
79
+ interface KillResult {
80
+ port: number;
81
+ pids: number[];
82
+ killed: boolean;
83
+ error?: string;
84
+ }
85
+ ```
86
+
87
+ ## How it works
88
+
89
+ | OS | Find PIDs | Kill |
90
+ | ------- | ---------------------------------------------- | -------------- |
91
+ | Linux | `fuser {port}/tcp` → `ss -tlnp` → `lsof -ti` | `SIGKILL` |
92
+ | macOS | `lsof -iTCP:{port} -t -sTCP:LISTEN` | `SIGKILL` |
93
+ | Windows | `netstat -ano` (exact port + LISTENING) | `taskkill /F` |
94
+
95
+ On Linux, `fuser` is tried first because it directly queries the kernel's socket table — no parsing, no regex, no false matches. If `fuser` isn't available (minimal containers), it falls back to `ss`, then `lsof`.
96
+
97
+ ## License
98
+
99
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { kill } from "./index.js";
3
+ const args = process.argv.slice(2);
4
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
5
+ console.log(`
6
+ zport — kill processes on ports, fast
7
+
8
+ Usage
9
+ $ zport <port> [port ...]
10
+ $ zport 3000
11
+ $ zport 3000 3001 8080
12
+
13
+ Options
14
+ --method, -m Protocol: tcp (default) or udp
15
+ --help, -h Show this help
16
+ --version, -v Show version
17
+ `);
18
+ process.exit(0);
19
+ }
20
+ if (args.includes("--version") || args.includes("-v")) {
21
+ console.log("1.0.0");
22
+ process.exit(0);
23
+ }
24
+ let method = "tcp";
25
+ const ports = [];
26
+ for (let i = 0; i < args.length; i++) {
27
+ const arg = args[i];
28
+ if (arg === "--method" || arg === "-m") {
29
+ const next = args[++i];
30
+ if (next === "udp")
31
+ method = "udp";
32
+ continue;
33
+ }
34
+ if (arg.startsWith("-"))
35
+ continue;
36
+ // Support comma-separated: 3000,3001
37
+ for (const part of arg.split(",")) {
38
+ const n = parseInt(part, 10);
39
+ if (n > 0 && n <= 65535)
40
+ ports.push(n);
41
+ }
42
+ }
43
+ if (ports.length === 0) {
44
+ console.error("No valid ports provided");
45
+ process.exit(1);
46
+ }
47
+ const results = await Promise.all(ports.map((p) => kill(p, method)));
48
+ let hasError = false;
49
+ for (const r of results) {
50
+ if (r.killed) {
51
+ const pidStr = r.pids.length > 1 ? `pids ${r.pids.join(", ")}` : `pid ${r.pids[0]}`;
52
+ console.log(`✔ :${r.port} — killed ${pidStr}`);
53
+ }
54
+ else {
55
+ console.log(`✘ :${r.port} — ${r.error}`);
56
+ hasError = true;
57
+ }
58
+ }
59
+ process.exit(hasError && results.every((r) => !r.killed) ? 1 : 0);
@@ -0,0 +1,7 @@
1
+ export interface KillResult {
2
+ port: number;
3
+ pids: number[];
4
+ killed: boolean;
5
+ error?: string;
6
+ }
7
+ export declare function kill(port: number, method?: "tcp" | "udp"): Promise<KillResult>;
package/dist/index.js ADDED
@@ -0,0 +1,176 @@
1
+ import { execFile } from "node:child_process";
2
+ function exec(cmd, args) {
3
+ return new Promise((resolve, reject) => {
4
+ execFile(cmd, args, { timeout: 5000 }, (err, stdout, stderr) => {
5
+ if (err) {
6
+ if ("stdout" in err) {
7
+ resolve({ stdout: stdout || "", stderr: stderr || "" });
8
+ }
9
+ else {
10
+ reject(err);
11
+ }
12
+ }
13
+ else {
14
+ resolve({ stdout: stdout || "", stderr: stderr || "" });
15
+ }
16
+ });
17
+ });
18
+ }
19
+ function parsePids(raw) {
20
+ const seen = new Set();
21
+ for (const token of raw.split(/[\s,]+/)) {
22
+ const n = parseInt(token, 10);
23
+ if (n > 0)
24
+ seen.add(n);
25
+ }
26
+ return [...seen];
27
+ }
28
+ async function findPidsLinux(port, method) {
29
+ // Try fuser first — fast, reliable, works even when lsof doesn't
30
+ try {
31
+ const proto = method === "udp" ? "udp" : "tcp";
32
+ const { stdout } = await exec("fuser", [`${port}/${proto}`]);
33
+ const pids = parsePids(stdout);
34
+ if (pids.length > 0)
35
+ return pids;
36
+ }
37
+ catch {
38
+ // fuser not available or no match, fall through
39
+ }
40
+ // Fallback: ss + awk to find pids from socket listing
41
+ try {
42
+ const flag = method === "udp" ? "-ulnp" : "-tlnp";
43
+ const { stdout } = await exec("ss", [flag, "sport", "=", `:${port}`]);
44
+ const pids = [];
45
+ const pidRegex = /pid=(\d+)/g;
46
+ let match;
47
+ while ((match = pidRegex.exec(stdout)) !== null) {
48
+ const n = parseInt(match[1], 10);
49
+ if (n > 0)
50
+ pids.push(n);
51
+ }
52
+ if (pids.length > 0)
53
+ return [...new Set(pids)];
54
+ }
55
+ catch {
56
+ // ss not available, fall through
57
+ }
58
+ // Last resort: lsof with exact port match
59
+ try {
60
+ const proto = method === "udp" ? "UDP" : "TCP";
61
+ const { stdout } = await exec("lsof", [
62
+ `-i${proto}:${port}`,
63
+ "-t",
64
+ "-sTCP:LISTEN",
65
+ ]);
66
+ return parsePids(stdout);
67
+ }
68
+ catch {
69
+ return [];
70
+ }
71
+ }
72
+ async function findPidsDarwin(port, method) {
73
+ try {
74
+ const proto = method === "udp" ? "UDP" : "TCP";
75
+ const { stdout } = await exec("lsof", [
76
+ `-i${proto}:${port}`,
77
+ "-t",
78
+ "-sTCP:LISTEN",
79
+ ]);
80
+ return parsePids(stdout);
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
86
+ async function findPidsWindows(port, method) {
87
+ try {
88
+ const { stdout } = await exec("netstat", ["-ano"]);
89
+ const proto = method === "udp" ? "UDP" : "TCP";
90
+ const pids = new Set();
91
+ for (const line of stdout.split("\n")) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed.startsWith(proto))
94
+ continue;
95
+ // Match exact port — columns: Proto LocalAddr ForeignAddr State PID
96
+ const parts = trimmed.split(/\s+/);
97
+ if (parts.length < 5)
98
+ continue;
99
+ const localAddr = parts[1];
100
+ const localPort = localAddr.split(":").pop();
101
+ if (localPort !== String(port))
102
+ continue;
103
+ const state = parts[3];
104
+ if (method === "tcp" && state !== "LISTENING")
105
+ continue;
106
+ const pid = parseInt(parts[parts.length - 1], 10);
107
+ if (pid > 0)
108
+ pids.add(pid);
109
+ }
110
+ return [...pids];
111
+ }
112
+ catch {
113
+ return [];
114
+ }
115
+ }
116
+ async function killPids(pids) {
117
+ if (pids.length === 0)
118
+ return;
119
+ if (process.platform === "win32") {
120
+ const args = pids.flatMap((pid) => ["/F", "/PID", String(pid)]);
121
+ await exec("taskkill", args);
122
+ }
123
+ else {
124
+ // SIGKILL — force kill, don't wait for graceful shutdown
125
+ for (const pid of pids) {
126
+ try {
127
+ process.kill(pid, "SIGKILL");
128
+ }
129
+ catch {
130
+ // already dead, fine
131
+ }
132
+ }
133
+ }
134
+ }
135
+ export async function kill(port, method = "tcp") {
136
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
137
+ return { port, pids: [], killed: false, error: "Invalid port number" };
138
+ }
139
+ let pids;
140
+ try {
141
+ switch (process.platform) {
142
+ case "win32":
143
+ pids = await findPidsWindows(port, method);
144
+ break;
145
+ case "darwin":
146
+ pids = await findPidsDarwin(port, method);
147
+ break;
148
+ default:
149
+ pids = await findPidsLinux(port, method);
150
+ break;
151
+ }
152
+ }
153
+ catch {
154
+ return {
155
+ port,
156
+ pids: [],
157
+ killed: false,
158
+ error: "Failed to find processes",
159
+ };
160
+ }
161
+ if (pids.length === 0) {
162
+ return { port, pids: [], killed: false, error: "No process on this port" };
163
+ }
164
+ try {
165
+ await killPids(pids);
166
+ return { port, pids, killed: true };
167
+ }
168
+ catch {
169
+ return {
170
+ port,
171
+ pids,
172
+ killed: false,
173
+ error: "Failed to kill processes",
174
+ };
175
+ }
176
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "zport",
3
+ "version": "1.0.0",
4
+ "description": "Kill processes running on specified ports. Fast, zero-dependency, cross-platform.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "zport": "dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "tsc"
23
+ },
24
+ "keywords": [
25
+ "port",
26
+ "kill",
27
+ "process",
28
+ "kill-port",
29
+ "free-port",
30
+ "zport",
31
+ "cli",
32
+ "cross-platform"
33
+ ],
34
+ "author": "Dogita",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/itorz7/zport.git"
39
+ },
40
+ "homepage": "https://github.com/itorz7/zport",
41
+ "bugs": "https://github.com/itorz7/zport/issues",
42
+ "engines": {
43
+ "node": ">=18"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^25.7.0",
47
+ "typescript": "^5.8.0"
48
+ }
49
+ }