port-reclaim 0.1.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 port-reclaim contributors
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,72 @@
1
+ # port-reclaim
2
+
3
+ A zero-configuration, cross-platform CLI for safely reclaiming ports held by stale local development processes.
4
+
5
+ ## Install
6
+
7
+ Run it once without installing:
8
+
9
+ ```sh
10
+ npx port-reclaim 3000
11
+ ```
12
+
13
+ Or install it globally:
14
+
15
+ ```sh
16
+ npm install --global port-reclaim
17
+ port-reclaim 3000
18
+ ```
19
+
20
+ Requires Node.js 18 or newer. Supports macOS, Linux, Windows, WSL, PowerShell, and CMD.
21
+
22
+ ## How It Works
23
+
24
+ ```sh
25
+ port-reclaim <PORT>
26
+ ```
27
+
28
+ If the port is free, the command exits successfully. If the process working directory matches the current project, the process is terminated automatically. Processes from another directory, system services, and processes whose working directory cannot be read require explicit confirmation.
29
+
30
+ The command uses a graceful termination signal first on Unix-like systems and falls back to a forceful signal if the process remains alive. Windows uses `taskkill`.
31
+
32
+ If confirmation is required in a non-interactive environment, the command declines safely and exits with status `1`.
33
+
34
+ ## Script Integration
35
+
36
+ Use it in a package script:
37
+
38
+ ```json
39
+ {
40
+ "scripts": {
41
+ "dev": "port-reclaim 3000 && next dev"
42
+ }
43
+ }
44
+ ```
45
+
46
+ The same command can be used from Python, Ruby, Go, Make, or shell scripts.
47
+
48
+ ## Exit Codes
49
+
50
+ | Code | Meaning |
51
+ | --- | --- |
52
+ | `0` | The port was free or was successfully released. |
53
+ | `1` | The port is still in use, the user declined, or an operation failed. |
54
+ | `2` | Invalid command-line input. |
55
+
56
+ ## Security Notes
57
+
58
+ `port-reclaim` only acts on processes listening on the port you provide. It does not scan remote hosts or kill Docker containers directly. Review the process name and working directory before confirming a process from another project.
59
+
60
+ ## Development
61
+
62
+ ```sh
63
+ npm install
64
+ npm test
65
+ npm run build
66
+ ```
67
+
68
+ Pull requests are tested on Ubuntu, macOS, and Windows with Node.js 18, 20, and 22.
69
+
70
+ ## License
71
+
72
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import readline from "node:readline/promises";
3
+ import process from "node:process";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { createProcessRunner } from "./process.js";
7
+ function usage() {
8
+ console.error("Usage: port-reclaim <PORT>");
9
+ process.exit(2);
10
+ }
11
+ function parsePort(value) {
12
+ if (!value || !/^\d+$/.test(value))
13
+ usage();
14
+ const port = Number(value);
15
+ if (port < 1 || port > 65535)
16
+ usage();
17
+ return port;
18
+ }
19
+ function sameDirectory(left, right) {
20
+ if (!left)
21
+ return false;
22
+ const normalizedLeft = path.normalize(path.resolve(left));
23
+ const normalizedRight = path.normalize(path.resolve(right));
24
+ return process.platform === "win32" ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight;
25
+ }
26
+ async function confirm(portProcess, port) {
27
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
28
+ console.error(`Port ${port} is used by '${portProcess.name}'${portProcess.cwd ? ` in '${portProcess.cwd}'` : ""}. Use an interactive terminal to confirm termination.`);
29
+ return false;
30
+ }
31
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
32
+ const answer = await prompt.question(`Port ${port} is used by '${portProcess.name}'${portProcess.cwd ? ` in '${portProcess.cwd}'` : ""}. Kill it? (y/N) `);
33
+ prompt.close();
34
+ return /^y(es)?$/i.test(answer.trim());
35
+ }
36
+ export async function main(runner = createProcessRunner()) {
37
+ const port = parsePort(process.argv[2]);
38
+ const processes = await runner.discover(port);
39
+ if (processes.length === 0)
40
+ return 0;
41
+ const currentDirectory = process.cwd();
42
+ for (const portProcess of processes) {
43
+ const shouldTerminate = sameDirectory(portProcess.cwd, currentDirectory) || await confirm(portProcess, port);
44
+ if (!shouldTerminate)
45
+ return 1;
46
+ await runner.terminate(portProcess.pid);
47
+ console.log(`Released port ${port} from ${portProcess.name} (PID ${portProcess.pid}).`);
48
+ }
49
+ return 0;
50
+ }
51
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
52
+ main().then((code) => process.exit(code)).catch((error) => {
53
+ console.error(`port-reclaim: ${error.message}`);
54
+ process.exit(1);
55
+ });
56
+ }
@@ -0,0 +1,78 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import process from "node:process";
4
+ import pidCwd from "pid-cwd";
5
+ const execFileAsync = promisify(execFile);
6
+ function parsePidList(output) {
7
+ return [...new Set(output.split(/\r?\n/).map((line) => Number(line.trim())).filter((pid) => Number.isInteger(pid) && pid > 0))];
8
+ }
9
+ export function parseWindowsPids(output) {
10
+ return parsePidList(output);
11
+ }
12
+ export function parseUnixLsof(output) {
13
+ return [...new Set([...output.matchAll(/^p(\d+)$/gm)].map((match) => Number(match[1])))];
14
+ }
15
+ async function run(command, args) {
16
+ try {
17
+ const result = await execFileAsync(command, args, { windowsHide: true, maxBuffer: 1024 * 1024 });
18
+ return result.stdout;
19
+ }
20
+ catch (error) {
21
+ const processError = error;
22
+ if (processError.stdout)
23
+ return processError.stdout;
24
+ if (processError.code === "ENOENT") {
25
+ throw new Error(`Required system command '${command}' was not found.`);
26
+ }
27
+ return "";
28
+ }
29
+ }
30
+ async function windowsPids(port) {
31
+ const script = `$ErrorActionPreference = 'SilentlyContinue'; Get-NetTCPConnection -LocalPort ${port} -State Listen | Select-Object -ExpandProperty OwningProcess`;
32
+ const output = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script]);
33
+ if (output.trim())
34
+ return parseWindowsPids(output);
35
+ const netstat = await run("netstat.exe", ["-ano", "-p", "TCP"]);
36
+ return parseWindowsPids(netstat.split(/\r?\n/).filter((line) => {
37
+ const columns = line.trim().split(/\s+/);
38
+ return columns[0]?.toUpperCase() === "TCP" && columns[1]?.endsWith(`:${port}`) && columns[3]?.toUpperCase() === "LISTENING";
39
+ }).map((line) => line.trim().split(/\s+/).at(-1)).join("\n"));
40
+ }
41
+ async function unixPids(port) {
42
+ const output = await run("lsof", ["-nP", "-a", "-iTCP:" + port, "-sTCP:LISTEN", "-Fp"]);
43
+ return parseUnixLsof(output);
44
+ }
45
+ async function processName(pid) {
46
+ if (process.platform === "win32") {
47
+ const output = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", `(Get-Process -Id ${pid}).ProcessName`]);
48
+ return output.trim() || "unknown";
49
+ }
50
+ const output = await run("ps", ["-p", String(pid), "-o", "comm="]);
51
+ return output.trim() || "unknown";
52
+ }
53
+ async function processCwd(pid) {
54
+ return (await pidCwd(pid)) ?? undefined;
55
+ }
56
+ export function createProcessRunner() {
57
+ return {
58
+ async discover(port) {
59
+ const pids = process.platform === "win32" ? await windowsPids(port) : await unixPids(port);
60
+ return Promise.all(pids.map(async (pid) => ({ pid, name: await processName(pid), cwd: await processCwd(pid) })));
61
+ },
62
+ async terminate(pid) {
63
+ if (process.platform === "win32") {
64
+ await run("taskkill.exe", ["/PID", String(pid), "/T"]);
65
+ return;
66
+ }
67
+ process.kill(pid, "SIGTERM");
68
+ await new Promise((resolve) => setTimeout(resolve, 250));
69
+ try {
70
+ process.kill(pid, 0);
71
+ process.kill(pid, "SIGKILL");
72
+ }
73
+ catch {
74
+ // The process exited after SIGTERM.
75
+ }
76
+ }
77
+ };
78
+ }
@@ -0,0 +1,9 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseUnixLsof, parseWindowsPids } from "../src/process.js";
4
+ test("parses unique Windows PIDs", () => {
5
+ assert.deepEqual(parseWindowsPids("1234\r\n5678\r\n1234\r\n"), [1234, 5678]);
6
+ });
7
+ test("parses lsof PID records", () => {
8
+ assert.deepEqual(parseUnixLsof("p1234\ncnode\np5678\np1234\n"), [1234, 5678]);
9
+ });
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "port-reclaim",
3
+ "version": "0.1.0",
4
+ "description": "Safely reclaim local ports from stale development processes.",
5
+ "author": "princegoel0",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/princegoel0/port-reclaim.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/princegoel0/port-reclaim/issues"
13
+ },
14
+ "homepage": "https://github.com/princegoel0/port-reclaim#readme",
15
+ "type": "module",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "bin": {
22
+ "port-reclaim": "dist/src/cli.js"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "test": "npm run build && node --test dist/tests/process.test.js",
27
+ "prepublishOnly": "npm test"
28
+ },
29
+ "keywords": [
30
+ "cli",
31
+ "port",
32
+ "process",
33
+ "developer-tools",
34
+ "cross-platform"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.2",
41
+ "typescript": "^5.7.2"
42
+ },
43
+ "dependencies": {
44
+ "pid-cwd": "^1.2.0"
45
+ }
46
+ }