@tidyports/guard 0.1.0 → 0.2.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/README.md +25 -4
- package/guard.js +67 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,16 +9,37 @@ $ tidyports-guard npm run dev
|
|
|
9
9
|
|
|
10
10
|
Error: listen EADDRINUSE: address already in use :::3000
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
:3000 is held by Claude Code, in Ghostty [PID 12345] in acme-web (feature/auth)
|
|
13
|
+
|
|
14
|
+
[1] use 3001 instead
|
|
15
|
+
[2] stop it (asks again first)
|
|
16
|
+
[enter] leave it
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
Pick `1` and the port lands on your clipboard. Enter leaves everything alone, so a stray
|
|
20
|
+
keystroke never stops anyone's server.
|
|
21
|
+
|
|
22
|
+
Naming the owner matters most when an agent is reading it. An agent that hits a port
|
|
18
23
|
conflict and can't tell it from a bug will often "fix" working code to get around the
|
|
19
24
|
port — usually by editing your app's configuration. Naming the owner is what makes the
|
|
20
25
|
conflict legible as a conflict.
|
|
21
26
|
|
|
27
|
+
### When you get the choice, and when you don't
|
|
28
|
+
|
|
29
|
+
Not everything dies when it can't bind. Vite prints `Port 5173 is in use` and carries on
|
|
30
|
+
at 5174, and a prompt underneath a live dev server would be competing with its output for
|
|
31
|
+
the same terminal. So the choice appears only once the command has actually exited and
|
|
32
|
+
the terminal is free.
|
|
33
|
+
|
|
34
|
+
If it's still running — or you're piped, in CI, or an agent is reading — you get the same
|
|
35
|
+
answer as plain text instead, and nothing ever waits for input:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
[tidyports] :3000 is held by Claude Code, in Ghostty [PID 12345] in acme-web (feature/auth)
|
|
39
|
+
To take a different port: PORT=$(tidy-ports alloc web)
|
|
40
|
+
To stop it: tidy-ports kill --match 3000
|
|
41
|
+
```
|
|
42
|
+
|
|
22
43
|
## Use it
|
|
23
44
|
|
|
24
45
|
```bash
|
package/guard.js
CHANGED
|
@@ -65,14 +65,33 @@ let reported = false;
|
|
|
65
65
|
const TAIL_MAX = 8192;
|
|
66
66
|
let tail = "";
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
tail = (tail + chunk).slice(-TAIL_MAX);
|
|
71
|
-
const port = portFrom(tail);
|
|
72
|
-
if (!port) return;
|
|
73
|
-
reported = true;
|
|
74
|
-
tail = "";
|
|
68
|
+
/* Two forms of the same answer, and which one you get depends on whether the
|
|
69
|
+
thing that failed is still running.
|
|
75
70
|
|
|
71
|
+
A process that cannot bind usually dies — node, rails, flask — and then the
|
|
72
|
+
terminal is free, you are sitting looking at the error, and the useful thing
|
|
73
|
+
is the choice `who` offers a person: take this other port, stop it, leave it.
|
|
74
|
+
But not everything dies. Vite prints "Port 5173 is in use" and carries on at
|
|
75
|
+
5174, and putting a prompt under a live dev server means competing with its
|
|
76
|
+
output for the same terminal.
|
|
77
|
+
|
|
78
|
+
So detection starts a short timer instead of deciding immediately. If the
|
|
79
|
+
child is gone before it fires, the terminal belongs to us and `who` runs
|
|
80
|
+
interactively. If it is still alive, the plain lines print and no prompt ever
|
|
81
|
+
appears. Exactly one of the two happens. */
|
|
82
|
+
const SETTLE_MS = 400;
|
|
83
|
+
|
|
84
|
+
let pending = null;
|
|
85
|
+
|
|
86
|
+
// Can a person answer? `who` makes the same check itself, but it would see the
|
|
87
|
+
// pipe we hand it and never prompt, so the decision has to be made out here.
|
|
88
|
+
function canPrompt() {
|
|
89
|
+
return Boolean(
|
|
90
|
+
process.stdout.isTTY && !process.env.TP_PORCELAIN && !process.env.CI,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printPlain(port) {
|
|
76
95
|
const answer = whoHolds(port);
|
|
77
96
|
if (!answer) {
|
|
78
97
|
// No CLI, so there is nothing useful to add. Say so once, briefly, rather
|
|
@@ -85,6 +104,28 @@ function explain(chunk) {
|
|
|
85
104
|
process.stderr.write(`\n[tidyports] ${answer}\n`);
|
|
86
105
|
}
|
|
87
106
|
|
|
107
|
+
function explain(chunk) {
|
|
108
|
+
if (reported) return;
|
|
109
|
+
tail = (tail + chunk).slice(-TAIL_MAX);
|
|
110
|
+
const port = portFrom(tail);
|
|
111
|
+
if (!port) return;
|
|
112
|
+
reported = true;
|
|
113
|
+
tail = "";
|
|
114
|
+
|
|
115
|
+
if (!canPrompt()) {
|
|
116
|
+
printPlain(port);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Hold briefly to see whether the child is dying. `unref` so a wrapper around
|
|
120
|
+
// a long-lived server is never the reason the process stays up.
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
pending = null;
|
|
123
|
+
printPlain(port);
|
|
124
|
+
}, SETTLE_MS);
|
|
125
|
+
timer.unref?.();
|
|
126
|
+
pending = { port, timer };
|
|
127
|
+
}
|
|
128
|
+
|
|
88
129
|
const child = spawn(argv[0], argv.slice(1), {
|
|
89
130
|
stdio: ["inherit", "inherit", "pipe"],
|
|
90
131
|
shell: false,
|
|
@@ -108,6 +149,25 @@ child.on("error", (err) => {
|
|
|
108
149
|
});
|
|
109
150
|
|
|
110
151
|
child.on("close", (code, signal) => {
|
|
152
|
+
/* The child is gone, so the terminal is ours and the prompt can't collide with
|
|
153
|
+
anything. Skipped on a signal: ctrl-c means you wanted out, and answering a
|
|
154
|
+
question is not what you asked for. stdio is inherited so `who` sees a real
|
|
155
|
+
tty and offers its own choice — including the second confirmation before it
|
|
156
|
+
stops anything. */
|
|
157
|
+
if (pending) {
|
|
158
|
+
clearTimeout(pending.timer);
|
|
159
|
+
const { port } = pending;
|
|
160
|
+
pending = null;
|
|
161
|
+
if (signal) {
|
|
162
|
+
printPlain(port);
|
|
163
|
+
} else {
|
|
164
|
+
process.stderr.write("\n");
|
|
165
|
+
const res = spawnSync("tidy-ports", ["who", port], { stdio: "inherit" });
|
|
166
|
+
// No CLI to be interactive with, so fall back to saying what we can.
|
|
167
|
+
if (res.error) printPlain(port);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
111
171
|
// Reproduce how the child ended, so this is invisible to anything upstream:
|
|
112
172
|
// a shell, a CI step, or an agent reading the exit status.
|
|
113
173
|
if (signal) {
|