@gachlab/devup 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +74 -450
- package/dist/config/cli.d.ts +2 -1
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/diff.d.ts +19 -0
- package/dist/config/diff.d.ts.map +1 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validator.d.ts +9 -0
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/control-plane/socket-server.d.ts +31 -0
- package/dist/control-plane/socket-server.d.ts.map +1 -0
- package/dist/index.js +512 -63
- package/dist/index.js.map +1 -1
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts +10 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/dist/tui/StatsPanel.d.ts +2 -1
- package/dist/tui/StatsPanel.d.ts.map +1 -1
- package/dist/tui/hooks/useKeyBindings.d.ts +5 -0
- package/dist/tui/hooks/useKeyBindings.d.ts.map +1 -1
- package/dist/tui/hooks/useProcessManager.d.ts +2 -0
- package/dist/tui/hooks/useProcessManager.d.ts.map +1 -1
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
|
-
import { homedir as
|
|
9
|
+
import { homedir as homedir4 } from "os";
|
|
10
10
|
|
|
11
11
|
// src/config/loader.ts
|
|
12
12
|
import { existsSync } from "fs";
|
|
@@ -74,6 +74,26 @@ function rewriteServicePort(svc) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// src/config/validator.ts
|
|
77
|
+
function collectWarnings(config) {
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (!config.services?.length) return warnings;
|
|
80
|
+
for (const svc of config.services) {
|
|
81
|
+
const ep = svc.extraEnv?.["PORT"];
|
|
82
|
+
if (ep !== void 0) {
|
|
83
|
+
const expected = String(svc.port);
|
|
84
|
+
if (ep !== expected) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
field: `services[${svc.name}].extraEnv.PORT`,
|
|
87
|
+
message: `extraEnv.PORT="${ep}" does not match port=${svc.port}. devup will health-check :${svc.port} but the service will probably bind to :${ep}.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
function formatValidationWarnings(warnings) {
|
|
95
|
+
return warnings.map((w) => ` \u26A0 ${w.field}: ${w.message}`).join("\n");
|
|
96
|
+
}
|
|
77
97
|
function validateConfig(config, cwd) {
|
|
78
98
|
const errors = [];
|
|
79
99
|
if (!config.name?.trim()) {
|
|
@@ -114,6 +134,19 @@ function validateConfig(config, cwd) {
|
|
|
114
134
|
if (svc.cwd && !existsSync2(resolve2(cwd, svc.cwd))) {
|
|
115
135
|
errors.push({ field: `services[${svc.name}].cwd`, message: `Directory not found: ${svc.cwd}` });
|
|
116
136
|
}
|
|
137
|
+
if (svc.errorPattern !== void 0) {
|
|
138
|
+
if (typeof svc.errorPattern !== "string" || !svc.errorPattern.length) {
|
|
139
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `errorPattern must be a non-empty string` });
|
|
140
|
+
} else {
|
|
141
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(svc.errorPattern);
|
|
142
|
+
try {
|
|
143
|
+
if (slashed) new RegExp(slashed[1], slashed[2] || "i");
|
|
144
|
+
else new RegExp(svc.errorPattern, "i");
|
|
145
|
+
} catch (e) {
|
|
146
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `Invalid regex: ${e.message}` });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
117
150
|
if (svc.readyPattern !== void 0) {
|
|
118
151
|
if (typeof svc.readyPattern !== "string" || !svc.readyPattern.length) {
|
|
119
152
|
errors.push({ field: `services[${svc.name}].readyPattern`, message: `readyPattern must be a non-empty string` });
|
|
@@ -138,6 +171,9 @@ function validateConfig(config, cwd) {
|
|
|
138
171
|
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
139
172
|
errors.push({ field: `services[${svc.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type} (must be "tcp" or "http")` });
|
|
140
173
|
}
|
|
174
|
+
if (hc.startPeriod !== void 0 && (typeof hc.startPeriod !== "number" || hc.startPeriod < 0)) {
|
|
175
|
+
errors.push({ field: `services[${svc.name}].healthCheck.startPeriod`, message: `startPeriod must be a non-negative number (seconds), got ${hc.startPeriod}` });
|
|
176
|
+
}
|
|
141
177
|
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
142
178
|
errors.push({ field: `services[${svc.name}].healthCheck.path`, message: `healthCheck.path must start with "/": got "${hc.path}"` });
|
|
143
179
|
}
|
|
@@ -256,6 +292,10 @@ Log files:
|
|
|
256
292
|
--no-log-file Disable persistent log files
|
|
257
293
|
--log-dir <path> Override log root (default: ~/.devup/logs)
|
|
258
294
|
|
|
295
|
+
Hot reload:
|
|
296
|
+
--watch-config Watch devup.config.* and apply add/remove/restart
|
|
297
|
+
service changes without exiting the TUI
|
|
298
|
+
|
|
259
299
|
Other:
|
|
260
300
|
-h, --help Show this help and exit
|
|
261
301
|
-v, --version Show version and exit
|
|
@@ -272,7 +312,8 @@ function parseCliArgs(argv) {
|
|
|
272
312
|
dryRun: false,
|
|
273
313
|
once: false,
|
|
274
314
|
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
275
|
-
logFile: true
|
|
315
|
+
logFile: true,
|
|
316
|
+
watchConfig: false
|
|
276
317
|
};
|
|
277
318
|
for (let i = 0; i < argv.length; i++) {
|
|
278
319
|
const arg = argv[i];
|
|
@@ -346,6 +387,9 @@ function parseCliArgs(argv) {
|
|
|
346
387
|
args.logDir = next;
|
|
347
388
|
i++;
|
|
348
389
|
break;
|
|
390
|
+
case "--watch-config":
|
|
391
|
+
args.watchConfig = true;
|
|
392
|
+
break;
|
|
349
393
|
}
|
|
350
394
|
}
|
|
351
395
|
return args;
|
|
@@ -489,6 +533,41 @@ function parseEnvFile(filePath, baseEnv = {}) {
|
|
|
489
533
|
}
|
|
490
534
|
return env;
|
|
491
535
|
}
|
|
536
|
+
function nextRamBannerVisibility(usagePct, previousVisible, highWatermark = 80, lowWatermark = 75) {
|
|
537
|
+
if (usagePct >= highWatermark) return true;
|
|
538
|
+
if (usagePct < lowWatermark) return false;
|
|
539
|
+
return previousVisible;
|
|
540
|
+
}
|
|
541
|
+
function redactSecrets(env) {
|
|
542
|
+
if (!env) return {};
|
|
543
|
+
const out = {};
|
|
544
|
+
for (const [k, v] of Object.entries(env)) {
|
|
545
|
+
out[k] = /secret|token|password|api[_-]?key|auth/i.test(k) ? "***" : v;
|
|
546
|
+
}
|
|
547
|
+
return out;
|
|
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";
|
|
554
|
+
}
|
|
555
|
+
function compileSearchPattern(term) {
|
|
556
|
+
if (!term) return null;
|
|
557
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(term);
|
|
558
|
+
if (slashed) {
|
|
559
|
+
const flags = slashed[2].includes("i") ? slashed[2] : slashed[2] + "i";
|
|
560
|
+
try {
|
|
561
|
+
const re = new RegExp(slashed[1], flags);
|
|
562
|
+
return { test: (l) => re.test(l), regex: re };
|
|
563
|
+
} catch {
|
|
564
|
+
const lower2 = term.toLowerCase();
|
|
565
|
+
return { test: (l) => l.toLowerCase().includes(lower2), invalid: true };
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const lower = term.toLowerCase();
|
|
569
|
+
return { test: (l) => l.toLowerCase().includes(lower) };
|
|
570
|
+
}
|
|
492
571
|
function fmtUptime(ms) {
|
|
493
572
|
if (!ms || ms < 0) return "-";
|
|
494
573
|
const s = Math.floor(ms / 1e3);
|
|
@@ -903,7 +982,7 @@ function detectProxyProvider(name) {
|
|
|
903
982
|
}
|
|
904
983
|
|
|
905
984
|
// src/tui/App.tsx
|
|
906
|
-
import { useEffect as useEffect5, useState as
|
|
985
|
+
import { useEffect as useEffect5, useState as useState6, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
907
986
|
import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
908
987
|
|
|
909
988
|
// src/tui/hooks/useProcessManager.ts
|
|
@@ -1072,12 +1151,14 @@ var ProcessManager = class {
|
|
|
1072
1151
|
this.events.onStateChange(svc.name, state);
|
|
1073
1152
|
}
|
|
1074
1153
|
};
|
|
1154
|
+
const errorRegex = compileReadyPattern(svc.errorPattern);
|
|
1155
|
+
const countsAsError = (line) => errorRegex ? errorRegex.test(line) : true;
|
|
1075
1156
|
const stdoutBuf = lineBuffer((line) => {
|
|
1076
1157
|
markReadyIfMatch(line);
|
|
1077
1158
|
this.log(svc.name, line, colorIdx);
|
|
1078
1159
|
});
|
|
1079
1160
|
const stderrBuf = lineBuffer((line) => {
|
|
1080
|
-
state.errors += 1;
|
|
1161
|
+
if (countsAsError(line)) state.errors += 1;
|
|
1081
1162
|
markReadyIfMatch(line);
|
|
1082
1163
|
this.log(svc.name, line, colorIdx);
|
|
1083
1164
|
});
|
|
@@ -1212,6 +1293,10 @@ var ProcessManager = class {
|
|
|
1212
1293
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
1213
1294
|
continue;
|
|
1214
1295
|
}
|
|
1296
|
+
const startPeriodMs = (st.svc.healthCheck?.startPeriod ?? 0) * 1e3;
|
|
1297
|
+
if (startPeriodMs > 0 && st.startedAt && Date.now() - st.startedAt < startPeriodMs) {
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1215
1300
|
const isUp = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
1216
1301
|
const prev = st.health;
|
|
1217
1302
|
st.health = deriveHealth(isUp, st.status);
|
|
@@ -1283,7 +1368,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1283
1368
|
events: {
|
|
1284
1369
|
onLog: (svcName, text, colorIdx) => {
|
|
1285
1370
|
sinkRef.current?.write(svcName, text);
|
|
1286
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1371
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1287
1372
|
if (pausedRef.current) {
|
|
1288
1373
|
pendingLogsRef.current.push(entry);
|
|
1289
1374
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1341,7 +1426,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1341
1426
|
}, []);
|
|
1342
1427
|
const pushLog = useCallback((svcName, text, colorIdx = 0) => {
|
|
1343
1428
|
sinkRef.current?.write(svcName, text);
|
|
1344
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1429
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1345
1430
|
if (pausedRef.current) {
|
|
1346
1431
|
pendingLogsRef.current.push(entry);
|
|
1347
1432
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1414,8 +1499,11 @@ function useKeyBindings(opts) {
|
|
|
1414
1499
|
sortIdx: 0,
|
|
1415
1500
|
proxyEnabled: false,
|
|
1416
1501
|
logsScrollOffset: 0,
|
|
1417
|
-
statsScrollOffset: 0
|
|
1502
|
+
statsScrollOffset: 0,
|
|
1503
|
+
levelFilter: "all",
|
|
1504
|
+
verboseStats: false
|
|
1418
1505
|
});
|
|
1506
|
+
const LEVEL_CYCLE = ["all", "error", "warn"];
|
|
1419
1507
|
const setModal = useCallback2((modal) => setState((s) => ({ ...s, modal })), []);
|
|
1420
1508
|
const setFilter = useCallback2((f) => setState((s) => ({ ...s, logFilter: f, modal: "none" })), []);
|
|
1421
1509
|
const setSearch = useCallback2((t) => setState((s) => ({ ...s, searchTerm: t, modal: "none" })), []);
|
|
@@ -1437,14 +1525,15 @@ function useKeyBindings(opts) {
|
|
|
1437
1525
|
else if (input === "r") setModal("restart");
|
|
1438
1526
|
else if (input === "o") setModal("open");
|
|
1439
1527
|
else if (input === "/") setModal("search");
|
|
1440
|
-
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null }));
|
|
1528
|
+
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null, levelFilter: "all" }));
|
|
1441
1529
|
else if (input === "p") setState((s) => ({ ...s, logsPaused: !s.logsPaused }));
|
|
1442
1530
|
else if (input === "t") setState((s) => ({ ...s, showTimestamps: !s.showTimestamps }));
|
|
1443
1531
|
else if (input === "s") setState((s) => ({ ...s, sortIdx: (s.sortIdx + 1) % SORT_MODES.length }));
|
|
1444
1532
|
else if (input === "T") {
|
|
1445
1533
|
opts.onToggleProxy();
|
|
1446
1534
|
setState((s) => ({ ...s, proxyEnabled: !s.proxyEnabled }));
|
|
1447
|
-
}
|
|
1535
|
+
} else if (input === "L") setState((s) => ({ ...s, levelFilter: LEVEL_CYCLE[(LEVEL_CYCLE.indexOf(s.levelFilter) + 1) % LEVEL_CYCLE.length] }));
|
|
1536
|
+
else if (input === "v") setState((s) => ({ ...s, verboseStats: !s.verboseStats }));
|
|
1448
1537
|
}, { isActive });
|
|
1449
1538
|
return {
|
|
1450
1539
|
...state,
|
|
@@ -1486,11 +1575,19 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1486
1575
|
}
|
|
1487
1576
|
|
|
1488
1577
|
// src/tui/LogsPanel.tsx
|
|
1489
|
-
import { useEffect as useEffect3 } from "react";
|
|
1578
|
+
import { useEffect as useEffect3, useMemo } from "react";
|
|
1490
1579
|
import { Box, Text } from "ink";
|
|
1491
1580
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1492
|
-
function
|
|
1493
|
-
|
|
1581
|
+
function resolveBorder(focused, filter, filteredColorIdx) {
|
|
1582
|
+
if (focused) return "cyan";
|
|
1583
|
+
if (filter && filteredColorIdx !== null && filteredColorIdx >= 0) {
|
|
1584
|
+
return tagColors[filteredColorIdx % tagColors.length];
|
|
1585
|
+
}
|
|
1586
|
+
return "gray";
|
|
1587
|
+
}
|
|
1588
|
+
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll, levelFilter = "all", filteredColorIdx = null }) {
|
|
1589
|
+
const byService = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
1590
|
+
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
1494
1591
|
const contentHeight = Math.max(1, height - 2);
|
|
1495
1592
|
const totalLines = filtered.length;
|
|
1496
1593
|
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
@@ -1501,17 +1598,20 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1501
1598
|
useEffect3(() => {
|
|
1502
1599
|
resetScroll();
|
|
1503
1600
|
}, [filter, searchTerm, resetScroll]);
|
|
1601
|
+
const matcher = useMemo(() => compileSearchPattern(searchTerm), [searchTerm]);
|
|
1504
1602
|
const scrolled = effectiveOffset > 0;
|
|
1505
1603
|
const label = [
|
|
1506
1604
|
"Logs",
|
|
1507
1605
|
filter ? `[${filter}]` : "",
|
|
1508
1606
|
searchTerm ? `/${searchTerm}` : "",
|
|
1607
|
+
matcher?.invalid ? "(invalid regex)" : "",
|
|
1608
|
+
levelFilter !== "all" ? `[level: ${levelFilter}${levelFilter === "warn" ? "+error" : ""}]` : "",
|
|
1509
1609
|
paused ? "[PAUSED]" : "",
|
|
1510
1610
|
scrolled ? "[SCROLL]" : "",
|
|
1511
1611
|
`${filtered.length} lines`,
|
|
1512
1612
|
focused && totalLines > 0 ? `(${startIndex + 1}-${endIndex}/${totalLines})` : ""
|
|
1513
1613
|
].filter(Boolean).join(" ");
|
|
1514
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused
|
|
1614
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: resolveBorder(focused, filter, filteredColorIdx), height, children: [
|
|
1515
1615
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1516
1616
|
" ",
|
|
1517
1617
|
label,
|
|
@@ -1521,7 +1621,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1521
1621
|
const color = tagColors[entry.colorIdx % tagColors.length];
|
|
1522
1622
|
const ts = showTimestamps ? new Date(entry.ts).toLocaleTimeString("en-GB") + " " : "";
|
|
1523
1623
|
const line = entry.text;
|
|
1524
|
-
const isMatch =
|
|
1624
|
+
const isMatch = matcher ? matcher.test(line) : false;
|
|
1525
1625
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1526
1626
|
showTimestamps && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ts }),
|
|
1527
1627
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
@@ -1537,7 +1637,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1537
1637
|
}
|
|
1538
1638
|
|
|
1539
1639
|
// src/tui/StatsPanel.tsx
|
|
1540
|
-
import { useEffect as useEffect4 } from "react";
|
|
1640
|
+
import { useEffect as useEffect4, useState as useState3 } from "react";
|
|
1541
1641
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
1542
1642
|
import os from "os";
|
|
1543
1643
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1551,31 +1651,67 @@ var MAX_RESTARTS2 = 3;
|
|
|
1551
1651
|
function isCrashLooped(st) {
|
|
1552
1652
|
return st.status === "crashed" && st.restarts >= MAX_RESTARTS2;
|
|
1553
1653
|
}
|
|
1554
|
-
function Row({ name, st, stat, ml }) {
|
|
1654
|
+
function Row({ name, st, stat, ml, verbose }) {
|
|
1555
1655
|
const looped = isCrashLooped(st);
|
|
1556
1656
|
const indicator = looped ? /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "\u2716" }) : /* @__PURE__ */ jsx2(Text2, { color: (H[st.health] ?? H["down"]).color, children: (H[st.health] ?? H["down"]).c });
|
|
1557
1657
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1558
1658
|
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1559
1659
|
const statusLabel = looped ? "looping" : st.status;
|
|
1560
1660
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1661
|
+
if (!verbose) {
|
|
1662
|
+
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1663
|
+
indicator,
|
|
1664
|
+
" ",
|
|
1665
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1666
|
+
" ",
|
|
1667
|
+
String(st.svc.port).padStart(5),
|
|
1668
|
+
" ",
|
|
1669
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1670
|
+
" ",
|
|
1671
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1672
|
+
" ",
|
|
1673
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1674
|
+
" ",
|
|
1675
|
+
String(st.errors).padStart(3),
|
|
1676
|
+
" ",
|
|
1677
|
+
String(st.restarts).padStart(3),
|
|
1678
|
+
" ",
|
|
1679
|
+
up.padStart(6)
|
|
1680
|
+
] });
|
|
1681
|
+
}
|
|
1682
|
+
const resolvedArgs = buildProcessArgs(st.svc).join(" ");
|
|
1683
|
+
const env = redactSecrets(st.svc.extraEnv);
|
|
1684
|
+
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1685
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1686
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1687
|
+
indicator,
|
|
1688
|
+
" ",
|
|
1689
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1690
|
+
" ",
|
|
1691
|
+
String(st.svc.port).padStart(5),
|
|
1692
|
+
" ",
|
|
1693
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1694
|
+
" ",
|
|
1695
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1696
|
+
" ",
|
|
1697
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1698
|
+
" ",
|
|
1699
|
+
String(st.errors).padStart(3),
|
|
1700
|
+
" ",
|
|
1701
|
+
String(st.restarts).padStart(3),
|
|
1702
|
+
" ",
|
|
1703
|
+
up.padStart(6)
|
|
1704
|
+
] }),
|
|
1705
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1706
|
+
" cmd: ",
|
|
1707
|
+
st.svc.cmd,
|
|
1708
|
+
" ",
|
|
1709
|
+
resolvedArgs
|
|
1710
|
+
] }),
|
|
1711
|
+
envStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1712
|
+
" env: ",
|
|
1713
|
+
envStr
|
|
1714
|
+
] })
|
|
1579
1715
|
] });
|
|
1580
1716
|
}
|
|
1581
1717
|
function ColHeader({ ml }) {
|
|
@@ -1594,7 +1730,7 @@ function ColHeader({ ml }) {
|
|
|
1594
1730
|
"Up".padStart(6)
|
|
1595
1731
|
] });
|
|
1596
1732
|
}
|
|
1597
|
-
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
1733
|
+
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll, verbose = false }) {
|
|
1598
1734
|
const names = [...states.keys()];
|
|
1599
1735
|
const stObj = Object.fromEntries([...states].map(([k, v]) => [k, { errors: v.errors }]));
|
|
1600
1736
|
const statsObj = Object.fromEntries([...stats].map(([k, v]) => [k, v]));
|
|
@@ -1633,6 +1769,12 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1633
1769
|
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1634
1770
|
const scrolled = effectiveOffset > 0;
|
|
1635
1771
|
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1772
|
+
const ramPct = parseFloat(usedGB) / parseFloat(totalGB) * 100;
|
|
1773
|
+
const [ramBanner, setRamBanner] = useState3(false);
|
|
1774
|
+
useEffect4(() => {
|
|
1775
|
+
setRamBanner((prev) => nextRamBannerVisibility(ramPct, prev));
|
|
1776
|
+
}, [ramPct]);
|
|
1777
|
+
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) : [];
|
|
1636
1778
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
1637
1779
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1638
1780
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
@@ -1674,6 +1816,14 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1674
1816
|
sortMode
|
|
1675
1817
|
] })
|
|
1676
1818
|
] }),
|
|
1819
|
+
ramBanner && /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1820
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
1821
|
+
" \u26A0 RAM ",
|
|
1822
|
+
ramPct.toFixed(0),
|
|
1823
|
+
"% \u2014 top: "
|
|
1824
|
+
] }),
|
|
1825
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: topConsumers.map((c) => `${c.name} ${c.mb.toFixed(0)}MB`).join(", ") })
|
|
1826
|
+
] }),
|
|
1677
1827
|
/* @__PURE__ */ jsxs2(Box2, { flexGrow: 1, children: [
|
|
1678
1828
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
1679
1829
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
@@ -1682,7 +1832,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1682
1832
|
")"
|
|
1683
1833
|
] }),
|
|
1684
1834
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1685
|
-
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1835
|
+
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1686
1836
|
] }),
|
|
1687
1837
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: Array.from({ length: contentHeight }, (_, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2502" }, i)) }),
|
|
1688
1838
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
@@ -1692,7 +1842,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1692
1842
|
")"
|
|
1693
1843
|
] }),
|
|
1694
1844
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1695
|
-
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1845
|
+
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1696
1846
|
] })
|
|
1697
1847
|
] })
|
|
1698
1848
|
] });
|
|
@@ -1717,6 +1867,8 @@ function StatusBar() {
|
|
|
1717
1867
|
" Clear ",
|
|
1718
1868
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
1719
1869
|
" Filter ",
|
|
1870
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
1871
|
+
" Level ",
|
|
1720
1872
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
1721
1873
|
" All ",
|
|
1722
1874
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
@@ -1731,23 +1883,25 @@ function StatusBar() {
|
|
|
1731
1883
|
" Pause ",
|
|
1732
1884
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
1733
1885
|
" Time ",
|
|
1886
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
1887
|
+
" Verbose ",
|
|
1734
1888
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
1735
1889
|
" Proxy"
|
|
1736
1890
|
] }) });
|
|
1737
1891
|
}
|
|
1738
1892
|
|
|
1739
1893
|
// src/tui/ServiceList.tsx
|
|
1740
|
-
import { useState as
|
|
1894
|
+
import { useState as useState4, useMemo as useMemo2 } from "react";
|
|
1741
1895
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1742
1896
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1743
1897
|
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1744
|
-
const allNames =
|
|
1898
|
+
const allNames = useMemo2(
|
|
1745
1899
|
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1746
1900
|
[services, filterType]
|
|
1747
1901
|
);
|
|
1748
|
-
const [idx, setIdx] =
|
|
1749
|
-
const [query, setQuery] =
|
|
1750
|
-
const names =
|
|
1902
|
+
const [idx, setIdx] = useState4(0);
|
|
1903
|
+
const [query, setQuery] = useState4("");
|
|
1904
|
+
const names = useMemo2(() => {
|
|
1751
1905
|
if (!query) return allNames;
|
|
1752
1906
|
const q = query.toLowerCase();
|
|
1753
1907
|
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
@@ -1804,11 +1958,11 @@ function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
|
1804
1958
|
}
|
|
1805
1959
|
|
|
1806
1960
|
// src/tui/SearchInput.tsx
|
|
1807
|
-
import { useState as
|
|
1961
|
+
import { useState as useState5 } from "react";
|
|
1808
1962
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
1809
1963
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1810
1964
|
function SearchInput({ onSubmit, onClose }) {
|
|
1811
|
-
const [value, setValue] =
|
|
1965
|
+
const [value, setValue] = useState5("");
|
|
1812
1966
|
useInput3((input, key) => {
|
|
1813
1967
|
if (key.escape) onClose();
|
|
1814
1968
|
else if (key.return) onSubmit(value.trim() || null);
|
|
@@ -2029,6 +2183,186 @@ function pickTip(state) {
|
|
|
2029
2183
|
return null;
|
|
2030
2184
|
}
|
|
2031
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 };
|
|
2290
|
+
}
|
|
2291
|
+
case "logs.tail": {
|
|
2292
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2293
|
+
const lines = Math.max(1, Math.min(1e4, Number(params["lines"] ?? 100)));
|
|
2294
|
+
return { lines: await ctx.tailLogs(svc, lines) };
|
|
2295
|
+
}
|
|
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
|
+
}
|
|
2306
|
+
return v;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/tui/App.tsx
|
|
2310
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2311
|
+
import { createReadStream as createReadStream2, existsSync as existsSync11, watch as fsWatch } from "fs";
|
|
2312
|
+
|
|
2313
|
+
// src/config/diff.ts
|
|
2314
|
+
var SPAWN_RELEVANT = [
|
|
2315
|
+
"cwd",
|
|
2316
|
+
"cmd",
|
|
2317
|
+
"args",
|
|
2318
|
+
"port",
|
|
2319
|
+
"phase",
|
|
2320
|
+
"maxMem",
|
|
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);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const n = nextByName.get(name);
|
|
2349
|
+
if (hasSpawnRelevantChange(p, n)) changed.push({ prev: p, next: n });
|
|
2350
|
+
else unchanged.push(name);
|
|
2351
|
+
}
|
|
2352
|
+
for (const [name, n] of nextByName) {
|
|
2353
|
+
if (!prevByName.has(name)) added.push(n);
|
|
2354
|
+
}
|
|
2355
|
+
return { added, removed, changed, unchanged };
|
|
2356
|
+
}
|
|
2357
|
+
function summariseDiff(d) {
|
|
2358
|
+
const parts = [];
|
|
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
|
+
|
|
2032
2366
|
// src/tui/App.tsx
|
|
2033
2367
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2034
2368
|
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
@@ -2044,7 +2378,7 @@ function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
|
2044
2378
|
}
|
|
2045
2379
|
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
2046
2380
|
const { stdout } = useStdout();
|
|
2047
|
-
const [rows, setRows] =
|
|
2381
|
+
const [rows, setRows] = useState6(stdout?.rows ?? 40);
|
|
2048
2382
|
useEffect5(() => {
|
|
2049
2383
|
if (!stdout) return;
|
|
2050
2384
|
const onResize = () => setRows(stdout.rows ?? 40);
|
|
@@ -2057,11 +2391,12 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2057
2391
|
const statsHeight = rows - logsHeight - 2;
|
|
2058
2392
|
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
2059
2393
|
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
2060
|
-
const [booted, setBooted] =
|
|
2394
|
+
const [booted, setBooted] = useState6(false);
|
|
2061
2395
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
2062
2396
|
const externals = useRef3([]);
|
|
2397
|
+
const socketServer = useRef3(null);
|
|
2063
2398
|
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2064
|
-
const [activeTip, setActiveTip] =
|
|
2399
|
+
const [activeTip, setActiveTip] = useState6(null);
|
|
2065
2400
|
const kb = useKeyBindings({
|
|
2066
2401
|
onQuit: () => {
|
|
2067
2402
|
void shutdown();
|
|
@@ -2072,6 +2407,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2072
2407
|
});
|
|
2073
2408
|
const shutdown = useCallback3(async () => {
|
|
2074
2409
|
lazyProxies.current.forEach((p) => p.destroy());
|
|
2410
|
+
await socketServer.current?.close();
|
|
2411
|
+
socketServer.current = null;
|
|
2075
2412
|
await pm.cleanup();
|
|
2076
2413
|
if (externals.current.length) {
|
|
2077
2414
|
await stopExternals(externals.current, platform, {
|
|
@@ -2084,6 +2421,110 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2084
2421
|
await logSink?.close();
|
|
2085
2422
|
process.exit(0);
|
|
2086
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;
|
|
2462
|
+
try {
|
|
2463
|
+
configPath = findConfigFile(baseCwd, cliArgs.configPath);
|
|
2464
|
+
} catch (e) {
|
|
2465
|
+
pm.pushLog("devup", `\u26A0 watch-config disabled: ${e.message}`, 5);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
pm.pushLog("devup", `\u{1F440} watching ${configPath}`, 12);
|
|
2469
|
+
let reloadInFlight = false;
|
|
2470
|
+
let reloadAgain = false;
|
|
2471
|
+
const reload = async () => {
|
|
2472
|
+
if (reloadInFlight) {
|
|
2473
|
+
reloadAgain = true;
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
reloadInFlight = true;
|
|
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
|
+
}
|
|
2516
|
+
}
|
|
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]);
|
|
2087
2528
|
useEffect5(() => {
|
|
2088
2529
|
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
2089
2530
|
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
@@ -2254,7 +2695,9 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2254
2695
|
height: logsHeight,
|
|
2255
2696
|
focused: kb.panel === "logs",
|
|
2256
2697
|
scrollOffset: kb.logsScrollOffset,
|
|
2257
|
-
resetScroll: kb.resetLogsScroll
|
|
2698
|
+
resetScroll: kb.resetLogsScroll,
|
|
2699
|
+
levelFilter: kb.levelFilter,
|
|
2700
|
+
filteredColorIdx: kb.logFilter ? pm.states.get(kb.logFilter)?.colorIdx ?? null : null
|
|
2258
2701
|
}
|
|
2259
2702
|
),
|
|
2260
2703
|
/* @__PURE__ */ jsx6(
|
|
@@ -2267,7 +2710,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2267
2710
|
height: statsHeight,
|
|
2268
2711
|
focused: kb.panel === "stats",
|
|
2269
2712
|
scrollOffset: kb.statsScrollOffset,
|
|
2270
|
-
resetScroll: kb.resetStatsScroll
|
|
2713
|
+
resetScroll: kb.resetStatsScroll,
|
|
2714
|
+
verbose: kb.verboseStats
|
|
2271
2715
|
}
|
|
2272
2716
|
),
|
|
2273
2717
|
kb.modal === "filter" && /* @__PURE__ */ jsx6(ServiceList, { title: "Filter by service", services: pm.states, onSelect: handleFilterSelect, onClose: () => kb.setModal("none") }),
|
|
@@ -2279,23 +2723,23 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2279
2723
|
}
|
|
2280
2724
|
|
|
2281
2725
|
// src/process/log-sink.ts
|
|
2282
|
-
import { existsSync as
|
|
2283
|
-
import { join as
|
|
2284
|
-
import { homedir as
|
|
2726
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, renameSync, createWriteStream } from "fs";
|
|
2727
|
+
import { join as join7, dirname as dirname6 } from "path";
|
|
2728
|
+
import { homedir as homedir3 } from "os";
|
|
2285
2729
|
var LogSink = class {
|
|
2286
2730
|
dir;
|
|
2287
2731
|
rotateOnStart;
|
|
2288
2732
|
streams = /* @__PURE__ */ new Map();
|
|
2289
2733
|
seen = /* @__PURE__ */ new Set();
|
|
2290
2734
|
constructor(opts) {
|
|
2291
|
-
const root = opts.rootDir ??
|
|
2292
|
-
this.dir =
|
|
2735
|
+
const root = opts.rootDir ?? join7(homedir3(), ".devup", "logs");
|
|
2736
|
+
this.dir = join7(root, sanitize2(opts.projectName));
|
|
2293
2737
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
2294
|
-
|
|
2738
|
+
mkdirSync5(this.dir, { recursive: true });
|
|
2295
2739
|
}
|
|
2296
2740
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
2297
2741
|
pathFor(svcName) {
|
|
2298
|
-
return
|
|
2742
|
+
return join7(this.dir, `${sanitize2(svcName)}.log`);
|
|
2299
2743
|
}
|
|
2300
2744
|
write(svcName, line) {
|
|
2301
2745
|
const stream = this.streamFor(svcName);
|
|
@@ -2314,9 +2758,9 @@ var LogSink = class {
|
|
|
2314
2758
|
let s = this.streams.get(svcName);
|
|
2315
2759
|
if (s) return s;
|
|
2316
2760
|
const file = this.pathFor(svcName);
|
|
2317
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2761
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync12(file)) {
|
|
2318
2762
|
try {
|
|
2319
|
-
|
|
2763
|
+
mkdirSync5(dirname6(file), { recursive: true });
|
|
2320
2764
|
renameSync(file, file + ".prev");
|
|
2321
2765
|
} catch {
|
|
2322
2766
|
}
|
|
@@ -2502,8 +2946,8 @@ function defineConfig(config) {
|
|
|
2502
2946
|
// src/index.ts
|
|
2503
2947
|
function readVersion() {
|
|
2504
2948
|
try {
|
|
2505
|
-
const here =
|
|
2506
|
-
const pkgPath =
|
|
2949
|
+
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
2950
|
+
const pkgPath = join8(here, "..", "package.json");
|
|
2507
2951
|
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "unknown";
|
|
2508
2952
|
} catch {
|
|
2509
2953
|
return "unknown";
|
|
@@ -2554,6 +2998,11 @@ async function main() {
|
|
|
2554
2998
|
${formatValidationErrors(errors)}`);
|
|
2555
2999
|
process.exit(1);
|
|
2556
3000
|
}
|
|
3001
|
+
const warnings = collectWarnings(config);
|
|
3002
|
+
if (warnings.length) {
|
|
3003
|
+
console.warn(`\u26A0 Config warnings:
|
|
3004
|
+
${formatValidationWarnings(warnings)}`);
|
|
3005
|
+
}
|
|
2557
3006
|
let services;
|
|
2558
3007
|
try {
|
|
2559
3008
|
services = filterServices(config.services, cliArgs, config);
|
|
@@ -2566,7 +3015,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2566
3015
|
process.exit(1);
|
|
2567
3016
|
}
|
|
2568
3017
|
const platform = await detectPlatform();
|
|
2569
|
-
const envFile = config.envFile ?
|
|
3018
|
+
const envFile = config.envFile ? join8(cwd, config.envFile) : join8(cwd, ".env");
|
|
2570
3019
|
const env = parseEnvFile(envFile, process.env);
|
|
2571
3020
|
if (config.env) {
|
|
2572
3021
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -2583,7 +3032,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2583
3032
|
routes: config.proxy.routes,
|
|
2584
3033
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
2585
3034
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
2586
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
3035
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join8(homedir4(), ".traefik", "traefik_conf.yaml")
|
|
2587
3036
|
};
|
|
2588
3037
|
}
|
|
2589
3038
|
if (cliArgs.dryRun) {
|