portwiz 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 +221 -0
- package/dist/__tests__/commands/dev.test.d.ts +1 -0
- package/dist/__tests__/commands/dev.test.js +56 -0
- package/dist/__tests__/commands/doctor.test.d.ts +1 -0
- package/dist/__tests__/commands/doctor.test.js +87 -0
- package/dist/__tests__/commands/free.test.d.ts +1 -0
- package/dist/__tests__/commands/free.test.js +89 -0
- package/dist/__tests__/commands/switch.test.d.ts +1 -0
- package/dist/__tests__/commands/switch.test.js +39 -0
- package/dist/__tests__/platform/detector.test.d.ts +1 -0
- package/dist/__tests__/platform/detector.test.js +64 -0
- package/dist/__tests__/platform/killer.test.d.ts +1 -0
- package/dist/__tests__/platform/killer.test.js +64 -0
- package/dist/__tests__/utils/exec.test.d.ts +1 -0
- package/dist/__tests__/utils/exec.test.js +22 -0
- package/dist/__tests__/utils/ports.test.d.ts +1 -0
- package/dist/__tests__/utils/ports.test.js +66 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +69 -0
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.js +28 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +43 -0
- package/dist/commands/free.d.ts +5 -0
- package/dist/commands/free.js +54 -0
- package/dist/commands/switch.d.ts +1 -0
- package/dist/commands/switch.js +14 -0
- package/dist/platform/detector.d.ts +3 -0
- package/dist/platform/detector.js +96 -0
- package/dist/platform/killer.d.ts +2 -0
- package/dist/platform/killer.js +36 -0
- package/dist/platform/types.d.ts +11 -0
- package/dist/platform/types.js +1 -0
- package/dist/ui/output.d.ts +7 -0
- package/dist/ui/output.js +29 -0
- package/dist/ui/prompts.d.ts +2 -0
- package/dist/ui/prompts.js +17 -0
- package/dist/utils/exec.d.ts +4 -0
- package/dist/utils/exec.js +12 -0
- package/dist/utils/ports.d.ts +3 -0
- package/dist/utils/ports.js +28 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { validatePort, findNextFreePort, COMMON_DEV_PORTS } from "../../utils/ports.js";
|
|
4
|
+
describe("validatePort", () => {
|
|
5
|
+
it("parses a valid port number", () => {
|
|
6
|
+
expect(validatePort("3000")).toBe(3000);
|
|
7
|
+
expect(validatePort("1")).toBe(1);
|
|
8
|
+
expect(validatePort("65535")).toBe(65535);
|
|
9
|
+
expect(validatePort("8080")).toBe(8080);
|
|
10
|
+
});
|
|
11
|
+
it("throws for non-numeric input", () => {
|
|
12
|
+
expect(() => validatePort("abc")).toThrow("Invalid port");
|
|
13
|
+
expect(() => validatePort("")).toThrow("Invalid port");
|
|
14
|
+
expect(() => validatePort("foo3000")).toThrow("Invalid port");
|
|
15
|
+
});
|
|
16
|
+
it("throws for out-of-range ports", () => {
|
|
17
|
+
expect(() => validatePort("0")).toThrow("Invalid port");
|
|
18
|
+
expect(() => validatePort("-1")).toThrow("Invalid port");
|
|
19
|
+
expect(() => validatePort("65536")).toThrow("Invalid port");
|
|
20
|
+
expect(() => validatePort("100000")).toThrow("Invalid port");
|
|
21
|
+
});
|
|
22
|
+
it("throws for floating point numbers", () => {
|
|
23
|
+
// parseInt("3.5") returns 3, which is valid — this is acceptable behavior
|
|
24
|
+
expect(validatePort("3.5")).toBe(3);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("findNextFreePort", () => {
|
|
28
|
+
it("returns the given port if it is free", async () => {
|
|
29
|
+
const port = await findNextFreePort(49152);
|
|
30
|
+
expect(port).toBeGreaterThanOrEqual(49152);
|
|
31
|
+
expect(port).toBeLessThanOrEqual(65535);
|
|
32
|
+
});
|
|
33
|
+
it("skips a port that is in use and finds the next one", async () => {
|
|
34
|
+
// Occupy a port
|
|
35
|
+
const server = createServer();
|
|
36
|
+
const occupiedPort = await new Promise((resolve) => {
|
|
37
|
+
server.listen(0, () => {
|
|
38
|
+
const addr = server.address();
|
|
39
|
+
if (addr && typeof addr !== "string") {
|
|
40
|
+
resolve(addr.port);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
try {
|
|
45
|
+
const freePort = await findNextFreePort(occupiedPort);
|
|
46
|
+
expect(freePort).toBeGreaterThan(occupiedPort);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
server.close();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("COMMON_DEV_PORTS", () => {
|
|
54
|
+
it("contains well-known development ports", () => {
|
|
55
|
+
expect(COMMON_DEV_PORTS).toContain(3000);
|
|
56
|
+
expect(COMMON_DEV_PORTS).toContain(5173);
|
|
57
|
+
expect(COMMON_DEV_PORTS).toContain(8080);
|
|
58
|
+
expect(COMMON_DEV_PORTS.length).toBeGreaterThan(0);
|
|
59
|
+
});
|
|
60
|
+
it("contains only valid port numbers", () => {
|
|
61
|
+
for (const port of COMMON_DEV_PORTS) {
|
|
62
|
+
expect(port).toBeGreaterThanOrEqual(1);
|
|
63
|
+
expect(port).toBeLessThanOrEqual(65535);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { freePort } from "./commands/free.js";
|
|
4
|
+
import { switchPort } from "./commands/switch.js";
|
|
5
|
+
import { devMode } from "./commands/dev.js";
|
|
6
|
+
import { doctor } from "./commands/doctor.js";
|
|
7
|
+
import { error } from "./ui/output.js";
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name("portwiz")
|
|
11
|
+
.description("Fix port conflicts instantly and run your dev server without interruptions")
|
|
12
|
+
.version("1.0.0")
|
|
13
|
+
.enablePositionalOptions();
|
|
14
|
+
// Default command: portwiz <port>
|
|
15
|
+
program
|
|
16
|
+
.argument("[port]", "port number to check/free")
|
|
17
|
+
.option("-f, --force", "kill process without confirmation")
|
|
18
|
+
.option("-s, --switch", "find next available port instead of killing")
|
|
19
|
+
.action(async (port, options) => {
|
|
20
|
+
if (!port) {
|
|
21
|
+
program.help();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
if (options.switch) {
|
|
26
|
+
await switchPort(port);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
await freePort(port, options);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
error(err.message);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
// Dev command: portwiz dev <port> -- <cmd>
|
|
38
|
+
program
|
|
39
|
+
.command("dev")
|
|
40
|
+
.description("free a port and start your dev server")
|
|
41
|
+
.argument("<port>", "port number to free")
|
|
42
|
+
.argument("<cmd...>", "command to run after freeing the port")
|
|
43
|
+
.option("-f, --force", "kill process without confirmation")
|
|
44
|
+
.passThroughOptions()
|
|
45
|
+
.action(async (port, cmd, options) => {
|
|
46
|
+
try {
|
|
47
|
+
await devMode(port, cmd, options);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
error(err.message);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Doctor command: portwiz doctor
|
|
55
|
+
program
|
|
56
|
+
.command("doctor")
|
|
57
|
+
.description("scan common dev ports and report status")
|
|
58
|
+
.option("-f, --force", "kill all busy processes without confirmation")
|
|
59
|
+
.option("--ports <list>", "custom comma-separated port list")
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
try {
|
|
62
|
+
await doctor(options);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
error(err.message);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
program.parse();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { ensurePortFree } from "./free.js";
|
|
3
|
+
import { info, error } from "../ui/output.js";
|
|
4
|
+
import { validatePort } from "../utils/ports.js";
|
|
5
|
+
export async function devMode(portStr, command, options) {
|
|
6
|
+
const port = validatePort(portStr);
|
|
7
|
+
const freed = await ensurePortFree(port, !!options.force);
|
|
8
|
+
if (!freed) {
|
|
9
|
+
error("Cannot start — port is still in use");
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const cmd = command.join(" ");
|
|
14
|
+
info(`Starting: ${cmd}`);
|
|
15
|
+
const child = spawn(cmd, {
|
|
16
|
+
stdio: "inherit",
|
|
17
|
+
shell: true,
|
|
18
|
+
env: { ...process.env, PORT: String(port) },
|
|
19
|
+
});
|
|
20
|
+
const forwardSignal = (signal) => {
|
|
21
|
+
child.kill(signal);
|
|
22
|
+
};
|
|
23
|
+
process.on("SIGINT", forwardSignal);
|
|
24
|
+
process.on("SIGTERM", forwardSignal);
|
|
25
|
+
child.on("exit", (code) => {
|
|
26
|
+
process.exitCode = code ?? 1;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getPortStatuses } from "../platform/detector.js";
|
|
2
|
+
import { killAndVerify } from "../platform/killer.js";
|
|
3
|
+
import { info, success, warn, error, showPortTable } from "../ui/output.js";
|
|
4
|
+
import { confirmKillAll } from "../ui/prompts.js";
|
|
5
|
+
import { COMMON_DEV_PORTS } from "../utils/ports.js";
|
|
6
|
+
export async function doctor(options) {
|
|
7
|
+
const ports = options.ports
|
|
8
|
+
? options.ports.split(",").map((p) => parseInt(p.trim(), 10))
|
|
9
|
+
: COMMON_DEV_PORTS;
|
|
10
|
+
info("Scanning development ports...");
|
|
11
|
+
const statuses = await getPortStatuses(ports);
|
|
12
|
+
showPortTable(statuses);
|
|
13
|
+
const busy = statuses.filter((s) => s.inUse && s.process);
|
|
14
|
+
if (busy.length === 0) {
|
|
15
|
+
success("All ports are free");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
warn(`${busy.length} port${busy.length > 1 ? "s are" : " is"} in use`);
|
|
19
|
+
if (!options.force) {
|
|
20
|
+
const confirmed = await confirmKillAll(busy.length);
|
|
21
|
+
if (!confirmed) {
|
|
22
|
+
info("Aborted");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let allFreed = true;
|
|
27
|
+
for (const s of busy) {
|
|
28
|
+
const proc = s.process;
|
|
29
|
+
info(`Killing ${proc.name} (PID ${proc.pid}) on port ${s.port}...`);
|
|
30
|
+
const freed = await killAndVerify(proc.pid, s.port, !!options.force);
|
|
31
|
+
if (!freed) {
|
|
32
|
+
error(`Failed to free port ${s.port}`);
|
|
33
|
+
allFreed = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (allFreed) {
|
|
37
|
+
success("All ports are now free");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
warn("Some ports could not be freed. Try running with administrator/sudo privileges.");
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function freePort(portStr: string, options: {
|
|
2
|
+
force?: boolean;
|
|
3
|
+
}): Promise<void>;
|
|
4
|
+
/** Shared helper: free a port silently, returns true if freed or already free */
|
|
5
|
+
export declare function ensurePortFree(port: number, force: boolean): Promise<boolean>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getProcessOnPort } from "../platform/detector.js";
|
|
2
|
+
import { killAndVerify } from "../platform/killer.js";
|
|
3
|
+
import { info, success, warn, error, showProcess } from "../ui/output.js";
|
|
4
|
+
import { confirmKill } from "../ui/prompts.js";
|
|
5
|
+
import { validatePort } from "../utils/ports.js";
|
|
6
|
+
export async function freePort(portStr, options) {
|
|
7
|
+
const port = validatePort(portStr);
|
|
8
|
+
info(`Checking port ${port}...`);
|
|
9
|
+
const proc = await getProcessOnPort(port);
|
|
10
|
+
if (!proc) {
|
|
11
|
+
success(`Port ${port} is already free`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
warn(`Port ${port} is in use`);
|
|
15
|
+
showProcess(proc);
|
|
16
|
+
if (!options.force) {
|
|
17
|
+
const confirmed = await confirmKill(proc.name, proc.pid);
|
|
18
|
+
if (!confirmed) {
|
|
19
|
+
info("Aborted");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
info(`Killing ${proc.name} (PID ${proc.pid})...`);
|
|
24
|
+
const freed = await killAndVerify(proc.pid, port, options.force);
|
|
25
|
+
if (freed) {
|
|
26
|
+
success(`Port ${port} is now free`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
error(`Failed to free port ${port}. Try running with administrator/sudo privileges.`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Shared helper: free a port silently, returns true if freed or already free */
|
|
34
|
+
export async function ensurePortFree(port, force) {
|
|
35
|
+
const proc = await getProcessOnPort(port);
|
|
36
|
+
if (!proc)
|
|
37
|
+
return true;
|
|
38
|
+
warn(`Port ${port} is in use by ${proc.name} (PID ${proc.pid})`);
|
|
39
|
+
if (!force) {
|
|
40
|
+
const confirmed = await confirmKill(proc.name, proc.pid);
|
|
41
|
+
if (!confirmed)
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
info(`Killing ${proc.name} (PID ${proc.pid})...`);
|
|
45
|
+
const freed = await killAndVerify(proc.pid, port, force);
|
|
46
|
+
if (freed) {
|
|
47
|
+
success(`Port ${port} is now free`);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
error(`Failed to free port ${port}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function switchPort(portStr: string): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getProcessOnPort } from "../platform/detector.js";
|
|
2
|
+
import { info, success } from "../ui/output.js";
|
|
3
|
+
import { validatePort, findNextFreePort } from "../utils/ports.js";
|
|
4
|
+
export async function switchPort(portStr) {
|
|
5
|
+
const port = validatePort(portStr);
|
|
6
|
+
const proc = await getProcessOnPort(port);
|
|
7
|
+
if (!proc) {
|
|
8
|
+
success(`Port ${port} is already available`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
info(`Port ${port} is in use by ${proc.name} (PID ${proc.pid})`);
|
|
12
|
+
const nextPort = await findNextFreePort(port + 1);
|
|
13
|
+
success(`Port ${nextPort} is available`);
|
|
14
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { run } from "../utils/exec.js";
|
|
2
|
+
const platform = process.platform;
|
|
3
|
+
async function getProcessOnPortWin32(port) {
|
|
4
|
+
const { stdout } = await run(`netstat -ano | findstr :${port}`);
|
|
5
|
+
if (!stdout.trim())
|
|
6
|
+
return null;
|
|
7
|
+
for (const line of stdout.trim().split("\n")) {
|
|
8
|
+
const parts = line.trim().split(/\s+/);
|
|
9
|
+
// Look for LISTENING state with our port in local address
|
|
10
|
+
if (parts.length < 5)
|
|
11
|
+
continue;
|
|
12
|
+
const localAddr = parts[1];
|
|
13
|
+
const state = parts[3];
|
|
14
|
+
if (state !== "LISTENING")
|
|
15
|
+
continue;
|
|
16
|
+
// localAddr is like 0.0.0.0:3000 or [::]:3000
|
|
17
|
+
const addrPort = localAddr.split(":").pop();
|
|
18
|
+
if (addrPort !== String(port))
|
|
19
|
+
continue;
|
|
20
|
+
const pid = parseInt(parts[4], 10);
|
|
21
|
+
if (isNaN(pid) || pid === 0)
|
|
22
|
+
continue;
|
|
23
|
+
const name = await getProcessNameWin32(pid);
|
|
24
|
+
return { pid, name, port, protocol: "tcp" };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
async function getProcessNameWin32(pid) {
|
|
29
|
+
const { stdout } = await run(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`);
|
|
30
|
+
if (!stdout.trim())
|
|
31
|
+
return "unknown";
|
|
32
|
+
// Output: "name.exe","PID","Session Name","Session#","Mem Usage"
|
|
33
|
+
const match = stdout.match(/"([^"]+)"/);
|
|
34
|
+
return match ? match[1].replace(/\.exe$/i, "") : "unknown";
|
|
35
|
+
}
|
|
36
|
+
async function getProcessOnPortUnix(port) {
|
|
37
|
+
const { stdout } = await run(`lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
|
|
38
|
+
if (!stdout.trim())
|
|
39
|
+
return null;
|
|
40
|
+
const lines = stdout.trim().split("\n");
|
|
41
|
+
// Skip header line
|
|
42
|
+
for (let i = 1; i < lines.length; i++) {
|
|
43
|
+
const parts = lines[i].trim().split(/\s+/);
|
|
44
|
+
if (parts.length < 2)
|
|
45
|
+
continue;
|
|
46
|
+
const name = parts[0];
|
|
47
|
+
const pid = parseInt(parts[1], 10);
|
|
48
|
+
if (isNaN(pid))
|
|
49
|
+
continue;
|
|
50
|
+
return { pid, name, port, protocol: "tcp" };
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async function getProcessOnPortLinuxSs(port) {
|
|
55
|
+
const { stdout } = await run(`ss -tlnp sport = :${port}`);
|
|
56
|
+
if (!stdout.trim())
|
|
57
|
+
return null;
|
|
58
|
+
const lines = stdout.trim().split("\n");
|
|
59
|
+
for (let i = 1; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i];
|
|
61
|
+
// Match users:(("name",pid=1234,fd=5))
|
|
62
|
+
const match = line.match(/users:\(\("([^"]+)",pid=(\d+)/);
|
|
63
|
+
if (match) {
|
|
64
|
+
return { pid: parseInt(match[2], 10), name: match[1], port, protocol: "tcp" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export async function getProcessOnPort(port) {
|
|
70
|
+
if (platform === "win32") {
|
|
71
|
+
return getProcessOnPortWin32(port);
|
|
72
|
+
}
|
|
73
|
+
// Try lsof first (macOS + Linux)
|
|
74
|
+
const result = await getProcessOnPortUnix(port);
|
|
75
|
+
if (result)
|
|
76
|
+
return result;
|
|
77
|
+
// Fallback to ss on Linux
|
|
78
|
+
if (platform === "linux") {
|
|
79
|
+
return getProcessOnPortLinuxSs(port);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
export async function getPortStatuses(ports) {
|
|
84
|
+
const results = [];
|
|
85
|
+
// Run all checks in parallel
|
|
86
|
+
const checks = await Promise.all(ports.map((port) => getProcessOnPort(port)));
|
|
87
|
+
for (let i = 0; i < ports.length; i++) {
|
|
88
|
+
const proc = checks[i];
|
|
89
|
+
results.push({
|
|
90
|
+
port: ports[i],
|
|
91
|
+
inUse: proc !== null,
|
|
92
|
+
process: proc ?? undefined,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { run } from "../utils/exec.js";
|
|
2
|
+
import { getProcessOnPort } from "./detector.js";
|
|
3
|
+
const platform = process.platform;
|
|
4
|
+
function sleep(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
export async function killProcess(pid, force = false) {
|
|
8
|
+
try {
|
|
9
|
+
if (platform === "win32") {
|
|
10
|
+
const flag = force ? " /F" : "";
|
|
11
|
+
const { stderr } = await run(`taskkill /PID ${pid}${flag}`);
|
|
12
|
+
return !stderr.includes("ERROR");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const signal = force ? "SIGKILL" : "SIGTERM";
|
|
16
|
+
process.kill(pid, signal);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function killAndVerify(pid, port, force = false) {
|
|
25
|
+
const killed = await killProcess(pid, force);
|
|
26
|
+
if (!killed)
|
|
27
|
+
return false;
|
|
28
|
+
// Verify port is freed (retry up to 3 times)
|
|
29
|
+
for (let i = 0; i < 3; i++) {
|
|
30
|
+
await sleep(500);
|
|
31
|
+
const proc = await getProcessOnPort(port);
|
|
32
|
+
if (!proc)
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ProcessInfo, PortStatus } from "../platform/types.js";
|
|
2
|
+
export declare const info: (msg: string) => void;
|
|
3
|
+
export declare const success: (msg: string) => void;
|
|
4
|
+
export declare const warn: (msg: string) => void;
|
|
5
|
+
export declare const error: (msg: string) => void;
|
|
6
|
+
export declare function showProcess(p: ProcessInfo): void;
|
|
7
|
+
export declare function showPortTable(statuses: PortStatus[]): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export const info = (msg) => console.log(pc.cyan("i") + " " + msg);
|
|
3
|
+
export const success = (msg) => console.log(pc.green("\u2714") + " " + msg);
|
|
4
|
+
export const warn = (msg) => console.log(pc.yellow("!") + " " + msg);
|
|
5
|
+
export const error = (msg) => console.error(pc.red("\u2716") + " " + msg);
|
|
6
|
+
export function showProcess(p) {
|
|
7
|
+
console.log();
|
|
8
|
+
console.log(` ${pc.dim("PID:")} ${pc.bold(String(p.pid))}`);
|
|
9
|
+
console.log(` ${pc.dim("Process:")} ${pc.bold(p.name)}`);
|
|
10
|
+
console.log();
|
|
11
|
+
}
|
|
12
|
+
export function showPortTable(statuses) {
|
|
13
|
+
console.log();
|
|
14
|
+
console.log(` ${pc.bold(pad("PORT", 8))}${pc.bold(pad("STATUS", 10))}${pc.bold("PROCESS")}`);
|
|
15
|
+
for (const s of statuses) {
|
|
16
|
+
const port = pad(String(s.port), 8);
|
|
17
|
+
const status = s.inUse
|
|
18
|
+
? pc.red(pad("in use", 10))
|
|
19
|
+
: pc.green(pad("free", 10));
|
|
20
|
+
const proc = s.process
|
|
21
|
+
? `${s.process.name} (PID ${s.process.pid})`
|
|
22
|
+
: pc.dim("-");
|
|
23
|
+
console.log(` ${port}${status}${proc}`);
|
|
24
|
+
}
|
|
25
|
+
console.log();
|
|
26
|
+
}
|
|
27
|
+
function pad(str, len) {
|
|
28
|
+
return str.padEnd(len);
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import confirm from "@inquirer/confirm";
|
|
2
|
+
export async function confirmKill(processName, pid) {
|
|
3
|
+
if (!process.stdin.isTTY)
|
|
4
|
+
return false;
|
|
5
|
+
return confirm({
|
|
6
|
+
message: `Kill ${processName} (PID ${pid}) to free the port?`,
|
|
7
|
+
default: false,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export async function confirmKillAll(count) {
|
|
11
|
+
if (!process.stdin.isTTY)
|
|
12
|
+
return false;
|
|
13
|
+
return confirm({
|
|
14
|
+
message: `Free all ${count} busy port${count > 1 ? "s" : ""}?`,
|
|
15
|
+
default: false,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execAsync = promisify(exec);
|
|
4
|
+
export async function run(cmd) {
|
|
5
|
+
try {
|
|
6
|
+
return await execAsync(cmd, { timeout: 5000 });
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
const e = err;
|
|
10
|
+
return { stdout: e.stdout ?? "", stderr: e.stderr ?? "" };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
export const COMMON_DEV_PORTS = [
|
|
3
|
+
3000, 3001, 4200, 5000, 5173, 5174, 8000, 8080, 8081, 8443, 9000, 9090,
|
|
4
|
+
];
|
|
5
|
+
export function validatePort(input) {
|
|
6
|
+
const port = parseInt(input, 10);
|
|
7
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
8
|
+
throw new Error(`Invalid port: ${input}. Must be between 1 and 65535.`);
|
|
9
|
+
}
|
|
10
|
+
return port;
|
|
11
|
+
}
|
|
12
|
+
export function findNextFreePort(startFrom) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const tryPort = (port) => {
|
|
15
|
+
if (port > 65535) {
|
|
16
|
+
reject(new Error("No free port found"));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const server = createServer();
|
|
20
|
+
server.once("error", () => tryPort(port + 1));
|
|
21
|
+
server.once("listening", () => {
|
|
22
|
+
server.close(() => resolve(port));
|
|
23
|
+
});
|
|
24
|
+
server.listen(port);
|
|
25
|
+
};
|
|
26
|
+
tryPort(startFrom);
|
|
27
|
+
});
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "portwiz",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fix port conflicts instantly and run your dev server without interruptions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"portwiz": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"start": "node dist/cli.js",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"lint": "eslint src/",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"format:check": "prettier --check .",
|
|
26
|
+
"prepublishOnly": "npm run build && npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"port",
|
|
30
|
+
"kill",
|
|
31
|
+
"process",
|
|
32
|
+
"cli",
|
|
33
|
+
"developer-tools",
|
|
34
|
+
"devtools",
|
|
35
|
+
"port-conflict",
|
|
36
|
+
"free-port",
|
|
37
|
+
"lsof",
|
|
38
|
+
"netstat"
|
|
39
|
+
],
|
|
40
|
+
"author": "ashok1706",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/ashok1706/portwiz.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/ashok1706/portwiz/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/ashok1706/portwiz#readme",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@inquirer/confirm": "^5.0.0",
|
|
52
|
+
"commander": "^12.1.0",
|
|
53
|
+
"picocolors": "^1.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^20.14.0",
|
|
57
|
+
"eslint": "10.0.3",
|
|
58
|
+
"prettier": "3.8.1",
|
|
59
|
+
"typescript": "^5.5.0",
|
|
60
|
+
"vitest": "^2.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|