@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/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 dirname6, join as join7 } from "path";
7
+ import { dirname as dirname7, join as join8 } from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
- import { homedir as homedir3 } from "os";
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 useState5, useCallback as useCallback3, useRef as useRef3 } from "react";
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 LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll }) {
1493
- const filtered = filter ? logs.filter((l) => l.svcName === filter) : logs;
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 ? "cyan" : "gray", height, children: [
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 = searchTerm && line.toLowerCase().includes(searchTerm.toLowerCase());
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
- return /* @__PURE__ */ jsxs2(Text2, { children: [
1562
- indicator,
1563
- " ",
1564
- /* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
1565
- " ",
1566
- String(st.svc.port).padStart(5),
1567
- " ",
1568
- /* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
1569
- " ",
1570
- (stat?.cpu ?? "-").padStart(6),
1571
- " ",
1572
- (stat?.mem ?? "-").padStart(8),
1573
- " ",
1574
- String(st.errors).padStart(3),
1575
- " ",
1576
- String(st.restarts).padStart(3),
1577
- " ",
1578
- up.padStart(6)
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 useState3, useMemo } from "react";
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 = useMemo(
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] = useState3(0);
1749
- const [query, setQuery] = useState3("");
1750
- const names = useMemo(() => {
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 useState4 } from "react";
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] = useState4("");
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] = useState5(stdout?.rows ?? 40);
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] = useState5(false);
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] = useState5(null);
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 existsSync10, mkdirSync as mkdirSync4, renameSync, createWriteStream } from "fs";
2283
- import { join as join6, dirname as dirname5 } from "path";
2284
- import { homedir as homedir2 } from "os";
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 ?? join6(homedir2(), ".devup", "logs");
2292
- this.dir = join6(root, sanitize2(opts.projectName));
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
- mkdirSync4(this.dir, { recursive: true });
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 join6(this.dir, `${sanitize2(svcName)}.log`);
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) && existsSync10(file)) {
2761
+ if (this.rotateOnStart && !this.seen.has(svcName) && existsSync12(file)) {
2318
2762
  try {
2319
- mkdirSync4(dirname5(file), { recursive: true });
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 = dirname6(fileURLToPath2(import.meta.url));
2506
- const pkgPath = join7(here, "..", "package.json");
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 ? join7(cwd, config.envFile) : join7(cwd, ".env");
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 ?? join7(homedir3(), ".traefik", "traefik_conf.yaml")
3035
+ confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join8(homedir4(), ".traefik", "traefik_conf.yaml")
2587
3036
  };
2588
3037
  }
2589
3038
  if (cliArgs.dryRun) {