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
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
|
+
});
|