@gachlab/devup 0.3.0 → 0.4.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 +30 -0
- package/README.md +16 -0
- package/dist/config/cli.d.ts +1 -0
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +645 -268
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/subcommands.d.ts +20 -0
- package/dist/orchestrator/subcommands.d.ts.map +1 -0
- package/dist/process/manager.d.ts +3 -0
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/tui/App.d.ts +4 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/ServiceList.d.ts.map +1 -1
- package/dist/tui/StatsPanel.d.ts +3 -0
- package/dist/tui/StatsPanel.d.ts.map +1 -1
- package/dist/tui/tips.d.ts +17 -0
- package/dist/tui/tips.d.ts.map +1 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
+
import { dirname as dirname6, join as join7 } from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
|
+
import { homedir as homedir3 } from "os";
|
|
8
10
|
|
|
9
11
|
// src/config/loader.ts
|
|
10
12
|
import { existsSync } from "fs";
|
|
@@ -221,6 +223,44 @@ function formatValidationErrors(errors) {
|
|
|
221
223
|
// src/config/cli.ts
|
|
222
224
|
var DEFAULT_LAZY_TIMEOUT = 10;
|
|
223
225
|
var DEFAULT_ONCE_TIMEOUT = 90;
|
|
226
|
+
var USAGE = `devup \u2014 terminal UI dev stack runner
|
|
227
|
+
|
|
228
|
+
Usage: devup [options]
|
|
229
|
+
|
|
230
|
+
Service selection:
|
|
231
|
+
--only apis | webs Start only APIs or only webs
|
|
232
|
+
--services a,b,c Start only the named services
|
|
233
|
+
--profile <name> Start the services in a named profile (see ROADMAP)
|
|
234
|
+
--skip a,b,c Start everything except these
|
|
235
|
+
--config <path> Use a custom config file
|
|
236
|
+
|
|
237
|
+
Lazy mode:
|
|
238
|
+
--lazy Enable lazy mode (default)
|
|
239
|
+
--no-lazy Start every service immediately
|
|
240
|
+
--timeout <minutes> Idle timeout for lazy services. Default: 10
|
|
241
|
+
|
|
242
|
+
Reverse proxy:
|
|
243
|
+
--proxy Enable proxy config generation
|
|
244
|
+
--proxy-host <host> Override the target host (Docker/local)
|
|
245
|
+
--proxy-conf <path> Override the generated config file path
|
|
246
|
+
--proxy-tls Enable TLS in the generated config (default)
|
|
247
|
+
--no-proxy-tls Disable TLS
|
|
248
|
+
--proxy-entrypoint <n> Override entrypoint name (Traefik only)
|
|
249
|
+
|
|
250
|
+
CI / scripting:
|
|
251
|
+
--dry-run Print the resolved boot plan and exit
|
|
252
|
+
--once Boot, wait for readiness, exit 0/1 (no TUI)
|
|
253
|
+
--once-timeout <s> Max seconds to wait in --once mode. Default: 90
|
|
254
|
+
|
|
255
|
+
Log files:
|
|
256
|
+
--no-log-file Disable persistent log files
|
|
257
|
+
--log-dir <path> Override log root (default: ~/.devup/logs)
|
|
258
|
+
|
|
259
|
+
Other:
|
|
260
|
+
-h, --help Show this help and exit
|
|
261
|
+
-v, --version Show version and exit
|
|
262
|
+
|
|
263
|
+
See https://github.com/gachlab/devup for the full documentation.`;
|
|
224
264
|
function parseCliArgs(argv) {
|
|
225
265
|
const args = {
|
|
226
266
|
skip: [],
|
|
@@ -344,6 +384,359 @@ function filterServices(services, args, config) {
|
|
|
344
384
|
return result;
|
|
345
385
|
}
|
|
346
386
|
|
|
387
|
+
// src/orchestrator/subcommands.ts
|
|
388
|
+
import { spawn } from "child_process";
|
|
389
|
+
import { createReadStream, watchFile, unwatchFile, existsSync as existsSync4, statSync } from "fs";
|
|
390
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
391
|
+
import { join as join3, dirname } from "path";
|
|
392
|
+
import { fileURLToPath } from "url";
|
|
393
|
+
import { homedir } from "os";
|
|
394
|
+
import { createInterface } from "readline";
|
|
395
|
+
|
|
396
|
+
// src/process/health.ts
|
|
397
|
+
import net from "net";
|
|
398
|
+
import http from "http";
|
|
399
|
+
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
400
|
+
return new Promise((resolve4) => {
|
|
401
|
+
const socket = new net.Socket();
|
|
402
|
+
socket.setTimeout(timeoutMs);
|
|
403
|
+
socket.once("connect", () => {
|
|
404
|
+
socket.destroy();
|
|
405
|
+
resolve4(true);
|
|
406
|
+
});
|
|
407
|
+
socket.once("error", () => {
|
|
408
|
+
socket.destroy();
|
|
409
|
+
resolve4(false);
|
|
410
|
+
});
|
|
411
|
+
socket.once("timeout", () => {
|
|
412
|
+
socket.destroy();
|
|
413
|
+
resolve4(false);
|
|
414
|
+
});
|
|
415
|
+
socket.connect(port, host);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function checkHttp(port, opts = {}) {
|
|
419
|
+
const path = opts.path ?? "/";
|
|
420
|
+
const host = opts.host ?? "127.0.0.1";
|
|
421
|
+
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
422
|
+
const accept = (code) => {
|
|
423
|
+
if (opts.expect === void 0) return code >= 200 && code < 300;
|
|
424
|
+
if (Array.isArray(opts.expect)) return opts.expect.includes(code);
|
|
425
|
+
return code === opts.expect;
|
|
426
|
+
};
|
|
427
|
+
return new Promise((resolve4) => {
|
|
428
|
+
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
429
|
+
const ok = typeof res.statusCode === "number" && accept(res.statusCode);
|
|
430
|
+
res.resume();
|
|
431
|
+
resolve4(ok);
|
|
432
|
+
});
|
|
433
|
+
req.on("error", () => resolve4(false));
|
|
434
|
+
req.on("timeout", () => {
|
|
435
|
+
req.destroy();
|
|
436
|
+
resolve4(false);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
function checkHealth(port, hc) {
|
|
441
|
+
if (hc?.type === "http") {
|
|
442
|
+
return checkHttp(port, {
|
|
443
|
+
path: hc.path,
|
|
444
|
+
expect: hc.expect,
|
|
445
|
+
host: hc.host,
|
|
446
|
+
timeoutMs: hc.timeoutMs
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
450
|
+
}
|
|
451
|
+
function waitForPort(port, opts = {}) {
|
|
452
|
+
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
453
|
+
return new Promise((resolve4) => {
|
|
454
|
+
const start = Date.now();
|
|
455
|
+
const check = () => {
|
|
456
|
+
checkPort(port).then((ok) => {
|
|
457
|
+
if (ok) return resolve4(true);
|
|
458
|
+
if (Date.now() - start > timeout) return resolve4(false);
|
|
459
|
+
setTimeout(check, interval);
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
check();
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function deriveHealth(isUp, currentStatus) {
|
|
466
|
+
if (currentStatus === "idle") return "idle";
|
|
467
|
+
if (isUp) return "up";
|
|
468
|
+
return currentStatus === "starting" ? "wait" : "down";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/utils.ts
|
|
472
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
|
|
473
|
+
import { createHash } from "crypto";
|
|
474
|
+
import { join as join2 } from "path";
|
|
475
|
+
function parseEnvFile(filePath, baseEnv = {}) {
|
|
476
|
+
const env = { ...baseEnv };
|
|
477
|
+
if (!existsSync3(filePath)) return env;
|
|
478
|
+
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
479
|
+
const trimmed = line.trim();
|
|
480
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
481
|
+
const eqIdx = trimmed.indexOf("=");
|
|
482
|
+
if (eqIdx === -1) continue;
|
|
483
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
484
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
485
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
486
|
+
val = val.slice(1, -1);
|
|
487
|
+
}
|
|
488
|
+
if (!env[key]) env[key] = val;
|
|
489
|
+
}
|
|
490
|
+
return env;
|
|
491
|
+
}
|
|
492
|
+
function fmtUptime(ms) {
|
|
493
|
+
if (!ms || ms < 0) return "-";
|
|
494
|
+
const s = Math.floor(ms / 1e3);
|
|
495
|
+
if (s < 60) return `${s}s`;
|
|
496
|
+
const m = Math.floor(s / 60);
|
|
497
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
498
|
+
const h = Math.floor(m / 60);
|
|
499
|
+
if (h < 24) return `${h}h${m % 60}m`;
|
|
500
|
+
const d = Math.floor(h / 24);
|
|
501
|
+
return `${d}d${h % 24}h`;
|
|
502
|
+
}
|
|
503
|
+
function needsInstall(fullCwd) {
|
|
504
|
+
const nm = join2(fullCwd, "node_modules");
|
|
505
|
+
if (!existsSync3(nm)) return true;
|
|
506
|
+
try {
|
|
507
|
+
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
508
|
+
const stampFile = join2(nm, ".install-stamp");
|
|
509
|
+
if (existsSync3(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
function writeInstallStamp(fullCwd) {
|
|
515
|
+
try {
|
|
516
|
+
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
517
|
+
writeFileSync(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function sortServiceNames(names, sortMode, statsMap, procState) {
|
|
522
|
+
if (sortMode === "name") return names.slice().sort();
|
|
523
|
+
return names.slice().sort((a, b) => {
|
|
524
|
+
if (sortMode === "mem") {
|
|
525
|
+
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
526
|
+
}
|
|
527
|
+
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function groupByPhase(services) {
|
|
531
|
+
const phases = {};
|
|
532
|
+
for (const s of services) {
|
|
533
|
+
(phases[s.phase] ??= []).push(s);
|
|
534
|
+
}
|
|
535
|
+
return phases;
|
|
536
|
+
}
|
|
537
|
+
function buildProcessArgs(svc) {
|
|
538
|
+
const extra = svc.nodeArgs ?? [];
|
|
539
|
+
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
540
|
+
if (svc.cmd === "node") return [`--max-old-space-size=${svc.maxMem}`, ...extra, ...svc.args];
|
|
541
|
+
return [...extra, ...svc.args];
|
|
542
|
+
}
|
|
543
|
+
function buildProcessEnv(svc, baseEnv) {
|
|
544
|
+
const env = { ...baseEnv, ...svc.extraEnv ?? {} };
|
|
545
|
+
if (svc.maxMem && svc.cmd !== "node") {
|
|
546
|
+
const existing = env["NODE_OPTIONS"] ?? "";
|
|
547
|
+
const flag = `--max-old-space-size=${svc.maxMem}`;
|
|
548
|
+
if (!existing.includes("max-old-space-size")) {
|
|
549
|
+
env["NODE_OPTIONS"] = existing ? `${existing} ${flag}` : flag;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return env;
|
|
553
|
+
}
|
|
554
|
+
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
555
|
+
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
556
|
+
const cpuDelta = totalCpuSec - prevCpu;
|
|
557
|
+
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
558
|
+
}
|
|
559
|
+
var tagColors = [
|
|
560
|
+
"cyan",
|
|
561
|
+
"yellow",
|
|
562
|
+
"green",
|
|
563
|
+
"magenta",
|
|
564
|
+
"blue",
|
|
565
|
+
"red",
|
|
566
|
+
"#5faf5f",
|
|
567
|
+
"#d7af5f",
|
|
568
|
+
"#5f87d7",
|
|
569
|
+
"#af5faf",
|
|
570
|
+
"#5fd7d7",
|
|
571
|
+
"#d75f5f",
|
|
572
|
+
"white"
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
// src/orchestrator/subcommands.ts
|
|
576
|
+
var KNOWN = /* @__PURE__ */ new Set(["logs", "install", "status", "help"]);
|
|
577
|
+
function detectSubcommand(argv) {
|
|
578
|
+
const first = argv[0];
|
|
579
|
+
return first && KNOWN.has(first) ? first : null;
|
|
580
|
+
}
|
|
581
|
+
function logRoot(config, override) {
|
|
582
|
+
const root = override ?? join3(homedir(), ".devup", "logs");
|
|
583
|
+
return join3(root, sanitize(config.name));
|
|
584
|
+
}
|
|
585
|
+
function sanitize(name) {
|
|
586
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
587
|
+
}
|
|
588
|
+
async function runLogs(argv, opts) {
|
|
589
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
590
|
+
const follow = argv.includes("--follow") || argv.includes("-f");
|
|
591
|
+
const svcArg = argv.find((a) => !a.startsWith("-"));
|
|
592
|
+
if (!svcArg) {
|
|
593
|
+
out("usage: devup logs <service> [--follow]");
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
const knownSvcs = opts.config.services.map((s) => s.name);
|
|
597
|
+
if (!knownSvcs.includes(svcArg)) {
|
|
598
|
+
out(`Unknown service "${svcArg}". Known: ${knownSvcs.join(", ")}`);
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
const file = join3(logRoot(opts.config, opts.logDir), `${sanitize(svcArg)}.log`);
|
|
602
|
+
if (!existsSync4(file)) {
|
|
603
|
+
out(`No log file yet for "${svcArg}" (${file})`);
|
|
604
|
+
return follow ? await followFile(file, out) : 1;
|
|
605
|
+
}
|
|
606
|
+
await streamFile(file, out);
|
|
607
|
+
if (!follow) return 0;
|
|
608
|
+
return await followFile(file, out, statSync(file).size);
|
|
609
|
+
}
|
|
610
|
+
async function streamFile(file, out) {
|
|
611
|
+
return new Promise((resolve4, reject) => {
|
|
612
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: "utf8" }) });
|
|
613
|
+
rl.on("line", (l) => out(l));
|
|
614
|
+
rl.on("close", () => resolve4());
|
|
615
|
+
rl.on("error", reject);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
async function followFile(file, out, startAt = 0) {
|
|
619
|
+
let pos = startAt;
|
|
620
|
+
while (!existsSync4(file)) await new Promise((r) => setTimeout(r, 500));
|
|
621
|
+
return new Promise((resolve4) => {
|
|
622
|
+
const tick = async () => {
|
|
623
|
+
const size = statSync(file).size;
|
|
624
|
+
if (size > pos) {
|
|
625
|
+
await new Promise((res) => {
|
|
626
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: "utf8", start: pos, end: size - 1 }) });
|
|
627
|
+
rl.on("line", (l) => out(l));
|
|
628
|
+
rl.on("close", () => {
|
|
629
|
+
pos = size;
|
|
630
|
+
res();
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
} else if (size < pos) {
|
|
634
|
+
pos = 0;
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
watchFile(file, { interval: 500 }, () => {
|
|
638
|
+
void tick();
|
|
639
|
+
});
|
|
640
|
+
process.once("SIGINT", () => {
|
|
641
|
+
unwatchFile(file);
|
|
642
|
+
resolve4(0);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async function runInstall(opts) {
|
|
647
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
648
|
+
const concurrency = opts.concurrency ?? 4;
|
|
649
|
+
const items = opts.config.services.map((s) => ({ name: s.name, cwd: join3(opts.baseCwd, s.cwd) }));
|
|
650
|
+
const queue = [...items];
|
|
651
|
+
const failed = [];
|
|
652
|
+
let inFlight = 0;
|
|
653
|
+
await new Promise((resolve4) => {
|
|
654
|
+
const pump = () => {
|
|
655
|
+
while (inFlight < concurrency && queue.length) {
|
|
656
|
+
const item = queue.shift();
|
|
657
|
+
inFlight++;
|
|
658
|
+
installOne(item.cwd, opts.env).then((ok) => {
|
|
659
|
+
inFlight--;
|
|
660
|
+
if (ok) out(`\u2713 ${item.name}`);
|
|
661
|
+
else {
|
|
662
|
+
failed.push(item.name);
|
|
663
|
+
out(`\u2717 ${item.name}`);
|
|
664
|
+
}
|
|
665
|
+
if (queue.length === 0 && inFlight === 0) resolve4();
|
|
666
|
+
else pump();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
pump();
|
|
671
|
+
});
|
|
672
|
+
if (failed.length) {
|
|
673
|
+
out(`
|
|
674
|
+
failed: ${failed.join(", ")}`);
|
|
675
|
+
return 1;
|
|
676
|
+
}
|
|
677
|
+
out(`
|
|
678
|
+
${items.length} services up to date`);
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
function installOne(cwd, env) {
|
|
682
|
+
if (!existsSync4(cwd)) return Promise.resolve(false);
|
|
683
|
+
if (!needsInstall(cwd)) return Promise.resolve(true);
|
|
684
|
+
return new Promise((resolve4) => {
|
|
685
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
686
|
+
const proc = spawn(command, ["install"], { cwd, env, stdio: ["ignore", "ignore", "pipe"] });
|
|
687
|
+
proc.on("close", (code) => {
|
|
688
|
+
if (code === 0) {
|
|
689
|
+
writeInstallStamp(cwd);
|
|
690
|
+
resolve4(true);
|
|
691
|
+
} else resolve4(false);
|
|
692
|
+
});
|
|
693
|
+
proc.on("error", () => resolve4(false));
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
async function runStatus(opts) {
|
|
697
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
698
|
+
out(`${opts.config.icon ?? "\u{1F4E6}"} ${opts.config.name} \u2014 ${opts.config.services.length} services`);
|
|
699
|
+
out("");
|
|
700
|
+
const maxLen = Math.max(...opts.config.services.map((s) => s.name.length), 12);
|
|
701
|
+
out(`${"Service".padEnd(maxLen)} ${"Port".padStart(5)} ${"Type".padEnd(4)} Health`);
|
|
702
|
+
out("-".repeat(maxLen + 24));
|
|
703
|
+
for (const svc of opts.config.services) {
|
|
704
|
+
const up = await checkHealth(svc.port, svc.healthCheck);
|
|
705
|
+
const health = up ? "\u2713 up" : "\u2717 down";
|
|
706
|
+
out(`${svc.name.padEnd(maxLen)} ${String(svc.port).padStart(5)} ${svc.type.padEnd(4)} ${health}`);
|
|
707
|
+
}
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
710
|
+
function runHelp(argv, opts = {}) {
|
|
711
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
712
|
+
const sub = argv[0];
|
|
713
|
+
if (sub === "logs") {
|
|
714
|
+
out("Usage: devup logs <service> [--follow|-f]");
|
|
715
|
+
out(" Print the persisted log file for a service (works without devup running).");
|
|
716
|
+
out(" --follow tails new lines as they are appended.");
|
|
717
|
+
return 0;
|
|
718
|
+
}
|
|
719
|
+
if (sub === "install") {
|
|
720
|
+
out("Usage: devup install");
|
|
721
|
+
out(" Run `npm install` across every service.cwd in parallel (max 4 at a time).");
|
|
722
|
+
out(" Skips services whose .install-stamp matches package.json hash.");
|
|
723
|
+
return 0;
|
|
724
|
+
}
|
|
725
|
+
if (sub === "status") {
|
|
726
|
+
out("Usage: devup status");
|
|
727
|
+
out(" For each service, probes its health-check endpoint and prints up/down.");
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
out("Subcommands:");
|
|
731
|
+
out(" devup logs <service> [--follow] Read the persisted log file");
|
|
732
|
+
out(" devup install Concurrent npm install across services");
|
|
733
|
+
out(" devup status Health check every service in config");
|
|
734
|
+
out(" devup help [<subcommand>] Show detailed help for a subcommand");
|
|
735
|
+
out("");
|
|
736
|
+
out("No subcommand \u2192 launch the interactive TUI.");
|
|
737
|
+
return 0;
|
|
738
|
+
}
|
|
739
|
+
|
|
347
740
|
// src/platform/detect.ts
|
|
348
741
|
async function detectPlatform() {
|
|
349
742
|
switch (process.platform) {
|
|
@@ -365,8 +758,8 @@ async function detectPlatform() {
|
|
|
365
758
|
}
|
|
366
759
|
|
|
367
760
|
// src/proxy-config/traefik.ts
|
|
368
|
-
import { existsSync as
|
|
369
|
-
import { dirname } from "path";
|
|
761
|
+
import { existsSync as existsSync5, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
762
|
+
import { dirname as dirname2 } from "path";
|
|
370
763
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
371
764
|
var TraefikProvider = class {
|
|
372
765
|
name = "traefik";
|
|
@@ -403,9 +796,9 @@ ${svcs.join("\n")}
|
|
|
403
796
|
`;
|
|
404
797
|
}
|
|
405
798
|
write(content, opts) {
|
|
406
|
-
const dir =
|
|
407
|
-
if (!
|
|
408
|
-
|
|
799
|
+
const dir = dirname2(opts.confPath);
|
|
800
|
+
if (!existsSync5(dir)) mkdirSync(dir, { recursive: true });
|
|
801
|
+
writeFileSync2(opts.confPath, content);
|
|
409
802
|
}
|
|
410
803
|
clear(opts) {
|
|
411
804
|
this.write(EMPTY_CONFIG, opts);
|
|
@@ -413,8 +806,8 @@ ${svcs.join("\n")}
|
|
|
413
806
|
};
|
|
414
807
|
|
|
415
808
|
// src/proxy-config/nginx.ts
|
|
416
|
-
import { existsSync as
|
|
417
|
-
import { dirname as
|
|
809
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
810
|
+
import { dirname as dirname3 } from "path";
|
|
418
811
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
419
812
|
var NginxProvider = class {
|
|
420
813
|
name = "nginx";
|
|
@@ -451,9 +844,9 @@ var NginxProvider = class {
|
|
|
451
844
|
return blocks.join("\n\n") + "\n";
|
|
452
845
|
}
|
|
453
846
|
write(content, opts) {
|
|
454
|
-
const dir =
|
|
455
|
-
if (!
|
|
456
|
-
|
|
847
|
+
const dir = dirname3(opts.confPath);
|
|
848
|
+
if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
|
|
849
|
+
writeFileSync3(opts.confPath, content);
|
|
457
850
|
}
|
|
458
851
|
clear(opts) {
|
|
459
852
|
this.write(EMPTY_CONFIG2, opts);
|
|
@@ -461,8 +854,8 @@ var NginxProvider = class {
|
|
|
461
854
|
};
|
|
462
855
|
|
|
463
856
|
// src/proxy-config/caddy.ts
|
|
464
|
-
import { existsSync as
|
|
465
|
-
import { dirname as
|
|
857
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
858
|
+
import { dirname as dirname4 } from "path";
|
|
466
859
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
467
860
|
var CaddyProvider = class {
|
|
468
861
|
name = "caddy";
|
|
@@ -485,9 +878,9 @@ var CaddyProvider = class {
|
|
|
485
878
|
return blocks.join("\n\n") + "\n";
|
|
486
879
|
}
|
|
487
880
|
write(content, opts) {
|
|
488
|
-
const dir =
|
|
489
|
-
if (!
|
|
490
|
-
|
|
881
|
+
const dir = dirname4(opts.confPath);
|
|
882
|
+
if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
|
|
883
|
+
writeFileSync4(opts.confPath, content);
|
|
491
884
|
}
|
|
492
885
|
clear(opts) {
|
|
493
886
|
this.write(EMPTY_CONFIG3, opts);
|
|
@@ -509,110 +902,6 @@ function detectProxyProvider(name) {
|
|
|
509
902
|
return factory();
|
|
510
903
|
}
|
|
511
904
|
|
|
512
|
-
// src/utils.ts
|
|
513
|
-
import { existsSync as existsSync6, readFileSync, writeFileSync as writeFileSync4 } from "fs";
|
|
514
|
-
import { createHash } from "crypto";
|
|
515
|
-
import { join as join2 } from "path";
|
|
516
|
-
function parseEnvFile(filePath, baseEnv = {}) {
|
|
517
|
-
const env = { ...baseEnv };
|
|
518
|
-
if (!existsSync6(filePath)) return env;
|
|
519
|
-
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
520
|
-
const trimmed = line.trim();
|
|
521
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
522
|
-
const eqIdx = trimmed.indexOf("=");
|
|
523
|
-
if (eqIdx === -1) continue;
|
|
524
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
525
|
-
let val = trimmed.slice(eqIdx + 1).trim();
|
|
526
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
527
|
-
val = val.slice(1, -1);
|
|
528
|
-
}
|
|
529
|
-
if (!env[key]) env[key] = val;
|
|
530
|
-
}
|
|
531
|
-
return env;
|
|
532
|
-
}
|
|
533
|
-
function fmtUptime(ms) {
|
|
534
|
-
if (!ms || ms < 0) return "-";
|
|
535
|
-
const s = Math.floor(ms / 1e3);
|
|
536
|
-
if (s < 60) return `${s}s`;
|
|
537
|
-
const m = Math.floor(s / 60);
|
|
538
|
-
if (m < 60) return `${m}m${s % 60}s`;
|
|
539
|
-
const h = Math.floor(m / 60);
|
|
540
|
-
if (h < 24) return `${h}h${m % 60}m`;
|
|
541
|
-
const d = Math.floor(h / 24);
|
|
542
|
-
return `${d}d${h % 24}h`;
|
|
543
|
-
}
|
|
544
|
-
function needsInstall(fullCwd) {
|
|
545
|
-
const nm = join2(fullCwd, "node_modules");
|
|
546
|
-
if (!existsSync6(nm)) return true;
|
|
547
|
-
try {
|
|
548
|
-
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
549
|
-
const stampFile = join2(nm, ".install-stamp");
|
|
550
|
-
if (existsSync6(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
551
|
-
} catch {
|
|
552
|
-
}
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
|
-
function writeInstallStamp(fullCwd) {
|
|
556
|
-
try {
|
|
557
|
-
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
558
|
-
writeFileSync4(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
559
|
-
} catch {
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
function sortServiceNames(names, sortMode, statsMap, procState) {
|
|
563
|
-
if (sortMode === "name") return names.slice().sort();
|
|
564
|
-
return names.slice().sort((a, b) => {
|
|
565
|
-
if (sortMode === "mem") {
|
|
566
|
-
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
567
|
-
}
|
|
568
|
-
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
function groupByPhase(services) {
|
|
572
|
-
const phases = {};
|
|
573
|
-
for (const s of services) {
|
|
574
|
-
(phases[s.phase] ??= []).push(s);
|
|
575
|
-
}
|
|
576
|
-
return phases;
|
|
577
|
-
}
|
|
578
|
-
function buildProcessArgs(svc) {
|
|
579
|
-
const extra = svc.nodeArgs ?? [];
|
|
580
|
-
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
581
|
-
if (svc.cmd === "node") return [`--max-old-space-size=${svc.maxMem}`, ...extra, ...svc.args];
|
|
582
|
-
return [...extra, ...svc.args];
|
|
583
|
-
}
|
|
584
|
-
function buildProcessEnv(svc, baseEnv) {
|
|
585
|
-
const env = { ...baseEnv, ...svc.extraEnv ?? {} };
|
|
586
|
-
if (svc.maxMem && svc.cmd !== "node") {
|
|
587
|
-
const existing = env["NODE_OPTIONS"] ?? "";
|
|
588
|
-
const flag = `--max-old-space-size=${svc.maxMem}`;
|
|
589
|
-
if (!existing.includes("max-old-space-size")) {
|
|
590
|
-
env["NODE_OPTIONS"] = existing ? `${existing} ${flag}` : flag;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return env;
|
|
594
|
-
}
|
|
595
|
-
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
596
|
-
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
597
|
-
const cpuDelta = totalCpuSec - prevCpu;
|
|
598
|
-
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
599
|
-
}
|
|
600
|
-
var tagColors = [
|
|
601
|
-
"cyan",
|
|
602
|
-
"yellow",
|
|
603
|
-
"green",
|
|
604
|
-
"magenta",
|
|
605
|
-
"blue",
|
|
606
|
-
"red",
|
|
607
|
-
"#5faf5f",
|
|
608
|
-
"#d7af5f",
|
|
609
|
-
"#5f87d7",
|
|
610
|
-
"#af5faf",
|
|
611
|
-
"#5fd7d7",
|
|
612
|
-
"#d75f5f",
|
|
613
|
-
"white"
|
|
614
|
-
];
|
|
615
|
-
|
|
616
905
|
// src/tui/App.tsx
|
|
617
906
|
import { useEffect as useEffect5, useState as useState5, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
618
907
|
import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
@@ -621,89 +910,15 @@ import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
|
621
910
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
622
911
|
|
|
623
912
|
// src/process/manager.ts
|
|
624
|
-
import { spawn as
|
|
625
|
-
import {
|
|
626
|
-
|
|
627
|
-
// src/process/health.ts
|
|
628
|
-
import net from "net";
|
|
629
|
-
import http from "http";
|
|
630
|
-
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
631
|
-
return new Promise((resolve3) => {
|
|
632
|
-
const socket = new net.Socket();
|
|
633
|
-
socket.setTimeout(timeoutMs);
|
|
634
|
-
socket.once("connect", () => {
|
|
635
|
-
socket.destroy();
|
|
636
|
-
resolve3(true);
|
|
637
|
-
});
|
|
638
|
-
socket.once("error", () => {
|
|
639
|
-
socket.destroy();
|
|
640
|
-
resolve3(false);
|
|
641
|
-
});
|
|
642
|
-
socket.once("timeout", () => {
|
|
643
|
-
socket.destroy();
|
|
644
|
-
resolve3(false);
|
|
645
|
-
});
|
|
646
|
-
socket.connect(port, host);
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
function checkHttp(port, opts = {}) {
|
|
650
|
-
const path = opts.path ?? "/";
|
|
651
|
-
const host = opts.host ?? "127.0.0.1";
|
|
652
|
-
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
653
|
-
const accept = (code) => {
|
|
654
|
-
if (opts.expect === void 0) return code >= 200 && code < 300;
|
|
655
|
-
if (Array.isArray(opts.expect)) return opts.expect.includes(code);
|
|
656
|
-
return code === opts.expect;
|
|
657
|
-
};
|
|
658
|
-
return new Promise((resolve3) => {
|
|
659
|
-
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
660
|
-
const ok = typeof res.statusCode === "number" && accept(res.statusCode);
|
|
661
|
-
res.resume();
|
|
662
|
-
resolve3(ok);
|
|
663
|
-
});
|
|
664
|
-
req.on("error", () => resolve3(false));
|
|
665
|
-
req.on("timeout", () => {
|
|
666
|
-
req.destroy();
|
|
667
|
-
resolve3(false);
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
function checkHealth(port, hc) {
|
|
672
|
-
if (hc?.type === "http") {
|
|
673
|
-
return checkHttp(port, {
|
|
674
|
-
path: hc.path,
|
|
675
|
-
expect: hc.expect,
|
|
676
|
-
host: hc.host,
|
|
677
|
-
timeoutMs: hc.timeoutMs
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
return checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
681
|
-
}
|
|
682
|
-
function waitForPort(port, opts = {}) {
|
|
683
|
-
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
684
|
-
return new Promise((resolve3) => {
|
|
685
|
-
const start = Date.now();
|
|
686
|
-
const check = () => {
|
|
687
|
-
checkPort(port).then((ok) => {
|
|
688
|
-
if (ok) return resolve3(true);
|
|
689
|
-
if (Date.now() - start > timeout) return resolve3(false);
|
|
690
|
-
setTimeout(check, interval);
|
|
691
|
-
});
|
|
692
|
-
};
|
|
693
|
-
check();
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
function deriveHealth(isUp, currentStatus) {
|
|
697
|
-
if (currentStatus === "idle") return "idle";
|
|
698
|
-
if (isUp) return "up";
|
|
699
|
-
return currentStatus === "starting" ? "wait" : "down";
|
|
700
|
-
}
|
|
913
|
+
import { spawn as spawn3 } from "child_process";
|
|
914
|
+
import { existsSync as existsSync9 } from "fs";
|
|
915
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
701
916
|
|
|
702
917
|
// src/process/installer.ts
|
|
703
|
-
import { spawn } from "child_process";
|
|
704
|
-
import { existsSync as
|
|
918
|
+
import { spawn as spawn2 } from "child_process";
|
|
919
|
+
import { existsSync as existsSync8 } from "fs";
|
|
705
920
|
function installService(cwd, env, onLog) {
|
|
706
|
-
if (!
|
|
921
|
+
if (!existsSync8(cwd)) {
|
|
707
922
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
708
923
|
return Promise.resolve(false);
|
|
709
924
|
}
|
|
@@ -712,9 +927,9 @@ function installService(cwd, env, onLog) {
|
|
|
712
927
|
return Promise.resolve(true);
|
|
713
928
|
}
|
|
714
929
|
onLog?.("\u{1F4E6} npm install...");
|
|
715
|
-
return new Promise((
|
|
930
|
+
return new Promise((resolve4) => {
|
|
716
931
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
717
|
-
const proc =
|
|
932
|
+
const proc = spawn2(command, ["install"], { cwd, env, stdio: ["ignore", "ignore", "pipe"] });
|
|
718
933
|
let stderr = "";
|
|
719
934
|
proc.stderr?.on("data", (d) => {
|
|
720
935
|
stderr += d.toString();
|
|
@@ -722,16 +937,16 @@ function installService(cwd, env, onLog) {
|
|
|
722
937
|
proc.on("close", (code) => {
|
|
723
938
|
if (code !== 0) {
|
|
724
939
|
onLog?.(`\u26A0 npm install failed: ${stderr.split("\n")[0]}`);
|
|
725
|
-
|
|
940
|
+
resolve4(false);
|
|
726
941
|
} else {
|
|
727
942
|
writeInstallStamp(cwd);
|
|
728
943
|
onLog?.("\u2705 dependencies ready");
|
|
729
|
-
|
|
944
|
+
resolve4(true);
|
|
730
945
|
}
|
|
731
946
|
});
|
|
732
947
|
proc.on("error", (err) => {
|
|
733
948
|
onLog?.(`\u26A0 spawn error: ${err.message}`);
|
|
734
|
-
|
|
949
|
+
resolve4(false);
|
|
735
950
|
});
|
|
736
951
|
});
|
|
737
952
|
}
|
|
@@ -749,6 +964,26 @@ function compileReadyPattern(pattern) {
|
|
|
749
964
|
return null;
|
|
750
965
|
}
|
|
751
966
|
}
|
|
967
|
+
function extractWatchPaths(args) {
|
|
968
|
+
const watchFlags = /* @__PURE__ */ new Set(["--watch", "--watch-path"]);
|
|
969
|
+
const out = [];
|
|
970
|
+
for (let i = 0; i < args.length; i++) {
|
|
971
|
+
const a = args[i];
|
|
972
|
+
if (watchFlags.has(a)) {
|
|
973
|
+
const v = args[i + 1];
|
|
974
|
+
if (v && !v.startsWith("-")) {
|
|
975
|
+
out.push(v);
|
|
976
|
+
i++;
|
|
977
|
+
}
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
const eq = a.indexOf("=");
|
|
981
|
+
if (eq > 0 && watchFlags.has(a.slice(0, eq))) {
|
|
982
|
+
out.push(a.slice(eq + 1));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return out;
|
|
986
|
+
}
|
|
752
987
|
function lineBuffer(onLine) {
|
|
753
988
|
let buf = "";
|
|
754
989
|
return {
|
|
@@ -783,12 +1018,12 @@ var ProcessManager = class {
|
|
|
783
1018
|
this.events = opts.events;
|
|
784
1019
|
}
|
|
785
1020
|
async install(svc, colorIdx) {
|
|
786
|
-
const cwd =
|
|
1021
|
+
const cwd = join4(this.baseCwd, svc.cwd);
|
|
787
1022
|
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
788
1023
|
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
789
1024
|
}
|
|
790
1025
|
async start(svc, colorIdx, isRestart = false) {
|
|
791
|
-
const cwd =
|
|
1026
|
+
const cwd = join4(this.baseCwd, svc.cwd);
|
|
792
1027
|
if (svc.type === "api") {
|
|
793
1028
|
const occupied = await checkPort(svc.port);
|
|
794
1029
|
if (occupied && !isRestart) {
|
|
@@ -804,8 +1039,14 @@ var ProcessManager = class {
|
|
|
804
1039
|
}
|
|
805
1040
|
}
|
|
806
1041
|
const args = buildProcessArgs(svc);
|
|
1042
|
+
const missingWatchPaths = extractWatchPaths(args).filter((p) => !existsSync9(resolve3(cwd, p)));
|
|
1043
|
+
if (missingWatchPaths.length) {
|
|
1044
|
+
this.log(svc.name, `\u26A0 missing watch paths: ${missingWatchPaths.join(", ")}`, colorIdx);
|
|
1045
|
+
this.recordCrashedState(svc, colorIdx);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
807
1048
|
const env = buildProcessEnv(svc, this.env);
|
|
808
|
-
const proc =
|
|
1049
|
+
const proc = spawn3(svc.cmd, args, { cwd, env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
809
1050
|
const prev = this.state.get(svc.name);
|
|
810
1051
|
const state = {
|
|
811
1052
|
svc,
|
|
@@ -877,29 +1118,29 @@ var ProcessManager = class {
|
|
|
877
1118
|
}
|
|
878
1119
|
runPreBuild(svc, cwd, colorIdx) {
|
|
879
1120
|
this.log(svc.name, `\u{1F528} preBuild: ${svc.preBuild}`, colorIdx);
|
|
880
|
-
return new Promise((
|
|
1121
|
+
return new Promise((resolve4) => {
|
|
881
1122
|
const isWin = process.platform === "win32";
|
|
882
1123
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
883
1124
|
const shellFlag = isWin ? "/c" : "-c";
|
|
884
1125
|
const env = buildProcessEnv(svc, this.env);
|
|
885
|
-
const child =
|
|
1126
|
+
const child = spawn3(shell, [shellFlag, svc.preBuild], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
886
1127
|
const outBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
887
1128
|
const errBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
888
1129
|
child.stdout?.on("data", (d) => outBuf.push(d));
|
|
889
1130
|
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
890
1131
|
child.on("error", (err) => {
|
|
891
1132
|
this.log(svc.name, `[build] \u274C ${err.message}`, colorIdx);
|
|
892
|
-
|
|
1133
|
+
resolve4(false);
|
|
893
1134
|
});
|
|
894
1135
|
child.on("close", (code) => {
|
|
895
1136
|
outBuf.flush();
|
|
896
1137
|
errBuf.flush();
|
|
897
1138
|
if (code === 0) {
|
|
898
1139
|
this.log(svc.name, `[build] \u2705 done`, colorIdx);
|
|
899
|
-
|
|
1140
|
+
resolve4(true);
|
|
900
1141
|
} else {
|
|
901
1142
|
this.log(svc.name, `[build] \u274C exited with code ${code}`, colorIdx);
|
|
902
|
-
|
|
1143
|
+
resolve4(false);
|
|
903
1144
|
}
|
|
904
1145
|
});
|
|
905
1146
|
});
|
|
@@ -909,7 +1150,7 @@ var ProcessManager = class {
|
|
|
909
1150
|
const isWin = process.platform === "win32";
|
|
910
1151
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
911
1152
|
const shellFlag = isWin ? "/c" : "-c";
|
|
912
|
-
const child =
|
|
1153
|
+
const child = spawn3(shell, [shellFlag, svc.watchBuild], {
|
|
913
1154
|
cwd,
|
|
914
1155
|
env,
|
|
915
1156
|
detached: true,
|
|
@@ -992,14 +1233,14 @@ var ProcessManager = class {
|
|
|
992
1233
|
}
|
|
993
1234
|
for (const st of this.state.values()) this.stopWatchProc(st);
|
|
994
1235
|
const waits = procs.map(
|
|
995
|
-
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((
|
|
1236
|
+
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((resolve4) => p.once("close", () => resolve4()))
|
|
996
1237
|
);
|
|
997
1238
|
let timedOut = false;
|
|
998
1239
|
await Promise.race([
|
|
999
1240
|
Promise.all(waits),
|
|
1000
|
-
new Promise((
|
|
1241
|
+
new Promise((resolve4) => setTimeout(() => {
|
|
1001
1242
|
timedOut = true;
|
|
1002
|
-
|
|
1243
|
+
resolve4();
|
|
1003
1244
|
}, grace))
|
|
1004
1245
|
]);
|
|
1005
1246
|
if (timedOut) {
|
|
@@ -1010,7 +1251,7 @@ var ProcessManager = class {
|
|
|
1010
1251
|
}
|
|
1011
1252
|
await Promise.race([
|
|
1012
1253
|
Promise.all(waits),
|
|
1013
|
-
new Promise((
|
|
1254
|
+
new Promise((resolve4) => setTimeout(resolve4, 1e3))
|
|
1014
1255
|
]);
|
|
1015
1256
|
}
|
|
1016
1257
|
}
|
|
@@ -1306,19 +1547,25 @@ var H = {
|
|
|
1306
1547
|
down: { c: "\u25CF", color: "red" },
|
|
1307
1548
|
idle: { c: "\u25CB", color: "blue" }
|
|
1308
1549
|
};
|
|
1550
|
+
var MAX_RESTARTS2 = 3;
|
|
1551
|
+
function isCrashLooped(st) {
|
|
1552
|
+
return st.status === "crashed" && st.restarts >= MAX_RESTARTS2;
|
|
1553
|
+
}
|
|
1309
1554
|
function Row({ name, st, stat, ml }) {
|
|
1310
|
-
const
|
|
1555
|
+
const looped = isCrashLooped(st);
|
|
1556
|
+
const indicator = looped ? /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "\u2716" }) : /* @__PURE__ */ jsx2(Text2, { color: (H[st.health] ?? H["down"]).color, children: (H[st.health] ?? H["down"]).c });
|
|
1311
1557
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1312
|
-
const sc = st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1558
|
+
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1559
|
+
const statusLabel = looped ? "looping" : st.status;
|
|
1313
1560
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1314
1561
|
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1315
|
-
|
|
1562
|
+
indicator,
|
|
1316
1563
|
" ",
|
|
1317
1564
|
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1318
1565
|
" ",
|
|
1319
1566
|
String(st.svc.port).padStart(5),
|
|
1320
1567
|
" ",
|
|
1321
|
-
/* @__PURE__ */ jsx2(Text2, { color: sc, children:
|
|
1568
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1322
1569
|
" ",
|
|
1323
1570
|
(stat?.cpu ?? "-").padStart(6),
|
|
1324
1571
|
" ",
|
|
@@ -1385,6 +1632,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1385
1632
|
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
1386
1633
|
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1387
1634
|
const scrolled = effectiveOffset > 0;
|
|
1635
|
+
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1388
1636
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
1389
1637
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1390
1638
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
@@ -1392,6 +1640,11 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1392
1640
|
positionInfo
|
|
1393
1641
|
] }),
|
|
1394
1642
|
scrolled && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " [SCROLL]" }),
|
|
1643
|
+
loopedCount > 0 && /* @__PURE__ */ jsxs2(Text2, { color: "red", bold: true, children: [
|
|
1644
|
+
" \u26A0 ",
|
|
1645
|
+
loopedCount,
|
|
1646
|
+
" need attention"
|
|
1647
|
+
] }),
|
|
1395
1648
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1396
1649
|
" System: ",
|
|
1397
1650
|
cpus,
|
|
@@ -1484,33 +1737,69 @@ function StatusBar() {
|
|
|
1484
1737
|
}
|
|
1485
1738
|
|
|
1486
1739
|
// src/tui/ServiceList.tsx
|
|
1487
|
-
import { useState as useState3 } from "react";
|
|
1740
|
+
import { useState as useState3, useMemo } from "react";
|
|
1488
1741
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1489
1742
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1490
1743
|
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1491
|
-
const
|
|
1744
|
+
const allNames = useMemo(
|
|
1745
|
+
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1746
|
+
[services, filterType]
|
|
1747
|
+
);
|
|
1492
1748
|
const [idx, setIdx] = useState3(0);
|
|
1749
|
+
const [query, setQuery] = useState3("");
|
|
1750
|
+
const names = useMemo(() => {
|
|
1751
|
+
if (!query) return allNames;
|
|
1752
|
+
const q = query.toLowerCase();
|
|
1753
|
+
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
1754
|
+
}, [allNames, query]);
|
|
1755
|
+
const clamped = Math.min(idx, Math.max(0, names.length - 1));
|
|
1493
1756
|
useInput2((input, key) => {
|
|
1494
|
-
if (key.escape)
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1757
|
+
if (key.escape) {
|
|
1758
|
+
if (query) setQuery("");
|
|
1759
|
+
else onClose();
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
if (key.return) {
|
|
1763
|
+
if (names[clamped]) onSelect(names[clamped]);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (key.upArrow) {
|
|
1767
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (key.downArrow) {
|
|
1771
|
+
setIdx((i) => Math.min(names.length - 1, i + 1));
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (key.backspace || key.delete) {
|
|
1775
|
+
setQuery((q) => q.slice(0, -1));
|
|
1776
|
+
setIdx(0);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
1780
|
+
setQuery((q) => q + input);
|
|
1781
|
+
setIdx(0);
|
|
1782
|
+
}
|
|
1499
1783
|
}, { isActive: process.stdin.isTTY ?? false });
|
|
1500
1784
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
1501
1785
|
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
1502
1786
|
" ",
|
|
1503
1787
|
title,
|
|
1504
|
-
" "
|
|
1788
|
+
" ",
|
|
1789
|
+
query && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
1790
|
+
"[",
|
|
1791
|
+
query,
|
|
1792
|
+
"]"
|
|
1793
|
+
] })
|
|
1505
1794
|
] }),
|
|
1506
|
-
names.map((name, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i ===
|
|
1795
|
+
names.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (no matches) " }) : names.map((name, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === clamped ? "cyan" : void 0, inverse: i === clamped, children: [
|
|
1507
1796
|
" ",
|
|
1508
1797
|
name,
|
|
1509
1798
|
" :",
|
|
1510
1799
|
services.get(name).svc.port,
|
|
1511
1800
|
" "
|
|
1512
1801
|
] }) }, name)),
|
|
1513
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate Enter select Esc close" })
|
|
1802
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "type to filter \u2191\u2193 navigate Enter select Esc clear/close" })
|
|
1514
1803
|
] });
|
|
1515
1804
|
}
|
|
1516
1805
|
|
|
@@ -1647,8 +1936,8 @@ function createLazyProxy(opts) {
|
|
|
1647
1936
|
}
|
|
1648
1937
|
|
|
1649
1938
|
// src/process/external.ts
|
|
1650
|
-
import { spawn as
|
|
1651
|
-
import { join as
|
|
1939
|
+
import { spawn as spawn4 } from "child_process";
|
|
1940
|
+
import { join as join5 } from "path";
|
|
1652
1941
|
var DEFAULT_START_TIMEOUT_S = 60;
|
|
1653
1942
|
async function startExternals(externals, opts) {
|
|
1654
1943
|
const procs = [];
|
|
@@ -1681,16 +1970,16 @@ async function stopExternals(procs, platform, opts = {}) {
|
|
|
1681
1970
|
if (pid) platform.killTree(pid);
|
|
1682
1971
|
if (svc.stopCmd) {
|
|
1683
1972
|
opts.onLog?.(svc.name, `\u{1F9F9} ${svc.stopCmd}`);
|
|
1684
|
-
await new Promise((
|
|
1973
|
+
await new Promise((resolve4) => {
|
|
1685
1974
|
const isWin = process.platform === "win32";
|
|
1686
1975
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1687
1976
|
const flag = isWin ? "/c" : "-c";
|
|
1688
|
-
const cwd = svc.cwd ?
|
|
1977
|
+
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1689
1978
|
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1690
|
-
const child =
|
|
1691
|
-
child.on("close", () =>
|
|
1692
|
-
child.on("error", () =>
|
|
1693
|
-
setTimeout(() =>
|
|
1979
|
+
const child = spawn4(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
1980
|
+
child.on("close", () => resolve4());
|
|
1981
|
+
child.on("error", () => resolve4());
|
|
1982
|
+
setTimeout(() => resolve4(), 1e4);
|
|
1694
1983
|
});
|
|
1695
1984
|
}
|
|
1696
1985
|
} catch {
|
|
@@ -1702,10 +1991,10 @@ function spawnExternal(svc, opts) {
|
|
|
1702
1991
|
const isWin = process.platform === "win32";
|
|
1703
1992
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1704
1993
|
const flag = isWin ? "/c" : "-c";
|
|
1705
|
-
const cwd = svc.cwd ?
|
|
1994
|
+
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1706
1995
|
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1707
1996
|
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
1708
|
-
const child =
|
|
1997
|
+
const child = spawn4(shell, [flag, svc.cmd], {
|
|
1709
1998
|
cwd,
|
|
1710
1999
|
env,
|
|
1711
2000
|
detached: true,
|
|
@@ -1726,8 +2015,33 @@ async function waitHealthy(svc, timeoutMs) {
|
|
|
1726
2015
|
return false;
|
|
1727
2016
|
}
|
|
1728
2017
|
|
|
2018
|
+
// src/tui/tips.ts
|
|
2019
|
+
function pickTip(state) {
|
|
2020
|
+
if (state.crashLoopedCount > 0 && !state.shown.has("crashed")) {
|
|
2021
|
+
return { id: "crashed", message: "tip: press r to restart, or check the log of the failing service" };
|
|
2022
|
+
}
|
|
2023
|
+
if (state.totalLogs > 1e3 && !state.hasSearch && !state.shown.has("search")) {
|
|
2024
|
+
return { id: "search", message: "tip: press / to search in logs" };
|
|
2025
|
+
}
|
|
2026
|
+
if (state.totalLogs > 500 && !state.hasFilter && !state.shown.has("filter")) {
|
|
2027
|
+
return { id: "filter", message: "tip: press f to filter logs by service" };
|
|
2028
|
+
}
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1729
2032
|
// src/tui/App.tsx
|
|
1730
2033
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2034
|
+
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
2035
|
+
if (proxyActive && proxyOpts) {
|
|
2036
|
+
const sub = proxyOpts.routes[name];
|
|
2037
|
+
if (sub !== void 0) {
|
|
2038
|
+
const host = sub ? `${sub}.${proxyOpts.domain}` : proxyOpts.domain;
|
|
2039
|
+
const scheme = proxyOpts.tls ? "https" : "http";
|
|
2040
|
+
return `${scheme}://${host}`;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return `http://localhost:${port}`;
|
|
2044
|
+
}
|
|
1731
2045
|
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
1732
2046
|
const { stdout } = useStdout();
|
|
1733
2047
|
const [rows, setRows] = useState5(stdout?.rows ?? 40);
|
|
@@ -1746,6 +2060,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1746
2060
|
const [booted, setBooted] = useState5(false);
|
|
1747
2061
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
1748
2062
|
const externals = useRef3([]);
|
|
2063
|
+
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2064
|
+
const [activeTip, setActiveTip] = useState5(null);
|
|
1749
2065
|
const kb = useKeyBindings({
|
|
1750
2066
|
onQuit: () => {
|
|
1751
2067
|
void shutdown();
|
|
@@ -1771,6 +2087,21 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1771
2087
|
useEffect5(() => {
|
|
1772
2088
|
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
1773
2089
|
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
2090
|
+
useEffect5(() => {
|
|
2091
|
+
const tip = pickTip({
|
|
2092
|
+
totalLogs: pm.logs.length,
|
|
2093
|
+
hasSearch: !!kb.searchTerm,
|
|
2094
|
+
hasFilter: !!kb.logFilter,
|
|
2095
|
+
crashLoopedCount: [...pm.states.values()].filter(isCrashLooped).length,
|
|
2096
|
+
shown: shownTips.current
|
|
2097
|
+
});
|
|
2098
|
+
if (tip && tip.id !== activeTip) {
|
|
2099
|
+
shownTips.current.add(tip.id);
|
|
2100
|
+
setActiveTip(tip.message);
|
|
2101
|
+
const timer = setTimeout(() => setActiveTip(null), 12e3);
|
|
2102
|
+
return () => clearTimeout(timer);
|
|
2103
|
+
}
|
|
2104
|
+
}, [pm.logs.length, pm.states, kb.searchTerm, kb.logFilter, activeTip]);
|
|
1774
2105
|
useProxySync(proxyProvider, proxyOpts, pm.states, kb.proxyEnabled);
|
|
1775
2106
|
useEffect5(() => {
|
|
1776
2107
|
if (booted || !pm.manager) return;
|
|
@@ -1885,23 +2216,32 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1885
2216
|
}, [pm, kb]);
|
|
1886
2217
|
const handleOpenSelect = useCallback3((name) => {
|
|
1887
2218
|
const st = pm.states.get(name);
|
|
1888
|
-
if (st)
|
|
2219
|
+
if (st) {
|
|
2220
|
+
const url = buildServiceUrl(name, st.svc.port, cliArgs.proxy, proxyOpts);
|
|
2221
|
+
platform.openBrowser(url);
|
|
2222
|
+
}
|
|
1889
2223
|
kb.setModal("none");
|
|
1890
|
-
}, [pm, platform, kb]);
|
|
2224
|
+
}, [pm, platform, kb, cliArgs.proxy, proxyOpts]);
|
|
1891
2225
|
const icon = config.icon ?? "\u{1F4E6}";
|
|
1892
2226
|
const modeLabel = cliArgs.lazy && config.lazy ? "lazy" : "normal";
|
|
1893
2227
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: rows, children: [
|
|
1894
|
-
/* @__PURE__ */
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2228
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
2229
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
|
|
2230
|
+
" ",
|
|
2231
|
+
icon,
|
|
2232
|
+
" ",
|
|
2233
|
+
config.name,
|
|
2234
|
+
" \u2014 devup \u2014 ",
|
|
2235
|
+
services.length,
|
|
2236
|
+
" services (",
|
|
2237
|
+
modeLabel,
|
|
2238
|
+
") "
|
|
2239
|
+
] }),
|
|
2240
|
+
activeTip && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2241
|
+
" \xB7 ",
|
|
2242
|
+
activeTip
|
|
2243
|
+
] })
|
|
2244
|
+
] }),
|
|
1905
2245
|
/* @__PURE__ */ jsx6(
|
|
1906
2246
|
LogsPanel,
|
|
1907
2247
|
{
|
|
@@ -1939,23 +2279,23 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1939
2279
|
}
|
|
1940
2280
|
|
|
1941
2281
|
// src/process/log-sink.ts
|
|
1942
|
-
import { existsSync as
|
|
1943
|
-
import { join as
|
|
1944
|
-
import { homedir } from "os";
|
|
2282
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, renameSync, createWriteStream } from "fs";
|
|
2283
|
+
import { join as join6, dirname as dirname5 } from "path";
|
|
2284
|
+
import { homedir as homedir2 } from "os";
|
|
1945
2285
|
var LogSink = class {
|
|
1946
2286
|
dir;
|
|
1947
2287
|
rotateOnStart;
|
|
1948
2288
|
streams = /* @__PURE__ */ new Map();
|
|
1949
2289
|
seen = /* @__PURE__ */ new Set();
|
|
1950
2290
|
constructor(opts) {
|
|
1951
|
-
const root = opts.rootDir ??
|
|
1952
|
-
this.dir =
|
|
2291
|
+
const root = opts.rootDir ?? join6(homedir2(), ".devup", "logs");
|
|
2292
|
+
this.dir = join6(root, sanitize2(opts.projectName));
|
|
1953
2293
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1954
2294
|
mkdirSync4(this.dir, { recursive: true });
|
|
1955
2295
|
}
|
|
1956
2296
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1957
2297
|
pathFor(svcName) {
|
|
1958
|
-
return
|
|
2298
|
+
return join6(this.dir, `${sanitize2(svcName)}.log`);
|
|
1959
2299
|
}
|
|
1960
2300
|
write(svcName, line) {
|
|
1961
2301
|
const stream = this.streamFor(svcName);
|
|
@@ -1974,9 +2314,9 @@ var LogSink = class {
|
|
|
1974
2314
|
let s = this.streams.get(svcName);
|
|
1975
2315
|
if (s) return s;
|
|
1976
2316
|
const file = this.pathFor(svcName);
|
|
1977
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2317
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync10(file)) {
|
|
1978
2318
|
try {
|
|
1979
|
-
mkdirSync4(
|
|
2319
|
+
mkdirSync4(dirname5(file), { recursive: true });
|
|
1980
2320
|
renameSync(file, file + ".prev");
|
|
1981
2321
|
} catch {
|
|
1982
2322
|
}
|
|
@@ -1989,7 +2329,7 @@ var LogSink = class {
|
|
|
1989
2329
|
return s;
|
|
1990
2330
|
}
|
|
1991
2331
|
};
|
|
1992
|
-
function
|
|
2332
|
+
function sanitize2(name) {
|
|
1993
2333
|
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
1994
2334
|
}
|
|
1995
2335
|
|
|
@@ -2160,9 +2500,46 @@ function defineConfig(config) {
|
|
|
2160
2500
|
}
|
|
2161
2501
|
|
|
2162
2502
|
// src/index.ts
|
|
2503
|
+
function readVersion() {
|
|
2504
|
+
try {
|
|
2505
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
2506
|
+
const pkgPath = join7(here, "..", "package.json");
|
|
2507
|
+
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "unknown";
|
|
2508
|
+
} catch {
|
|
2509
|
+
return "unknown";
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2163
2512
|
async function main() {
|
|
2513
|
+
const raw = process.argv.slice(2);
|
|
2514
|
+
if (raw.includes("-v") || raw.includes("--version")) {
|
|
2515
|
+
console.log(readVersion());
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (raw.includes("-h") || raw.includes("--help")) {
|
|
2519
|
+
console.log(USAGE);
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
const subcmd = detectSubcommand(raw);
|
|
2523
|
+
if (subcmd === "help") {
|
|
2524
|
+
process.exit(runHelp(raw.slice(1)));
|
|
2525
|
+
}
|
|
2164
2526
|
const cwd = process.cwd();
|
|
2165
|
-
const cliArgs = parseCliArgs(
|
|
2527
|
+
const cliArgs = parseCliArgs(raw);
|
|
2528
|
+
if (subcmd) {
|
|
2529
|
+
const subArgs = raw.slice(1);
|
|
2530
|
+
let cfgPath;
|
|
2531
|
+
try {
|
|
2532
|
+
cfgPath = findConfigFile(cwd, cliArgs.configPath);
|
|
2533
|
+
} catch (e) {
|
|
2534
|
+
console.error(`\u274C ${e.message}`);
|
|
2535
|
+
process.exit(1);
|
|
2536
|
+
}
|
|
2537
|
+
const cfg = await loadConfig(cfgPath);
|
|
2538
|
+
const subOpts = { config: cfg, baseCwd: cwd, env: process.env, logDir: cliArgs.logDir };
|
|
2539
|
+
if (subcmd === "logs") process.exit(await runLogs(subArgs, subOpts));
|
|
2540
|
+
if (subcmd === "install") process.exit(await runInstall(subOpts));
|
|
2541
|
+
if (subcmd === "status") process.exit(await runStatus(subOpts));
|
|
2542
|
+
}
|
|
2166
2543
|
let configPath;
|
|
2167
2544
|
try {
|
|
2168
2545
|
configPath = findConfigFile(cwd, cliArgs.configPath);
|
|
@@ -2189,7 +2566,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2189
2566
|
process.exit(1);
|
|
2190
2567
|
}
|
|
2191
2568
|
const platform = await detectPlatform();
|
|
2192
|
-
const envFile = config.envFile ?
|
|
2569
|
+
const envFile = config.envFile ? join7(cwd, config.envFile) : join7(cwd, ".env");
|
|
2193
2570
|
const env = parseEnvFile(envFile, process.env);
|
|
2194
2571
|
if (config.env) {
|
|
2195
2572
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -2206,7 +2583,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2206
2583
|
routes: config.proxy.routes,
|
|
2207
2584
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
2208
2585
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
2209
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
2586
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join7(homedir3(), ".traefik", "traefik_conf.yaml")
|
|
2210
2587
|
};
|
|
2211
2588
|
}
|
|
2212
2589
|
if (cliArgs.dryRun) {
|