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 +21 -0
- package/README.md +99 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +59 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +176 -0
- package/package.json +49 -0
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
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);
|
package/dist/index.d.ts
ADDED
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
|
+
}
|