@gachlab/devup 0.9.0 → 0.9.2
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 +19 -0
- package/dist/control-plane/client.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.9.2] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
Critical hotfix. **All 0.9.x users should upgrade immediately.**
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Bundle's top-level `main()` would fire when devup was imported as a library.** When a user's `devup.config.ts` did `import { defineConfig } from '@gachlab/devup'`, Node loaded our compiled `dist/index.js` to satisfy the import — and the bundle's top-level `main()` invocation ran, starting a SECOND, concurrent devup process. Symptoms: every line of output duplicated, three or more racing instances of the same service spawning in milliseconds, ports clobbered, the daemon's socket created but never bound, `devup ctl ping` ECONNREFUSED. Fix: guard `main()` with a `realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)` check so it only fires when the script is invoked directly (as the `devup` binary), not when imported.
|
|
14
|
+
- **`devup ctl` crashed with an unhandled `error` event on ECONNREFUSED.** The socket-error handler was attached to the connection but Node's `readline.createInterface` re-forwarded the error through its own emitter, which had no listener, so the process crashed instead of reporting cleanly. Now we attach the handler on both the socket and the readline interface.
|
|
15
|
+
|
|
16
|
+
### Why this matters
|
|
17
|
+
Every devup invocation that loaded a config file (i.e. everything except `--version` / `--help`) was silently running two instances of itself. That is the root cause of every weird behaviour reported against 0.9.0 / 0.9.1: duplicated prompts, daemons that "die" right after starting, sockets that exist but refuse connections, port-takeover prompts firing for ports the user expected devup to own.
|
|
18
|
+
|
|
19
|
+
## [0.9.1] — 2026-05-22
|
|
20
|
+
|
|
21
|
+
Hotfix for two issues reported moments after 0.9.0 hit:
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
- **The y/N prompt no longer no-ops silently.** The conflict list would print, then the process would just continue without waiting for input on some terminals (IDE integrated terminals, multiplexers, custom shells where `process.stdin.isTTY` is misreported). Replaced `readline.question` with direct stdin handling. TTY detection now also accepts stderr / stdout being a TTY when stdin isn't — covers more real environments.
|
|
25
|
+
- **Daemon-already-running guard moved before the port scan.** Running `devup` (TUI) or `devup up -d` while a daemon was already up for the same project caused the scan to list the daemon's own services as conflicts, prompt the user to kill them, restart them (because the daemon's auto-restarter kicked in), then bail with "daemon already running". Now the daemon check runs first and short-circuits cleanly — no churn, single clear error.
|
|
26
|
+
|
|
8
27
|
## [0.9.0] — 2026-05-22
|
|
9
28
|
|
|
10
29
|
Pre-boot port-conflict resolution. When devup detects another process already holding a port it needs, it now offers to take it over instead of silently marking the service as crashed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/control-plane/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAMhF;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/control-plane/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhF;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAMhF;AAED,0EAA0E;AAC1E,wBAAgB,OAAO,CACrB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,OAAO,CAAC,CAyBlB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;+FAC+F;AAC/F,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,EACrC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAC7B,MAAM,IAAI,CA0BZ"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChG,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
6
|
+
import { readFileSync as readFileSync4, realpathSync } from "fs";
|
|
7
7
|
import { dirname as dirname7, join as join10 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
9
|
import { homedir as homedir5 } from "os";
|
|
@@ -884,17 +884,31 @@ Start it with \`devup\` first.`
|
|
|
884
884
|
}
|
|
885
885
|
function sendRpc(socketPath, method, params = {}) {
|
|
886
886
|
return new Promise((resolve4, reject) => {
|
|
887
|
+
let settled = false;
|
|
888
|
+
const fail = (err) => {
|
|
889
|
+
if (!settled) {
|
|
890
|
+
settled = true;
|
|
891
|
+
reject(err);
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
const ok = (v) => {
|
|
895
|
+
if (!settled) {
|
|
896
|
+
settled = true;
|
|
897
|
+
resolve4(v);
|
|
898
|
+
}
|
|
899
|
+
};
|
|
887
900
|
const c = createConnection(socketPath);
|
|
888
|
-
c.on("error", reject);
|
|
889
901
|
const rl = createInterface2({ input: c });
|
|
902
|
+
c.on("error", fail);
|
|
903
|
+
rl.on("error", fail);
|
|
890
904
|
rl.once("line", (l) => {
|
|
891
905
|
c.end();
|
|
892
906
|
try {
|
|
893
907
|
const msg = JSON.parse(l);
|
|
894
|
-
if (msg.error)
|
|
895
|
-
else
|
|
908
|
+
if (msg.error) fail(new Error(msg.error.message ?? String(msg.error)));
|
|
909
|
+
else ok(msg.result);
|
|
896
910
|
} catch (e) {
|
|
897
|
-
|
|
911
|
+
fail(e);
|
|
898
912
|
}
|
|
899
913
|
});
|
|
900
914
|
c.write(JSON.stringify({ id: 1, method, params }) + "\n");
|
|
@@ -904,7 +918,9 @@ function openStream(socketPath, method, params, onFrame, onError) {
|
|
|
904
918
|
const c = createConnection(socketPath);
|
|
905
919
|
const rl = createInterface2({ input: c });
|
|
906
920
|
let ackDone = false;
|
|
907
|
-
|
|
921
|
+
const onErr = (err) => onError?.(err);
|
|
922
|
+
c.on("error", onErr);
|
|
923
|
+
rl.on("error", onErr);
|
|
908
924
|
c.write(JSON.stringify({ id: 1, method, params }) + "\n");
|
|
909
925
|
rl.on("line", (l) => {
|
|
910
926
|
try {
|
|
@@ -2647,9 +2663,6 @@ async function killAll(conflicts, out) {
|
|
|
2647
2663
|
return allOk;
|
|
2648
2664
|
}
|
|
2649
2665
|
|
|
2650
|
-
// src/index.ts
|
|
2651
|
-
import { createInterface as createInterface6 } from "readline";
|
|
2652
|
-
|
|
2653
2666
|
// src/platform/detect.ts
|
|
2654
2667
|
async function detectPlatform() {
|
|
2655
2668
|
switch (process.platform) {
|
|
@@ -4106,6 +4119,19 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4106
4119
|
if (cliArgs.logFile) {
|
|
4107
4120
|
logSink = new LogSink({ projectName: config.name, rootDir: cliArgs.logDir });
|
|
4108
4121
|
}
|
|
4122
|
+
if (process.env.DEVUP_DAEMON_CHILD !== "1") {
|
|
4123
|
+
const daemonStatus = isDaemonRunning(config.name);
|
|
4124
|
+
if (daemonStatus.pid && !daemonStatus.stale) {
|
|
4125
|
+
console.error(`\u274C A devup daemon is already running for "${config.name}" (pid=${daemonStatus.pid}).`);
|
|
4126
|
+
console.error("");
|
|
4127
|
+
console.error("Stop it first with `devup down`, or interact via the control plane:");
|
|
4128
|
+
console.error(" devup ctl status");
|
|
4129
|
+
console.error(" devup ctl logs <svc> --follow");
|
|
4130
|
+
console.error(" devup ctl restart <svc>");
|
|
4131
|
+
await logSink?.close();
|
|
4132
|
+
process.exit(1);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4109
4135
|
if (process.env.DEVUP_DAEMON_CHILD !== "1") {
|
|
4110
4136
|
const conflicts = await scanPortConflicts(services);
|
|
4111
4137
|
if (conflicts.length) {
|
|
@@ -4153,16 +4179,6 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4153
4179
|
proxyOpts
|
|
4154
4180
|
}));
|
|
4155
4181
|
}
|
|
4156
|
-
const daemonStatus = isDaemonRunning(config.name);
|
|
4157
|
-
if (daemonStatus.pid && !daemonStatus.stale) {
|
|
4158
|
-
console.error(`\u274C A devup daemon is already running for "${config.name}" (pid=${daemonStatus.pid}).`);
|
|
4159
|
-
console.error("");
|
|
4160
|
-
console.error("Stop it first with `devup down`, or interact via the control plane:");
|
|
4161
|
-
console.error(" devup ctl status");
|
|
4162
|
-
console.error(" devup ctl logs <svc> --follow");
|
|
4163
|
-
console.error(" devup ctl restart <svc>");
|
|
4164
|
-
process.exit(1);
|
|
4165
|
-
}
|
|
4166
4182
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
4167
4183
|
const { waitUntilExit } = render(
|
|
4168
4184
|
React7.createElement(App, {
|
|
@@ -4182,21 +4198,47 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4182
4198
|
}
|
|
4183
4199
|
function askYesNo(question) {
|
|
4184
4200
|
return new Promise((resolve4) => {
|
|
4185
|
-
|
|
4201
|
+
const isTTY = Boolean(process.stdin.isTTY || process.stderr.isTTY || process.stdout.isTTY);
|
|
4202
|
+
if (!isTTY) {
|
|
4186
4203
|
resolve4(false);
|
|
4187
4204
|
return;
|
|
4188
4205
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4206
|
+
process.stderr.write(question);
|
|
4207
|
+
process.stdin.resume();
|
|
4208
|
+
process.stdin.setEncoding("utf8");
|
|
4209
|
+
const cleanup = () => {
|
|
4210
|
+
process.stdin.removeListener("data", onData);
|
|
4211
|
+
process.stdin.removeListener("end", onEnd);
|
|
4212
|
+
process.stdin.pause();
|
|
4213
|
+
};
|
|
4214
|
+
const onData = (data) => {
|
|
4215
|
+
cleanup();
|
|
4216
|
+
resolve4(/^y(es)?$/i.test(String(data).trim()));
|
|
4217
|
+
};
|
|
4218
|
+
const onEnd = () => {
|
|
4219
|
+
cleanup();
|
|
4220
|
+
resolve4(false);
|
|
4221
|
+
};
|
|
4222
|
+
process.stdin.once("data", onData);
|
|
4223
|
+
process.stdin.once("end", onEnd);
|
|
4224
|
+
});
|
|
4225
|
+
}
|
|
4226
|
+
function isInvokedDirectly() {
|
|
4227
|
+
const argvPath = process.argv[1];
|
|
4228
|
+
if (!argvPath) return false;
|
|
4229
|
+
const moduleFile = fileURLToPath2(import.meta.url);
|
|
4230
|
+
try {
|
|
4231
|
+
return realpathSync(argvPath) === moduleFile;
|
|
4232
|
+
} catch {
|
|
4233
|
+
return argvPath === moduleFile;
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
if (isInvokedDirectly()) {
|
|
4237
|
+
main().catch((e) => {
|
|
4238
|
+
console.error(e);
|
|
4239
|
+
process.exit(1);
|
|
4194
4240
|
});
|
|
4195
4241
|
}
|
|
4196
|
-
main().catch((e) => {
|
|
4197
|
-
console.error(e);
|
|
4198
|
-
process.exit(1);
|
|
4199
|
-
});
|
|
4200
4242
|
export {
|
|
4201
4243
|
defineConfig
|
|
4202
4244
|
};
|