@gachlab/devup 0.7.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 +12 -0
- package/dist/index.js +1050 -856
- 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/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,8 +3,8 @@
|
|
|
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 dirname7, join 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
9
|
import { homedir as homedir4 } from "os";
|
|
10
10
|
|
|
@@ -430,7 +430,7 @@ function filterServices(services, args, config) {
|
|
|
430
430
|
|
|
431
431
|
// src/orchestrator/subcommands.ts
|
|
432
432
|
import { spawn } from "child_process";
|
|
433
|
-
import { createReadStream, watchFile, unwatchFile, existsSync as
|
|
433
|
+
import { createReadStream, watchFile, unwatchFile, existsSync as existsSync5, statSync } from "fs";
|
|
434
434
|
import { readFile as readFile2 } from "fs/promises";
|
|
435
435
|
import { join as join3, dirname } from "path";
|
|
436
436
|
import { fileURLToPath } from "url";
|
|
@@ -512,10 +512,8 @@ function deriveHealth(isUp, currentStatus) {
|
|
|
512
512
|
return currentStatus === "starting" ? "wait" : "down";
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
// src/utils.ts
|
|
516
|
-
import { existsSync as existsSync3, readFileSync
|
|
517
|
-
import { createHash } from "crypto";
|
|
518
|
-
import { join as join2 } from "path";
|
|
515
|
+
// src/utils/env.ts
|
|
516
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
519
517
|
function parseEnvFile(filePath, baseEnv = {}) {
|
|
520
518
|
const env = { ...baseEnv };
|
|
521
519
|
if (!existsSync3(filePath)) return env;
|
|
@@ -533,25 +531,21 @@ function parseEnvFile(filePath, baseEnv = {}) {
|
|
|
533
531
|
}
|
|
534
532
|
return env;
|
|
535
533
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
return
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
return
|
|
548
|
-
}
|
|
549
|
-
function detectLogLevel(line) {
|
|
550
|
-
const l = line.toLowerCase();
|
|
551
|
-
if (/\b(?:error|err|fail(?:ed|ure|ures|s)?|fatal|exception|crash(?:ed|es)?)\b/.test(l) || /❌|✗|⛔/.test(line)) return "error";
|
|
552
|
-
if (/\b(?:warn(?:ed|ing|s|ings)?|deprec)\b/.test(l) || /⚠/.test(line)) return "warn";
|
|
553
|
-
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`;
|
|
554
546
|
}
|
|
547
|
+
|
|
548
|
+
// src/utils/search.ts
|
|
555
549
|
function compileSearchPattern(term) {
|
|
556
550
|
if (!term) return null;
|
|
557
551
|
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(term);
|
|
@@ -568,51 +562,47 @@ function compileSearchPattern(term) {
|
|
|
568
562
|
const lower = term.toLowerCase();
|
|
569
563
|
return { test: (l) => l.toLowerCase().includes(lower) };
|
|
570
564
|
}
|
|
571
|
-
function
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (s
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
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;
|
|
581
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";
|
|
582
586
|
function needsInstall(fullCwd) {
|
|
583
587
|
const nm = join2(fullCwd, "node_modules");
|
|
584
|
-
if (!
|
|
588
|
+
if (!existsSync4(nm)) return true;
|
|
585
589
|
try {
|
|
586
|
-
const pkgHash = createHash("md5").update(
|
|
590
|
+
const pkgHash = createHash("md5").update(readFileSync2(join2(fullCwd, "package.json"))).digest("hex");
|
|
587
591
|
const stampFile = join2(nm, ".install-stamp");
|
|
588
|
-
if (
|
|
592
|
+
if (existsSync4(stampFile) && readFileSync2(stampFile, "utf8") === pkgHash) return false;
|
|
589
593
|
} catch {
|
|
590
594
|
}
|
|
591
595
|
return true;
|
|
592
596
|
}
|
|
593
597
|
function writeInstallStamp(fullCwd) {
|
|
594
598
|
try {
|
|
595
|
-
const pkgHash = createHash("md5").update(
|
|
599
|
+
const pkgHash = createHash("md5").update(readFileSync2(join2(fullCwd, "package.json"))).digest("hex");
|
|
596
600
|
writeFileSync(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
597
601
|
} catch {
|
|
598
602
|
}
|
|
599
603
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
return names.slice().sort((a, b) => {
|
|
603
|
-
if (sortMode === "mem") {
|
|
604
|
-
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
605
|
-
}
|
|
606
|
-
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
function groupByPhase(services) {
|
|
610
|
-
const phases = {};
|
|
611
|
-
for (const s of services) {
|
|
612
|
-
(phases[s.phase] ??= []).push(s);
|
|
613
|
-
}
|
|
614
|
-
return phases;
|
|
615
|
-
}
|
|
604
|
+
|
|
605
|
+
// src/utils/process-args.ts
|
|
616
606
|
function buildProcessArgs(svc) {
|
|
617
607
|
const extra = svc.nodeArgs ?? [];
|
|
618
608
|
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
@@ -630,11 +620,38 @@ function buildProcessEnv(svc, baseEnv) {
|
|
|
630
620
|
}
|
|
631
621
|
return env;
|
|
632
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
|
+
}
|
|
633
634
|
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
634
635
|
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
635
636
|
const cpuDelta = totalCpuSec - prevCpu;
|
|
636
637
|
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
637
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
|
|
638
655
|
var tagColors = [
|
|
639
656
|
"cyan",
|
|
640
657
|
"yellow",
|
|
@@ -678,7 +695,7 @@ async function runLogs(argv, opts) {
|
|
|
678
695
|
return 1;
|
|
679
696
|
}
|
|
680
697
|
const file = join3(logRoot(opts.config, opts.logDir), `${sanitize(svcArg)}.log`);
|
|
681
|
-
if (!
|
|
698
|
+
if (!existsSync5(file)) {
|
|
682
699
|
out(`No log file yet for "${svcArg}" (${file})`);
|
|
683
700
|
return follow ? await followFile(file, out) : 1;
|
|
684
701
|
}
|
|
@@ -696,7 +713,7 @@ async function streamFile(file, out) {
|
|
|
696
713
|
}
|
|
697
714
|
async function followFile(file, out, startAt = 0) {
|
|
698
715
|
let pos = startAt;
|
|
699
|
-
while (!
|
|
716
|
+
while (!existsSync5(file)) await new Promise((r) => setTimeout(r, 500));
|
|
700
717
|
return new Promise((resolve4) => {
|
|
701
718
|
const tick = async () => {
|
|
702
719
|
const size = statSync(file).size;
|
|
@@ -758,7 +775,7 @@ ${items.length} services up to date`);
|
|
|
758
775
|
return 0;
|
|
759
776
|
}
|
|
760
777
|
function installOne(cwd, env) {
|
|
761
|
-
if (!
|
|
778
|
+
if (!existsSync5(cwd)) return Promise.resolve(false);
|
|
762
779
|
if (!needsInstall(cwd)) return Promise.resolve(true);
|
|
763
780
|
return new Promise((resolve4) => {
|
|
764
781
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
@@ -837,7 +854,7 @@ async function detectPlatform() {
|
|
|
837
854
|
}
|
|
838
855
|
|
|
839
856
|
// src/proxy-config/traefik.ts
|
|
840
|
-
import { existsSync as
|
|
857
|
+
import { existsSync as existsSync6, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
841
858
|
import { dirname as dirname2 } from "path";
|
|
842
859
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
843
860
|
var TraefikProvider = class {
|
|
@@ -876,7 +893,7 @@ ${svcs.join("\n")}
|
|
|
876
893
|
}
|
|
877
894
|
write(content, opts) {
|
|
878
895
|
const dir = dirname2(opts.confPath);
|
|
879
|
-
if (!
|
|
896
|
+
if (!existsSync6(dir)) mkdirSync(dir, { recursive: true });
|
|
880
897
|
writeFileSync2(opts.confPath, content);
|
|
881
898
|
}
|
|
882
899
|
clear(opts) {
|
|
@@ -885,7 +902,7 @@ ${svcs.join("\n")}
|
|
|
885
902
|
};
|
|
886
903
|
|
|
887
904
|
// src/proxy-config/nginx.ts
|
|
888
|
-
import { existsSync as
|
|
905
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
889
906
|
import { dirname as dirname3 } from "path";
|
|
890
907
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
891
908
|
var NginxProvider = class {
|
|
@@ -924,7 +941,7 @@ var NginxProvider = class {
|
|
|
924
941
|
}
|
|
925
942
|
write(content, opts) {
|
|
926
943
|
const dir = dirname3(opts.confPath);
|
|
927
|
-
if (!
|
|
944
|
+
if (!existsSync7(dir)) mkdirSync2(dir, { recursive: true });
|
|
928
945
|
writeFileSync3(opts.confPath, content);
|
|
929
946
|
}
|
|
930
947
|
clear(opts) {
|
|
@@ -933,7 +950,7 @@ var NginxProvider = class {
|
|
|
933
950
|
};
|
|
934
951
|
|
|
935
952
|
// src/proxy-config/caddy.ts
|
|
936
|
-
import { existsSync as
|
|
953
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
937
954
|
import { dirname as dirname4 } from "path";
|
|
938
955
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
939
956
|
var CaddyProvider = class {
|
|
@@ -958,7 +975,7 @@ var CaddyProvider = class {
|
|
|
958
975
|
}
|
|
959
976
|
write(content, opts) {
|
|
960
977
|
const dir = dirname4(opts.confPath);
|
|
961
|
-
if (!
|
|
978
|
+
if (!existsSync8(dir)) mkdirSync3(dir, { recursive: true });
|
|
962
979
|
writeFileSync4(opts.confPath, content);
|
|
963
980
|
}
|
|
964
981
|
clear(opts) {
|
|
@@ -982,22 +999,20 @@ function detectProxyProvider(name) {
|
|
|
982
999
|
}
|
|
983
1000
|
|
|
984
1001
|
// src/tui/App.tsx
|
|
985
|
-
import {
|
|
986
|
-
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";
|
|
987
1004
|
|
|
988
1005
|
// src/tui/hooks/useProcessManager.ts
|
|
989
1006
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
990
1007
|
|
|
991
1008
|
// src/process/manager.ts
|
|
992
|
-
import {
|
|
993
|
-
import { existsSync as existsSync9 } from "fs";
|
|
994
|
-
import { join as join4, resolve as resolve3 } from "path";
|
|
1009
|
+
import { join as join5 } from "path";
|
|
995
1010
|
|
|
996
1011
|
// src/process/installer.ts
|
|
997
1012
|
import { spawn as spawn2 } from "child_process";
|
|
998
|
-
import { existsSync as
|
|
1013
|
+
import { existsSync as existsSync9 } from "fs";
|
|
999
1014
|
function installService(cwd, env, onLog) {
|
|
1000
|
-
if (!
|
|
1015
|
+
if (!existsSync9(cwd)) {
|
|
1001
1016
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
1002
1017
|
return Promise.resolve(false);
|
|
1003
1018
|
}
|
|
@@ -1030,9 +1045,32 @@ function installService(cwd, env, onLog) {
|
|
|
1030
1045
|
});
|
|
1031
1046
|
}
|
|
1032
1047
|
|
|
1033
|
-
// src/process/
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
+
}
|
|
1036
1074
|
function compileReadyPattern(pattern) {
|
|
1037
1075
|
if (!pattern) return null;
|
|
1038
1076
|
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
|
|
@@ -1063,43 +1101,26 @@ function extractWatchPaths(args) {
|
|
|
1063
1101
|
}
|
|
1064
1102
|
return out;
|
|
1065
1103
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
let idx;
|
|
1072
|
-
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
1073
|
-
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
1074
|
-
buf = buf.slice(idx + 1);
|
|
1075
|
-
if (line.length) onLine(line);
|
|
1076
|
-
}
|
|
1077
|
-
},
|
|
1078
|
-
flush() {
|
|
1079
|
-
if (buf.length) {
|
|
1080
|
-
onLine(buf);
|
|
1081
|
-
buf = "";
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
var ProcessManager = class {
|
|
1087
|
-
state = /* @__PURE__ */ new Map();
|
|
1088
|
-
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 {
|
|
1089
1109
|
baseCwd;
|
|
1090
1110
|
env;
|
|
1091
|
-
|
|
1111
|
+
state;
|
|
1112
|
+
procs;
|
|
1092
1113
|
events;
|
|
1114
|
+
lifecycle;
|
|
1115
|
+
onCrash;
|
|
1093
1116
|
constructor(opts) {
|
|
1094
1117
|
this.baseCwd = opts.baseCwd;
|
|
1095
1118
|
this.env = opts.env;
|
|
1096
|
-
this.
|
|
1119
|
+
this.state = opts.state;
|
|
1120
|
+
this.procs = opts.procs;
|
|
1097
1121
|
this.events = opts.events;
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1101
|
-
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
1102
|
-
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
1122
|
+
this.lifecycle = opts.lifecycle;
|
|
1123
|
+
this.onCrash = opts.onCrash;
|
|
1103
1124
|
}
|
|
1104
1125
|
async start(svc, colorIdx, isRestart = false) {
|
|
1105
1126
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
@@ -1118,7 +1139,7 @@ var ProcessManager = class {
|
|
|
1118
1139
|
}
|
|
1119
1140
|
}
|
|
1120
1141
|
const args = buildProcessArgs(svc);
|
|
1121
|
-
const missingWatchPaths = extractWatchPaths(args).filter((p) => !
|
|
1142
|
+
const missingWatchPaths = extractWatchPaths(args).filter((p) => !existsSync10(resolve3(cwd, p)));
|
|
1122
1143
|
if (missingWatchPaths.length) {
|
|
1123
1144
|
this.log(svc.name, `\u26A0 missing watch paths: ${missingWatchPaths.join(", ")}`, colorIdx);
|
|
1124
1145
|
this.recordCrashedState(svc, colorIdx);
|
|
@@ -1142,6 +1163,14 @@ var ProcessManager = class {
|
|
|
1142
1163
|
this.state.set(svc.name, state);
|
|
1143
1164
|
this.procs.add(proc);
|
|
1144
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) {
|
|
1145
1174
|
const readyRegex = compileReadyPattern(svc.readyPattern);
|
|
1146
1175
|
const markReadyIfMatch = (line) => {
|
|
1147
1176
|
if (!readyRegex || state.health === "up") return;
|
|
@@ -1166,9 +1195,11 @@ var ProcessManager = class {
|
|
|
1166
1195
|
proc.stderr?.on("data", (d) => stderrBuf.push(d));
|
|
1167
1196
|
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
1168
1197
|
proc.stderr?.on("end", () => stderrBuf.flush());
|
|
1198
|
+
}
|
|
1199
|
+
wireCloseHandler(proc, svc, state, colorIdx) {
|
|
1169
1200
|
proc.on("close", (code) => {
|
|
1170
1201
|
this.procs.delete(proc);
|
|
1171
|
-
this.stopWatchProc(state);
|
|
1202
|
+
this.lifecycle.stopWatchProc(state);
|
|
1172
1203
|
if (state.intentionalStop) {
|
|
1173
1204
|
state.intentionalStop = false;
|
|
1174
1205
|
return;
|
|
@@ -1183,23 +1214,12 @@ var ProcessManager = class {
|
|
|
1183
1214
|
state.health = "down";
|
|
1184
1215
|
this.log(svc.name, `\u274C exited with code ${code}`, colorIdx);
|
|
1185
1216
|
this.events.onStateChange(svc.name, state);
|
|
1186
|
-
|
|
1187
|
-
state.restarts++;
|
|
1188
|
-
const delay = BACKOFF_BASE_MS * Math.pow(2, state.restarts - 1);
|
|
1189
|
-
this.log(svc.name, `\u{1F504} auto-restart ${state.restarts}/${MAX_RESTARTS} in ${delay}ms...`, colorIdx);
|
|
1190
|
-
setTimeout(() => this.start(svc, colorIdx, true), delay);
|
|
1191
|
-
} else {
|
|
1192
|
-
this.log(svc.name, "\u26D4 max restarts reached", colorIdx);
|
|
1193
|
-
}
|
|
1217
|
+
this.onCrash(svc, state, colorIdx);
|
|
1194
1218
|
});
|
|
1195
|
-
if (svc.watchBuild) {
|
|
1196
|
-
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
1197
|
-
}
|
|
1198
|
-
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
1199
1219
|
}
|
|
1200
1220
|
runPreBuild(svc, cwd, colorIdx) {
|
|
1201
1221
|
this.log(svc.name, `\u{1F528} preBuild: ${svc.preBuild}`, colorIdx);
|
|
1202
|
-
return new Promise((
|
|
1222
|
+
return new Promise((res) => {
|
|
1203
1223
|
const isWin = process.platform === "win32";
|
|
1204
1224
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1205
1225
|
const shellFlag = isWin ? "/c" : "-c";
|
|
@@ -1211,17 +1231,17 @@ var ProcessManager = class {
|
|
|
1211
1231
|
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
1212
1232
|
child.on("error", (err) => {
|
|
1213
1233
|
this.log(svc.name, `[build] \u274C ${err.message}`, colorIdx);
|
|
1214
|
-
|
|
1234
|
+
res(false);
|
|
1215
1235
|
});
|
|
1216
1236
|
child.on("close", (code) => {
|
|
1217
1237
|
outBuf.flush();
|
|
1218
1238
|
errBuf.flush();
|
|
1219
1239
|
if (code === 0) {
|
|
1220
1240
|
this.log(svc.name, `[build] \u2705 done`, colorIdx);
|
|
1221
|
-
|
|
1241
|
+
res(true);
|
|
1222
1242
|
} else {
|
|
1223
1243
|
this.log(svc.name, `[build] \u274C exited with code ${code}`, colorIdx);
|
|
1224
|
-
|
|
1244
|
+
res(false);
|
|
1225
1245
|
}
|
|
1226
1246
|
});
|
|
1227
1247
|
});
|
|
@@ -1244,7 +1264,8 @@ var ProcessManager = class {
|
|
|
1244
1264
|
child.on("error", (err) => this.log(svc.name, `[watch] \u274C ${err.message}`, colorIdx));
|
|
1245
1265
|
return child;
|
|
1246
1266
|
}
|
|
1247
|
-
/** 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). */
|
|
1248
1269
|
recordCrashedState(svc, colorIdx) {
|
|
1249
1270
|
const prev = this.state.get(svc.name);
|
|
1250
1271
|
this.state.set(svc.name, {
|
|
@@ -1261,33 +1282,54 @@ var ProcessManager = class {
|
|
|
1261
1282
|
});
|
|
1262
1283
|
this.events.onStateChange(svc.name, this.state.get(svc.name));
|
|
1263
1284
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (!st?.proc || !st.pid) return;
|
|
1267
|
-
st.intentionalStop = true;
|
|
1268
|
-
this.platform.killTree(st.pid);
|
|
1269
|
-
this.stopWatchProc(st);
|
|
1285
|
+
log(name, text, colorIdx) {
|
|
1286
|
+
this.events.onLog(name, text, colorIdx);
|
|
1270
1287
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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;
|
|
1279
1301
|
}
|
|
1280
1302
|
async restart(name) {
|
|
1281
1303
|
const st = this.state.get(name);
|
|
1282
1304
|
if (!st) return;
|
|
1283
|
-
this.stop(name);
|
|
1305
|
+
this.lifecycle.stop(name);
|
|
1284
1306
|
st.restarts = 0;
|
|
1285
1307
|
const delay = st.proc ? 1500 : 100;
|
|
1286
1308
|
await new Promise((r) => setTimeout(r, delay));
|
|
1287
|
-
await this.start(st.svc, st.colorIdx, true);
|
|
1288
|
-
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;
|
|
1289
1331
|
}
|
|
1290
|
-
async
|
|
1332
|
+
async checkAll() {
|
|
1291
1333
|
for (const [name, st] of this.state) {
|
|
1292
1334
|
if (!st.pid || st.status === "idle") {
|
|
1293
1335
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
@@ -1304,6 +1346,41 @@ var ProcessManager = class {
|
|
|
1304
1346
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
1305
1347
|
}
|
|
1306
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. */
|
|
1307
1384
|
async cleanup(opts = {}) {
|
|
1308
1385
|
const grace = opts.gracePeriodMs ?? 3e3;
|
|
1309
1386
|
const procs = [...this.procs];
|
|
@@ -1344,8 +1421,69 @@ var ProcessManager = class {
|
|
|
1344
1421
|
for (const st of this.state.values()) if (st.proc === proc) return st;
|
|
1345
1422
|
return void 0;
|
|
1346
1423
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1424
|
+
};
|
|
1425
|
+
|
|
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);
|
|
1349
1487
|
}
|
|
1350
1488
|
};
|
|
1351
1489
|
|
|
@@ -1574,73 +1712,352 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1574
1712
|
}, [provider, opts, enabled]);
|
|
1575
1713
|
}
|
|
1576
1714
|
|
|
1577
|
-
// src/tui/
|
|
1578
|
-
import { useEffect as useEffect3,
|
|
1579
|
-
import {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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);
|
|
1721
|
+
useEffect3(() => {
|
|
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
|
+
}
|
|
1585
1765
|
}
|
|
1586
|
-
|
|
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
|
+
};
|
|
1587
1792
|
}
|
|
1588
|
-
function
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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;
|
|
1637
2054
|
}
|
|
1638
2055
|
|
|
1639
2056
|
// src/tui/StatsPanel.tsx
|
|
1640
|
-
import { useEffect as
|
|
1641
|
-
import { Box
|
|
2057
|
+
import { useEffect as useEffect7, useState as useState4 } from "react";
|
|
2058
|
+
import { Box, Text } from "ink";
|
|
1642
2059
|
import os from "os";
|
|
1643
|
-
import { jsx
|
|
2060
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1644
2061
|
var H = {
|
|
1645
2062
|
up: { c: "\u25CF", color: "green" },
|
|
1646
2063
|
wait: { c: "\u25CF", color: "yellow" },
|
|
@@ -1653,20 +2070,20 @@ function isCrashLooped(st) {
|
|
|
1653
2070
|
}
|
|
1654
2071
|
function Row({ name, st, stat, ml, verbose }) {
|
|
1655
2072
|
const looped = isCrashLooped(st);
|
|
1656
|
-
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 });
|
|
1657
2074
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1658
2075
|
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1659
2076
|
const statusLabel = looped ? "looping" : st.status;
|
|
1660
2077
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1661
2078
|
if (!verbose) {
|
|
1662
|
-
return /* @__PURE__ */
|
|
2079
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
1663
2080
|
indicator,
|
|
1664
2081
|
" ",
|
|
1665
|
-
/* @__PURE__ */
|
|
2082
|
+
/* @__PURE__ */ jsx(Text, { color, children: name.padEnd(ml) }),
|
|
1666
2083
|
" ",
|
|
1667
2084
|
String(st.svc.port).padStart(5),
|
|
1668
2085
|
" ",
|
|
1669
|
-
/* @__PURE__ */
|
|
2086
|
+
/* @__PURE__ */ jsx(Text, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1670
2087
|
" ",
|
|
1671
2088
|
(stat?.cpu ?? "-").padStart(6),
|
|
1672
2089
|
" ",
|
|
@@ -1682,15 +2099,15 @@ function Row({ name, st, stat, ml, verbose }) {
|
|
|
1682
2099
|
const resolvedArgs = buildProcessArgs(st.svc).join(" ");
|
|
1683
2100
|
const env = redactSecrets(st.svc.extraEnv);
|
|
1684
2101
|
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1685
|
-
return /* @__PURE__ */
|
|
1686
|
-
/* @__PURE__ */
|
|
2102
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2103
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1687
2104
|
indicator,
|
|
1688
2105
|
" ",
|
|
1689
|
-
/* @__PURE__ */
|
|
2106
|
+
/* @__PURE__ */ jsx(Text, { color, children: name.padEnd(ml) }),
|
|
1690
2107
|
" ",
|
|
1691
2108
|
String(st.svc.port).padStart(5),
|
|
1692
2109
|
" ",
|
|
1693
|
-
/* @__PURE__ */
|
|
2110
|
+
/* @__PURE__ */ jsx(Text, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1694
2111
|
" ",
|
|
1695
2112
|
(stat?.cpu ?? "-").padStart(6),
|
|
1696
2113
|
" ",
|
|
@@ -1702,20 +2119,20 @@ function Row({ name, st, stat, ml, verbose }) {
|
|
|
1702
2119
|
" ",
|
|
1703
2120
|
up.padStart(6)
|
|
1704
2121
|
] }),
|
|
1705
|
-
/* @__PURE__ */
|
|
2122
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1706
2123
|
" cmd: ",
|
|
1707
2124
|
st.svc.cmd,
|
|
1708
2125
|
" ",
|
|
1709
2126
|
resolvedArgs
|
|
1710
2127
|
] }),
|
|
1711
|
-
envStr && /* @__PURE__ */
|
|
2128
|
+
envStr && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1712
2129
|
" env: ",
|
|
1713
2130
|
envStr
|
|
1714
2131
|
] })
|
|
1715
2132
|
] });
|
|
1716
2133
|
}
|
|
1717
2134
|
function ColHeader({ ml }) {
|
|
1718
|
-
return /* @__PURE__ */
|
|
2135
|
+
return /* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
1719
2136
|
"H ",
|
|
1720
2137
|
"Service".padEnd(ml),
|
|
1721
2138
|
" ",
|
|
@@ -1762,7 +2179,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1762
2179
|
const webStartIndex = Math.min(effectiveOffset, Math.max(0, webs.length - rowsPerCol));
|
|
1763
2180
|
const visibleApis = apis.slice(apiStartIndex, apiStartIndex + rowsPerCol);
|
|
1764
2181
|
const visibleWebs = webs.slice(webStartIndex, webStartIndex + rowsPerCol);
|
|
1765
|
-
|
|
2182
|
+
useEffect7(() => {
|
|
1766
2183
|
resetScroll();
|
|
1767
2184
|
}, [sortMode, resetScroll]);
|
|
1768
2185
|
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
@@ -1770,24 +2187,24 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1770
2187
|
const scrolled = effectiveOffset > 0;
|
|
1771
2188
|
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1772
2189
|
const ramPct = parseFloat(usedGB) / parseFloat(totalGB) * 100;
|
|
1773
|
-
const [ramBanner, setRamBanner] =
|
|
1774
|
-
|
|
2190
|
+
const [ramBanner, setRamBanner] = useState4(false);
|
|
2191
|
+
useEffect7(() => {
|
|
1775
2192
|
setRamBanner((prev) => nextRamBannerVisibility(ramPct, prev));
|
|
1776
2193
|
}, [ramPct]);
|
|
1777
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) : [];
|
|
1778
|
-
return /* @__PURE__ */
|
|
1779
|
-
/* @__PURE__ */
|
|
1780
|
-
/* @__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: [
|
|
1781
2198
|
" Stats ",
|
|
1782
2199
|
positionInfo
|
|
1783
2200
|
] }),
|
|
1784
|
-
scrolled && /* @__PURE__ */
|
|
1785
|
-
loopedCount > 0 && /* @__PURE__ */
|
|
2201
|
+
scrolled && /* @__PURE__ */ jsx(Text, { color: "yellow", children: " [SCROLL]" }),
|
|
2202
|
+
loopedCount > 0 && /* @__PURE__ */ jsxs(Text, { color: "red", bold: true, children: [
|
|
1786
2203
|
" \u26A0 ",
|
|
1787
2204
|
loopedCount,
|
|
1788
2205
|
" need attention"
|
|
1789
2206
|
] }),
|
|
1790
|
-
/* @__PURE__ */
|
|
2207
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1791
2208
|
" System: ",
|
|
1792
2209
|
cpus,
|
|
1793
2210
|
"c Load ",
|
|
@@ -1798,8 +2215,8 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1798
2215
|
totalGB,
|
|
1799
2216
|
"GB"
|
|
1800
2217
|
] }),
|
|
1801
|
-
/* @__PURE__ */
|
|
1802
|
-
/* @__PURE__ */
|
|
2218
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
|
|
2219
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1803
2220
|
"Stack: CPU ",
|
|
1804
2221
|
totalCpu.toFixed(1),
|
|
1805
2222
|
"% RAM ",
|
|
@@ -1811,171 +2228,68 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1811
2228
|
" Svcs ",
|
|
1812
2229
|
names.length
|
|
1813
2230
|
] }),
|
|
1814
|
-
sortMode !== "name" && /* @__PURE__ */
|
|
2231
|
+
sortMode !== "name" && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1815
2232
|
" \u2502 Sort: ",
|
|
1816
2233
|
sortMode
|
|
1817
2234
|
] })
|
|
1818
2235
|
] }),
|
|
1819
|
-
ramBanner && /* @__PURE__ */
|
|
1820
|
-
/* @__PURE__ */
|
|
2236
|
+
ramBanner && /* @__PURE__ */ jsxs(Box, { children: [
|
|
2237
|
+
/* @__PURE__ */ jsxs(Text, { color: "yellow", bold: true, children: [
|
|
1821
2238
|
" \u26A0 RAM ",
|
|
1822
2239
|
ramPct.toFixed(0),
|
|
1823
2240
|
"% \u2014 top: "
|
|
1824
2241
|
] }),
|
|
1825
|
-
/* @__PURE__ */
|
|
2242
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: topConsumers.map((c) => `${c.name} ${c.mb.toFixed(0)}MB`).join(", ") })
|
|
1826
2243
|
] }),
|
|
1827
|
-
/* @__PURE__ */
|
|
1828
|
-
/* @__PURE__ */
|
|
1829
|
-
/* @__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: [
|
|
1830
2247
|
" APIs (",
|
|
1831
2248
|
apis.length,
|
|
1832
2249
|
")"
|
|
1833
|
-
] }),
|
|
1834
|
-
/* @__PURE__ */
|
|
1835
|
-
visibleApis.map((n) => /* @__PURE__ */
|
|
1836
|
-
] }),
|
|
1837
|
-
/* @__PURE__ */
|
|
1838
|
-
/* @__PURE__ */
|
|
1839
|
-
/* @__PURE__ */
|
|
1840
|
-
" Webs (",
|
|
1841
|
-
webs.length,
|
|
1842
|
-
")"
|
|
1843
|
-
] }),
|
|
1844
|
-
/* @__PURE__ */
|
|
1845
|
-
visibleWebs.map((n) => /* @__PURE__ */
|
|
1846
|
-
] })
|
|
1847
|
-
] })
|
|
1848
|
-
] });
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// src/tui/StatusBar.tsx
|
|
1852
|
-
import { Box as Box3, Text as Text3 } from "ink";
|
|
1853
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1854
|
-
function StatusBar() {
|
|
1855
|
-
return /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1856
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "q" }),
|
|
1857
|
-
" Quit ",
|
|
1858
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Tab" }),
|
|
1859
|
-
" Switch ",
|
|
1860
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "\u2191\u2193" }),
|
|
1861
|
-
" Scroll ",
|
|
1862
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "PgUp/PgDn" }),
|
|
1863
|
-
" Page ",
|
|
1864
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "Ctrl+A/E" }),
|
|
1865
|
-
" Home/End ",
|
|
1866
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "c" }),
|
|
1867
|
-
" Clear ",
|
|
1868
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
1869
|
-
" Filter ",
|
|
1870
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
1871
|
-
" Level ",
|
|
1872
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
1873
|
-
" All ",
|
|
1874
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
1875
|
-
" Restart ",
|
|
1876
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "/" }),
|
|
1877
|
-
" Search ",
|
|
1878
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "s" }),
|
|
1879
|
-
" Sort ",
|
|
1880
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "o" }),
|
|
1881
|
-
" Open ",
|
|
1882
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "p" }),
|
|
1883
|
-
" Pause ",
|
|
1884
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
1885
|
-
" Time ",
|
|
1886
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
1887
|
-
" Verbose ",
|
|
1888
|
-
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
1889
|
-
" Proxy"
|
|
1890
|
-
] }) });
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
// src/tui/ServiceList.tsx
|
|
1894
|
-
import { useState as useState4, useMemo as useMemo2 } from "react";
|
|
1895
|
-
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1896
|
-
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1897
|
-
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1898
|
-
const allNames = useMemo2(
|
|
1899
|
-
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1900
|
-
[services, filterType]
|
|
1901
|
-
);
|
|
1902
|
-
const [idx, setIdx] = useState4(0);
|
|
1903
|
-
const [query, setQuery] = useState4("");
|
|
1904
|
-
const names = useMemo2(() => {
|
|
1905
|
-
if (!query) return allNames;
|
|
1906
|
-
const q = query.toLowerCase();
|
|
1907
|
-
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
1908
|
-
}, [allNames, query]);
|
|
1909
|
-
const clamped = Math.min(idx, Math.max(0, names.length - 1));
|
|
1910
|
-
useInput2((input, key) => {
|
|
1911
|
-
if (key.escape) {
|
|
1912
|
-
if (query) setQuery("");
|
|
1913
|
-
else onClose();
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
if (key.return) {
|
|
1917
|
-
if (names[clamped]) onSelect(names[clamped]);
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
if (key.upArrow) {
|
|
1921
|
-
setIdx((i) => Math.max(0, i - 1));
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
if (key.downArrow) {
|
|
1925
|
-
setIdx((i) => Math.min(names.length - 1, i + 1));
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
if (key.backspace || key.delete) {
|
|
1929
|
-
setQuery((q) => q.slice(0, -1));
|
|
1930
|
-
setIdx(0);
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
1934
|
-
setQuery((q) => q + input);
|
|
1935
|
-
setIdx(0);
|
|
1936
|
-
}
|
|
1937
|
-
}, { isActive: process.stdin.isTTY ?? false });
|
|
1938
|
-
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
1939
|
-
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
1940
|
-
" ",
|
|
1941
|
-
title,
|
|
1942
|
-
" ",
|
|
1943
|
-
query && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
1944
|
-
"[",
|
|
1945
|
-
query,
|
|
1946
|
-
"]"
|
|
2250
|
+
] }),
|
|
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))
|
|
2253
|
+
] }),
|
|
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: [
|
|
2257
|
+
" Webs (",
|
|
2258
|
+
webs.length,
|
|
2259
|
+
")"
|
|
2260
|
+
] }),
|
|
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))
|
|
1947
2263
|
] })
|
|
1948
|
-
] })
|
|
1949
|
-
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: [
|
|
1950
|
-
" ",
|
|
1951
|
-
name,
|
|
1952
|
-
" :",
|
|
1953
|
-
services.get(name).svc.port,
|
|
1954
|
-
" "
|
|
1955
|
-
] }) }, name)),
|
|
1956
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "type to filter \u2191\u2193 navigate Enter select Esc clear/close" })
|
|
2264
|
+
] })
|
|
1957
2265
|
] });
|
|
1958
2266
|
}
|
|
1959
2267
|
|
|
1960
|
-
// src/tui/
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
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);
|
|
2285
|
+
}
|
|
2286
|
+
}, [totalLogs, states, hasSearch, hasFilter, activeTip]);
|
|
2287
|
+
return activeTip;
|
|
1977
2288
|
}
|
|
1978
2289
|
|
|
2290
|
+
// src/tui/hooks/useBootSequence.ts
|
|
2291
|
+
import { useEffect as useEffect9, useState as useState6 } from "react";
|
|
2292
|
+
|
|
1979
2293
|
// src/lazy/proxy.ts
|
|
1980
2294
|
import net2 from "net";
|
|
1981
2295
|
function createLazyProxy(opts) {
|
|
@@ -2066,488 +2380,116 @@ function createLazyProxy(opts) {
|
|
|
2066
2380
|
const conns = pendingConns.splice(0);
|
|
2067
2381
|
if (!ok) {
|
|
2068
2382
|
for (const conn of conns) {
|
|
2069
|
-
if (!conn.destroyed) conn.destroy();
|
|
2070
|
-
}
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
for (const conn of conns) {
|
|
2074
|
-
if (!conn.destroyed) pipeToTarget(conn);
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
const server = net2.createServer({ allowHalfOpen: true }, (socket) => handleConnection(socket));
|
|
2078
|
-
server.listen(listenPort, "0.0.0.0");
|
|
2079
|
-
scheduleIdleCheck();
|
|
2080
|
-
return {
|
|
2081
|
-
server,
|
|
2082
|
-
resetTimer: bumpActivity,
|
|
2083
|
-
destroy: () => {
|
|
2084
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
2085
|
-
pendingConns.forEach((s) => s.destroy());
|
|
2086
|
-
activeConns.forEach((s) => s.destroy());
|
|
2087
|
-
server.close();
|
|
2088
|
-
}
|
|
2089
|
-
};
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// src/process/external.ts
|
|
2093
|
-
import { spawn as spawn4 } from "child_process";
|
|
2094
|
-
import { join as join5 } from "path";
|
|
2095
|
-
var DEFAULT_START_TIMEOUT_S = 60;
|
|
2096
|
-
async function startExternals(externals, opts) {
|
|
2097
|
-
const procs = [];
|
|
2098
|
-
const failed = [];
|
|
2099
|
-
for (const svc of externals) {
|
|
2100
|
-
const proc = spawnExternal(svc, opts);
|
|
2101
|
-
procs.push({ svc, proc, pid: proc.pid ?? null });
|
|
2102
|
-
if (!svc.healthCheck) {
|
|
2103
|
-
opts.onLog?.(svc.name, "\u2705 started (no healthCheck)");
|
|
2104
|
-
continue;
|
|
2105
|
-
}
|
|
2106
|
-
if (svc.healthCheck.type === "tcp" && !svc.port) {
|
|
2107
|
-
opts.onLog?.(svc.name, "\u26A0 tcp healthCheck requires `port` \u2014 skipping wait");
|
|
2108
|
-
continue;
|
|
2109
|
-
}
|
|
2110
|
-
const timeoutMs = (svc.startTimeout ?? DEFAULT_START_TIMEOUT_S) * 1e3;
|
|
2111
|
-
const ok = await waitHealthy(svc, timeoutMs);
|
|
2112
|
-
if (ok) {
|
|
2113
|
-
opts.onLog?.(svc.name, "\u2705 healthy");
|
|
2114
|
-
} else {
|
|
2115
|
-
opts.onLog?.(svc.name, `\u274C never became healthy (timeout ${timeoutMs / 1e3}s)`);
|
|
2116
|
-
failed.push(svc.name);
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
return { procs, allHealthy: failed.length === 0, failed };
|
|
2120
|
-
}
|
|
2121
|
-
async function stopExternals(procs, platform, opts = {}) {
|
|
2122
|
-
for (const { svc, proc, pid } of procs) {
|
|
2123
|
-
try {
|
|
2124
|
-
if (pid) platform.killTree(pid);
|
|
2125
|
-
if (svc.stopCmd) {
|
|
2126
|
-
opts.onLog?.(svc.name, `\u{1F9F9} ${svc.stopCmd}`);
|
|
2127
|
-
await new Promise((resolve4) => {
|
|
2128
|
-
const isWin = process.platform === "win32";
|
|
2129
|
-
const shell = isWin ? "cmd.exe" : "sh";
|
|
2130
|
-
const flag = isWin ? "/c" : "-c";
|
|
2131
|
-
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2132
|
-
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2133
|
-
const child = spawn4(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
2134
|
-
child.on("close", () => resolve4());
|
|
2135
|
-
child.on("error", () => resolve4());
|
|
2136
|
-
setTimeout(() => resolve4(), 1e4);
|
|
2137
|
-
});
|
|
2138
|
-
}
|
|
2139
|
-
} catch {
|
|
2140
|
-
}
|
|
2141
|
-
void proc;
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
function spawnExternal(svc, opts) {
|
|
2145
|
-
const isWin = process.platform === "win32";
|
|
2146
|
-
const shell = isWin ? "cmd.exe" : "sh";
|
|
2147
|
-
const flag = isWin ? "/c" : "-c";
|
|
2148
|
-
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2149
|
-
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2150
|
-
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
2151
|
-
const child = spawn4(shell, [flag, svc.cmd], {
|
|
2152
|
-
cwd,
|
|
2153
|
-
env,
|
|
2154
|
-
detached: true,
|
|
2155
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2156
|
-
});
|
|
2157
|
-
child.stdout?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2158
|
-
child.stderr?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
2159
|
-
child.on("error", (err) => opts.onLog?.(svc.name, `\u274C spawn error: ${err.message}`));
|
|
2160
|
-
return child;
|
|
2161
|
-
}
|
|
2162
|
-
async function waitHealthy(svc, timeoutMs) {
|
|
2163
|
-
const deadline = Date.now() + timeoutMs;
|
|
2164
|
-
const port = svc.port;
|
|
2165
|
-
while (Date.now() < deadline) {
|
|
2166
|
-
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
2167
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
2168
|
-
}
|
|
2169
|
-
return false;
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// src/tui/tips.ts
|
|
2173
|
-
function pickTip(state) {
|
|
2174
|
-
if (state.crashLoopedCount > 0 && !state.shown.has("crashed")) {
|
|
2175
|
-
return { id: "crashed", message: "tip: press r to restart, or check the log of the failing service" };
|
|
2176
|
-
}
|
|
2177
|
-
if (state.totalLogs > 1e3 && !state.hasSearch && !state.shown.has("search")) {
|
|
2178
|
-
return { id: "search", message: "tip: press / to search in logs" };
|
|
2179
|
-
}
|
|
2180
|
-
if (state.totalLogs > 500 && !state.hasFilter && !state.shown.has("filter")) {
|
|
2181
|
-
return { id: "filter", message: "tip: press f to filter logs by service" };
|
|
2182
|
-
}
|
|
2183
|
-
return null;
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
// src/control-plane/socket-server.ts
|
|
2187
|
-
import { createServer } from "net";
|
|
2188
|
-
import { createInterface as createInterface2 } from "readline";
|
|
2189
|
-
import { existsSync as existsSync10, unlinkSync, chmodSync, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
2190
|
-
import { dirname as dirname5 } from "path";
|
|
2191
|
-
import { join as join6 } from "path";
|
|
2192
|
-
import { homedir as homedir2 } from "os";
|
|
2193
|
-
function defaultSocketPath(projectName) {
|
|
2194
|
-
const safe = projectName.replace(/[^a-zA-Z0-9._-]+/g, "_") || "devup";
|
|
2195
|
-
return join6(homedir2(), ".devup", `sock-${safe}.sock`);
|
|
2196
|
-
}
|
|
2197
|
-
async function startSocketServer(projectName, ctx, opts = {}) {
|
|
2198
|
-
const path = opts.path ?? defaultSocketPath(projectName);
|
|
2199
|
-
mkdirSync4(dirname5(path), { recursive: true });
|
|
2200
|
-
if (existsSync10(path)) {
|
|
2201
|
-
try {
|
|
2202
|
-
const st = statSync2(path);
|
|
2203
|
-
if (st.isSocket()) unlinkSync(path);
|
|
2204
|
-
} catch {
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
const server = createServer((socket) => handleClient(socket, ctx));
|
|
2208
|
-
await new Promise((resolve4, reject) => {
|
|
2209
|
-
server.once("error", reject);
|
|
2210
|
-
server.listen(path, () => {
|
|
2211
|
-
server.off("error", reject);
|
|
2212
|
-
try {
|
|
2213
|
-
chmodSync(path, 384);
|
|
2214
|
-
} catch {
|
|
2215
|
-
}
|
|
2216
|
-
opts.onLog?.(`\u{1F50C} control plane at ${path}`);
|
|
2217
|
-
resolve4();
|
|
2218
|
-
});
|
|
2219
|
-
});
|
|
2220
|
-
return {
|
|
2221
|
-
server,
|
|
2222
|
-
path,
|
|
2223
|
-
async close() {
|
|
2224
|
-
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
2225
|
-
if (existsSync10(path)) {
|
|
2226
|
-
try {
|
|
2227
|
-
unlinkSync(path);
|
|
2228
|
-
} catch {
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
};
|
|
2233
|
-
}
|
|
2234
|
-
function handleClient(socket, ctx) {
|
|
2235
|
-
const rl = createInterface2({ input: socket });
|
|
2236
|
-
rl.on("line", async (line) => {
|
|
2237
|
-
if (!line.trim()) return;
|
|
2238
|
-
let req;
|
|
2239
|
-
try {
|
|
2240
|
-
req = JSON.parse(line);
|
|
2241
|
-
} catch (e) {
|
|
2242
|
-
respond(socket, { error: { code: -32700, message: `parse error: ${e.message}` } });
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
|
-
if (typeof req.method !== "string") {
|
|
2246
|
-
respond(socket, { id: req.id, error: { code: -32600, message: "method required" } });
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
try {
|
|
2250
|
-
const result = await dispatch(req.method, req.params ?? {}, ctx);
|
|
2251
|
-
respond(socket, { id: req.id, result });
|
|
2252
|
-
} catch (e) {
|
|
2253
|
-
respond(socket, { id: req.id, error: { code: -32603, message: e.message ?? String(e) } });
|
|
2254
|
-
}
|
|
2255
|
-
});
|
|
2256
|
-
socket.on("error", () => {
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
function respond(socket, payload) {
|
|
2260
|
-
if (socket.writable) socket.write(JSON.stringify(payload) + "\n");
|
|
2261
|
-
}
|
|
2262
|
-
async function dispatch(method, params, ctx) {
|
|
2263
|
-
switch (method) {
|
|
2264
|
-
case "status": {
|
|
2265
|
-
const out = [];
|
|
2266
|
-
for (const [name, st] of ctx.states()) {
|
|
2267
|
-
out.push({
|
|
2268
|
-
name,
|
|
2269
|
-
status: st.status,
|
|
2270
|
-
health: st.health,
|
|
2271
|
-
port: st.svc.port,
|
|
2272
|
-
type: st.svc.type,
|
|
2273
|
-
errors: st.errors,
|
|
2274
|
-
restarts: st.restarts,
|
|
2275
|
-
pid: st.pid,
|
|
2276
|
-
startedAt: st.startedAt
|
|
2277
|
-
});
|
|
2278
|
-
}
|
|
2279
|
-
return { services: out };
|
|
2280
|
-
}
|
|
2281
|
-
case "restart": {
|
|
2282
|
-
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2283
|
-
await ctx.restart(svc);
|
|
2284
|
-
return { ok: true };
|
|
2285
|
-
}
|
|
2286
|
-
case "stop": {
|
|
2287
|
-
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2288
|
-
ctx.stop(svc);
|
|
2289
|
-
return { ok: true };
|
|
2383
|
+
if (!conn.destroyed) conn.destroy();
|
|
2384
|
+
}
|
|
2385
|
+
return;
|
|
2290
2386
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const lines = Math.max(1, Math.min(1e4, Number(params["lines"] ?? 100)));
|
|
2294
|
-
return { lines: await ctx.tailLogs(svc, lines) };
|
|
2387
|
+
for (const conn of conns) {
|
|
2388
|
+
if (!conn.destroyed) pipeToTarget(conn);
|
|
2295
2389
|
}
|
|
2296
|
-
case "ping":
|
|
2297
|
-
return { ok: true, ts: Date.now() };
|
|
2298
|
-
default:
|
|
2299
|
-
throw new Error(`unknown method: ${method}`);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
function stringOrThrow(v, paramName) {
|
|
2303
|
-
if (typeof v !== "string" || !v.trim()) {
|
|
2304
|
-
throw new Error(`param "${paramName}" must be a non-empty string`);
|
|
2305
2390
|
}
|
|
2306
|
-
|
|
2391
|
+
const server = net2.createServer({ allowHalfOpen: true }, (socket) => handleConnection(socket));
|
|
2392
|
+
server.listen(listenPort, "0.0.0.0");
|
|
2393
|
+
scheduleIdleCheck();
|
|
2394
|
+
return {
|
|
2395
|
+
server,
|
|
2396
|
+
resetTimer: bumpActivity,
|
|
2397
|
+
destroy: () => {
|
|
2398
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
2399
|
+
pendingConns.forEach((s) => s.destroy());
|
|
2400
|
+
activeConns.forEach((s) => s.destroy());
|
|
2401
|
+
server.close();
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2307
2404
|
}
|
|
2308
2405
|
|
|
2309
|
-
// src/
|
|
2310
|
-
import {
|
|
2311
|
-
import {
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
"preBuild",
|
|
2322
|
-
"watchBuild",
|
|
2323
|
-
"nodeArgs",
|
|
2324
|
-
"extraEnv",
|
|
2325
|
-
"healthCheck",
|
|
2326
|
-
"readyPattern",
|
|
2327
|
-
"errorPattern",
|
|
2328
|
-
"type"
|
|
2329
|
-
];
|
|
2330
|
-
function hasSpawnRelevantChange(prev, next) {
|
|
2331
|
-
for (const k of SPAWN_RELEVANT) {
|
|
2332
|
-
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) return true;
|
|
2333
|
-
}
|
|
2334
|
-
return false;
|
|
2335
|
-
}
|
|
2336
|
-
function diffServices(prev, next) {
|
|
2337
|
-
const prevByName = new Map(prev.map((s) => [s.name, s]));
|
|
2338
|
-
const nextByName = new Map(next.map((s) => [s.name, s]));
|
|
2339
|
-
const added = [];
|
|
2340
|
-
const removed = [];
|
|
2341
|
-
const changed = [];
|
|
2342
|
-
const unchanged = [];
|
|
2343
|
-
for (const [name, p] of prevByName) {
|
|
2344
|
-
if (!nextByName.has(name)) {
|
|
2345
|
-
removed.push(name);
|
|
2406
|
+
// src/process/external.ts
|
|
2407
|
+
import { spawn as spawn4 } from "child_process";
|
|
2408
|
+
import { join as join7 } from "path";
|
|
2409
|
+
var DEFAULT_START_TIMEOUT_S = 60;
|
|
2410
|
+
async function startExternals(externals, opts) {
|
|
2411
|
+
const procs = [];
|
|
2412
|
+
const failed = [];
|
|
2413
|
+
for (const svc of externals) {
|
|
2414
|
+
const proc = spawnExternal(svc, opts);
|
|
2415
|
+
procs.push({ svc, proc, pid: proc.pid ?? null });
|
|
2416
|
+
if (!svc.healthCheck) {
|
|
2417
|
+
opts.onLog?.(svc.name, "\u2705 started (no healthCheck)");
|
|
2346
2418
|
continue;
|
|
2347
2419
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
if (d.added.length) parts.push(`+${d.added.length} added`);
|
|
2360
|
-
if (d.removed.length) parts.push(`-${d.removed.length} removed`);
|
|
2361
|
-
if (d.changed.length) parts.push(`~${d.changed.length} changed`);
|
|
2362
|
-
if (!parts.length) parts.push("no changes");
|
|
2363
|
-
return parts.join(", ");
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// src/tui/App.tsx
|
|
2367
|
-
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2368
|
-
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
2369
|
-
if (proxyActive && proxyOpts) {
|
|
2370
|
-
const sub = proxyOpts.routes[name];
|
|
2371
|
-
if (sub !== void 0) {
|
|
2372
|
-
const host = sub ? `${sub}.${proxyOpts.domain}` : proxyOpts.domain;
|
|
2373
|
-
const scheme = proxyOpts.tls ? "https" : "http";
|
|
2374
|
-
return `${scheme}://${host}`;
|
|
2420
|
+
if (svc.healthCheck.type === "tcp" && !svc.port) {
|
|
2421
|
+
opts.onLog?.(svc.name, "\u26A0 tcp healthCheck requires `port` \u2014 skipping wait");
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
const timeoutMs = (svc.startTimeout ?? DEFAULT_START_TIMEOUT_S) * 1e3;
|
|
2425
|
+
const ok = await waitHealthy(svc, timeoutMs);
|
|
2426
|
+
if (ok) {
|
|
2427
|
+
opts.onLog?.(svc.name, "\u2705 healthy");
|
|
2428
|
+
} else {
|
|
2429
|
+
opts.onLog?.(svc.name, `\u274C never became healthy (timeout ${timeoutMs / 1e3}s)`);
|
|
2430
|
+
failed.push(svc.name);
|
|
2375
2431
|
}
|
|
2376
2432
|
}
|
|
2377
|
-
return
|
|
2433
|
+
return { procs, allHealthy: failed.length === 0, failed };
|
|
2378
2434
|
}
|
|
2379
|
-
function
|
|
2380
|
-
const {
|
|
2381
|
-
const [rows, setRows] = useState6(stdout?.rows ?? 40);
|
|
2382
|
-
useEffect5(() => {
|
|
2383
|
-
if (!stdout) return;
|
|
2384
|
-
const onResize = () => setRows(stdout.rows ?? 40);
|
|
2385
|
-
stdout.on("resize", onResize);
|
|
2386
|
-
return () => {
|
|
2387
|
-
stdout.off("resize", onResize);
|
|
2388
|
-
};
|
|
2389
|
-
}, [stdout]);
|
|
2390
|
-
const logsHeight = Math.floor(rows * 0.65);
|
|
2391
|
-
const statsHeight = rows - logsHeight - 2;
|
|
2392
|
-
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
2393
|
-
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
2394
|
-
const [booted, setBooted] = useState6(false);
|
|
2395
|
-
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
2396
|
-
const externals = useRef3([]);
|
|
2397
|
-
const socketServer = useRef3(null);
|
|
2398
|
-
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2399
|
-
const [activeTip, setActiveTip] = useState6(null);
|
|
2400
|
-
const kb = useKeyBindings({
|
|
2401
|
-
onQuit: () => {
|
|
2402
|
-
void shutdown();
|
|
2403
|
-
},
|
|
2404
|
-
onClearLogs: pm.clearLogs,
|
|
2405
|
-
onToggleProxy: () => {
|
|
2406
|
-
}
|
|
2407
|
-
});
|
|
2408
|
-
const shutdown = useCallback3(async () => {
|
|
2409
|
-
lazyProxies.current.forEach((p) => p.destroy());
|
|
2410
|
-
await socketServer.current?.close();
|
|
2411
|
-
socketServer.current = null;
|
|
2412
|
-
await pm.cleanup();
|
|
2413
|
-
if (externals.current.length) {
|
|
2414
|
-
await stopExternals(externals.current, platform, {
|
|
2415
|
-
baseCwd,
|
|
2416
|
-
env,
|
|
2417
|
-
onLog: (svc, msg) => pm.pushLog(`ext:${svc}`, msg, 12)
|
|
2418
|
-
});
|
|
2419
|
-
externals.current = [];
|
|
2420
|
-
}
|
|
2421
|
-
await logSink?.close();
|
|
2422
|
-
process.exit(0);
|
|
2423
|
-
}, [pm, logSink, platform, baseCwd, env]);
|
|
2424
|
-
useEffect5(() => {
|
|
2425
|
-
if (!pm.manager) return;
|
|
2426
|
-
let handle = null;
|
|
2427
|
-
(async () => {
|
|
2428
|
-
try {
|
|
2429
|
-
handle = await startSocketServer(config.name, {
|
|
2430
|
-
states: () => pm.manager.state,
|
|
2431
|
-
restart: (name) => pm.manager.restart(name),
|
|
2432
|
-
stop: (name) => pm.manager.stop(name),
|
|
2433
|
-
tailLogs: async (svcName, lines) => {
|
|
2434
|
-
if (!logSink) return [];
|
|
2435
|
-
const file = logSink.pathFor(svcName);
|
|
2436
|
-
if (!existsSync11(file)) return [];
|
|
2437
|
-
return new Promise((resolve4, reject) => {
|
|
2438
|
-
const buf = [];
|
|
2439
|
-
const rl = createInterface3({ input: createReadStream2(file, { encoding: "utf8" }) });
|
|
2440
|
-
rl.on("line", (l) => {
|
|
2441
|
-
buf.push(l);
|
|
2442
|
-
if (buf.length > lines) buf.shift();
|
|
2443
|
-
});
|
|
2444
|
-
rl.on("close", () => resolve4(buf));
|
|
2445
|
-
rl.on("error", reject);
|
|
2446
|
-
});
|
|
2447
|
-
}
|
|
2448
|
-
}, { onLog: (msg) => pm.pushLog("devup", msg, 12) });
|
|
2449
|
-
socketServer.current = handle;
|
|
2450
|
-
} catch (e) {
|
|
2451
|
-
pm.pushLog("devup", `\u26A0 control plane disabled: ${e.message}`, 5);
|
|
2452
|
-
}
|
|
2453
|
-
})();
|
|
2454
|
-
return () => {
|
|
2455
|
-
void handle?.close();
|
|
2456
|
-
};
|
|
2457
|
-
}, [pm.manager, config.name, logSink]);
|
|
2458
|
-
useEffect5(() => {
|
|
2459
|
-
if (!cliArgs.watchConfig || !pm.manager) return;
|
|
2460
|
-
let watcher = null;
|
|
2461
|
-
let configPath;
|
|
2435
|
+
async function stopExternals(procs, platform, opts = {}) {
|
|
2436
|
+
for (const { svc, proc, pid } of procs) {
|
|
2462
2437
|
try {
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
try {
|
|
2478
|
-
const nextCfg = await loadConfig(configPath);
|
|
2479
|
-
const errs = validateConfig(nextCfg, baseCwd);
|
|
2480
|
-
if (errs.length) {
|
|
2481
|
-
pm.pushLog("devup", `\u26A0 config reload failed:
|
|
2482
|
-
${formatValidationErrors(errs)}`, 5);
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
const mgr = pm.manager;
|
|
2486
|
-
const currentSvcs = [...mgr.state.values()].map((s) => s.svc);
|
|
2487
|
-
const diff = diffServices(currentSvcs, nextCfg.services);
|
|
2488
|
-
if (!diff.added.length && !diff.removed.length && !diff.changed.length) return;
|
|
2489
|
-
for (const name of diff.removed) {
|
|
2490
|
-
mgr.stop(name);
|
|
2491
|
-
mgr.state.delete(name);
|
|
2492
|
-
}
|
|
2493
|
-
let colorIdx = currentSvcs.length;
|
|
2494
|
-
for (const { next } of diff.changed) {
|
|
2495
|
-
const prev = mgr.state.get(next.name);
|
|
2496
|
-
const ci = prev?.colorIdx ?? colorIdx++;
|
|
2497
|
-
mgr.stop(next.name);
|
|
2498
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
2499
|
-
await mgr.install(next, ci);
|
|
2500
|
-
await mgr.start(next, ci, true);
|
|
2501
|
-
}
|
|
2502
|
-
for (const next of diff.added) {
|
|
2503
|
-
const ci = colorIdx++;
|
|
2504
|
-
await mgr.install(next, ci);
|
|
2505
|
-
await mgr.start(next, ci);
|
|
2506
|
-
}
|
|
2507
|
-
pm.pushLog("devup", `\u{1F501} config reloaded: ${summariseDiff(diff)}`, 12);
|
|
2508
|
-
} catch (e) {
|
|
2509
|
-
pm.pushLog("devup", `\u26A0 config reload error: ${e.message}`, 5);
|
|
2510
|
-
} finally {
|
|
2511
|
-
reloadInFlight = false;
|
|
2512
|
-
if (reloadAgain) {
|
|
2513
|
-
reloadAgain = false;
|
|
2514
|
-
void reload();
|
|
2515
|
-
}
|
|
2438
|
+
if (pid) platform.killTree(pid);
|
|
2439
|
+
if (svc.stopCmd) {
|
|
2440
|
+
opts.onLog?.(svc.name, `\u{1F9F9} ${svc.stopCmd}`);
|
|
2441
|
+
await new Promise((resolve4) => {
|
|
2442
|
+
const isWin = process.platform === "win32";
|
|
2443
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
2444
|
+
const flag = isWin ? "/c" : "-c";
|
|
2445
|
+
const cwd = svc.cwd ? join7(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
2446
|
+
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
2447
|
+
const child = spawn4(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
2448
|
+
child.on("close", () => resolve4());
|
|
2449
|
+
child.on("error", () => resolve4());
|
|
2450
|
+
setTimeout(() => resolve4(), 1e4);
|
|
2451
|
+
});
|
|
2516
2452
|
}
|
|
2517
|
-
}
|
|
2518
|
-
let debounceTimer = null;
|
|
2519
|
-
watcher = fsWatch(configPath, () => {
|
|
2520
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2521
|
-
debounceTimer = setTimeout(() => void reload(), 250);
|
|
2522
|
-
});
|
|
2523
|
-
return () => {
|
|
2524
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2525
|
-
watcher?.close();
|
|
2526
|
-
};
|
|
2527
|
-
}, [cliArgs.watchConfig, cliArgs.configPath, baseCwd, pm.manager, pm]);
|
|
2528
|
-
useEffect5(() => {
|
|
2529
|
-
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
2530
|
-
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
2531
|
-
useEffect5(() => {
|
|
2532
|
-
const tip = pickTip({
|
|
2533
|
-
totalLogs: pm.logs.length,
|
|
2534
|
-
hasSearch: !!kb.searchTerm,
|
|
2535
|
-
hasFilter: !!kb.logFilter,
|
|
2536
|
-
crashLoopedCount: [...pm.states.values()].filter(isCrashLooped).length,
|
|
2537
|
-
shown: shownTips.current
|
|
2538
|
-
});
|
|
2539
|
-
if (tip && tip.id !== activeTip) {
|
|
2540
|
-
shownTips.current.add(tip.id);
|
|
2541
|
-
setActiveTip(tip.message);
|
|
2542
|
-
const timer = setTimeout(() => setActiveTip(null), 12e3);
|
|
2543
|
-
return () => clearTimeout(timer);
|
|
2453
|
+
} catch {
|
|
2544
2454
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
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;
|
|
2549
2491
|
setBooted(true);
|
|
2550
|
-
const mgr =
|
|
2492
|
+
const mgr = manager;
|
|
2551
2493
|
(async () => {
|
|
2552
2494
|
const lazyMode = cliArgs.lazy;
|
|
2553
2495
|
const lazyTimeout = cliArgs.lazyTimeout;
|
|
@@ -2556,11 +2498,11 @@ ${formatValidationErrors(errs)}`, 5);
|
|
|
2556
2498
|
baseCwd,
|
|
2557
2499
|
env,
|
|
2558
2500
|
platform,
|
|
2559
|
-
onLog: (svc, msg) =>
|
|
2501
|
+
onLog: (svc, msg) => pushLog(`ext:${svc}`, msg, 12)
|
|
2560
2502
|
});
|
|
2561
|
-
externals.current = result.procs;
|
|
2503
|
+
refs.externals.current = result.procs;
|
|
2562
2504
|
if (!result.allHealthy) {
|
|
2563
|
-
|
|
2505
|
+
pushLog("devup", `\u274C external(s) failed: ${result.failed.join(", ")}. Aborting boot.`, 5);
|
|
2564
2506
|
return;
|
|
2565
2507
|
}
|
|
2566
2508
|
}
|
|
@@ -2628,7 +2570,7 @@ ${formatValidationErrors(errs)}`, 5);
|
|
|
2628
2570
|
return !!st && !!st.proc && !st.proc.killed && st.health === "up";
|
|
2629
2571
|
}
|
|
2630
2572
|
});
|
|
2631
|
-
lazyProxies.current.set(svc.name, proxy);
|
|
2573
|
+
refs.lazyProxies.current.set(svc.name, proxy);
|
|
2632
2574
|
}
|
|
2633
2575
|
} else {
|
|
2634
2576
|
const phases = groupByPhase(services);
|
|
@@ -2649,7 +2591,259 @@ ${formatValidationErrors(errs)}`, 5);
|
|
|
2649
2591
|
}
|
|
2650
2592
|
}
|
|
2651
2593
|
})();
|
|
2652
|
-
}, [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
|
+
);
|
|
2653
2847
|
const handleFilterSelect = useCallback3((name) => kb.setFilter(name), [kb]);
|
|
2654
2848
|
const handleRestartSelect = useCallback3((name) => {
|
|
2655
2849
|
pm.restart(name);
|
|
@@ -2723,8 +2917,8 @@ ${formatValidationErrors(errs)}`, 5);
|
|
|
2723
2917
|
}
|
|
2724
2918
|
|
|
2725
2919
|
// src/process/log-sink.ts
|
|
2726
|
-
import { existsSync as
|
|
2727
|
-
import { join as
|
|
2920
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, renameSync, createWriteStream } from "fs";
|
|
2921
|
+
import { join as join8, dirname as dirname6 } from "path";
|
|
2728
2922
|
import { homedir as homedir3 } from "os";
|
|
2729
2923
|
var LogSink = class {
|
|
2730
2924
|
dir;
|
|
@@ -2732,14 +2926,14 @@ var LogSink = class {
|
|
|
2732
2926
|
streams = /* @__PURE__ */ new Map();
|
|
2733
2927
|
seen = /* @__PURE__ */ new Set();
|
|
2734
2928
|
constructor(opts) {
|
|
2735
|
-
const root = opts.rootDir ??
|
|
2736
|
-
this.dir =
|
|
2929
|
+
const root = opts.rootDir ?? join8(homedir3(), ".devup", "logs");
|
|
2930
|
+
this.dir = join8(root, sanitize2(opts.projectName));
|
|
2737
2931
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
2738
2932
|
mkdirSync5(this.dir, { recursive: true });
|
|
2739
2933
|
}
|
|
2740
2934
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
2741
2935
|
pathFor(svcName) {
|
|
2742
|
-
return
|
|
2936
|
+
return join8(this.dir, `${sanitize2(svcName)}.log`);
|
|
2743
2937
|
}
|
|
2744
2938
|
write(svcName, line) {
|
|
2745
2939
|
const stream = this.streamFor(svcName);
|
|
@@ -2758,7 +2952,7 @@ var LogSink = class {
|
|
|
2758
2952
|
let s = this.streams.get(svcName);
|
|
2759
2953
|
if (s) return s;
|
|
2760
2954
|
const file = this.pathFor(svcName);
|
|
2761
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2955
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync13(file)) {
|
|
2762
2956
|
try {
|
|
2763
2957
|
mkdirSync5(dirname6(file), { recursive: true });
|
|
2764
2958
|
renameSync(file, file + ".prev");
|
|
@@ -2947,8 +3141,8 @@ function defineConfig(config) {
|
|
|
2947
3141
|
function readVersion() {
|
|
2948
3142
|
try {
|
|
2949
3143
|
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
2950
|
-
const pkgPath =
|
|
2951
|
-
return JSON.parse(
|
|
3144
|
+
const pkgPath = join9(here, "..", "package.json");
|
|
3145
|
+
return JSON.parse(readFileSync3(pkgPath, "utf8")).version ?? "unknown";
|
|
2952
3146
|
} catch {
|
|
2953
3147
|
return "unknown";
|
|
2954
3148
|
}
|
|
@@ -3015,7 +3209,7 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3015
3209
|
process.exit(1);
|
|
3016
3210
|
}
|
|
3017
3211
|
const platform = await detectPlatform();
|
|
3018
|
-
const envFile = config.envFile ?
|
|
3212
|
+
const envFile = config.envFile ? join9(cwd, config.envFile) : join9(cwd, ".env");
|
|
3019
3213
|
const env = parseEnvFile(envFile, process.env);
|
|
3020
3214
|
if (config.env) {
|
|
3021
3215
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -3032,7 +3226,7 @@ ${formatValidationWarnings(warnings)}`);
|
|
|
3032
3226
|
routes: config.proxy.routes,
|
|
3033
3227
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
3034
3228
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
3035
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
3229
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join9(homedir4(), ".traefik", "traefik_conf.yaml")
|
|
3036
3230
|
};
|
|
3037
3231
|
}
|
|
3038
3232
|
if (cliArgs.dryRun) {
|