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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +221 -0
  3. package/dist/__tests__/commands/dev.test.d.ts +1 -0
  4. package/dist/__tests__/commands/dev.test.js +56 -0
  5. package/dist/__tests__/commands/doctor.test.d.ts +1 -0
  6. package/dist/__tests__/commands/doctor.test.js +87 -0
  7. package/dist/__tests__/commands/free.test.d.ts +1 -0
  8. package/dist/__tests__/commands/free.test.js +89 -0
  9. package/dist/__tests__/commands/switch.test.d.ts +1 -0
  10. package/dist/__tests__/commands/switch.test.js +39 -0
  11. package/dist/__tests__/platform/detector.test.d.ts +1 -0
  12. package/dist/__tests__/platform/detector.test.js +64 -0
  13. package/dist/__tests__/platform/killer.test.d.ts +1 -0
  14. package/dist/__tests__/platform/killer.test.js +64 -0
  15. package/dist/__tests__/utils/exec.test.d.ts +1 -0
  16. package/dist/__tests__/utils/exec.test.js +22 -0
  17. package/dist/__tests__/utils/ports.test.d.ts +1 -0
  18. package/dist/__tests__/utils/ports.test.js +66 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +69 -0
  21. package/dist/commands/dev.d.ts +3 -0
  22. package/dist/commands/dev.js +28 -0
  23. package/dist/commands/doctor.d.ts +4 -0
  24. package/dist/commands/doctor.js +43 -0
  25. package/dist/commands/free.d.ts +5 -0
  26. package/dist/commands/free.js +54 -0
  27. package/dist/commands/switch.d.ts +1 -0
  28. package/dist/commands/switch.js +14 -0
  29. package/dist/platform/detector.d.ts +3 -0
  30. package/dist/platform/detector.js +96 -0
  31. package/dist/platform/killer.d.ts +2 -0
  32. package/dist/platform/killer.js +36 -0
  33. package/dist/platform/types.d.ts +11 -0
  34. package/dist/platform/types.js +1 -0
  35. package/dist/ui/output.d.ts +7 -0
  36. package/dist/ui/output.js +29 -0
  37. package/dist/ui/prompts.d.ts +2 -0
  38. package/dist/ui/prompts.js +17 -0
  39. package/dist/utils/exec.d.ts +4 -0
  40. package/dist/utils/exec.js +12 -0
  41. package/dist/utils/ports.d.ts +3 -0
  42. package/dist/utils/ports.js +28 -0
  43. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 portwiz
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,221 @@
1
+ # πŸš€ portwiz
2
+
3
+ **Fix port conflicts instantly and run your dev server without interruptions.**
4
+
5
+ ```bash
6
+ npx portwiz 3000
7
+ ```
8
+
9
+ ---
10
+
11
+ ## 😩 The Problem
12
+
13
+ Every developer has seen this:
14
+
15
+ ```
16
+ Error: EADDRINUSE: address already in use :::3000
17
+ ```
18
+
19
+ Then you:
20
+
21
+ * Search for the process using the port
22
+ * Run `lsof` / `netstat`
23
+ * Kill processes manually
24
+ * Repeat again tomorrow πŸ˜‘
25
+
26
+ πŸ‘‰ It breaks your flow.
27
+
28
+ ---
29
+
30
+ ## ⚑ The Solution
31
+
32
+ **portwiz** handles everything in one command:
33
+
34
+ ```bash
35
+ npx portwiz 3000
36
+ ```
37
+
38
+ * Detects what’s using the port
39
+ * Shows the process clearly
40
+ * Frees it instantly
41
+
42
+ No guesswork. No manual steps.
43
+
44
+ ---
45
+
46
+ ## πŸ“¦ Installation
47
+
48
+ ```bash
49
+ # Run instantly (recommended)
50
+ npx portwiz 3000
51
+
52
+ # Or install globally
53
+ npm install -g portwiz
54
+ ```
55
+
56
+ **Requirements:** Node.js 18+
57
+
58
+ ---
59
+
60
+ ## πŸ§ͺ Usage
61
+
62
+ ---
63
+
64
+ ### πŸ”§ Free a port
65
+
66
+ ```bash
67
+ portwiz 3000
68
+ ```
69
+
70
+ ```
71
+ β„Ή Checking port 3000...
72
+ ⚠ Port 3000 is in use
73
+
74
+ PID: 1234
75
+ Process: node
76
+
77
+ ? Kill this process to free port 3000? (y/N) y
78
+ βœ” Port 3000 is now free
79
+ ```
80
+
81
+ ---
82
+
83
+ ### ⚑ Force mode
84
+
85
+ Skip confirmation:
86
+
87
+ ```bash
88
+ portwiz 3000 --force
89
+ ```
90
+
91
+ ---
92
+
93
+ ### πŸ”€ Smart switch
94
+
95
+ Find the next available port instead of killing:
96
+
97
+ ```bash
98
+ portwiz 3000 --switch
99
+ ```
100
+
101
+ ```
102
+ β„Ή Port 3000 is in use by node (PID 1234)
103
+ βœ” Port 3001 is available
104
+ ```
105
+
106
+ ---
107
+
108
+ ### πŸ”₯ Dev mode (run + fix in one command)
109
+
110
+ ```bash
111
+ portwiz dev 3000 -- npm run dev
112
+ ```
113
+
114
+ ```
115
+ ⚠ Port 3000 is in use by node (PID 1234)
116
+ ? Kill node (PID 1234)? (y/N) y
117
+ βœ” Port 3000 is now free
118
+
119
+ β„Ή Starting: npm run dev
120
+ ```
121
+
122
+ πŸ‘‰ Automatically sets the `PORT` environment variable.
123
+
124
+ ---
125
+
126
+ ### 🧠 Doctor mode
127
+
128
+ Scan common development ports:
129
+
130
+ ```bash
131
+ portwiz doctor
132
+ ```
133
+
134
+ ```
135
+ β„Ή Scanning development ports...
136
+
137
+ PORT STATUS PROCESS
138
+ 3000 in use node (PID 1234)
139
+ 3001 free -
140
+ 4200 free -
141
+ 5000 in use python3 (PID 5678)
142
+ 5173 free -
143
+ 8080 in use java (PID 9012)
144
+
145
+ ⚠ 3 ports are in use
146
+
147
+ ? Free all 3 busy ports? (y/N) y
148
+ βœ” All ports are now free
149
+ ```
150
+
151
+ Custom ports:
152
+
153
+ ```bash
154
+ portwiz doctor --ports 4000,4001,4002
155
+ ```
156
+
157
+ ---
158
+
159
+ ## πŸ“– Commands
160
+
161
+ | Command | Description |
162
+ | ----------------------------- | ------------------------- |
163
+ | `portwiz <port>` | Detect and free a port |
164
+ | `portwiz <port> --switch` | Find next available port |
165
+ | `portwiz dev <port> -- <cmd>` | Free port and run command |
166
+ | `portwiz doctor` | Scan common dev ports |
167
+
168
+ ---
169
+
170
+ ## βš™οΈ Options
171
+
172
+ | Flag | Short | Description |
173
+ | ---------------- | ----- | -------------------------------------- |
174
+ | `--force` | `-f` | Kill without confirmation |
175
+ | `--switch` | `-s` | Find next free port instead of killing |
176
+ | `--ports <list>` | β€” | Custom ports (doctor mode) |
177
+ | `--version` | `-V` | Show version |
178
+ | `--help` | `-h` | Show help |
179
+
180
+ ---
181
+
182
+ ## 🌍 Cross-platform
183
+
184
+ Works out of the box on all major platforms:
185
+
186
+ | Platform | Detection | Kill |
187
+ | -------- | ---------------------- | ------------------- |
188
+ | Windows | `netstat` + `tasklist` | `taskkill` |
189
+ | macOS | `lsof` | `SIGTERM / SIGKILL` |
190
+ | Linux | `lsof` / `ss` | `SIGTERM / SIGKILL` |
191
+
192
+ ---
193
+
194
+ ## 🎯 Use Cases
195
+
196
+ * React / Vite / Next.js dev servers
197
+ * Node.js / Express apps
198
+ * Full-stack local development
199
+ * Docker port conflicts
200
+ * Multi-service environments
201
+
202
+ ---
203
+
204
+ ## πŸ’‘ Why portwiz?
205
+
206
+ * ⚑ Zero setup
207
+ * 🧠 Smart defaults
208
+ * πŸ”₯ Dev-friendly workflow
209
+ * ⏱ Saves time every day
210
+
211
+ ---
212
+
213
+ ## πŸš€ One-line Pitch
214
+
215
+ > Stop fixing ports. Start building.
216
+
217
+ ---
218
+
219
+ ## πŸ“„ License
220
+
221
+ MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ vi.mock("../../commands/free.js", () => ({
3
+ ensurePortFree: vi.fn(),
4
+ }));
5
+ vi.mock("../../ui/output.js", () => ({
6
+ info: vi.fn(),
7
+ error: vi.fn(),
8
+ }));
9
+ // Mock child_process.spawn
10
+ const mockSpawn = vi.fn();
11
+ vi.mock("node:child_process", () => ({
12
+ spawn: (...args) => mockSpawn(...args),
13
+ }));
14
+ import { devMode } from "../../commands/dev.js";
15
+ import { ensurePortFree } from "../../commands/free.js";
16
+ import { info, error } from "../../ui/output.js";
17
+ const mockEnsureFree = vi.mocked(ensurePortFree);
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ process.exitCode = undefined;
21
+ // Default spawn mock: returns an EventEmitter-like object
22
+ mockSpawn.mockReturnValue({
23
+ kill: vi.fn(),
24
+ on: vi.fn(),
25
+ });
26
+ });
27
+ describe("devMode", () => {
28
+ it("frees port and spawns the command", async () => {
29
+ mockEnsureFree.mockResolvedValue(true);
30
+ await devMode("3000", ["npm", "run", "dev"], {});
31
+ expect(mockEnsureFree).toHaveBeenCalledWith(3000, false);
32
+ expect(info).toHaveBeenCalledWith(expect.stringContaining("Starting: npm run dev"));
33
+ expect(mockSpawn).toHaveBeenCalledWith("npm run dev", expect.objectContaining({
34
+ stdio: "inherit",
35
+ shell: true,
36
+ }));
37
+ });
38
+ it("sets PORT env variable", async () => {
39
+ mockEnsureFree.mockResolvedValue(true);
40
+ await devMode("4000", ["node", "server.js"], {});
41
+ const spawnCall = mockSpawn.mock.calls[0];
42
+ expect(spawnCall[1].env.PORT).toBe("4000");
43
+ });
44
+ it("errors when port cannot be freed", async () => {
45
+ mockEnsureFree.mockResolvedValue(false);
46
+ await devMode("3000", ["npm", "start"], {});
47
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("port is still in use"));
48
+ expect(process.exitCode).toBe(1);
49
+ expect(mockSpawn).not.toHaveBeenCalled();
50
+ });
51
+ it("passes force option through", async () => {
52
+ mockEnsureFree.mockResolvedValue(true);
53
+ await devMode("3000", ["npm", "start"], { force: true });
54
+ expect(mockEnsureFree).toHaveBeenCalledWith(3000, true);
55
+ });
56
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ vi.mock("../../platform/detector.js", () => ({
3
+ getPortStatuses: vi.fn(),
4
+ }));
5
+ vi.mock("../../platform/killer.js", () => ({
6
+ killAndVerify: vi.fn(),
7
+ }));
8
+ vi.mock("../../ui/prompts.js", () => ({
9
+ confirmKillAll: vi.fn(),
10
+ }));
11
+ vi.mock("../../ui/output.js", () => ({
12
+ info: vi.fn(),
13
+ success: vi.fn(),
14
+ warn: vi.fn(),
15
+ error: vi.fn(),
16
+ showPortTable: vi.fn(),
17
+ }));
18
+ import { doctor } from "../../commands/doctor.js";
19
+ import { getPortStatuses } from "../../platform/detector.js";
20
+ import { killAndVerify } from "../../platform/killer.js";
21
+ import { confirmKillAll } from "../../ui/prompts.js";
22
+ import { success, warn, error } from "../../ui/output.js";
23
+ const mockGetStatuses = vi.mocked(getPortStatuses);
24
+ const mockKillAndVerify = vi.mocked(killAndVerify);
25
+ const mockConfirmAll = vi.mocked(confirmKillAll);
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ process.exitCode = undefined;
29
+ });
30
+ describe("doctor", () => {
31
+ it("reports all ports free when nothing is busy", async () => {
32
+ mockGetStatuses.mockResolvedValue([
33
+ { port: 3000, inUse: false },
34
+ { port: 8080, inUse: false },
35
+ ]);
36
+ await doctor({});
37
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("All ports are free"));
38
+ });
39
+ it("shows busy ports and prompts to kill", async () => {
40
+ mockGetStatuses.mockResolvedValue([
41
+ { port: 3000, inUse: true, process: { pid: 1234, name: "node", port: 3000, protocol: "tcp" } },
42
+ { port: 8080, inUse: false },
43
+ ]);
44
+ mockConfirmAll.mockResolvedValue(true);
45
+ mockKillAndVerify.mockResolvedValue(true);
46
+ await doctor({});
47
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining("1 port is in use"));
48
+ expect(mockKillAndVerify).toHaveBeenCalledWith(1234, 3000, false);
49
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("All ports are now free"));
50
+ });
51
+ it("aborts when user declines", async () => {
52
+ mockGetStatuses.mockResolvedValue([
53
+ { port: 3000, inUse: true, process: { pid: 1234, name: "node", port: 3000, protocol: "tcp" } },
54
+ ]);
55
+ mockConfirmAll.mockResolvedValue(false);
56
+ await doctor({});
57
+ expect(mockKillAndVerify).not.toHaveBeenCalled();
58
+ });
59
+ it("force-kills without prompting", async () => {
60
+ mockGetStatuses.mockResolvedValue([
61
+ { port: 3000, inUse: true, process: { pid: 1234, name: "node", port: 3000, protocol: "tcp" } },
62
+ { port: 5173, inUse: true, process: { pid: 5678, name: "vite", port: 5173, protocol: "tcp" } },
63
+ ]);
64
+ mockKillAndVerify.mockResolvedValue(true);
65
+ await doctor({ force: true });
66
+ expect(mockConfirmAll).not.toHaveBeenCalled();
67
+ expect(mockKillAndVerify).toHaveBeenCalledTimes(2);
68
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("All ports are now free"));
69
+ });
70
+ it("uses custom port list when provided", async () => {
71
+ mockGetStatuses.mockResolvedValue([
72
+ { port: 4000, inUse: false },
73
+ { port: 4001, inUse: false },
74
+ ]);
75
+ await doctor({ ports: "4000,4001" });
76
+ expect(mockGetStatuses).toHaveBeenCalledWith([4000, 4001]);
77
+ });
78
+ it("sets exitCode when some kills fail", async () => {
79
+ mockGetStatuses.mockResolvedValue([
80
+ { port: 3000, inUse: true, process: { pid: 1234, name: "node", port: 3000, protocol: "tcp" } },
81
+ ]);
82
+ mockKillAndVerify.mockResolvedValue(false);
83
+ await doctor({ force: true });
84
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("Failed"));
85
+ expect(process.exitCode).toBe(1);
86
+ });
87
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // Mock modules before importing the command
3
+ vi.mock("../../platform/detector.js", () => ({
4
+ getProcessOnPort: vi.fn(),
5
+ }));
6
+ vi.mock("../../platform/killer.js", () => ({
7
+ killAndVerify: vi.fn(),
8
+ }));
9
+ vi.mock("../../ui/prompts.js", () => ({
10
+ confirmKill: vi.fn(),
11
+ }));
12
+ vi.mock("../../ui/output.js", () => ({
13
+ info: vi.fn(),
14
+ success: vi.fn(),
15
+ warn: vi.fn(),
16
+ error: vi.fn(),
17
+ showProcess: vi.fn(),
18
+ }));
19
+ import { freePort, ensurePortFree } from "../../commands/free.js";
20
+ import { getProcessOnPort } from "../../platform/detector.js";
21
+ import { killAndVerify } from "../../platform/killer.js";
22
+ import { confirmKill } from "../../ui/prompts.js";
23
+ import { success, error } from "../../ui/output.js";
24
+ const mockGetProcess = vi.mocked(getProcessOnPort);
25
+ const mockKillAndVerify = vi.mocked(killAndVerify);
26
+ const mockConfirmKill = vi.mocked(confirmKill);
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ process.exitCode = undefined;
30
+ });
31
+ describe("freePort", () => {
32
+ it("reports port is free when nothing is using it", async () => {
33
+ mockGetProcess.mockResolvedValue(null);
34
+ await freePort("3000", {});
35
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("already free"));
36
+ });
37
+ it("kills process with --force without prompting", async () => {
38
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
39
+ mockKillAndVerify.mockResolvedValue(true);
40
+ await freePort("3000", { force: true });
41
+ expect(mockConfirmKill).not.toHaveBeenCalled();
42
+ expect(mockKillAndVerify).toHaveBeenCalledWith(1234, 3000, true);
43
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("now free"));
44
+ });
45
+ it("prompts for confirmation and aborts when declined", async () => {
46
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
47
+ mockConfirmKill.mockResolvedValue(false);
48
+ await freePort("3000", {});
49
+ expect(mockKillAndVerify).not.toHaveBeenCalled();
50
+ });
51
+ it("prompts for confirmation and kills when accepted", async () => {
52
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
53
+ mockConfirmKill.mockResolvedValue(true);
54
+ mockKillAndVerify.mockResolvedValue(true);
55
+ await freePort("3000", {});
56
+ expect(mockKillAndVerify).toHaveBeenCalledWith(1234, 3000, undefined);
57
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("now free"));
58
+ });
59
+ it("sets exitCode when kill fails", async () => {
60
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
61
+ mockKillAndVerify.mockResolvedValue(false);
62
+ await freePort("3000", { force: true });
63
+ expect(error).toHaveBeenCalledWith(expect.stringContaining("Failed"));
64
+ expect(process.exitCode).toBe(1);
65
+ });
66
+ it("throws for invalid port", async () => {
67
+ await expect(freePort("abc", {})).rejects.toThrow("Invalid port");
68
+ });
69
+ });
70
+ describe("ensurePortFree", () => {
71
+ it("returns true when port is already free", async () => {
72
+ mockGetProcess.mockResolvedValue(null);
73
+ const result = await ensurePortFree(3000, false);
74
+ expect(result).toBe(true);
75
+ });
76
+ it("returns false when user declines kill", async () => {
77
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
78
+ mockConfirmKill.mockResolvedValue(false);
79
+ const result = await ensurePortFree(3000, false);
80
+ expect(result).toBe(false);
81
+ });
82
+ it("force-kills without prompting", async () => {
83
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
84
+ mockKillAndVerify.mockResolvedValue(true);
85
+ const result = await ensurePortFree(3000, true);
86
+ expect(result).toBe(true);
87
+ expect(mockConfirmKill).not.toHaveBeenCalled();
88
+ });
89
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ vi.mock("../../platform/detector.js", () => ({
3
+ getProcessOnPort: vi.fn(),
4
+ }));
5
+ vi.mock("../../ui/output.js", () => ({
6
+ info: vi.fn(),
7
+ success: vi.fn(),
8
+ }));
9
+ vi.mock("../../utils/ports.js", async (importOriginal) => {
10
+ const actual = await importOriginal();
11
+ return {
12
+ ...actual,
13
+ findNextFreePort: vi.fn(),
14
+ };
15
+ });
16
+ import { switchPort } from "../../commands/switch.js";
17
+ import { getProcessOnPort } from "../../platform/detector.js";
18
+ import { info, success } from "../../ui/output.js";
19
+ import { findNextFreePort } from "../../utils/ports.js";
20
+ const mockGetProcess = vi.mocked(getProcessOnPort);
21
+ const mockFindNext = vi.mocked(findNextFreePort);
22
+ beforeEach(() => {
23
+ vi.clearAllMocks();
24
+ });
25
+ describe("switchPort", () => {
26
+ it("reports port is available when not in use", async () => {
27
+ mockGetProcess.mockResolvedValue(null);
28
+ await switchPort("3000");
29
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("already available"));
30
+ });
31
+ it("finds next free port when port is busy", async () => {
32
+ mockGetProcess.mockResolvedValue({ pid: 1234, name: "node", port: 3000, protocol: "tcp" });
33
+ mockFindNext.mockResolvedValue(3001);
34
+ await switchPort("3000");
35
+ expect(info).toHaveBeenCalledWith(expect.stringContaining("in use"));
36
+ expect(mockFindNext).toHaveBeenCalledWith(3001);
37
+ expect(success).toHaveBeenCalledWith(expect.stringContaining("3001"));
38
+ });
39
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createServer } from "node:net";
3
+ import { getProcessOnPort, getPortStatuses } from "../../platform/detector.js";
4
+ describe("getProcessOnPort", () => {
5
+ it("returns null for a port that is not in use", async () => {
6
+ // Use a high ephemeral port unlikely to be in use
7
+ const result = await getProcessOnPort(59999);
8
+ expect(result).toBeNull();
9
+ });
10
+ it("detects a process listening on a port", async () => {
11
+ const server = createServer();
12
+ const port = await new Promise((resolve) => {
13
+ server.listen(0, () => {
14
+ const addr = server.address();
15
+ if (addr && typeof addr !== "string") {
16
+ resolve(addr.port);
17
+ }
18
+ });
19
+ });
20
+ try {
21
+ const result = await getProcessOnPort(port);
22
+ expect(result).not.toBeNull();
23
+ expect(result.port).toBe(port);
24
+ expect(result.pid).toBe(process.pid);
25
+ expect(result.protocol).toBe("tcp");
26
+ expect(typeof result.name).toBe("string");
27
+ expect(result.name.length).toBeGreaterThan(0);
28
+ }
29
+ finally {
30
+ server.close();
31
+ }
32
+ });
33
+ });
34
+ describe("getPortStatuses", () => {
35
+ it("returns statuses for multiple ports", async () => {
36
+ const server = createServer();
37
+ const busyPort = await new Promise((resolve) => {
38
+ server.listen(0, () => {
39
+ const addr = server.address();
40
+ if (addr && typeof addr !== "string") {
41
+ resolve(addr.port);
42
+ }
43
+ });
44
+ });
45
+ try {
46
+ const statuses = await getPortStatuses([busyPort, 59998]);
47
+ expect(statuses).toHaveLength(2);
48
+ const busyStatus = statuses.find((s) => s.port === busyPort);
49
+ expect(busyStatus.inUse).toBe(true);
50
+ expect(busyStatus.process).toBeDefined();
51
+ expect(busyStatus.process.pid).toBe(process.pid);
52
+ const freeStatus = statuses.find((s) => s.port === 59998);
53
+ expect(freeStatus.inUse).toBe(false);
54
+ expect(freeStatus.process).toBeUndefined();
55
+ }
56
+ finally {
57
+ server.close();
58
+ }
59
+ });
60
+ it("returns empty array for empty input", async () => {
61
+ const statuses = await getPortStatuses([]);
62
+ expect(statuses).toHaveLength(0);
63
+ });
64
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { fork } from "node:child_process";
3
+ import { killProcess, killAndVerify } from "../../platform/killer.js";
4
+ function spawnDummyServer(port) {
5
+ return new Promise((resolve, reject) => {
6
+ const child = fork("-e", [`require("net").createServer().listen(${port}, () => process.send("ready"))`], { execArgv: [], stdio: ["pipe", "pipe", "pipe", "ipc"] });
7
+ child.on("message", (msg) => {
8
+ if (msg === "ready") {
9
+ resolve({
10
+ pid: child.pid,
11
+ cleanup: () => {
12
+ try {
13
+ child.kill("SIGKILL");
14
+ }
15
+ catch { }
16
+ },
17
+ });
18
+ }
19
+ });
20
+ child.on("error", reject);
21
+ setTimeout(() => reject(new Error("Timed out waiting for child server")), 5000);
22
+ });
23
+ }
24
+ describe("killProcess", () => {
25
+ it("kills a running process", async () => {
26
+ const child = fork("-e", ["setTimeout(() => {}, 60000)"], {
27
+ execArgv: [],
28
+ stdio: "pipe",
29
+ });
30
+ // Give it a moment to start
31
+ await new Promise((r) => setTimeout(r, 200));
32
+ const result = await killProcess(child.pid, true);
33
+ expect(result).toBe(true);
34
+ // Verify the process is gone
35
+ await new Promise((r) => setTimeout(r, 500));
36
+ let alive = true;
37
+ try {
38
+ process.kill(child.pid, 0);
39
+ }
40
+ catch {
41
+ alive = false;
42
+ }
43
+ expect(alive).toBe(false);
44
+ });
45
+ it("returns false for a non-existent PID", async () => {
46
+ const result = await killProcess(999999, true);
47
+ expect(result).toBe(false);
48
+ });
49
+ });
50
+ describe("killAndVerify", () => {
51
+ it("kills a process and verifies the port is freed", async () => {
52
+ const port = 49321;
53
+ let helper = null;
54
+ try {
55
+ helper = await spawnDummyServer(port);
56
+ const result = await killAndVerify(helper.pid, port, true);
57
+ expect(result).toBe(true);
58
+ }
59
+ finally {
60
+ if (helper)
61
+ helper.cleanup();
62
+ }
63
+ });
64
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { run } from "../../utils/exec.js";
3
+ describe("run", () => {
4
+ it("executes a simple command and returns stdout", async () => {
5
+ const { stdout } = await run("echo hello");
6
+ expect(stdout.trim()).toBe("hello");
7
+ });
8
+ it("returns stderr on error without throwing", async () => {
9
+ const result = await run("node -e \"process.stderr.write('oops'); process.exit(1)\"");
10
+ expect(result.stderr).toContain("oops");
11
+ });
12
+ it("returns empty strings for a command that produces no output", async () => {
13
+ const result = await run("node -e \"\"");
14
+ expect(result.stdout).toBe("");
15
+ });
16
+ it("handles non-existent commands gracefully", async () => {
17
+ const result = await run("nonexistentcommand12345");
18
+ // Should not throw, returns empty or error output
19
+ expect(result).toHaveProperty("stdout");
20
+ expect(result).toHaveProperty("stderr");
21
+ });
22
+ });