@gachlab/devup 0.8.1 → 0.9.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 +35 -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 +177 -11
- package/dist/index.js.map +1 -1
- package/dist/process/port-conflicts.d.ts +33 -0
- package/dist/process/port-conflicts.d.ts.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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.1] — 2026-05-22
|
|
9
|
+
|
|
10
|
+
Hotfix for two issues reported moments after 0.9.0 hit:
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
16
|
+
## [0.9.0] — 2026-05-22
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **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`):
|
|
22
|
+
```
|
|
23
|
+
⚠ Port conflicts detected on the following services:
|
|
24
|
+
|
|
25
|
+
:3002 authorization-api pid=12345 process=node
|
|
26
|
+
:3013 files-api pid=99999 process=docker-proxy
|
|
27
|
+
|
|
28
|
+
Kill these processes and continue? [y/N]:
|
|
29
|
+
```
|
|
30
|
+
Three flavours of resolution:
|
|
31
|
+
- **Interactive TTY** → y/N prompt (default).
|
|
32
|
+
- **`--kill-port-conflicts` flag** → auto-kill, no prompt. Required for `devup up -d`, `--once`, and any non-TTY context (CI).
|
|
33
|
+
- **Non-interactive without the flag** → fail fast with the conflict list as instructions.
|
|
34
|
+
- **`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`.
|
|
35
|
+
|
|
36
|
+
### Internals
|
|
37
|
+
- 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.
|
|
38
|
+
- 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.
|
|
39
|
+
|
|
40
|
+
### Notes
|
|
41
|
+
- 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.
|
|
42
|
+
|
|
8
43
|
## [0.8.1] — 2026-05-22
|
|
9
44
|
|
|
10
45
|
Patch focused on two real-world footguns surfaced as soon as 0.8.0 hit users.
|
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":"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
|
@@ -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;
|
|
@@ -2525,6 +2534,119 @@ function runHelp(argv, opts = {}) {
|
|
|
2525
2534
|
return 0;
|
|
2526
2535
|
}
|
|
2527
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
|
+
|
|
2528
2650
|
// src/platform/detect.ts
|
|
2529
2651
|
async function detectPlatform() {
|
|
2530
2652
|
switch (process.platform) {
|
|
@@ -3981,6 +4103,33 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3981
4103
|
if (cliArgs.logFile) {
|
|
3982
4104
|
logSink = new LogSink({ projectName: config.name, rootDir: cliArgs.logDir });
|
|
3983
4105
|
}
|
|
4106
|
+
if (process.env.DEVUP_DAEMON_CHILD !== "1") {
|
|
4107
|
+
const daemonStatus = isDaemonRunning(config.name);
|
|
4108
|
+
if (daemonStatus.pid && !daemonStatus.stale) {
|
|
4109
|
+
console.error(`\u274C A devup daemon is already running for "${config.name}" (pid=${daemonStatus.pid}).`);
|
|
4110
|
+
console.error("");
|
|
4111
|
+
console.error("Stop it first with `devup down`, or interact via the control plane:");
|
|
4112
|
+
console.error(" devup ctl status");
|
|
4113
|
+
console.error(" devup ctl logs <svc> --follow");
|
|
4114
|
+
console.error(" devup ctl restart <svc>");
|
|
4115
|
+
await logSink?.close();
|
|
4116
|
+
process.exit(1);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
if (process.env.DEVUP_DAEMON_CHILD !== "1") {
|
|
4120
|
+
const conflicts = await scanPortConflicts(services);
|
|
4121
|
+
if (conflicts.length) {
|
|
4122
|
+
const resolved = await resolvePortConflicts(conflicts, {
|
|
4123
|
+
autoKill: cliArgs.killPortConflicts,
|
|
4124
|
+
out: (msg) => process.stderr.write(msg + "\n"),
|
|
4125
|
+
prompt: () => askYesNo("Kill these processes and continue? [y/N]: ")
|
|
4126
|
+
});
|
|
4127
|
+
if (!resolved) {
|
|
4128
|
+
await logSink?.close();
|
|
4129
|
+
process.exit(1);
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
3984
4133
|
if (cliArgs.once) {
|
|
3985
4134
|
const code = await runOnce({
|
|
3986
4135
|
config,
|
|
@@ -4014,16 +4163,6 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4014
4163
|
proxyOpts
|
|
4015
4164
|
}));
|
|
4016
4165
|
}
|
|
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
|
-
}
|
|
4027
4166
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
4028
4167
|
const { waitUntilExit } = render(
|
|
4029
4168
|
React7.createElement(App, {
|
|
@@ -4041,6 +4180,33 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
4041
4180
|
);
|
|
4042
4181
|
await waitUntilExit();
|
|
4043
4182
|
}
|
|
4183
|
+
function askYesNo(question) {
|
|
4184
|
+
return new Promise((resolve4) => {
|
|
4185
|
+
const isTTY = Boolean(process.stdin.isTTY || process.stderr.isTTY || process.stdout.isTTY);
|
|
4186
|
+
if (!isTTY) {
|
|
4187
|
+
resolve4(false);
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
process.stderr.write(question);
|
|
4191
|
+
process.stdin.resume();
|
|
4192
|
+
process.stdin.setEncoding("utf8");
|
|
4193
|
+
const cleanup = () => {
|
|
4194
|
+
process.stdin.removeListener("data", onData);
|
|
4195
|
+
process.stdin.removeListener("end", onEnd);
|
|
4196
|
+
process.stdin.pause();
|
|
4197
|
+
};
|
|
4198
|
+
const onData = (data) => {
|
|
4199
|
+
cleanup();
|
|
4200
|
+
resolve4(/^y(es)?$/i.test(String(data).trim()));
|
|
4201
|
+
};
|
|
4202
|
+
const onEnd = () => {
|
|
4203
|
+
cleanup();
|
|
4204
|
+
resolve4(false);
|
|
4205
|
+
};
|
|
4206
|
+
process.stdin.once("data", onData);
|
|
4207
|
+
process.stdin.once("end", onEnd);
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4044
4210
|
main().catch((e) => {
|
|
4045
4211
|
console.error(e);
|
|
4046
4212
|
process.exit(1);
|