@gachlab/devup 0.5.0 → 0.7.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 +42 -0
- package/README.md +74 -454
- package/dist/config/cli.d.ts +2 -1
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/diff.d.ts +19 -0
- package/dist/config/diff.d.ts.map +1 -0
- package/dist/config/validator.d.ts +9 -0
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/control-plane/socket-server.d.ts +31 -0
- package/dist/control-plane/socket-server.d.ts.map +1 -0
- package/dist/index.js +1047 -525
- package/dist/index.js.map +1 -1
- package/dist/process/health-poller.d.ts +15 -0
- package/dist/process/health-poller.d.ts.map +1 -0
- package/dist/process/internals.d.ts +14 -0
- package/dist/process/internals.d.ts.map +1 -0
- package/dist/process/lifecycle.d.ts +31 -0
- package/dist/process/lifecycle.d.ts.map +1 -0
- package/dist/process/manager.d.ts +11 -13
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/process/restarter.d.ts +26 -0
- package/dist/process/restarter.d.ts.map +1 -0
- package/dist/process/spawner.d.ts +38 -0
- package/dist/process/spawner.d.ts.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts +9 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/dist/tui/hooks/useBootSequence.d.ts +20 -0
- package/dist/tui/hooks/useBootSequence.d.ts.map +1 -0
- package/dist/tui/hooks/useContextualTips.d.ts +6 -0
- package/dist/tui/hooks/useContextualTips.d.ts.map +1 -0
- package/dist/tui/hooks/useControlPlane.d.ts +10 -0
- package/dist/tui/hooks/useControlPlane.d.ts.map +1 -0
- package/dist/tui/hooks/useHotReload.d.ts +7 -0
- package/dist/tui/hooks/useHotReload.d.ts.map +1 -0
- package/dist/tui/hooks/useLogsPause.d.ts +4 -0
- package/dist/tui/hooks/useLogsPause.d.ts.map +1 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +4 -0
- package/dist/tui/hooks/useTerminalSize.d.ts.map +1 -0
- package/dist/utils/colors.d.ts +4 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/env.d.ts +5 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/format.d.ts +3 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/install-stamp.d.ts +6 -0
- package/dist/utils/install-stamp.d.ts.map +1 -0
- package/dist/utils/phases.d.ts +4 -0
- package/dist/utils/phases.d.ts.map +1 -0
- package/dist/utils/process-args.d.ts +8 -0
- package/dist/utils/process-args.d.ts.map +1 -0
- package/dist/utils/redact.d.ts +4 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/search.d.ts +17 -0
- package/dist/utils/search.d.ts.map +1 -0
- package/dist/utils/stats.d.ts +19 -0
- package/dist/utils/stats.d.ts.map +1 -0
- package/dist/utils.d.ts +10 -41
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import { dirname as
|
|
6
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
7
|
+
import { dirname as dirname7, join as join9 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
|
-
import { homedir as
|
|
9
|
+
import { homedir as homedir4 } from "os";
|
|
10
10
|
|
|
11
11
|
// src/config/loader.ts
|
|
12
12
|
import { existsSync } from "fs";
|
|
@@ -74,6 +74,26 @@ function rewriteServicePort(svc) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// src/config/validator.ts
|
|
77
|
+
function collectWarnings(config) {
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (!config.services?.length) return warnings;
|
|
80
|
+
for (const svc of config.services) {
|
|
81
|
+
const ep = svc.extraEnv?.["PORT"];
|
|
82
|
+
if (ep !== void 0) {
|
|
83
|
+
const expected = String(svc.port);
|
|
84
|
+
if (ep !== expected) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
field: `services[${svc.name}].extraEnv.PORT`,
|
|
87
|
+
message: `extraEnv.PORT="${ep}" does not match port=${svc.port}. devup will health-check :${svc.port} but the service will probably bind to :${ep}.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
function formatValidationWarnings(warnings) {
|
|
95
|
+
return warnings.map((w) => ` \u26A0 ${w.field}: ${w.message}`).join("\n");
|
|
96
|
+
}
|
|
77
97
|
function validateConfig(config, cwd) {
|
|
78
98
|
const errors = [];
|
|
79
99
|
if (!config.name?.trim()) {
|
|
@@ -272,6 +292,10 @@ Log files:
|
|
|
272
292
|
--no-log-file Disable persistent log files
|
|
273
293
|
--log-dir <path> Override log root (default: ~/.devup/logs)
|
|
274
294
|
|
|
295
|
+
Hot reload:
|
|
296
|
+
--watch-config Watch devup.config.* and apply add/remove/restart
|
|
297
|
+
service changes without exiting the TUI
|
|
298
|
+
|
|
275
299
|
Other:
|
|
276
300
|
-h, --help Show this help and exit
|
|
277
301
|
-v, --version Show version and exit
|
|
@@ -288,7 +312,8 @@ function parseCliArgs(argv) {
|
|
|
288
312
|
dryRun: false,
|
|
289
313
|
once: false,
|
|
290
314
|
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
291
|
-
logFile: true
|
|
315
|
+
logFile: true,
|
|
316
|
+
watchConfig: false
|
|
292
317
|
};
|
|
293
318
|
for (let i = 0; i < argv.length; i++) {
|
|
294
319
|
const arg = argv[i];
|
|
@@ -362,6 +387,9 @@ function parseCliArgs(argv) {
|
|
|
362
387
|
args.logDir = next;
|
|
363
388
|
i++;
|
|
364
389
|
break;
|
|
390
|
+
case "--watch-config":
|
|
391
|
+
args.watchConfig = true;
|
|
392
|
+
break;
|
|
365
393
|
}
|
|
366
394
|
}
|
|
367
395
|
return args;
|
|
@@ -402,7 +430,7 @@ function filterServices(services, args, config) {
|
|
|
402
430
|
|
|
403
431
|
// src/orchestrator/subcommands.ts
|
|
404
432
|
import { spawn } from "child_process";
|
|
405
|
-
import { createReadStream, watchFile, unwatchFile, existsSync as
|
|
433
|
+
import { createReadStream, watchFile, unwatchFile, existsSync as existsSync5, statSync } from "fs";
|
|
406
434
|
import { readFile as readFile2 } from "fs/promises";
|
|
407
435
|
import { join as join3, dirname } from "path";
|
|
408
436
|
import { fileURLToPath } from "url";
|
|
@@ -484,10 +512,8 @@ function deriveHealth(isUp, currentStatus) {
|
|
|
484
512
|
return currentStatus === "starting" ? "wait" : "down";
|
|
485
513
|
}
|
|
486
514
|
|
|
487
|
-
// src/utils.ts
|
|
488
|
-
import { existsSync as existsSync3, readFileSync
|
|
489
|
-
import { createHash } from "crypto";
|
|
490
|
-
import { join as join2 } from "path";
|
|
515
|
+
// src/utils/env.ts
|
|
516
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
491
517
|
function parseEnvFile(filePath, baseEnv = {}) {
|
|
492
518
|
const env = { ...baseEnv };
|
|
493
519
|
if (!existsSync3(filePath)) return env;
|
|
@@ -505,25 +531,21 @@ function parseEnvFile(filePath, baseEnv = {}) {
|
|
|
505
531
|
}
|
|
506
532
|
return env;
|
|
507
533
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return
|
|
520
|
-
}
|
|
521
|
-
function detectLogLevel(line) {
|
|
522
|
-
const l = line.toLowerCase();
|
|
523
|
-
if (/\b(?:error|err|fail(?:ed|ure|ures|s)?|fatal|exception|crash(?:ed|es)?)\b/.test(l) || /❌|✗|⛔/.test(line)) return "error";
|
|
524
|
-
if (/\b(?:warn(?:ed|ing|s|ings)?|deprec)\b/.test(l) || /⚠/.test(line)) return "warn";
|
|
525
|
-
return "info";
|
|
534
|
+
|
|
535
|
+
// src/utils/format.ts
|
|
536
|
+
function fmtUptime(ms) {
|
|
537
|
+
if (!ms || ms < 0) return "-";
|
|
538
|
+
const s = Math.floor(ms / 1e3);
|
|
539
|
+
if (s < 60) return `${s}s`;
|
|
540
|
+
const m = Math.floor(s / 60);
|
|
541
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
542
|
+
const h = Math.floor(m / 60);
|
|
543
|
+
if (h < 24) return `${h}h${m % 60}m`;
|
|
544
|
+
const d = Math.floor(h / 24);
|
|
545
|
+
return `${d}d${h % 24}h`;
|
|
526
546
|
}
|
|
547
|
+
|
|
548
|
+
// src/utils/search.ts
|
|
527
549
|
function compileSearchPattern(term) {
|
|
528
550
|
if (!term) return null;
|
|
529
551
|
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(term);
|
|
@@ -540,51 +562,47 @@ function compileSearchPattern(term) {
|
|
|
540
562
|
const lower = term.toLowerCase();
|
|
541
563
|
return { test: (l) => l.toLowerCase().includes(lower) };
|
|
542
564
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if (s
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
return
|
|
565
|
+
function detectLogLevel(line) {
|
|
566
|
+
const l = line.toLowerCase();
|
|
567
|
+
if (/\b(?:error|err|fail(?:ed|ure|ures|s)?|fatal|exception|crash(?:ed|es)?)\b/.test(l) || /❌|✗|⛔/.test(line)) return "error";
|
|
568
|
+
if (/\b(?:warn(?:ed|ing|s|ings)?|deprec)\b/.test(l) || /⚠/.test(line)) return "warn";
|
|
569
|
+
return "info";
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/utils/redact.ts
|
|
573
|
+
function redactSecrets(env) {
|
|
574
|
+
if (!env) return {};
|
|
575
|
+
const out = {};
|
|
576
|
+
for (const [k, v] of Object.entries(env)) {
|
|
577
|
+
out[k] = /secret|token|password|api[_-]?key|auth/i.test(k) ? "***" : v;
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
553
580
|
}
|
|
581
|
+
|
|
582
|
+
// src/utils/install-stamp.ts
|
|
583
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
584
|
+
import { createHash } from "crypto";
|
|
585
|
+
import { join as join2 } from "path";
|
|
554
586
|
function needsInstall(fullCwd) {
|
|
555
587
|
const nm = join2(fullCwd, "node_modules");
|
|
556
|
-
if (!
|
|
588
|
+
if (!existsSync4(nm)) return true;
|
|
557
589
|
try {
|
|
558
|
-
const pkgHash = createHash("md5").update(
|
|
590
|
+
const pkgHash = createHash("md5").update(readFileSync2(join2(fullCwd, "package.json"))).digest("hex");
|
|
559
591
|
const stampFile = join2(nm, ".install-stamp");
|
|
560
|
-
if (
|
|
592
|
+
if (existsSync4(stampFile) && readFileSync2(stampFile, "utf8") === pkgHash) return false;
|
|
561
593
|
} catch {
|
|
562
594
|
}
|
|
563
595
|
return true;
|
|
564
596
|
}
|
|
565
597
|
function writeInstallStamp(fullCwd) {
|
|
566
598
|
try {
|
|
567
|
-
const pkgHash = createHash("md5").update(
|
|
599
|
+
const pkgHash = createHash("md5").update(readFileSync2(join2(fullCwd, "package.json"))).digest("hex");
|
|
568
600
|
writeFileSync(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
569
601
|
} catch {
|
|
570
602
|
}
|
|
571
603
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
return names.slice().sort((a, b) => {
|
|
575
|
-
if (sortMode === "mem") {
|
|
576
|
-
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
577
|
-
}
|
|
578
|
-
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
function groupByPhase(services) {
|
|
582
|
-
const phases = {};
|
|
583
|
-
for (const s of services) {
|
|
584
|
-
(phases[s.phase] ??= []).push(s);
|
|
585
|
-
}
|
|
586
|
-
return phases;
|
|
587
|
-
}
|
|
604
|
+
|
|
605
|
+
// src/utils/process-args.ts
|
|
588
606
|
function buildProcessArgs(svc) {
|
|
589
607
|
const extra = svc.nodeArgs ?? [];
|
|
590
608
|
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
@@ -602,11 +620,38 @@ function buildProcessEnv(svc, baseEnv) {
|
|
|
602
620
|
}
|
|
603
621
|
return env;
|
|
604
622
|
}
|
|
623
|
+
|
|
624
|
+
// src/utils/stats.ts
|
|
625
|
+
function sortServiceNames(names, sortMode, statsMap, procState) {
|
|
626
|
+
if (sortMode === "name") return names.slice().sort();
|
|
627
|
+
return names.slice().sort((a, b) => {
|
|
628
|
+
if (sortMode === "mem") {
|
|
629
|
+
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
630
|
+
}
|
|
631
|
+
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
605
634
|
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
606
635
|
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
607
636
|
const cpuDelta = totalCpuSec - prevCpu;
|
|
608
637
|
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
609
638
|
}
|
|
639
|
+
function nextRamBannerVisibility(usagePct, previousVisible, highWatermark = 80, lowWatermark = 75) {
|
|
640
|
+
if (usagePct >= highWatermark) return true;
|
|
641
|
+
if (usagePct < lowWatermark) return false;
|
|
642
|
+
return previousVisible;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/utils/phases.ts
|
|
646
|
+
function groupByPhase(services) {
|
|
647
|
+
const phases = {};
|
|
648
|
+
for (const s of services) {
|
|
649
|
+
(phases[s.phase] ??= []).push(s);
|
|
650
|
+
}
|
|
651
|
+
return phases;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/utils/colors.ts
|
|
610
655
|
var tagColors = [
|
|
611
656
|
"cyan",
|
|
612
657
|
"yellow",
|
|
@@ -650,7 +695,7 @@ async function runLogs(argv, opts) {
|
|
|
650
695
|
return 1;
|
|
651
696
|
}
|
|
652
697
|
const file = join3(logRoot(opts.config, opts.logDir), `${sanitize(svcArg)}.log`);
|
|
653
|
-
if (!
|
|
698
|
+
if (!existsSync5(file)) {
|
|
654
699
|
out(`No log file yet for "${svcArg}" (${file})`);
|
|
655
700
|
return follow ? await followFile(file, out) : 1;
|
|
656
701
|
}
|
|
@@ -668,7 +713,7 @@ async function streamFile(file, out) {
|
|
|
668
713
|
}
|
|
669
714
|
async function followFile(file, out, startAt = 0) {
|
|
670
715
|
let pos = startAt;
|
|
671
|
-
while (!
|
|
716
|
+
while (!existsSync5(file)) await new Promise((r) => setTimeout(r, 500));
|
|
672
717
|
return new Promise((resolve4) => {
|
|
673
718
|
const tick = async () => {
|
|
674
719
|
const size = statSync(file).size;
|
|
@@ -730,7 +775,7 @@ ${items.length} services up to date`);
|
|
|
730
775
|
return 0;
|
|
731
776
|
}
|
|
732
777
|
function installOne(cwd, env) {
|
|
733
|
-
if (!
|
|
778
|
+
if (!existsSync5(cwd)) return Promise.resolve(false);
|
|
734
779
|
if (!needsInstall(cwd)) return Promise.resolve(true);
|
|
735
780
|
return new Promise((resolve4) => {
|
|
736
781
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
@@ -809,7 +854,7 @@ async function detectPlatform() {
|
|
|
809
854
|
}
|
|
810
855
|
|
|
811
856
|
// src/proxy-config/traefik.ts
|
|
812
|
-
import { existsSync as
|
|
857
|
+
import { existsSync as existsSync6, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
813
858
|
import { dirname as dirname2 } from "path";
|
|
814
859
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
815
860
|
var TraefikProvider = class {
|
|
@@ -848,7 +893,7 @@ ${svcs.join("\n")}
|
|
|
848
893
|
}
|
|
849
894
|
write(content, opts) {
|
|
850
895
|
const dir = dirname2(opts.confPath);
|
|
851
|
-
if (!
|
|
896
|
+
if (!existsSync6(dir)) mkdirSync(dir, { recursive: true });
|
|
852
897
|
writeFileSync2(opts.confPath, content);
|
|
853
898
|
}
|
|
854
899
|
clear(opts) {
|
|
@@ -857,7 +902,7 @@ ${svcs.join("\n")}
|
|
|
857
902
|
};
|
|
858
903
|
|
|
859
904
|
// src/proxy-config/nginx.ts
|
|
860
|
-
import { existsSync as
|
|
905
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
861
906
|
import { dirname as dirname3 } from "path";
|
|
862
907
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
863
908
|
var NginxProvider = class {
|
|
@@ -896,7 +941,7 @@ var NginxProvider = class {
|
|
|
896
941
|
}
|
|
897
942
|
write(content, opts) {
|
|
898
943
|
const dir = dirname3(opts.confPath);
|
|
899
|
-
if (!
|
|
944
|
+
if (!existsSync7(dir)) mkdirSync2(dir, { recursive: true });
|
|
900
945
|
writeFileSync3(opts.confPath, content);
|
|
901
946
|
}
|
|
902
947
|
clear(opts) {
|
|
@@ -905,7 +950,7 @@ var NginxProvider = class {
|
|
|
905
950
|
};
|
|
906
951
|
|
|
907
952
|
// src/proxy-config/caddy.ts
|
|
908
|
-
import { existsSync as
|
|
953
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
909
954
|
import { dirname as dirname4 } from "path";
|
|
910
955
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
911
956
|
var CaddyProvider = class {
|
|
@@ -930,7 +975,7 @@ var CaddyProvider = class {
|
|
|
930
975
|
}
|
|
931
976
|
write(content, opts) {
|
|
932
977
|
const dir = dirname4(opts.confPath);
|
|
933
|
-
if (!
|
|
978
|
+
if (!existsSync8(dir)) mkdirSync3(dir, { recursive: true });
|
|
934
979
|
writeFileSync4(opts.confPath, content);
|
|
935
980
|
}
|
|
936
981
|
clear(opts) {
|
|
@@ -954,22 +999,20 @@ function detectProxyProvider(name) {
|
|
|
954
999
|
}
|
|
955
1000
|
|
|
956
1001
|
// src/tui/App.tsx
|
|
957
|
-
import {
|
|
958
|
-
import { Box as Box6, Text as Text6
|
|
1002
|
+
import { useCallback as useCallback3, useRef as useRef5 } from "react";
|
|
1003
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
959
1004
|
|
|
960
1005
|
// src/tui/hooks/useProcessManager.ts
|
|
961
1006
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
962
1007
|
|
|
963
1008
|
// src/process/manager.ts
|
|
964
|
-
import {
|
|
965
|
-
import { existsSync as existsSync9 } from "fs";
|
|
966
|
-
import { join as join4, resolve as resolve3 } from "path";
|
|
1009
|
+
import { join as join5 } from "path";
|
|
967
1010
|
|
|
968
1011
|
// src/process/installer.ts
|
|
969
1012
|
import { spawn as spawn2 } from "child_process";
|
|
970
|
-
import { existsSync as
|
|
1013
|
+
import { existsSync as existsSync9 } from "fs";
|
|
971
1014
|
function installService(cwd, env, onLog) {
|
|
972
|
-
if (!
|
|
1015
|
+
if (!existsSync9(cwd)) {
|
|
973
1016
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
974
1017
|
return Promise.resolve(false);
|
|
975
1018
|
}
|
|
@@ -1002,9 +1045,32 @@ function installService(cwd, env, onLog) {
|
|
|
1002
1045
|
});
|
|
1003
1046
|
}
|
|
1004
1047
|
|
|
1005
|
-
// src/process/
|
|
1006
|
-
|
|
1007
|
-
|
|
1048
|
+
// src/process/spawner.ts
|
|
1049
|
+
import { spawn as spawn3 } from "child_process";
|
|
1050
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1051
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1052
|
+
|
|
1053
|
+
// src/process/internals.ts
|
|
1054
|
+
function lineBuffer(onLine) {
|
|
1055
|
+
let buf = "";
|
|
1056
|
+
return {
|
|
1057
|
+
push(chunk) {
|
|
1058
|
+
buf += chunk.toString();
|
|
1059
|
+
let idx;
|
|
1060
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
1061
|
+
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
1062
|
+
buf = buf.slice(idx + 1);
|
|
1063
|
+
if (line.length) onLine(line);
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
flush() {
|
|
1067
|
+
if (buf.length) {
|
|
1068
|
+
onLine(buf);
|
|
1069
|
+
buf = "";
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1008
1074
|
function compileReadyPattern(pattern) {
|
|
1009
1075
|
if (!pattern) return null;
|
|
1010
1076
|
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
|
|
@@ -1035,43 +1101,26 @@ function extractWatchPaths(args) {
|
|
|
1035
1101
|
}
|
|
1036
1102
|
return out;
|
|
1037
1103
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
let idx;
|
|
1044
|
-
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
1045
|
-
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
1046
|
-
buf = buf.slice(idx + 1);
|
|
1047
|
-
if (line.length) onLine(line);
|
|
1048
|
-
}
|
|
1049
|
-
},
|
|
1050
|
-
flush() {
|
|
1051
|
-
if (buf.length) {
|
|
1052
|
-
onLine(buf);
|
|
1053
|
-
buf = "";
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
var ProcessManager = class {
|
|
1059
|
-
state = /* @__PURE__ */ new Map();
|
|
1060
|
-
procs = /* @__PURE__ */ new Set();
|
|
1104
|
+
var MAX_RESTARTS = 3;
|
|
1105
|
+
var BACKOFF_BASE_MS = 2e3;
|
|
1106
|
+
|
|
1107
|
+
// src/process/spawner.ts
|
|
1108
|
+
var Spawner = class {
|
|
1061
1109
|
baseCwd;
|
|
1062
1110
|
env;
|
|
1063
|
-
|
|
1111
|
+
state;
|
|
1112
|
+
procs;
|
|
1064
1113
|
events;
|
|
1114
|
+
lifecycle;
|
|
1115
|
+
onCrash;
|
|
1065
1116
|
constructor(opts) {
|
|
1066
1117
|
this.baseCwd = opts.baseCwd;
|
|
1067
1118
|
this.env = opts.env;
|
|
1068
|
-
this.
|
|
1119
|
+
this.state = opts.state;
|
|
1120
|
+
this.procs = opts.procs;
|
|
1069
1121
|
this.events = opts.events;
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1073
|
-
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
1074
|
-
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
1122
|
+
this.lifecycle = opts.lifecycle;
|
|
1123
|
+
this.onCrash = opts.onCrash;
|
|
1075
1124
|
}
|
|
1076
1125
|
async start(svc, colorIdx, isRestart = false) {
|
|
1077
1126
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
@@ -1090,7 +1139,7 @@ var ProcessManager = class {
|
|
|
1090
1139
|
}
|
|
1091
1140
|
}
|
|
1092
1141
|
const args = buildProcessArgs(svc);
|
|
1093
|
-
const missingWatchPaths = extractWatchPaths(args).filter((p) => !
|
|
1142
|
+
const missingWatchPaths = extractWatchPaths(args).filter((p) => !existsSync10(resolve3(cwd, p)));
|
|
1094
1143
|
if (missingWatchPaths.length) {
|
|
1095
1144
|
this.log(svc.name, `\u26A0 missing watch paths: ${missingWatchPaths.join(", ")}`, colorIdx);
|
|
1096
1145
|
this.recordCrashedState(svc, colorIdx);
|
|
@@ -1114,6 +1163,14 @@ var ProcessManager = class {
|
|
|
1114
1163
|
this.state.set(svc.name, state);
|
|
1115
1164
|
this.procs.add(proc);
|
|
1116
1165
|
this.events.onStateChange(svc.name, state);
|
|
1166
|
+
this.wireStdio(proc, svc, state, colorIdx);
|
|
1167
|
+
this.wireCloseHandler(proc, svc, state, colorIdx);
|
|
1168
|
+
if (svc.watchBuild) {
|
|
1169
|
+
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
1170
|
+
}
|
|
1171
|
+
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
1172
|
+
}
|
|
1173
|
+
wireStdio(proc, svc, state, colorIdx) {
|
|
1117
1174
|
const readyRegex = compileReadyPattern(svc.readyPattern);
|
|
1118
1175
|
const markReadyIfMatch = (line) => {
|
|
1119
1176
|
if (!readyRegex || state.health === "up") return;
|
|
@@ -1138,9 +1195,11 @@ var ProcessManager = class {
|
|
|
1138
1195
|
proc.stderr?.on("data", (d) => stderrBuf.push(d));
|
|
1139
1196
|
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
1140
1197
|
proc.stderr?.on("end", () => stderrBuf.flush());
|
|
1198
|
+
}
|
|
1199
|
+
wireCloseHandler(proc, svc, state, colorIdx) {
|
|
1141
1200
|
proc.on("close", (code) => {
|
|
1142
1201
|
this.procs.delete(proc);
|
|
1143
|
-
this.stopWatchProc(state);
|
|
1202
|
+
this.lifecycle.stopWatchProc(state);
|
|
1144
1203
|
if (state.intentionalStop) {
|
|
1145
1204
|
state.intentionalStop = false;
|
|
1146
1205
|
return;
|
|
@@ -1155,23 +1214,12 @@ var ProcessManager = class {
|
|
|
1155
1214
|
state.health = "down";
|
|
1156
1215
|
this.log(svc.name, `\u274C exited with code ${code}`, colorIdx);
|
|
1157
1216
|
this.events.onStateChange(svc.name, state);
|
|
1158
|
-
|
|
1159
|
-
state.restarts++;
|
|
1160
|
-
const delay = BACKOFF_BASE_MS * Math.pow(2, state.restarts - 1);
|
|
1161
|
-
this.log(svc.name, `\u{1F504} auto-restart ${state.restarts}/${MAX_RESTARTS} in ${delay}ms...`, colorIdx);
|
|
1162
|
-
setTimeout(() => this.start(svc, colorIdx, true), delay);
|
|
1163
|
-
} else {
|
|
1164
|
-
this.log(svc.name, "\u26D4 max restarts reached", colorIdx);
|
|
1165
|
-
}
|
|
1217
|
+
this.onCrash(svc, state, colorIdx);
|
|
1166
1218
|
});
|
|
1167
|
-
if (svc.watchBuild) {
|
|
1168
|
-
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
1169
|
-
}
|
|
1170
|
-
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
1171
1219
|
}
|
|
1172
1220
|
runPreBuild(svc, cwd, colorIdx) {
|
|
1173
1221
|
this.log(svc.name, `\u{1F528} preBuild: ${svc.preBuild}`, colorIdx);
|
|
1174
|
-
return new Promise((
|
|
1222
|
+
return new Promise((res) => {
|
|
1175
1223
|
const isWin = process.platform === "win32";
|
|
1176
1224
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1177
1225
|
const shellFlag = isWin ? "/c" : "-c";
|
|
@@ -1183,17 +1231,17 @@ var ProcessManager = class {
|
|
|
1183
1231
|
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
1184
1232
|
child.on("error", (err) => {
|
|
1185
1233
|
this.log(svc.name, `[build] \u274C ${err.message}`, colorIdx);
|
|
1186
|
-
|
|
1234
|
+
res(false);
|
|
1187
1235
|
});
|
|
1188
1236
|
child.on("close", (code) => {
|
|
1189
1237
|
outBuf.flush();
|
|
1190
1238
|
errBuf.flush();
|
|
1191
1239
|
if (code === 0) {
|
|
1192
1240
|
this.log(svc.name, `[build] \u2705 done`, colorIdx);
|
|
1193
|
-
|
|
1241
|
+
res(true);
|
|
1194
1242
|
} else {
|
|
1195
1243
|
this.log(svc.name, `[build] \u274C exited with code ${code}`, colorIdx);
|
|
1196
|
-
|
|
1244
|
+
res(false);
|
|
1197
1245
|
}
|
|
1198
1246
|
});
|
|
1199
1247
|
});
|
|
@@ -1216,7 +1264,8 @@ var ProcessManager = class {
|
|
|
1216
1264
|
child.on("error", (err) => this.log(svc.name, `[watch] \u274C ${err.message}`, colorIdx));
|
|
1217
1265
|
return child;
|
|
1218
1266
|
}
|
|
1219
|
-
/** Create a state entry in 'crashed' status without spawning a process
|
|
1267
|
+
/** Create a state entry in 'crashed' status without spawning a process
|
|
1268
|
+
* (used when preBuild fails or pre-flight checks fail). */
|
|
1220
1269
|
recordCrashedState(svc, colorIdx) {
|
|
1221
1270
|
const prev = this.state.get(svc.name);
|
|
1222
1271
|
this.state.set(svc.name, {
|
|
@@ -1233,33 +1282,54 @@ var ProcessManager = class {
|
|
|
1233
1282
|
});
|
|
1234
1283
|
this.events.onStateChange(svc.name, this.state.get(svc.name));
|
|
1235
1284
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (!st?.proc || !st.pid) return;
|
|
1239
|
-
st.intentionalStop = true;
|
|
1240
|
-
this.platform.killTree(st.pid);
|
|
1241
|
-
this.stopWatchProc(st);
|
|
1285
|
+
log(name, text, colorIdx) {
|
|
1286
|
+
this.events.onLog(name, text, colorIdx);
|
|
1242
1287
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// src/process/restarter.ts
|
|
1291
|
+
var Restarter = class {
|
|
1292
|
+
state;
|
|
1293
|
+
events;
|
|
1294
|
+
spawner;
|
|
1295
|
+
lifecycle;
|
|
1296
|
+
constructor(opts) {
|
|
1297
|
+
this.state = opts.state;
|
|
1298
|
+
this.events = opts.events;
|
|
1299
|
+
this.spawner = opts.spawner;
|
|
1300
|
+
this.lifecycle = opts.lifecycle;
|
|
1251
1301
|
}
|
|
1252
1302
|
async restart(name) {
|
|
1253
1303
|
const st = this.state.get(name);
|
|
1254
1304
|
if (!st) return;
|
|
1255
|
-
this.stop(name);
|
|
1305
|
+
this.lifecycle.stop(name);
|
|
1256
1306
|
st.restarts = 0;
|
|
1257
1307
|
const delay = st.proc ? 1500 : 100;
|
|
1258
1308
|
await new Promise((r) => setTimeout(r, delay));
|
|
1259
|
-
await this.start(st.svc, st.colorIdx, true);
|
|
1260
|
-
this.
|
|
1309
|
+
await this.spawner.start(st.svc, st.colorIdx, true);
|
|
1310
|
+
this.events.onLog(name, "\u{1F504} manual restart", st.colorIdx);
|
|
1311
|
+
}
|
|
1312
|
+
scheduleAutoRestart(svc, state, colorIdx) {
|
|
1313
|
+
if (state.restarts >= MAX_RESTARTS) {
|
|
1314
|
+
this.events.onLog(svc.name, "\u26D4 max restarts reached", colorIdx);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
state.restarts++;
|
|
1318
|
+
const delay = BACKOFF_BASE_MS * Math.pow(2, state.restarts - 1);
|
|
1319
|
+
this.events.onLog(svc.name, `\u{1F504} auto-restart ${state.restarts}/${MAX_RESTARTS} in ${delay}ms...`, colorIdx);
|
|
1320
|
+
setTimeout(() => void this.spawner.start(svc, colorIdx, true), delay);
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
// src/process/health-poller.ts
|
|
1325
|
+
var HealthPoller = class {
|
|
1326
|
+
state;
|
|
1327
|
+
events;
|
|
1328
|
+
constructor(opts) {
|
|
1329
|
+
this.state = opts.state;
|
|
1330
|
+
this.events = opts.events;
|
|
1261
1331
|
}
|
|
1262
|
-
async
|
|
1332
|
+
async checkAll() {
|
|
1263
1333
|
for (const [name, st] of this.state) {
|
|
1264
1334
|
if (!st.pid || st.status === "idle") {
|
|
1265
1335
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
@@ -1276,6 +1346,41 @@ var ProcessManager = class {
|
|
|
1276
1346
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
1277
1347
|
}
|
|
1278
1348
|
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// src/process/lifecycle.ts
|
|
1352
|
+
var Lifecycle = class {
|
|
1353
|
+
state;
|
|
1354
|
+
procs;
|
|
1355
|
+
platform;
|
|
1356
|
+
constructor(opts) {
|
|
1357
|
+
this.state = opts.state;
|
|
1358
|
+
this.procs = opts.procs;
|
|
1359
|
+
this.platform = opts.platform;
|
|
1360
|
+
}
|
|
1361
|
+
/** Manual / external stop of a single service. Marks `intentionalStop` so the
|
|
1362
|
+
* close handler doesn't auto-restart, kills the process tree, tears down the
|
|
1363
|
+
* side-car watchBuild process if any. */
|
|
1364
|
+
stop(name) {
|
|
1365
|
+
const st = this.state.get(name);
|
|
1366
|
+
if (!st?.proc || !st.pid) return;
|
|
1367
|
+
st.intentionalStop = true;
|
|
1368
|
+
this.platform.killTree(st.pid);
|
|
1369
|
+
this.stopWatchProc(st);
|
|
1370
|
+
}
|
|
1371
|
+
/** Tears down the side-car `watchBuild` process for a service (if any) and
|
|
1372
|
+
* clears the reference. Safe to call repeatedly. */
|
|
1373
|
+
stopWatchProc(state) {
|
|
1374
|
+
const wp = state.watchProc;
|
|
1375
|
+
if (!wp || !wp.pid) return;
|
|
1376
|
+
try {
|
|
1377
|
+
this.platform.killTree(wp.pid);
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
state.watchProc = null;
|
|
1381
|
+
}
|
|
1382
|
+
/** Graceful shutdown of every spawned process. Waits `gracePeriodMs` (default
|
|
1383
|
+
* 3000) for clean exits, then SIGKILLs anything still alive. */
|
|
1279
1384
|
async cleanup(opts = {}) {
|
|
1280
1385
|
const grace = opts.gracePeriodMs ?? 3e3;
|
|
1281
1386
|
const procs = [...this.procs];
|
|
@@ -1316,27 +1421,88 @@ var ProcessManager = class {
|
|
|
1316
1421
|
for (const st of this.state.values()) if (st.proc === proc) return st;
|
|
1317
1422
|
return void 0;
|
|
1318
1423
|
}
|
|
1319
|
-
log(name, text, colorIdx) {
|
|
1320
|
-
this.events.onLog(name, text, colorIdx);
|
|
1321
|
-
}
|
|
1322
1424
|
};
|
|
1323
1425
|
|
|
1324
|
-
// src/
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1426
|
+
// src/process/manager.ts
|
|
1427
|
+
var ProcessManager = class {
|
|
1428
|
+
state = /* @__PURE__ */ new Map();
|
|
1429
|
+
procs = /* @__PURE__ */ new Set();
|
|
1430
|
+
baseCwd;
|
|
1431
|
+
env;
|
|
1432
|
+
events;
|
|
1433
|
+
spawner;
|
|
1434
|
+
restarter;
|
|
1435
|
+
healthPoller;
|
|
1436
|
+
lifecycle;
|
|
1437
|
+
constructor(opts) {
|
|
1438
|
+
this.baseCwd = opts.baseCwd;
|
|
1439
|
+
this.env = opts.env;
|
|
1440
|
+
this.events = opts.events;
|
|
1441
|
+
this.lifecycle = new Lifecycle({
|
|
1442
|
+
state: this.state,
|
|
1443
|
+
procs: this.procs,
|
|
1444
|
+
platform: opts.platform
|
|
1445
|
+
});
|
|
1446
|
+
let restarterRef = null;
|
|
1447
|
+
this.spawner = new Spawner({
|
|
1448
|
+
baseCwd: opts.baseCwd,
|
|
1449
|
+
env: opts.env,
|
|
1450
|
+
state: this.state,
|
|
1451
|
+
procs: this.procs,
|
|
1452
|
+
events: opts.events,
|
|
1453
|
+
lifecycle: this.lifecycle,
|
|
1454
|
+
onCrash: (svc, state, colorIdx) => restarterRef?.scheduleAutoRestart(svc, state, colorIdx)
|
|
1455
|
+
});
|
|
1456
|
+
this.restarter = new Restarter({
|
|
1457
|
+
state: this.state,
|
|
1458
|
+
events: opts.events,
|
|
1459
|
+
spawner: this.spawner,
|
|
1460
|
+
lifecycle: this.lifecycle
|
|
1461
|
+
});
|
|
1462
|
+
restarterRef = this.restarter;
|
|
1463
|
+
this.healthPoller = new HealthPoller({
|
|
1464
|
+
state: this.state,
|
|
1465
|
+
events: opts.events
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
install(svc, colorIdx) {
|
|
1469
|
+
const cwd = join5(this.baseCwd, svc.cwd);
|
|
1470
|
+
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
1471
|
+
return installService(cwd, this.env, (msg) => this.events.onLog(svc.name, msg, idx));
|
|
1472
|
+
}
|
|
1473
|
+
start(svc, colorIdx, isRestart = false) {
|
|
1474
|
+
return this.spawner.start(svc, colorIdx, isRestart);
|
|
1475
|
+
}
|
|
1476
|
+
stop(name) {
|
|
1477
|
+
this.lifecycle.stop(name);
|
|
1478
|
+
}
|
|
1479
|
+
restart(name) {
|
|
1480
|
+
return this.restarter.restart(name);
|
|
1481
|
+
}
|
|
1482
|
+
checkAllHealth() {
|
|
1483
|
+
return this.healthPoller.checkAll();
|
|
1484
|
+
}
|
|
1485
|
+
cleanup(opts = {}) {
|
|
1486
|
+
return this.lifecycle.cleanup(opts);
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/tui/hooks/useProcessManager.ts
|
|
1491
|
+
function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
1492
|
+
const [states, setStates] = useState(/* @__PURE__ */ new Map());
|
|
1493
|
+
const [logs, setLogs] = useState([]);
|
|
1494
|
+
const [stats, setStats] = useState(/* @__PURE__ */ new Map());
|
|
1495
|
+
const mgrRef = useRef(null);
|
|
1496
|
+
const prevCpu = useRef(/* @__PURE__ */ new Map());
|
|
1497
|
+
const pausedRef = useRef(false);
|
|
1498
|
+
const pendingLogsRef = useRef([]);
|
|
1499
|
+
const sinkRef = useRef(logSink);
|
|
1500
|
+
sinkRef.current = logSink;
|
|
1501
|
+
useEffect(() => {
|
|
1502
|
+
const mgr2 = new ProcessManager({
|
|
1503
|
+
baseCwd,
|
|
1504
|
+
env,
|
|
1505
|
+
platform,
|
|
1340
1506
|
events: {
|
|
1341
1507
|
onLog: (svcName, text, colorIdx) => {
|
|
1342
1508
|
sinkRef.current?.write(svcName, text);
|
|
@@ -1546,66 +1712,352 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1546
1712
|
}, [provider, opts, enabled]);
|
|
1547
1713
|
}
|
|
1548
1714
|
|
|
1549
|
-
// src/tui/
|
|
1550
|
-
import { useEffect as useEffect3,
|
|
1551
|
-
import {
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
const
|
|
1555
|
-
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
1556
|
-
const contentHeight = Math.max(1, height - 2);
|
|
1557
|
-
const totalLines = filtered.length;
|
|
1558
|
-
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
1559
|
-
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxOffset : Math.min(scrollOffset, maxOffset);
|
|
1560
|
-
const startIndex = Math.max(0, totalLines - contentHeight - effectiveOffset);
|
|
1561
|
-
const endIndex = Math.min(startIndex + contentHeight, totalLines);
|
|
1562
|
-
const visible = filtered.slice(startIndex, endIndex);
|
|
1715
|
+
// src/tui/hooks/useTerminalSize.ts
|
|
1716
|
+
import { useEffect as useEffect3, useState as useState3 } from "react";
|
|
1717
|
+
import { useStdout } from "ink";
|
|
1718
|
+
function useTerminalSize() {
|
|
1719
|
+
const { stdout } = useStdout();
|
|
1720
|
+
const [rows, setRows] = useState3(stdout?.rows ?? 40);
|
|
1563
1721
|
useEffect3(() => {
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1722
|
+
if (!stdout) return;
|
|
1723
|
+
const onResize = () => setRows(stdout.rows ?? 40);
|
|
1724
|
+
stdout.on("resize", onResize);
|
|
1725
|
+
return () => {
|
|
1726
|
+
stdout.off("resize", onResize);
|
|
1727
|
+
};
|
|
1728
|
+
}, [stdout]);
|
|
1729
|
+
return rows;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/tui/hooks/useLogsPause.ts
|
|
1733
|
+
import { useEffect as useEffect4 } from "react";
|
|
1734
|
+
function useLogsPause(setPaused, logsPaused, logsScrollOffset) {
|
|
1735
|
+
useEffect4(() => {
|
|
1736
|
+
setPaused(logsPaused || logsScrollOffset > 0);
|
|
1737
|
+
}, [logsPaused, logsScrollOffset, setPaused]);
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/tui/hooks/useControlPlane.ts
|
|
1741
|
+
import { useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
1742
|
+
import { createInterface as createInterface3 } from "readline";
|
|
1743
|
+
import { createReadStream as createReadStream2, existsSync as existsSync12 } from "fs";
|
|
1744
|
+
|
|
1745
|
+
// src/control-plane/socket-server.ts
|
|
1746
|
+
import { createServer } from "net";
|
|
1747
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1748
|
+
import { existsSync as existsSync11, unlinkSync, chmodSync, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
1749
|
+
import { dirname as dirname5 } from "path";
|
|
1750
|
+
import { join as join6 } from "path";
|
|
1751
|
+
import { homedir as homedir2 } from "os";
|
|
1752
|
+
function defaultSocketPath(projectName) {
|
|
1753
|
+
const safe = projectName.replace(/[^a-zA-Z0-9._-]+/g, "_") || "devup";
|
|
1754
|
+
return join6(homedir2(), ".devup", `sock-${safe}.sock`);
|
|
1755
|
+
}
|
|
1756
|
+
async function startSocketServer(projectName, ctx, opts = {}) {
|
|
1757
|
+
const path = opts.path ?? defaultSocketPath(projectName);
|
|
1758
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
1759
|
+
if (existsSync11(path)) {
|
|
1760
|
+
try {
|
|
1761
|
+
const st = statSync2(path);
|
|
1762
|
+
if (st.isSocket()) unlinkSync(path);
|
|
1763
|
+
} catch {
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
const server = createServer((socket) => handleClient(socket, ctx));
|
|
1767
|
+
await new Promise((resolve4, reject) => {
|
|
1768
|
+
server.once("error", reject);
|
|
1769
|
+
server.listen(path, () => {
|
|
1770
|
+
server.off("error", reject);
|
|
1771
|
+
try {
|
|
1772
|
+
chmodSync(path, 384);
|
|
1773
|
+
} catch {
|
|
1774
|
+
}
|
|
1775
|
+
opts.onLog?.(`\u{1F50C} control plane at ${path}`);
|
|
1776
|
+
resolve4();
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
return {
|
|
1780
|
+
server,
|
|
1781
|
+
path,
|
|
1782
|
+
async close() {
|
|
1783
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
1784
|
+
if (existsSync11(path)) {
|
|
1785
|
+
try {
|
|
1786
|
+
unlinkSync(path);
|
|
1787
|
+
} catch {
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function handleClient(socket, ctx) {
|
|
1794
|
+
const rl = createInterface2({ input: socket });
|
|
1795
|
+
rl.on("line", async (line) => {
|
|
1796
|
+
if (!line.trim()) return;
|
|
1797
|
+
let req;
|
|
1798
|
+
try {
|
|
1799
|
+
req = JSON.parse(line);
|
|
1800
|
+
} catch (e) {
|
|
1801
|
+
respond(socket, { error: { code: -32700, message: `parse error: ${e.message}` } });
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (typeof req.method !== "string") {
|
|
1805
|
+
respond(socket, { id: req.id, error: { code: -32600, message: "method required" } });
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
try {
|
|
1809
|
+
const result = await dispatch(req.method, req.params ?? {}, ctx);
|
|
1810
|
+
respond(socket, { id: req.id, result });
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
respond(socket, { id: req.id, error: { code: -32603, message: e.message ?? String(e) } });
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
socket.on("error", () => {
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
function respond(socket, payload) {
|
|
1819
|
+
if (socket.writable) socket.write(JSON.stringify(payload) + "\n");
|
|
1820
|
+
}
|
|
1821
|
+
async function dispatch(method, params, ctx) {
|
|
1822
|
+
switch (method) {
|
|
1823
|
+
case "status": {
|
|
1824
|
+
const out = [];
|
|
1825
|
+
for (const [name, st] of ctx.states()) {
|
|
1826
|
+
out.push({
|
|
1827
|
+
name,
|
|
1828
|
+
status: st.status,
|
|
1829
|
+
health: st.health,
|
|
1830
|
+
port: st.svc.port,
|
|
1831
|
+
type: st.svc.type,
|
|
1832
|
+
errors: st.errors,
|
|
1833
|
+
restarts: st.restarts,
|
|
1834
|
+
pid: st.pid,
|
|
1835
|
+
startedAt: st.startedAt
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
return { services: out };
|
|
1839
|
+
}
|
|
1840
|
+
case "restart": {
|
|
1841
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
1842
|
+
await ctx.restart(svc);
|
|
1843
|
+
return { ok: true };
|
|
1844
|
+
}
|
|
1845
|
+
case "stop": {
|
|
1846
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
1847
|
+
ctx.stop(svc);
|
|
1848
|
+
return { ok: true };
|
|
1849
|
+
}
|
|
1850
|
+
case "logs.tail": {
|
|
1851
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
1852
|
+
const lines = Math.max(1, Math.min(1e4, Number(params["lines"] ?? 100)));
|
|
1853
|
+
return { lines: await ctx.tailLogs(svc, lines) };
|
|
1854
|
+
}
|
|
1855
|
+
case "ping":
|
|
1856
|
+
return { ok: true, ts: Date.now() };
|
|
1857
|
+
default:
|
|
1858
|
+
throw new Error(`unknown method: ${method}`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function stringOrThrow(v, paramName) {
|
|
1862
|
+
if (typeof v !== "string" || !v.trim()) {
|
|
1863
|
+
throw new Error(`param "${paramName}" must be a non-empty string`);
|
|
1864
|
+
}
|
|
1865
|
+
return v;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// src/tui/hooks/useControlPlane.ts
|
|
1869
|
+
function useControlPlane(manager, projectName, logSink, pushLog) {
|
|
1870
|
+
const handleRef = useRef3(null);
|
|
1871
|
+
useEffect5(() => {
|
|
1872
|
+
if (!manager) return;
|
|
1873
|
+
let handle = null;
|
|
1874
|
+
(async () => {
|
|
1875
|
+
try {
|
|
1876
|
+
handle = await startSocketServer(projectName, {
|
|
1877
|
+
states: () => manager.state,
|
|
1878
|
+
restart: (name) => manager.restart(name),
|
|
1879
|
+
stop: (name) => manager.stop(name),
|
|
1880
|
+
tailLogs: async (svcName, lines) => {
|
|
1881
|
+
if (!logSink) return [];
|
|
1882
|
+
const file = logSink.pathFor(svcName);
|
|
1883
|
+
if (!existsSync12(file)) return [];
|
|
1884
|
+
return new Promise((resolve4, reject) => {
|
|
1885
|
+
const buf = [];
|
|
1886
|
+
const rl = createInterface3({ input: createReadStream2(file, { encoding: "utf8" }) });
|
|
1887
|
+
rl.on("line", (l) => {
|
|
1888
|
+
buf.push(l);
|
|
1889
|
+
if (buf.length > lines) buf.shift();
|
|
1890
|
+
});
|
|
1891
|
+
rl.on("close", () => resolve4(buf));
|
|
1892
|
+
rl.on("error", reject);
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}, { onLog: (msg) => pushLog("devup", msg, 12) });
|
|
1896
|
+
handleRef.current = handle;
|
|
1897
|
+
} catch (e) {
|
|
1898
|
+
pushLog("devup", `\u26A0 control plane disabled: ${e.message}`, 5);
|
|
1899
|
+
}
|
|
1900
|
+
})();
|
|
1901
|
+
return () => {
|
|
1902
|
+
void handle?.close();
|
|
1903
|
+
handleRef.current = null;
|
|
1904
|
+
};
|
|
1905
|
+
}, [manager, projectName, logSink, pushLog]);
|
|
1906
|
+
return handleRef;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
// src/tui/hooks/useHotReload.ts
|
|
1910
|
+
import { useEffect as useEffect6 } from "react";
|
|
1911
|
+
import { watch as fsWatch } from "fs";
|
|
1912
|
+
|
|
1913
|
+
// src/config/diff.ts
|
|
1914
|
+
var SPAWN_RELEVANT = [
|
|
1915
|
+
"cwd",
|
|
1916
|
+
"cmd",
|
|
1917
|
+
"args",
|
|
1918
|
+
"port",
|
|
1919
|
+
"phase",
|
|
1920
|
+
"maxMem",
|
|
1921
|
+
"preBuild",
|
|
1922
|
+
"watchBuild",
|
|
1923
|
+
"nodeArgs",
|
|
1924
|
+
"extraEnv",
|
|
1925
|
+
"healthCheck",
|
|
1926
|
+
"readyPattern",
|
|
1927
|
+
"errorPattern",
|
|
1928
|
+
"type"
|
|
1929
|
+
];
|
|
1930
|
+
function hasSpawnRelevantChange(prev, next) {
|
|
1931
|
+
for (const k of SPAWN_RELEVANT) {
|
|
1932
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) return true;
|
|
1933
|
+
}
|
|
1934
|
+
return false;
|
|
1935
|
+
}
|
|
1936
|
+
function diffServices(prev, next) {
|
|
1937
|
+
const prevByName = new Map(prev.map((s) => [s.name, s]));
|
|
1938
|
+
const nextByName = new Map(next.map((s) => [s.name, s]));
|
|
1939
|
+
const added = [];
|
|
1940
|
+
const removed = [];
|
|
1941
|
+
const changed = [];
|
|
1942
|
+
const unchanged = [];
|
|
1943
|
+
for (const [name, p] of prevByName) {
|
|
1944
|
+
if (!nextByName.has(name)) {
|
|
1945
|
+
removed.push(name);
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
const n = nextByName.get(name);
|
|
1949
|
+
if (hasSpawnRelevantChange(p, n)) changed.push({ prev: p, next: n });
|
|
1950
|
+
else unchanged.push(name);
|
|
1951
|
+
}
|
|
1952
|
+
for (const [name, n] of nextByName) {
|
|
1953
|
+
if (!prevByName.has(name)) added.push(n);
|
|
1954
|
+
}
|
|
1955
|
+
return { added, removed, changed, unchanged };
|
|
1956
|
+
}
|
|
1957
|
+
function summariseDiff(d) {
|
|
1958
|
+
const parts = [];
|
|
1959
|
+
if (d.added.length) parts.push(`+${d.added.length} added`);
|
|
1960
|
+
if (d.removed.length) parts.push(`-${d.removed.length} removed`);
|
|
1961
|
+
if (d.changed.length) parts.push(`~${d.changed.length} changed`);
|
|
1962
|
+
if (!parts.length) parts.push("no changes");
|
|
1963
|
+
return parts.join(", ");
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// src/tui/hooks/useHotReload.ts
|
|
1967
|
+
function useHotReload(manager, cliArgs, baseCwd, pushLog) {
|
|
1968
|
+
useEffect6(() => {
|
|
1969
|
+
if (!cliArgs.watchConfig || !manager) return;
|
|
1970
|
+
let watcher = null;
|
|
1971
|
+
let configPath;
|
|
1972
|
+
try {
|
|
1973
|
+
configPath = findConfigFile(baseCwd, cliArgs.configPath);
|
|
1974
|
+
} catch (e) {
|
|
1975
|
+
pushLog("devup", `\u26A0 watch-config disabled: ${e.message}`, 5);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
pushLog("devup", `\u{1F440} watching ${configPath}`, 12);
|
|
1979
|
+
let reloadInFlight = false;
|
|
1980
|
+
let reloadAgain = false;
|
|
1981
|
+
const reload = async () => {
|
|
1982
|
+
if (reloadInFlight) {
|
|
1983
|
+
reloadAgain = true;
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
reloadInFlight = true;
|
|
1987
|
+
try {
|
|
1988
|
+
const nextCfg = await loadConfig(configPath);
|
|
1989
|
+
const errs = validateConfig(nextCfg, baseCwd);
|
|
1990
|
+
if (errs.length) {
|
|
1991
|
+
pushLog("devup", `\u26A0 config reload failed:
|
|
1992
|
+
${formatValidationErrors(errs)}`, 5);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const currentSvcs = [...manager.state.values()].map((s) => s.svc);
|
|
1996
|
+
const diff = diffServices(currentSvcs, nextCfg.services);
|
|
1997
|
+
if (!diff.added.length && !diff.removed.length && !diff.changed.length) return;
|
|
1998
|
+
for (const name of diff.removed) {
|
|
1999
|
+
manager.stop(name);
|
|
2000
|
+
manager.state.delete(name);
|
|
2001
|
+
}
|
|
2002
|
+
let colorIdx = currentSvcs.length;
|
|
2003
|
+
for (const { next } of diff.changed) {
|
|
2004
|
+
const prev = manager.state.get(next.name);
|
|
2005
|
+
const ci = prev?.colorIdx ?? colorIdx++;
|
|
2006
|
+
manager.stop(next.name);
|
|
2007
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
2008
|
+
await manager.install(next, ci);
|
|
2009
|
+
await manager.start(next, ci, true);
|
|
2010
|
+
}
|
|
2011
|
+
for (const next of diff.added) {
|
|
2012
|
+
const ci = colorIdx++;
|
|
2013
|
+
await manager.install(next, ci);
|
|
2014
|
+
await manager.start(next, ci);
|
|
2015
|
+
}
|
|
2016
|
+
pushLog("devup", `\u{1F501} config reloaded: ${summariseDiff(diff)}`, 12);
|
|
2017
|
+
} catch (e) {
|
|
2018
|
+
pushLog("devup", `\u26A0 config reload error: ${e.message}`, 5);
|
|
2019
|
+
} finally {
|
|
2020
|
+
reloadInFlight = false;
|
|
2021
|
+
if (reloadAgain) {
|
|
2022
|
+
reloadAgain = false;
|
|
2023
|
+
void reload();
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
let debounceTimer = null;
|
|
2028
|
+
watcher = fsWatch(configPath, () => {
|
|
2029
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2030
|
+
debounceTimer = setTimeout(() => void reload(), 250);
|
|
2031
|
+
});
|
|
2032
|
+
return () => {
|
|
2033
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2034
|
+
watcher?.close();
|
|
2035
|
+
};
|
|
2036
|
+
}, [cliArgs.watchConfig, cliArgs.configPath, baseCwd, manager, pushLog]);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/tui/hooks/useContextualTips.ts
|
|
2040
|
+
import { useEffect as useEffect8, useRef as useRef4, useState as useState5 } from "react";
|
|
2041
|
+
|
|
2042
|
+
// src/tui/tips.ts
|
|
2043
|
+
function pickTip(state) {
|
|
2044
|
+
if (state.crashLoopedCount > 0 && !state.shown.has("crashed")) {
|
|
2045
|
+
return { id: "crashed", message: "tip: press r to restart, or check the log of the failing service" };
|
|
2046
|
+
}
|
|
2047
|
+
if (state.totalLogs > 1e3 && !state.hasSearch && !state.shown.has("search")) {
|
|
2048
|
+
return { id: "search", message: "tip: press / to search in logs" };
|
|
2049
|
+
}
|
|
2050
|
+
if (state.totalLogs > 500 && !state.hasFilter && !state.shown.has("filter")) {
|
|
2051
|
+
return { id: "filter", message: "tip: press f to filter logs by service" };
|
|
2052
|
+
}
|
|
2053
|
+
return null;
|
|
1602
2054
|
}
|
|
1603
2055
|
|
|
1604
2056
|
// src/tui/StatsPanel.tsx
|
|
1605
|
-
import { useEffect as
|
|
1606
|
-
import { Box
|
|
2057
|
+
import { useEffect as useEffect7, useState as useState4 } from "react";
|
|
2058
|
+
import { Box, Text } from "ink";
|
|
1607
2059
|
import os from "os";
|
|
1608
|
-
import { jsx
|
|
2060
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1609
2061
|
var H = {
|
|
1610
2062
|
up: { c: "\u25CF", color: "green" },
|
|
1611
2063
|
wait: { c: "\u25CF", color: "yellow" },
|
|
@@ -1618,20 +2070,20 @@ function isCrashLooped(st) {
|
|
|
1618
2070
|
}
|
|
1619
2071
|
function Row({ name, st, stat, ml, verbose }) {
|
|
1620
2072
|
const looped = isCrashLooped(st);
|
|
1621
|
-
const indicator = looped ? /* @__PURE__ */
|
|
2073
|
+
const indicator = looped ? /* @__PURE__ */ jsx(Text, { color: "red", bold: true, children: "\u2716" }) : /* @__PURE__ */ jsx(Text, { color: (H[st.health] ?? H["down"]).color, children: (H[st.health] ?? H["down"]).c });
|
|
1622
2074
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1623
2075
|
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1624
2076
|
const statusLabel = looped ? "looping" : st.status;
|
|
1625
2077
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1626
2078
|
if (!verbose) {
|
|
1627
|
-
return /* @__PURE__ */
|
|
2079
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
1628
2080
|
indicator,
|
|
1629
2081
|
" ",
|
|
1630
|
-
/* @__PURE__ */
|
|
2082
|
+
/* @__PURE__ */ jsx(Text, { color, children: name.padEnd(ml) }),
|
|
1631
2083
|
" ",
|
|
1632
2084
|
String(st.svc.port).padStart(5),
|
|
1633
2085
|
" ",
|
|
1634
|
-
/* @__PURE__ */
|
|
2086
|
+
/* @__PURE__ */ jsx(Text, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1635
2087
|
" ",
|
|
1636
2088
|
(stat?.cpu ?? "-").padStart(6),
|
|
1637
2089
|
" ",
|
|
@@ -1647,15 +2099,15 @@ function Row({ name, st, stat, ml, verbose }) {
|
|
|
1647
2099
|
const resolvedArgs = buildProcessArgs(st.svc).join(" ");
|
|
1648
2100
|
const env = redactSecrets(st.svc.extraEnv);
|
|
1649
2101
|
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1650
|
-
return /* @__PURE__ */
|
|
1651
|
-
/* @__PURE__ */
|
|
2102
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2103
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1652
2104
|
indicator,
|
|
1653
2105
|
" ",
|
|
1654
|
-
/* @__PURE__ */
|
|
2106
|
+
/* @__PURE__ */ jsx(Text, { color, children: name.padEnd(ml) }),
|
|
1655
2107
|
" ",
|
|
1656
2108
|
String(st.svc.port).padStart(5),
|
|
1657
2109
|
" ",
|
|
1658
|
-
/* @__PURE__ */
|
|
2110
|
+
/* @__PURE__ */ jsx(Text, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1659
2111
|
" ",
|
|
1660
2112
|
(stat?.cpu ?? "-").padStart(6),
|
|
1661
2113
|
" ",
|
|
@@ -1667,20 +2119,20 @@ function Row({ name, st, stat, ml, verbose }) {
|
|
|
1667
2119
|
" ",
|
|
1668
2120
|
up.padStart(6)
|
|
1669
2121
|
] }),
|
|
1670
|
-
/* @__PURE__ */
|
|
2122
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1671
2123
|
" cmd: ",
|
|
1672
2124
|
st.svc.cmd,
|
|
1673
2125
|
" ",
|
|
1674
2126
|
resolvedArgs
|
|
1675
2127
|
] }),
|
|
1676
|
-
envStr && /* @__PURE__ */
|
|
2128
|
+
envStr && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1677
2129
|
" env: ",
|
|
1678
2130
|
envStr
|
|
1679
2131
|
] })
|
|
1680
2132
|
] });
|
|
1681
2133
|
}
|
|
1682
2134
|
function ColHeader({ ml }) {
|
|
1683
|
-
return /* @__PURE__ */
|
|
2135
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
1684
2136
|
"H ",
|
|
1685
2137
|
"Service".padEnd(ml),
|
|
1686
2138
|
" ",
|
|
@@ -1727,7 +2179,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1727
2179
|
const webStartIndex = Math.min(effectiveOffset, Math.max(0, webs.length - rowsPerCol));
|
|
1728
2180
|
const visibleApis = apis.slice(apiStartIndex, apiStartIndex + rowsPerCol);
|
|
1729
2181
|
const visibleWebs = webs.slice(webStartIndex, webStartIndex + rowsPerCol);
|
|
1730
|
-
|
|
2182
|
+
useEffect7(() => {
|
|
1731
2183
|
resetScroll();
|
|
1732
2184
|
}, [sortMode, resetScroll]);
|
|
1733
2185
|
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
@@ -1735,24 +2187,24 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1735
2187
|
const scrolled = effectiveOffset > 0;
|
|
1736
2188
|
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1737
2189
|
const ramPct = parseFloat(usedGB) / parseFloat(totalGB) * 100;
|
|
1738
|
-
const [ramBanner, setRamBanner] =
|
|
1739
|
-
|
|
2190
|
+
const [ramBanner, setRamBanner] = useState4(false);
|
|
2191
|
+
useEffect7(() => {
|
|
1740
2192
|
setRamBanner((prev) => nextRamBannerVisibility(ramPct, prev));
|
|
1741
2193
|
}, [ramPct]);
|
|
1742
2194
|
const topConsumers = ramBanner ? [...stats.entries()].map(([n, s]) => ({ name: n, mb: parseFloat(s.mem) || 0 })).sort((a, b) => b.mb - a.mb).slice(0, 3) : [];
|
|
1743
|
-
return /* @__PURE__ */
|
|
1744
|
-
/* @__PURE__ */
|
|
1745
|
-
/* @__PURE__ */
|
|
2195
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
2196
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2197
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "green", children: [
|
|
1746
2198
|
" Stats ",
|
|
1747
2199
|
positionInfo
|
|
1748
2200
|
] }),
|
|
1749
|
-
scrolled && /* @__PURE__ */
|
|
1750
|
-
loopedCount > 0 && /* @__PURE__ */
|
|
2201
|
+
scrolled && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " [SCROLL]" }),
|
|
2202
|
+
loopedCount > 0 && /* @__PURE__ */ jsxs(Text, { color: "red", bold: true, children: [
|
|
1751
2203
|
" \u26A0 ",
|
|
1752
2204
|
loopedCount,
|
|
1753
2205
|
" need attention"
|
|
1754
2206
|
] }),
|
|
1755
|
-
/* @__PURE__ */
|
|
2207
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1756
2208
|
" System: ",
|
|
1757
2209
|
cpus,
|
|
1758
2210
|
"c Load ",
|
|
@@ -1763,8 +2215,8 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1763
2215
|
totalGB,
|
|
1764
2216
|
"GB"
|
|
1765
2217
|
] }),
|
|
1766
|
-
/* @__PURE__ */
|
|
1767
|
-
/* @__PURE__ */
|
|
2218
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
|
|
2219
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1768
2220
|
"Stack: CPU ",
|
|
1769
2221
|
totalCpu.toFixed(1),
|
|
1770
2222
|
"% RAM ",
|
|
@@ -1776,170 +2228,67 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1776
2228
|
" Svcs ",
|
|
1777
2229
|
names.length
|
|
1778
2230
|
] }),
|
|
1779
|
-
sortMode !== "name" && /* @__PURE__ */
|
|
2231
|
+
sortMode !== "name" && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1780
2232
|
" \u2502 Sort: ",
|
|
1781
2233
|
sortMode
|
|
1782
2234
|
] })
|
|
1783
2235
|
] }),
|
|
1784
|
-
ramBanner && /* @__PURE__ */
|
|
1785
|
-
/* @__PURE__ */
|
|
2236
|
+
ramBanner && /* @__PURE__ */ jsxs(Box, { children: [
|
|
2237
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
|
|
1786
2238
|
" \u26A0 RAM ",
|
|
1787
2239
|
ramPct.toFixed(0),
|
|
1788
2240
|
"% \u2014 top: "
|
|
1789
2241
|
] }),
|
|
1790
|
-
/* @__PURE__ */
|
|
2242
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: topConsumers.map((c) => `${c.name} ${c.mb.toFixed(0)}MB`).join(", ") })
|
|
1791
2243
|
] }),
|
|
1792
|
-
/* @__PURE__ */
|
|
1793
|
-
/* @__PURE__ */
|
|
1794
|
-
/* @__PURE__ */
|
|
2244
|
+
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
|
|
2245
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
2246
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1795
2247
|
" APIs (",
|
|
1796
2248
|
apis.length,
|
|
1797
2249
|
")"
|
|
1798
2250
|
] }),
|
|
1799
|
-
/* @__PURE__ */
|
|
1800
|
-
visibleApis.map((n) => /* @__PURE__ */
|
|
2251
|
+
/* @__PURE__ */ jsx(ColHeader, { ml }),
|
|
2252
|
+
visibleApis.map((n) => /* @__PURE__ */ jsx(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1801
2253
|
] }),
|
|
1802
|
-
/* @__PURE__ */
|
|
1803
|
-
/* @__PURE__ */
|
|
1804
|
-
/* @__PURE__ */
|
|
2254
|
+
/* @__PURE__ */ jsx(Box, { flexDirection: "column", width: 1, children: Array.from({ length: contentHeight }, (_, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2502" }, i)) }),
|
|
2255
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
2256
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "magenta", children: [
|
|
1805
2257
|
" Webs (",
|
|
1806
2258
|
webs.length,
|
|
1807
2259
|
")"
|
|
1808
2260
|
] }),
|
|
1809
|
-
/* @__PURE__ */
|
|
1810
|
-
visibleWebs.map((n) => /* @__PURE__ */
|
|
2261
|
+
/* @__PURE__ */ jsx(ColHeader, { ml }),
|
|
2262
|
+
visibleWebs.map((n) => /* @__PURE__ */ jsx(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1811
2263
|
] })
|
|
1812
2264
|
] })
|
|
1813
2265
|
] });
|
|
1814
2266
|
}
|
|
1815
2267
|
|
|
1816
|
-
// src/tui/
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
1834
|
-
" Filter ",
|
|
1835
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
1836
|
-
" Level ",
|
|
1837
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
1838
|
-
" All ",
|
|
1839
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
1840
|
-
" Restart ",
|
|
1841
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "/" }),
|
|
1842
|
-
" Search ",
|
|
1843
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "s" }),
|
|
1844
|
-
" Sort ",
|
|
1845
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "o" }),
|
|
1846
|
-
" Open ",
|
|
1847
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "p" }),
|
|
1848
|
-
" Pause ",
|
|
1849
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
1850
|
-
" Time ",
|
|
1851
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
1852
|
-
" Verbose ",
|
|
1853
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
1854
|
-
" Proxy"
|
|
1855
|
-
] }) });
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// src/tui/ServiceList.tsx
|
|
1859
|
-
import { useState as useState4, useMemo as useMemo2 } from "react";
|
|
1860
|
-
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1861
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1862
|
-
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1863
|
-
const allNames = useMemo2(
|
|
1864
|
-
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1865
|
-
[services, filterType]
|
|
1866
|
-
);
|
|
1867
|
-
const [idx, setIdx] = useState4(0);
|
|
1868
|
-
const [query, setQuery] = useState4("");
|
|
1869
|
-
const names = useMemo2(() => {
|
|
1870
|
-
if (!query) return allNames;
|
|
1871
|
-
const q = query.toLowerCase();
|
|
1872
|
-
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
1873
|
-
}, [allNames, query]);
|
|
1874
|
-
const clamped = Math.min(idx, Math.max(0, names.length - 1));
|
|
1875
|
-
useInput2((input, key) => {
|
|
1876
|
-
if (key.escape) {
|
|
1877
|
-
if (query) setQuery("");
|
|
1878
|
-
else onClose();
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
if (key.return) {
|
|
1882
|
-
if (names[clamped]) onSelect(names[clamped]);
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
|
-
if (key.upArrow) {
|
|
1886
|
-
setIdx((i) => Math.max(0, i - 1));
|
|
1887
|
-
return;
|
|
1888
|
-
}
|
|
1889
|
-
if (key.downArrow) {
|
|
1890
|
-
setIdx((i) => Math.min(names.length - 1, i + 1));
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
if (key.backspace || key.delete) {
|
|
1894
|
-
setQuery((q) => q.slice(0, -1));
|
|
1895
|
-
setIdx(0);
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
1899
|
-
setQuery((q) => q + input);
|
|
1900
|
-
setIdx(0);
|
|
2268
|
+
// src/tui/hooks/useContextualTips.ts
|
|
2269
|
+
function useContextualTips(totalLogs, hasSearch, hasFilter, states) {
|
|
2270
|
+
const shownTips = useRef4(/* @__PURE__ */ new Set());
|
|
2271
|
+
const [activeTip, setActiveTip] = useState5(null);
|
|
2272
|
+
useEffect8(() => {
|
|
2273
|
+
const tip = pickTip({
|
|
2274
|
+
totalLogs,
|
|
2275
|
+
hasSearch,
|
|
2276
|
+
hasFilter,
|
|
2277
|
+
crashLoopedCount: [...states.values()].filter(isCrashLooped).length,
|
|
2278
|
+
shown: shownTips.current
|
|
2279
|
+
});
|
|
2280
|
+
if (tip && tip.message !== activeTip) {
|
|
2281
|
+
shownTips.current.add(tip.id);
|
|
2282
|
+
setActiveTip(tip.message);
|
|
2283
|
+
const timer = setTimeout(() => setActiveTip(null), 12e3);
|
|
2284
|
+
return () => clearTimeout(timer);
|
|
1901
2285
|
}
|
|
1902
|
-
},
|
|
1903
|
-
return
|
|
1904
|
-
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
1905
|
-
" ",
|
|
1906
|
-
title,
|
|
1907
|
-
" ",
|
|
1908
|
-
query && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
1909
|
-
"[",
|
|
1910
|
-
query,
|
|
1911
|
-
"]"
|
|
1912
|
-
] })
|
|
1913
|
-
] }),
|
|
1914
|
-
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: [
|
|
1915
|
-
" ",
|
|
1916
|
-
name,
|
|
1917
|
-
" :",
|
|
1918
|
-
services.get(name).svc.port,
|
|
1919
|
-
" "
|
|
1920
|
-
] }) }, name)),
|
|
1921
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "type to filter \u2191\u2193 navigate Enter select Esc clear/close" })
|
|
1922
|
-
] });
|
|
2286
|
+
}, [totalLogs, states, hasSearch, hasFilter, activeTip]);
|
|
2287
|
+
return activeTip;
|
|
1923
2288
|
}
|
|
1924
2289
|
|
|
1925
|
-
// src/tui/
|
|
1926
|
-
import { useState as
|
|
1927
|
-
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
1928
|
-
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1929
|
-
function SearchInput({ onSubmit, onClose }) {
|
|
1930
|
-
const [value, setValue] = useState5("");
|
|
1931
|
-
useInput3((input, key) => {
|
|
1932
|
-
if (key.escape) onClose();
|
|
1933
|
-
else if (key.return) onSubmit(value.trim() || null);
|
|
1934
|
-
else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
|
|
1935
|
-
else if (input && !key.ctrl && !key.meta) setValue((v) => v + input);
|
|
1936
|
-
}, { isActive: process.stdin.isTTY ?? false });
|
|
1937
|
-
return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
1938
|
-
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "Search: " }),
|
|
1939
|
-
/* @__PURE__ */ jsx5(Text5, { children: value }),
|
|
1940
|
-
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2588" })
|
|
1941
|
-
] });
|
|
1942
|
-
}
|
|
2290
|
+
// src/tui/hooks/useBootSequence.ts
|
|
2291
|
+
import { useEffect as useEffect9, useState as useState6 } from "react";
|
|
1943
2292
|
|
|
1944
2293
|
// src/lazy/proxy.ts
|
|
1945
2294
|
import net2 from "net";
|
|
@@ -2056,7 +2405,7 @@ function createLazyProxy(opts) {
|
|
|
2056
2405
|
|
|
2057
2406
|
// src/process/external.ts
|
|
2058
2407
|
import { spawn as spawn4 } from "child_process";
|
|
2059
|
-
import { join as
|
|
2408
|
+
import { join as join7 } from "path";
|
|
2060
2409
|
var DEFAULT_START_TIMEOUT_S = 60;
|
|
2061
2410
|
async function startExternals(externals, opts) {
|
|
2062
2411
|
const procs = [];
|
|
@@ -2093,139 +2442,54 @@ async function stopExternals(procs, platform, opts = {}) {
|
|
|
2093
2442
|
const isWin = process.platform === "win32";
|
|
2094
2443
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
2095
2444
|
const flag = isWin ? "/c" : "-c";
|
|
2096
|
-
const cwd = svc.cwd ?
|
|
2445
|
+
const cwd = svc.cwd ? join7(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2097
2446
|
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2098
2447
|
const child = spawn4(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
2099
2448
|
child.on("close", () => resolve4());
|
|
2100
2449
|
child.on("error", () => resolve4());
|
|
2101
|
-
setTimeout(() => resolve4(), 1e4);
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
} catch {
|
|
2105
|
-
}
|
|
2106
|
-
void proc;
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
function spawnExternal(svc, opts) {
|
|
2110
|
-
const isWin = process.platform === "win32";
|
|
2111
|
-
const shell = isWin ? "cmd.exe" : "sh";
|
|
2112
|
-
const flag = isWin ? "/c" : "-c";
|
|
2113
|
-
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2114
|
-
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2115
|
-
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
2116
|
-
const child = spawn4(shell, [flag, svc.cmd], {
|
|
2117
|
-
cwd,
|
|
2118
|
-
env,
|
|
2119
|
-
detached: true,
|
|
2120
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2121
|
-
});
|
|
2122
|
-
child.stdout?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2123
|
-
child.stderr?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2124
|
-
child.on("error", (err) => opts.onLog?.(svc.name, `\u274C spawn error: ${err.message}`));
|
|
2125
|
-
return child;
|
|
2126
|
-
}
|
|
2127
|
-
async function waitHealthy(svc, timeoutMs) {
|
|
2128
|
-
const deadline = Date.now() + timeoutMs;
|
|
2129
|
-
const port = svc.port;
|
|
2130
|
-
while (Date.now() < deadline) {
|
|
2131
|
-
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
2132
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
2133
|
-
}
|
|
2134
|
-
return false;
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
// src/tui/tips.ts
|
|
2138
|
-
function pickTip(state) {
|
|
2139
|
-
if (state.crashLoopedCount > 0 && !state.shown.has("crashed")) {
|
|
2140
|
-
return { id: "crashed", message: "tip: press r to restart, or check the log of the failing service" };
|
|
2141
|
-
}
|
|
2142
|
-
if (state.totalLogs > 1e3 && !state.hasSearch && !state.shown.has("search")) {
|
|
2143
|
-
return { id: "search", message: "tip: press / to search in logs" };
|
|
2144
|
-
}
|
|
2145
|
-
if (state.totalLogs > 500 && !state.hasFilter && !state.shown.has("filter")) {
|
|
2146
|
-
return { id: "filter", message: "tip: press f to filter logs by service" };
|
|
2147
|
-
}
|
|
2148
|
-
return null;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
// src/tui/App.tsx
|
|
2152
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2153
|
-
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
2154
|
-
if (proxyActive && proxyOpts) {
|
|
2155
|
-
const sub = proxyOpts.routes[name];
|
|
2156
|
-
if (sub !== void 0) {
|
|
2157
|
-
const host = sub ? `${sub}.${proxyOpts.domain}` : proxyOpts.domain;
|
|
2158
|
-
const scheme = proxyOpts.tls ? "https" : "http";
|
|
2159
|
-
return `${scheme}://${host}`;
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
return `http://localhost:${port}`;
|
|
2163
|
-
}
|
|
2164
|
-
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
2165
|
-
const { stdout } = useStdout();
|
|
2166
|
-
const [rows, setRows] = useState6(stdout?.rows ?? 40);
|
|
2167
|
-
useEffect5(() => {
|
|
2168
|
-
if (!stdout) return;
|
|
2169
|
-
const onResize = () => setRows(stdout.rows ?? 40);
|
|
2170
|
-
stdout.on("resize", onResize);
|
|
2171
|
-
return () => {
|
|
2172
|
-
stdout.off("resize", onResize);
|
|
2173
|
-
};
|
|
2174
|
-
}, [stdout]);
|
|
2175
|
-
const logsHeight = Math.floor(rows * 0.65);
|
|
2176
|
-
const statsHeight = rows - logsHeight - 2;
|
|
2177
|
-
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
2178
|
-
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
2179
|
-
const [booted, setBooted] = useState6(false);
|
|
2180
|
-
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
2181
|
-
const externals = useRef3([]);
|
|
2182
|
-
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2183
|
-
const [activeTip, setActiveTip] = useState6(null);
|
|
2184
|
-
const kb = useKeyBindings({
|
|
2185
|
-
onQuit: () => {
|
|
2186
|
-
void shutdown();
|
|
2187
|
-
},
|
|
2188
|
-
onClearLogs: pm.clearLogs,
|
|
2189
|
-
onToggleProxy: () => {
|
|
2190
|
-
}
|
|
2191
|
-
});
|
|
2192
|
-
const shutdown = useCallback3(async () => {
|
|
2193
|
-
lazyProxies.current.forEach((p) => p.destroy());
|
|
2194
|
-
await pm.cleanup();
|
|
2195
|
-
if (externals.current.length) {
|
|
2196
|
-
await stopExternals(externals.current, platform, {
|
|
2197
|
-
baseCwd,
|
|
2198
|
-
env,
|
|
2199
|
-
onLog: (svc, msg) => pm.pushLog(`ext:${svc}`, msg, 12)
|
|
2200
|
-
});
|
|
2201
|
-
externals.current = [];
|
|
2202
|
-
}
|
|
2203
|
-
await logSink?.close();
|
|
2204
|
-
process.exit(0);
|
|
2205
|
-
}, [pm, logSink, platform, baseCwd, env]);
|
|
2206
|
-
useEffect5(() => {
|
|
2207
|
-
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
2208
|
-
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
2209
|
-
useEffect5(() => {
|
|
2210
|
-
const tip = pickTip({
|
|
2211
|
-
totalLogs: pm.logs.length,
|
|
2212
|
-
hasSearch: !!kb.searchTerm,
|
|
2213
|
-
hasFilter: !!kb.logFilter,
|
|
2214
|
-
crashLoopedCount: [...pm.states.values()].filter(isCrashLooped).length,
|
|
2215
|
-
shown: shownTips.current
|
|
2216
|
-
});
|
|
2217
|
-
if (tip && tip.id !== activeTip) {
|
|
2218
|
-
shownTips.current.add(tip.id);
|
|
2219
|
-
setActiveTip(tip.message);
|
|
2220
|
-
const timer = setTimeout(() => setActiveTip(null), 12e3);
|
|
2221
|
-
return () => clearTimeout(timer);
|
|
2450
|
+
setTimeout(() => resolve4(), 1e4);
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
} catch {
|
|
2222
2454
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2455
|
+
void proc;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
function spawnExternal(svc, opts) {
|
|
2459
|
+
const isWin = process.platform === "win32";
|
|
2460
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
2461
|
+
const flag = isWin ? "/c" : "-c";
|
|
2462
|
+
const cwd = svc.cwd ? join7(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2463
|
+
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2464
|
+
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
2465
|
+
const child = spawn4(shell, [flag, svc.cmd], {
|
|
2466
|
+
cwd,
|
|
2467
|
+
env,
|
|
2468
|
+
detached: true,
|
|
2469
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2470
|
+
});
|
|
2471
|
+
child.stdout?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2472
|
+
child.stderr?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2473
|
+
child.on("error", (err) => opts.onLog?.(svc.name, `\u274C spawn error: ${err.message}`));
|
|
2474
|
+
return child;
|
|
2475
|
+
}
|
|
2476
|
+
async function waitHealthy(svc, timeoutMs) {
|
|
2477
|
+
const deadline = Date.now() + timeoutMs;
|
|
2478
|
+
const port = svc.port;
|
|
2479
|
+
while (Date.now() < deadline) {
|
|
2480
|
+
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
2481
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2482
|
+
}
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// src/tui/hooks/useBootSequence.ts
|
|
2487
|
+
function useBootSequence(manager, config, services, cliArgs, platform, env, baseCwd, refs, pushLog) {
|
|
2488
|
+
const [booted, setBooted] = useState6(false);
|
|
2489
|
+
useEffect9(() => {
|
|
2490
|
+
if (booted || !manager) return;
|
|
2227
2491
|
setBooted(true);
|
|
2228
|
-
const mgr =
|
|
2492
|
+
const mgr = manager;
|
|
2229
2493
|
(async () => {
|
|
2230
2494
|
const lazyMode = cliArgs.lazy;
|
|
2231
2495
|
const lazyTimeout = cliArgs.lazyTimeout;
|
|
@@ -2234,11 +2498,11 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2234
2498
|
baseCwd,
|
|
2235
2499
|
env,
|
|
2236
2500
|
platform,
|
|
2237
|
-
onLog: (svc, msg) =>
|
|
2501
|
+
onLog: (svc, msg) => pushLog(`ext:${svc}`, msg, 12)
|
|
2238
2502
|
});
|
|
2239
|
-
externals.current = result.procs;
|
|
2503
|
+
refs.externals.current = result.procs;
|
|
2240
2504
|
if (!result.allHealthy) {
|
|
2241
|
-
|
|
2505
|
+
pushLog("devup", `\u274C external(s) failed: ${result.failed.join(", ")}. Aborting boot.`, 5);
|
|
2242
2506
|
return;
|
|
2243
2507
|
}
|
|
2244
2508
|
}
|
|
@@ -2306,7 +2570,7 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2306
2570
|
return !!st && !!st.proc && !st.proc.killed && st.health === "up";
|
|
2307
2571
|
}
|
|
2308
2572
|
});
|
|
2309
|
-
lazyProxies.current.set(svc.name, proxy);
|
|
2573
|
+
refs.lazyProxies.current.set(svc.name, proxy);
|
|
2310
2574
|
}
|
|
2311
2575
|
} else {
|
|
2312
2576
|
const phases = groupByPhase(services);
|
|
@@ -2327,7 +2591,259 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2327
2591
|
}
|
|
2328
2592
|
}
|
|
2329
2593
|
})();
|
|
2330
|
-
}, [booted,
|
|
2594
|
+
}, [booted, manager, services, cliArgs, config.lazy, config.external, baseCwd, env, platform, refs, pushLog]);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
// src/tui/LogsPanel.tsx
|
|
2598
|
+
import { useEffect as useEffect10, useMemo } from "react";
|
|
2599
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
2600
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2601
|
+
function resolveBorder(focused, filter, filteredColorIdx) {
|
|
2602
|
+
if (focused) return "cyan";
|
|
2603
|
+
if (filter && filteredColorIdx !== null && filteredColorIdx >= 0) {
|
|
2604
|
+
return tagColors[filteredColorIdx % tagColors.length];
|
|
2605
|
+
}
|
|
2606
|
+
return "gray";
|
|
2607
|
+
}
|
|
2608
|
+
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll, levelFilter = "all", filteredColorIdx = null }) {
|
|
2609
|
+
const byService = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
2610
|
+
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
2611
|
+
const contentHeight = Math.max(1, height - 2);
|
|
2612
|
+
const totalLines = filtered.length;
|
|
2613
|
+
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
2614
|
+
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxOffset : Math.min(scrollOffset, maxOffset);
|
|
2615
|
+
const startIndex = Math.max(0, totalLines - contentHeight - effectiveOffset);
|
|
2616
|
+
const endIndex = Math.min(startIndex + contentHeight, totalLines);
|
|
2617
|
+
const visible = filtered.slice(startIndex, endIndex);
|
|
2618
|
+
useEffect10(() => {
|
|
2619
|
+
resetScroll();
|
|
2620
|
+
}, [filter, searchTerm, resetScroll]);
|
|
2621
|
+
const matcher = useMemo(() => compileSearchPattern(searchTerm), [searchTerm]);
|
|
2622
|
+
const scrolled = effectiveOffset > 0;
|
|
2623
|
+
const label = [
|
|
2624
|
+
"Logs",
|
|
2625
|
+
filter ? `[${filter}]` : "",
|
|
2626
|
+
searchTerm ? `/${searchTerm}` : "",
|
|
2627
|
+
matcher?.invalid ? "(invalid regex)" : "",
|
|
2628
|
+
levelFilter !== "all" ? `[level: ${levelFilter}${levelFilter === "warn" ? "+error" : ""}]` : "",
|
|
2629
|
+
paused ? "[PAUSED]" : "",
|
|
2630
|
+
scrolled ? "[SCROLL]" : "",
|
|
2631
|
+
`${filtered.length} lines`,
|
|
2632
|
+
focused && totalLines > 0 ? `(${startIndex + 1}-${endIndex}/${totalLines})` : ""
|
|
2633
|
+
].filter(Boolean).join(" ");
|
|
2634
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: resolveBorder(focused, filter, filteredColorIdx), height, children: [
|
|
2635
|
+
/* @__PURE__ */ jsx2(Box2, { children: /* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
2636
|
+
" ",
|
|
2637
|
+
label,
|
|
2638
|
+
" "
|
|
2639
|
+
] }) }),
|
|
2640
|
+
visible.map((entry, i) => {
|
|
2641
|
+
const color = tagColors[entry.colorIdx % tagColors.length];
|
|
2642
|
+
const ts = showTimestamps ? new Date(entry.ts).toLocaleTimeString("en-GB") + " " : "";
|
|
2643
|
+
const line = entry.text;
|
|
2644
|
+
const isMatch = matcher ? matcher.test(line) : false;
|
|
2645
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
2646
|
+
showTimestamps && /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: ts }),
|
|
2647
|
+
/* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
2648
|
+
"[",
|
|
2649
|
+
entry.svcName.padEnd(maxNameLen),
|
|
2650
|
+
"]"
|
|
2651
|
+
] }),
|
|
2652
|
+
/* @__PURE__ */ jsx2(Text2, { children: " " }),
|
|
2653
|
+
isMatch ? /* @__PURE__ */ jsx2(Text2, { backgroundColor: "yellow", color: "black", children: line }) : /* @__PURE__ */ jsx2(Text2, { children: line })
|
|
2654
|
+
] }, i);
|
|
2655
|
+
})
|
|
2656
|
+
] });
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// src/tui/StatusBar.tsx
|
|
2660
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
2661
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2662
|
+
function StatusBar() {
|
|
2663
|
+
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2664
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "q" }),
|
|
2665
|
+
" Quit ",
|
|
2666
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Tab" }),
|
|
2667
|
+
" Switch ",
|
|
2668
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "\u2191\u2193" }),
|
|
2669
|
+
" Scroll ",
|
|
2670
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "PgUp/PgDn" }),
|
|
2671
|
+
" Page ",
|
|
2672
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Ctrl+A/E" }),
|
|
2673
|
+
" Home/End ",
|
|
2674
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "c" }),
|
|
2675
|
+
" Clear ",
|
|
2676
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
2677
|
+
" Filter ",
|
|
2678
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
2679
|
+
" Level ",
|
|
2680
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
2681
|
+
" All ",
|
|
2682
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
2683
|
+
" Restart ",
|
|
2684
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "/" }),
|
|
2685
|
+
" Search ",
|
|
2686
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "s" }),
|
|
2687
|
+
" Sort ",
|
|
2688
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "o" }),
|
|
2689
|
+
" Open ",
|
|
2690
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "p" }),
|
|
2691
|
+
" Pause ",
|
|
2692
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
2693
|
+
" Time ",
|
|
2694
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
2695
|
+
" Verbose ",
|
|
2696
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
2697
|
+
" Proxy"
|
|
2698
|
+
] }) });
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
// src/tui/ServiceList.tsx
|
|
2702
|
+
import { useState as useState7, useMemo as useMemo2 } from "react";
|
|
2703
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
2704
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2705
|
+
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
2706
|
+
const allNames = useMemo2(
|
|
2707
|
+
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
2708
|
+
[services, filterType]
|
|
2709
|
+
);
|
|
2710
|
+
const [idx, setIdx] = useState7(0);
|
|
2711
|
+
const [query, setQuery] = useState7("");
|
|
2712
|
+
const names = useMemo2(() => {
|
|
2713
|
+
if (!query) return allNames;
|
|
2714
|
+
const q = query.toLowerCase();
|
|
2715
|
+
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
2716
|
+
}, [allNames, query]);
|
|
2717
|
+
const clamped = Math.min(idx, Math.max(0, names.length - 1));
|
|
2718
|
+
useInput2((input, key) => {
|
|
2719
|
+
if (key.escape) {
|
|
2720
|
+
if (query) setQuery("");
|
|
2721
|
+
else onClose();
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (key.return) {
|
|
2725
|
+
if (names[clamped]) onSelect(names[clamped]);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (key.upArrow) {
|
|
2729
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
if (key.downArrow) {
|
|
2733
|
+
setIdx((i) => Math.min(names.length - 1, i + 1));
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
if (key.backspace || key.delete) {
|
|
2737
|
+
setQuery((q) => q.slice(0, -1));
|
|
2738
|
+
setIdx(0);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
2742
|
+
setQuery((q) => q + input);
|
|
2743
|
+
setIdx(0);
|
|
2744
|
+
}
|
|
2745
|
+
}, { isActive: process.stdin.isTTY ?? false });
|
|
2746
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
2747
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
2748
|
+
" ",
|
|
2749
|
+
title,
|
|
2750
|
+
" ",
|
|
2751
|
+
query && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
2752
|
+
"[",
|
|
2753
|
+
query,
|
|
2754
|
+
"]"
|
|
2755
|
+
] })
|
|
2756
|
+
] }),
|
|
2757
|
+
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: [
|
|
2758
|
+
" ",
|
|
2759
|
+
name,
|
|
2760
|
+
" :",
|
|
2761
|
+
services.get(name).svc.port,
|
|
2762
|
+
" "
|
|
2763
|
+
] }) }, name)),
|
|
2764
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "type to filter \u2191\u2193 navigate Enter select Esc clear/close" })
|
|
2765
|
+
] });
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// src/tui/SearchInput.tsx
|
|
2769
|
+
import { useState as useState8 } from "react";
|
|
2770
|
+
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
2771
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2772
|
+
function SearchInput({ onSubmit, onClose }) {
|
|
2773
|
+
const [value, setValue] = useState8("");
|
|
2774
|
+
useInput3((input, key) => {
|
|
2775
|
+
if (key.escape) onClose();
|
|
2776
|
+
else if (key.return) onSubmit(value.trim() || null);
|
|
2777
|
+
else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
|
|
2778
|
+
else if (input && !key.ctrl && !key.meta) setValue((v) => v + input);
|
|
2779
|
+
}, { isActive: process.stdin.isTTY ?? false });
|
|
2780
|
+
return /* @__PURE__ */ jsxs5(Box5, { borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2781
|
+
/* @__PURE__ */ jsx5(Text5, { bold: true, color: "yellow", children: "Search: " }),
|
|
2782
|
+
/* @__PURE__ */ jsx5(Text5, { children: value }),
|
|
2783
|
+
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2588" })
|
|
2784
|
+
] });
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
// src/tui/App.tsx
|
|
2788
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2789
|
+
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
2790
|
+
if (proxyActive && proxyOpts) {
|
|
2791
|
+
const sub = proxyOpts.routes[name];
|
|
2792
|
+
if (sub !== void 0) {
|
|
2793
|
+
const host = sub ? `${sub}.${proxyOpts.domain}` : proxyOpts.domain;
|
|
2794
|
+
const scheme = proxyOpts.tls ? "https" : "http";
|
|
2795
|
+
return `${scheme}://${host}`;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
return `http://localhost:${port}`;
|
|
2799
|
+
}
|
|
2800
|
+
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
2801
|
+
const rows = useTerminalSize();
|
|
2802
|
+
const logsHeight = Math.floor(rows * 0.65);
|
|
2803
|
+
const statsHeight = rows - logsHeight - 2;
|
|
2804
|
+
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
2805
|
+
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
2806
|
+
const lazyProxies = useRef5(/* @__PURE__ */ new Map());
|
|
2807
|
+
const externals = useRef5([]);
|
|
2808
|
+
const kb = useKeyBindings({
|
|
2809
|
+
onQuit: () => {
|
|
2810
|
+
void shutdown();
|
|
2811
|
+
},
|
|
2812
|
+
onClearLogs: pm.clearLogs,
|
|
2813
|
+
onToggleProxy: () => {
|
|
2814
|
+
}
|
|
2815
|
+
});
|
|
2816
|
+
const socketServer = useControlPlane(pm.manager, config.name, logSink, pm.pushLog);
|
|
2817
|
+
const shutdown = useCallback3(async () => {
|
|
2818
|
+
lazyProxies.current.forEach((p) => p.destroy());
|
|
2819
|
+
await socketServer.current?.close();
|
|
2820
|
+
await pm.cleanup();
|
|
2821
|
+
if (externals.current.length) {
|
|
2822
|
+
await stopExternals(externals.current, platform, {
|
|
2823
|
+
baseCwd,
|
|
2824
|
+
env,
|
|
2825
|
+
onLog: (svc, msg) => pm.pushLog(`ext:${svc}`, msg, 12)
|
|
2826
|
+
});
|
|
2827
|
+
externals.current = [];
|
|
2828
|
+
}
|
|
2829
|
+
await logSink?.close();
|
|
2830
|
+
process.exit(0);
|
|
2831
|
+
}, [pm, logSink, platform, baseCwd, env, socketServer]);
|
|
2832
|
+
useHotReload(pm.manager, cliArgs, baseCwd, pm.pushLog);
|
|
2833
|
+
useLogsPause(pm.setPaused, kb.logsPaused, kb.logsScrollOffset);
|
|
2834
|
+
const activeTip = useContextualTips(pm.logs.length, !!kb.searchTerm, !!kb.logFilter, pm.states);
|
|
2835
|
+
useProxySync(proxyProvider, proxyOpts, pm.states, kb.proxyEnabled);
|
|
2836
|
+
useBootSequence(
|
|
2837
|
+
pm.manager,
|
|
2838
|
+
config,
|
|
2839
|
+
services,
|
|
2840
|
+
cliArgs,
|
|
2841
|
+
platform,
|
|
2842
|
+
env,
|
|
2843
|
+
baseCwd,
|
|
2844
|
+
{ lazyProxies, externals },
|
|
2845
|
+
pm.pushLog
|
|
2846
|
+
);
|
|
2331
2847
|
const handleFilterSelect = useCallback3((name) => kb.setFilter(name), [kb]);
|
|
2332
2848
|
const handleRestartSelect = useCallback3((name) => {
|
|
2333
2849
|
pm.restart(name);
|
|
@@ -2374,7 +2890,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2374
2890
|
focused: kb.panel === "logs",
|
|
2375
2891
|
scrollOffset: kb.logsScrollOffset,
|
|
2376
2892
|
resetScroll: kb.resetLogsScroll,
|
|
2377
|
-
levelFilter: kb.levelFilter
|
|
2893
|
+
levelFilter: kb.levelFilter,
|
|
2894
|
+
filteredColorIdx: kb.logFilter ? pm.states.get(kb.logFilter)?.colorIdx ?? null : null
|
|
2378
2895
|
}
|
|
2379
2896
|
),
|
|
2380
2897
|
/* @__PURE__ */ jsx6(
|
|
@@ -2400,23 +2917,23 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2400
2917
|
}
|
|
2401
2918
|
|
|
2402
2919
|
// src/process/log-sink.ts
|
|
2403
|
-
import { existsSync as
|
|
2404
|
-
import { join as
|
|
2405
|
-
import { homedir as
|
|
2920
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, renameSync, createWriteStream } from "fs";
|
|
2921
|
+
import { join as join8, dirname as dirname6 } from "path";
|
|
2922
|
+
import { homedir as homedir3 } from "os";
|
|
2406
2923
|
var LogSink = class {
|
|
2407
2924
|
dir;
|
|
2408
2925
|
rotateOnStart;
|
|
2409
2926
|
streams = /* @__PURE__ */ new Map();
|
|
2410
2927
|
seen = /* @__PURE__ */ new Set();
|
|
2411
2928
|
constructor(opts) {
|
|
2412
|
-
const root = opts.rootDir ??
|
|
2413
|
-
this.dir =
|
|
2929
|
+
const root = opts.rootDir ?? join8(homedir3(), ".devup", "logs");
|
|
2930
|
+
this.dir = join8(root, sanitize2(opts.projectName));
|
|
2414
2931
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
2415
|
-
|
|
2932
|
+
mkdirSync5(this.dir, { recursive: true });
|
|
2416
2933
|
}
|
|
2417
2934
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
2418
2935
|
pathFor(svcName) {
|
|
2419
|
-
return
|
|
2936
|
+
return join8(this.dir, `${sanitize2(svcName)}.log`);
|
|
2420
2937
|
}
|
|
2421
2938
|
write(svcName, line) {
|
|
2422
2939
|
const stream = this.streamFor(svcName);
|
|
@@ -2435,9 +2952,9 @@ var LogSink = class {
|
|
|
2435
2952
|
let s = this.streams.get(svcName);
|
|
2436
2953
|
if (s) return s;
|
|
2437
2954
|
const file = this.pathFor(svcName);
|
|
2438
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2955
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync13(file)) {
|
|
2439
2956
|
try {
|
|
2440
|
-
|
|
2957
|
+
mkdirSync5(dirname6(file), { recursive: true });
|
|
2441
2958
|
renameSync(file, file + ".prev");
|
|
2442
2959
|
} catch {
|
|
2443
2960
|
}
|
|
@@ -2623,9 +3140,9 @@ function defineConfig(config) {
|
|
|
2623
3140
|
// src/index.ts
|
|
2624
3141
|
function readVersion() {
|
|
2625
3142
|
try {
|
|
2626
|
-
const here =
|
|
2627
|
-
const pkgPath =
|
|
2628
|
-
return JSON.parse(
|
|
3143
|
+
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
3144
|
+
const pkgPath = join9(here, "..", "package.json");
|
|
3145
|
+
return JSON.parse(readFileSync3(pkgPath, "utf8")).version ?? "unknown";
|
|
2629
3146
|
} catch {
|
|
2630
3147
|
return "unknown";
|
|
2631
3148
|
}
|
|
@@ -2675,6 +3192,11 @@ async function main() {
|
|
|
2675
3192
|
${formatValidationErrors(errors)}`);
|
|
2676
3193
|
process.exit(1);
|
|
2677
3194
|
}
|
|
3195
|
+
const warnings = collectWarnings(config);
|
|
3196
|
+
if (warnings.length) {
|
|
3197
|
+
console.warn(`\u26A0 Config warnings:
|
|
3198
|
+
${formatValidationWarnings(warnings)}`);
|
|
3199
|
+
}
|
|
2678
3200
|
let services;
|
|
2679
3201
|
try {
|
|
2680
3202
|
services = filterServices(config.services, cliArgs, config);
|
|
@@ -2687,7 +3209,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2687
3209
|
process.exit(1);
|
|
2688
3210
|
}
|
|
2689
3211
|
const platform = await detectPlatform();
|
|
2690
|
-
const envFile = config.envFile ?
|
|
3212
|
+
const envFile = config.envFile ? join9(cwd, config.envFile) : join9(cwd, ".env");
|
|
2691
3213
|
const env = parseEnvFile(envFile, process.env);
|
|
2692
3214
|
if (config.env) {
|
|
2693
3215
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -2704,7 +3226,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2704
3226
|
routes: config.proxy.routes,
|
|
2705
3227
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
2706
3228
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
2707
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
3229
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join9(homedir4(), ".traefik", "traefik_conf.yaml")
|
|
2708
3230
|
};
|
|
2709
3231
|
}
|
|
2710
3232
|
if (cliArgs.dryRun) {
|