@gachlab/devup 0.8.0 → 0.8.1
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/CHANGELOG.md +14 -0
- package/dist/index.js +30 -2
- package/dist/index.js.map +1 -1
- package/dist/process/health.d.ts +17 -0
- package/dist/process/health.d.ts.map +1 -1
- package/dist/process/spawner.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to `@gachlab/devup` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.8.1] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
Patch focused on two real-world footguns surfaced as soon as 0.8.0 hit users.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Spawner port pre-flight was unreliable.** The check used `checkPort` — a connect-based test designed for health checks — which can miss bindings (e.g. a server bound but not yet accepting connections, or listening on a different address family). When the check missed, the service crashed with a raw `EADDRINUSE` Node stack trace dumped to the logs panel. Replaced with `isPortBindable()` — a bind-based test using `net.createServer().listen()` — which catches every case that would actually conflict.
|
|
14
|
+
- **Pre-flight failures now record a crashed state.** Previously the spawner returned silently when the port was occupied, leaving no entry in the services list. Now the failed service appears in the stats panel as `crashed` with the `⚠ port N already in use` line in its log, matching the behaviour of other pre-flight failures.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **TUI refuses to start when a daemon is already running for the project.** Running `devup` (TUI) on top of `devup up -d` would race for the same ports — every API would fail with EADDRINUSE. The TUI now detects the PID file before mounting Ink and exits with a clear pointer to `devup down` and the `devup ctl` family.
|
|
18
|
+
|
|
19
|
+
### Internals
|
|
20
|
+
- New public `isPortBindable(port, host?)` in `src/process/health.ts`. `checkPort` keeps its connect-based semantics for health checks and `waitForPort`. Three new tests cover free / occupied / bound-but-not-accepting cases.
|
|
21
|
+
|
|
8
22
|
## [0.8.0] — 2026-05-22
|
|
9
23
|
|
|
10
24
|
**Headless devup.** The control plane grows up: streaming events, a CLI client that speaks it end-to-end, and the long-requested daemon mode so the stack can be left running while you keep working in the same terminal — `docker compose up -d` for Node monorepos.
|
package/dist/index.js
CHANGED
|
@@ -459,6 +459,23 @@ function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
|
459
459
|
socket.connect(port, host);
|
|
460
460
|
});
|
|
461
461
|
}
|
|
462
|
+
async function isPortBindable(port) {
|
|
463
|
+
for (const host of ["0.0.0.0", "::"]) {
|
|
464
|
+
if (!await tryBind(port, host)) return false;
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
function tryBind(port, host) {
|
|
469
|
+
return new Promise((resolve4) => {
|
|
470
|
+
const server = net.createServer();
|
|
471
|
+
server.once("error", (err) => {
|
|
472
|
+
if (err.code === "EADDRINUSE" || err.code === "EACCES") resolve4(false);
|
|
473
|
+
else resolve4(true);
|
|
474
|
+
});
|
|
475
|
+
server.once("listening", () => server.close(() => resolve4(true)));
|
|
476
|
+
server.listen(port, host);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
462
479
|
function checkHttp(port, opts = {}) {
|
|
463
480
|
const path = opts.path ?? "/";
|
|
464
481
|
const host = opts.host ?? "127.0.0.1";
|
|
@@ -1026,9 +1043,10 @@ var Spawner = class {
|
|
|
1026
1043
|
async start(svc, colorIdx, isRestart = false) {
|
|
1027
1044
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1028
1045
|
if (svc.type === "api") {
|
|
1029
|
-
const
|
|
1030
|
-
if (
|
|
1046
|
+
const bindable = await isPortBindable(svc.port);
|
|
1047
|
+
if (!bindable && !isRestart) {
|
|
1031
1048
|
this.log(svc.name, `\u26A0 port ${svc.port} already in use \u2014 skipping`, colorIdx);
|
|
1049
|
+
this.recordCrashedState(svc, colorIdx);
|
|
1032
1050
|
return;
|
|
1033
1051
|
}
|
|
1034
1052
|
}
|
|
@@ -3996,6 +4014,16 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3996
4014
|
proxyOpts
|
|
3997
4015
|
}));
|
|
3998
4016
|
}
|
|
4017
|
+
const daemonStatus = isDaemonRunning(config.name);
|
|
4018
|
+
if (daemonStatus.pid && !daemonStatus.stale) {
|
|
4019
|
+
console.error(`\u274C A devup daemon is already running for "${config.name}" (pid=${daemonStatus.pid}).`);
|
|
4020
|
+
console.error("");
|
|
4021
|
+
console.error("Stop it first with `devup down`, or interact via the control plane:");
|
|
4022
|
+
console.error(" devup ctl status");
|
|
4023
|
+
console.error(" devup ctl logs <svc> --follow");
|
|
4024
|
+
console.error(" devup ctl restart <svc>");
|
|
4025
|
+
process.exit(1);
|
|
4026
|
+
}
|
|
3999
4027
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
4000
4028
|
const { waitUntilExit } = render(
|
|
4001
4029
|
React7.createElement(App, {
|