@gachlab/devup 0.8.0 → 0.9.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/CHANGELOG.md +41 -0
- package/README.md +1 -0
- package/dist/config/cli.d.ts +2 -1
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +183 -3
- 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/port-conflicts.d.ts +33 -0
- package/dist/process/port-conflicts.d.ts.map +1 -0
- package/dist/process/spawner.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.0] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Pre-boot port conflict scan.** Before mounting the TUI or starting the daemon, devup checks every API-typed service's port. Any conflict is shown to the user with the PID and process name (via `lsof`):
|
|
14
|
+
```
|
|
15
|
+
⚠ Port conflicts detected on the following services:
|
|
16
|
+
|
|
17
|
+
:3002 authorization-api pid=12345 process=node
|
|
18
|
+
:3013 files-api pid=99999 process=docker-proxy
|
|
19
|
+
|
|
20
|
+
Kill these processes and continue? [y/N]:
|
|
21
|
+
```
|
|
22
|
+
Three flavours of resolution:
|
|
23
|
+
- **Interactive TTY** → y/N prompt (default).
|
|
24
|
+
- **`--kill-port-conflicts` flag** → auto-kill, no prompt. Required for `devup up -d`, `--once`, and any non-TTY context (CI).
|
|
25
|
+
- **Non-interactive without the flag** → fail fast with the conflict list as instructions.
|
|
26
|
+
- **`devup up -d` runs the scan in the parent** before forking the daemon child, so the user gets the prompt (or the error) in their terminal, not buried in `~/.devup/<project>.boot-error`.
|
|
27
|
+
|
|
28
|
+
### Internals
|
|
29
|
+
- New `src/process/port-conflicts.ts` houses the scan, the lsof-based holder lookup (`findPortHolder`), the resolution flow (`resolvePortConflicts`), and the SIGTERM-then-SIGKILL helper (`killHolder`). Linux + macOS only — Windows holder detection (netstat / Get-NetTCPConnection) is deferred until daemon mode itself supports Windows.
|
|
30
|
+
- Test suite: 372 → 384 (+12). New: 4 scan tests, 2 killHolder tests, 4 resolution-flow tests, 1 CLI flag test, 1 lsof-output parser test.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
- Webs are intentionally skipped from the scan — their dev servers (Vite, Angular CLI, etc.) often handle port reuse themselves, and the user's editor / preview tabs hold web ports far more often than stale dev processes do.
|
|
34
|
+
|
|
35
|
+
## [0.8.1] — 2026-05-22
|
|
36
|
+
|
|
37
|
+
Patch focused on two real-world footguns surfaced as soon as 0.8.0 hit users.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- **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.
|
|
41
|
+
- **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.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
- **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.
|
|
45
|
+
|
|
46
|
+
### Internals
|
|
47
|
+
- 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.
|
|
48
|
+
|
|
8
49
|
## [0.8.0] — 2026-05-22
|
|
9
50
|
|
|
10
51
|
**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/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Built with TypeScript 6, Ink (React for terminals), and zero test dependencies (
|
|
|
41
41
|
- **Subcommands** — `devup logs <svc> [--follow]`, `devup install`, `devup status`, `devup help` work without launching the TUI.
|
|
42
42
|
- **CI-ready** — `--dry-run` prints the resolved boot plan; `--once` boots, waits for readiness, exits `0/1` without a TUI.
|
|
43
43
|
- **Daemon mode** — `devup up -d` boots the stack detached (like `docker compose up -d`) so you can keep using the same terminal. `devup down` stops it. Linux + macOS.
|
|
44
|
+
- **Port-conflict takeover** — when something else is already on a configured port, devup shows you the holder (PID + process name) and offers to kill it. `--kill-port-conflicts` for non-interactive runs.
|
|
44
45
|
- **`devup ctl`** — CLI client for the control plane: `ping`, `status [--follow]`, `logs <svc> [--follow]`, `restart`, `stop`.
|
|
45
46
|
- **Reverse-proxy config** — generate Traefik, Nginx, or Caddy config from running services; health-aware.
|
|
46
47
|
- **Unix-socket control plane** — local JSON-RPC at `~/.devup/sock-<project>.sock` (chmod 0600); `status`, `restart`, `stop`, `logs.tail`, `logs.follow`, `status.follow`, `ping`.
|
package/dist/config/cli.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export interface CliArgs {
|
|
|
18
18
|
logFile: boolean;
|
|
19
19
|
logDir?: string;
|
|
20
20
|
watchConfig: boolean;
|
|
21
|
+
killPortConflicts: boolean;
|
|
21
22
|
}
|
|
22
|
-
export declare const USAGE = "devup \u2014 terminal UI dev stack runner\n\nUsage: devup [options]\n\nService selection:\n --only apis | webs Start only APIs or only webs\n --services a,b,c Start only the named services\n --profile <name> Start the services in a named profile (see ROADMAP)\n --skip a,b,c Start everything except these\n --config <path> Use a custom config file\n\nLazy mode:\n --lazy Enable lazy mode (default)\n --no-lazy Start every service immediately\n --timeout <minutes> Idle timeout for lazy services. Default: 10\n\nReverse proxy:\n --proxy Enable proxy config generation\n --proxy-host <host> Override the target host (Docker/local)\n --proxy-conf <path> Override the generated config file path\n --proxy-tls Enable TLS in the generated config (default)\n --no-proxy-tls Disable TLS\n --proxy-entrypoint <n> Override entrypoint name (Traefik only)\n\nCI / scripting:\n --dry-run Print the resolved boot plan and exit\n --once Boot, wait for readiness, exit 0/1 (no TUI)\n --once-timeout <s> Max seconds to wait in --once mode. Default: 90\n\nLog files:\n --no-log-file Disable persistent log files\n --log-dir <path> Override log root (default: ~/.devup/logs)\n\nHot reload:\n --watch-config Watch devup.config.* and apply add/remove/restart\n service changes without exiting the TUI\n\nOther:\n -h, --help Show this help and exit\n -v, --version Show version and exit\n\nSee https://github.com/gachlab/devup for the full documentation.";
|
|
23
|
+
export declare const USAGE = "devup \u2014 terminal UI dev stack runner\n\nUsage: devup [options]\n\nService selection:\n --only apis | webs Start only APIs or only webs\n --services a,b,c Start only the named services\n --profile <name> Start the services in a named profile (see ROADMAP)\n --skip a,b,c Start everything except these\n --config <path> Use a custom config file\n\nLazy mode:\n --lazy Enable lazy mode (default)\n --no-lazy Start every service immediately\n --timeout <minutes> Idle timeout for lazy services. Default: 10\n\nReverse proxy:\n --proxy Enable proxy config generation\n --proxy-host <host> Override the target host (Docker/local)\n --proxy-conf <path> Override the generated config file path\n --proxy-tls Enable TLS in the generated config (default)\n --no-proxy-tls Disable TLS\n --proxy-entrypoint <n> Override entrypoint name (Traefik only)\n\nCI / scripting:\n --dry-run Print the resolved boot plan and exit\n --once Boot, wait for readiness, exit 0/1 (no TUI)\n --once-timeout <s> Max seconds to wait in --once mode. Default: 90\n\nLog files:\n --no-log-file Disable persistent log files\n --log-dir <path> Override log root (default: ~/.devup/logs)\n\nHot reload:\n --watch-config Watch devup.config.* and apply add/remove/restart\n service changes without exiting the TUI\n\nPort conflicts:\n --kill-port-conflicts Kill any processes already holding a configured\n port before boot. Interactive prompt without it;\n required for non-TTY (daemon, --once, CI)\n\nOther:\n -h, --help Show this help and exit\n -v, --version Show version and exit\n\nSee https://github.com/gachlab/devup for the full documentation.";
|
|
23
24
|
export declare function parseCliArgs(argv: string[]): CliArgs;
|
|
24
25
|
export declare function filterServices(services: ServiceConfig[], args: CliArgs, config?: Pick<DevStackConfig, 'profiles'>): ServiceConfig[];
|
|
25
26
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/config/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAKD,eAAO,MAAM,KAAK,i6DA8C+C,CAAC;AAElE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CA8CpD;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,GACxC,aAAa,EAAE,CA6BjB"}
|
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":"AAwBA,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
|
@@ -296,6 +296,11 @@ Hot reload:
|
|
|
296
296
|
--watch-config Watch devup.config.* and apply add/remove/restart
|
|
297
297
|
service changes without exiting the TUI
|
|
298
298
|
|
|
299
|
+
Port conflicts:
|
|
300
|
+
--kill-port-conflicts Kill any processes already holding a configured
|
|
301
|
+
port before boot. Interactive prompt without it;
|
|
302
|
+
required for non-TTY (daemon, --once, CI)
|
|
303
|
+
|
|
299
304
|
Other:
|
|
300
305
|
-h, --help Show this help and exit
|
|
301
306
|
-v, --version Show version and exit
|
|
@@ -313,7 +318,8 @@ function parseCliArgs(argv) {
|
|
|
313
318
|
once: false,
|
|
314
319
|
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
315
320
|
logFile: true,
|
|
316
|
-
watchConfig: false
|
|
321
|
+
watchConfig: false,
|
|
322
|
+
killPortConflicts: false
|
|
317
323
|
};
|
|
318
324
|
for (let i = 0; i < argv.length; i++) {
|
|
319
325
|
const arg = argv[i];
|
|
@@ -390,6 +396,9 @@ function parseCliArgs(argv) {
|
|
|
390
396
|
case "--watch-config":
|
|
391
397
|
args.watchConfig = true;
|
|
392
398
|
break;
|
|
399
|
+
case "--kill-port-conflicts":
|
|
400
|
+
args.killPortConflicts = true;
|
|
401
|
+
break;
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
return args;
|
|
@@ -459,6 +468,23 @@ function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
|
459
468
|
socket.connect(port, host);
|
|
460
469
|
});
|
|
461
470
|
}
|
|
471
|
+
async function isPortBindable(port) {
|
|
472
|
+
for (const host of ["0.0.0.0", "::"]) {
|
|
473
|
+
if (!await tryBind(port, host)) return false;
|
|
474
|
+
}
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
function tryBind(port, host) {
|
|
478
|
+
return new Promise((resolve4) => {
|
|
479
|
+
const server = net.createServer();
|
|
480
|
+
server.once("error", (err) => {
|
|
481
|
+
if (err.code === "EADDRINUSE" || err.code === "EACCES") resolve4(false);
|
|
482
|
+
else resolve4(true);
|
|
483
|
+
});
|
|
484
|
+
server.once("listening", () => server.close(() => resolve4(true)));
|
|
485
|
+
server.listen(port, host);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
462
488
|
function checkHttp(port, opts = {}) {
|
|
463
489
|
const path = opts.path ?? "/";
|
|
464
490
|
const host = opts.host ?? "127.0.0.1";
|
|
@@ -1026,9 +1052,10 @@ var Spawner = class {
|
|
|
1026
1052
|
async start(svc, colorIdx, isRestart = false) {
|
|
1027
1053
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1028
1054
|
if (svc.type === "api") {
|
|
1029
|
-
const
|
|
1030
|
-
if (
|
|
1055
|
+
const bindable = await isPortBindable(svc.port);
|
|
1056
|
+
if (!bindable && !isRestart) {
|
|
1031
1057
|
this.log(svc.name, `\u26A0 port ${svc.port} already in use \u2014 skipping`, colorIdx);
|
|
1058
|
+
this.recordCrashedState(svc, colorIdx);
|
|
1032
1059
|
return;
|
|
1033
1060
|
}
|
|
1034
1061
|
}
|
|
@@ -2507,6 +2534,122 @@ function runHelp(argv, opts = {}) {
|
|
|
2507
2534
|
return 0;
|
|
2508
2535
|
}
|
|
2509
2536
|
|
|
2537
|
+
// src/process/port-conflicts.ts
|
|
2538
|
+
import { exec } from "child_process";
|
|
2539
|
+
import { promisify } from "util";
|
|
2540
|
+
import { setTimeout as sleep2 } from "timers/promises";
|
|
2541
|
+
var execAsync = promisify(exec);
|
|
2542
|
+
var isUnix = process.platform === "linux" || process.platform === "darwin";
|
|
2543
|
+
async function findPortHolder(port) {
|
|
2544
|
+
if (!isUnix) return null;
|
|
2545
|
+
try {
|
|
2546
|
+
const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -F pcn`);
|
|
2547
|
+
return parseLsof(stdout);
|
|
2548
|
+
} catch {
|
|
2549
|
+
return null;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
function parseLsof(stdout) {
|
|
2553
|
+
let pid = null;
|
|
2554
|
+
let cmd = "";
|
|
2555
|
+
for (const line of stdout.split("\n")) {
|
|
2556
|
+
if (!line) continue;
|
|
2557
|
+
const tag = line[0];
|
|
2558
|
+
const value = line.slice(1);
|
|
2559
|
+
if (tag === "p") {
|
|
2560
|
+
if (pid != null) return { pid, command: cmd || "unknown" };
|
|
2561
|
+
pid = Number(value);
|
|
2562
|
+
} else if (tag === "c") {
|
|
2563
|
+
cmd = value;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (pid != null) return { pid, command: cmd || "unknown" };
|
|
2567
|
+
return null;
|
|
2568
|
+
}
|
|
2569
|
+
async function scanPortConflicts(services) {
|
|
2570
|
+
const apis = services.filter((s) => s.type === "api");
|
|
2571
|
+
const conflicts = [];
|
|
2572
|
+
for (const svc of apis) {
|
|
2573
|
+
const bindable = await isPortBindable(svc.port);
|
|
2574
|
+
if (bindable) continue;
|
|
2575
|
+
const holder = await findPortHolder(svc.port);
|
|
2576
|
+
conflicts.push({ service: svc.name, port: svc.port, holder });
|
|
2577
|
+
}
|
|
2578
|
+
return conflicts;
|
|
2579
|
+
}
|
|
2580
|
+
async function killHolder(pid, graceMs = 3e3) {
|
|
2581
|
+
try {
|
|
2582
|
+
process.kill(pid, "SIGTERM");
|
|
2583
|
+
} catch {
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
const deadline = Date.now() + graceMs;
|
|
2587
|
+
while (Date.now() < deadline) {
|
|
2588
|
+
if (!pidAlive2(pid)) return true;
|
|
2589
|
+
await sleep2(100);
|
|
2590
|
+
}
|
|
2591
|
+
try {
|
|
2592
|
+
process.kill(pid, "SIGKILL");
|
|
2593
|
+
} catch {
|
|
2594
|
+
}
|
|
2595
|
+
await sleep2(100);
|
|
2596
|
+
return !pidAlive2(pid);
|
|
2597
|
+
}
|
|
2598
|
+
function pidAlive2(pid) {
|
|
2599
|
+
try {
|
|
2600
|
+
process.kill(pid, 0);
|
|
2601
|
+
return true;
|
|
2602
|
+
} catch {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
async function resolvePortConflicts(conflicts, opts) {
|
|
2607
|
+
if (!conflicts.length) return true;
|
|
2608
|
+
const { autoKill, out, prompt, isInteractive = process.stdin.isTTY ?? false } = opts;
|
|
2609
|
+
out("\u26A0 Port conflicts detected on the following services:");
|
|
2610
|
+
out("");
|
|
2611
|
+
const maxName = Math.max(...conflicts.map((c) => c.service.length), 8);
|
|
2612
|
+
for (const c of conflicts) {
|
|
2613
|
+
const holder = c.holder ? `pid=${c.holder.pid} process=${c.holder.command}` : `(unable to identify holder${isUnix ? "" : " \u2014 Windows not supported"})`;
|
|
2614
|
+
out(` :${String(c.port).padEnd(6)} ${c.service.padEnd(maxName)} ${holder}`);
|
|
2615
|
+
}
|
|
2616
|
+
out("");
|
|
2617
|
+
if (autoKill) {
|
|
2618
|
+
return await killAll(conflicts, out);
|
|
2619
|
+
}
|
|
2620
|
+
if (!isInteractive || !prompt) {
|
|
2621
|
+
out("Re-run with --kill-port-conflicts to take them over, or stop them yourself.");
|
|
2622
|
+
return false;
|
|
2623
|
+
}
|
|
2624
|
+
const confirmed = await prompt();
|
|
2625
|
+
if (!confirmed) {
|
|
2626
|
+
out("Aborted \u2014 no processes killed.");
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
return await killAll(conflicts, out);
|
|
2630
|
+
}
|
|
2631
|
+
async function killAll(conflicts, out) {
|
|
2632
|
+
let allOk = true;
|
|
2633
|
+
for (const c of conflicts) {
|
|
2634
|
+
if (!c.holder) {
|
|
2635
|
+
out(`\u2717 :${c.port} ${c.service}: holder unknown, cannot kill \u2014 skipped`);
|
|
2636
|
+
allOk = false;
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const ok = await killHolder(c.holder.pid);
|
|
2640
|
+
if (ok) {
|
|
2641
|
+
out(`\u2713 :${c.port} ${c.service}: killed pid=${c.holder.pid}`);
|
|
2642
|
+
} else {
|
|
2643
|
+
out(`\u2717 :${c.port} ${c.service}: pid=${c.holder.pid} survived SIGKILL`);
|
|
2644
|
+
allOk = false;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return allOk;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// src/index.ts
|
|
2651
|
+
import { createInterface as createInterface6 } from "readline";
|
|
2652
|
+
|
|
2510
2653
|
// src/platform/detect.ts
|
|
2511
2654
|
async function detectPlatform() {
|
|
2512
2655
|
switch (process.platform) {
|
|
@@ -3963,6 +4106,20 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3963
4106
|
if (cliArgs.logFile) {
|
|
3964
4107
|
logSink = new LogSink({ projectName: config.name, rootDir: cliArgs.logDir });
|
|
3965
4108
|
}
|
|
4109
|
+
if (process.env.DEVUP_DAEMON_CHILD !== "1") {
|
|
4110
|
+
const conflicts = await scanPortConflicts(services);
|
|
4111
|
+
if (conflicts.length) {
|
|
4112
|
+
const resolved = await resolvePortConflicts(conflicts, {
|
|
4113
|
+
autoKill: cliArgs.killPortConflicts,
|
|
4114
|
+
out: (msg) => process.stderr.write(msg + "\n"),
|
|
4115
|
+
prompt: () => askYesNo("Kill these processes and continue? [y/N]: ")
|
|
4116
|
+
});
|
|
4117
|
+
if (!resolved) {
|
|
4118
|
+
await logSink?.close();
|
|
4119
|
+
process.exit(1);
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
3966
4123
|
if (cliArgs.once) {
|
|
3967
4124
|
const code = await runOnce({
|
|
3968
4125
|
config,
|
|
@@ -3996,6 +4153,16 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3996
4153
|
proxyOpts
|
|
3997
4154
|
}));
|
|
3998
4155
|
}
|
|
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
|
+
}
|
|
3999
4166
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
4000
4167
|
const { waitUntilExit } = render(
|
|
4001
4168
|
React7.createElement(App, {
|
|
@@ -4013,6 +4180,19 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4013
4180
|
);
|
|
4014
4181
|
await waitUntilExit();
|
|
4015
4182
|
}
|
|
4183
|
+
function askYesNo(question) {
|
|
4184
|
+
return new Promise((resolve4) => {
|
|
4185
|
+
if (!process.stdin.isTTY) {
|
|
4186
|
+
resolve4(false);
|
|
4187
|
+
return;
|
|
4188
|
+
}
|
|
4189
|
+
const rl = createInterface6({ input: process.stdin, output: process.stderr });
|
|
4190
|
+
rl.question(question, (answer) => {
|
|
4191
|
+
rl.close();
|
|
4192
|
+
resolve4(/^y(es)?$/i.test(answer.trim()));
|
|
4193
|
+
});
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4016
4196
|
main().catch((e) => {
|
|
4017
4197
|
console.error(e);
|
|
4018
4198
|
process.exit(1);
|