@gachlab/devup 0.3.0 → 0.5.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 +23 -3
- package/dist/config/cli.d.ts +1 -0
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +805 -307
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/subcommands.d.ts +20 -0
- package/dist/orchestrator/subcommands.d.ts.map +1 -0
- package/dist/process/manager.d.ts +3 -0
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/tui/App.d.ts +4 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts +2 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/dist/tui/ServiceList.d.ts.map +1 -1
- package/dist/tui/StatsPanel.d.ts +5 -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/tui/tips.d.ts +17 -0
- package/dist/tui/tips.d.ts.map +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
+
import { dirname as dirname6, join as join7 } from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
|
+
import { homedir as homedir3 } from "os";
|
|
8
10
|
|
|
9
11
|
// src/config/loader.ts
|
|
10
12
|
import { existsSync } from "fs";
|
|
@@ -112,6 +114,19 @@ function validateConfig(config, cwd) {
|
|
|
112
114
|
if (svc.cwd && !existsSync2(resolve2(cwd, svc.cwd))) {
|
|
113
115
|
errors.push({ field: `services[${svc.name}].cwd`, message: `Directory not found: ${svc.cwd}` });
|
|
114
116
|
}
|
|
117
|
+
if (svc.errorPattern !== void 0) {
|
|
118
|
+
if (typeof svc.errorPattern !== "string" || !svc.errorPattern.length) {
|
|
119
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `errorPattern must be a non-empty string` });
|
|
120
|
+
} else {
|
|
121
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(svc.errorPattern);
|
|
122
|
+
try {
|
|
123
|
+
if (slashed) new RegExp(slashed[1], slashed[2] || "i");
|
|
124
|
+
else new RegExp(svc.errorPattern, "i");
|
|
125
|
+
} catch (e) {
|
|
126
|
+
errors.push({ field: `services[${svc.name}].errorPattern`, message: `Invalid regex: ${e.message}` });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
115
130
|
if (svc.readyPattern !== void 0) {
|
|
116
131
|
if (typeof svc.readyPattern !== "string" || !svc.readyPattern.length) {
|
|
117
132
|
errors.push({ field: `services[${svc.name}].readyPattern`, message: `readyPattern must be a non-empty string` });
|
|
@@ -136,6 +151,9 @@ function validateConfig(config, cwd) {
|
|
|
136
151
|
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
137
152
|
errors.push({ field: `services[${svc.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type} (must be "tcp" or "http")` });
|
|
138
153
|
}
|
|
154
|
+
if (hc.startPeriod !== void 0 && (typeof hc.startPeriod !== "number" || hc.startPeriod < 0)) {
|
|
155
|
+
errors.push({ field: `services[${svc.name}].healthCheck.startPeriod`, message: `startPeriod must be a non-negative number (seconds), got ${hc.startPeriod}` });
|
|
156
|
+
}
|
|
139
157
|
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
140
158
|
errors.push({ field: `services[${svc.name}].healthCheck.path`, message: `healthCheck.path must start with "/": got "${hc.path}"` });
|
|
141
159
|
}
|
|
@@ -221,6 +239,44 @@ function formatValidationErrors(errors) {
|
|
|
221
239
|
// src/config/cli.ts
|
|
222
240
|
var DEFAULT_LAZY_TIMEOUT = 10;
|
|
223
241
|
var DEFAULT_ONCE_TIMEOUT = 90;
|
|
242
|
+
var USAGE = `devup \u2014 terminal UI dev stack runner
|
|
243
|
+
|
|
244
|
+
Usage: devup [options]
|
|
245
|
+
|
|
246
|
+
Service selection:
|
|
247
|
+
--only apis | webs Start only APIs or only webs
|
|
248
|
+
--services a,b,c Start only the named services
|
|
249
|
+
--profile <name> Start the services in a named profile (see ROADMAP)
|
|
250
|
+
--skip a,b,c Start everything except these
|
|
251
|
+
--config <path> Use a custom config file
|
|
252
|
+
|
|
253
|
+
Lazy mode:
|
|
254
|
+
--lazy Enable lazy mode (default)
|
|
255
|
+
--no-lazy Start every service immediately
|
|
256
|
+
--timeout <minutes> Idle timeout for lazy services. Default: 10
|
|
257
|
+
|
|
258
|
+
Reverse proxy:
|
|
259
|
+
--proxy Enable proxy config generation
|
|
260
|
+
--proxy-host <host> Override the target host (Docker/local)
|
|
261
|
+
--proxy-conf <path> Override the generated config file path
|
|
262
|
+
--proxy-tls Enable TLS in the generated config (default)
|
|
263
|
+
--no-proxy-tls Disable TLS
|
|
264
|
+
--proxy-entrypoint <n> Override entrypoint name (Traefik only)
|
|
265
|
+
|
|
266
|
+
CI / scripting:
|
|
267
|
+
--dry-run Print the resolved boot plan and exit
|
|
268
|
+
--once Boot, wait for readiness, exit 0/1 (no TUI)
|
|
269
|
+
--once-timeout <s> Max seconds to wait in --once mode. Default: 90
|
|
270
|
+
|
|
271
|
+
Log files:
|
|
272
|
+
--no-log-file Disable persistent log files
|
|
273
|
+
--log-dir <path> Override log root (default: ~/.devup/logs)
|
|
274
|
+
|
|
275
|
+
Other:
|
|
276
|
+
-h, --help Show this help and exit
|
|
277
|
+
-v, --version Show version and exit
|
|
278
|
+
|
|
279
|
+
See https://github.com/gachlab/devup for the full documentation.`;
|
|
224
280
|
function parseCliArgs(argv) {
|
|
225
281
|
const args = {
|
|
226
282
|
skip: [],
|
|
@@ -344,6 +400,394 @@ function filterServices(services, args, config) {
|
|
|
344
400
|
return result;
|
|
345
401
|
}
|
|
346
402
|
|
|
403
|
+
// src/orchestrator/subcommands.ts
|
|
404
|
+
import { spawn } from "child_process";
|
|
405
|
+
import { createReadStream, watchFile, unwatchFile, existsSync as existsSync4, statSync } from "fs";
|
|
406
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
407
|
+
import { join as join3, dirname } from "path";
|
|
408
|
+
import { fileURLToPath } from "url";
|
|
409
|
+
import { homedir } from "os";
|
|
410
|
+
import { createInterface } from "readline";
|
|
411
|
+
|
|
412
|
+
// src/process/health.ts
|
|
413
|
+
import net from "net";
|
|
414
|
+
import http from "http";
|
|
415
|
+
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
416
|
+
return new Promise((resolve4) => {
|
|
417
|
+
const socket = new net.Socket();
|
|
418
|
+
socket.setTimeout(timeoutMs);
|
|
419
|
+
socket.once("connect", () => {
|
|
420
|
+
socket.destroy();
|
|
421
|
+
resolve4(true);
|
|
422
|
+
});
|
|
423
|
+
socket.once("error", () => {
|
|
424
|
+
socket.destroy();
|
|
425
|
+
resolve4(false);
|
|
426
|
+
});
|
|
427
|
+
socket.once("timeout", () => {
|
|
428
|
+
socket.destroy();
|
|
429
|
+
resolve4(false);
|
|
430
|
+
});
|
|
431
|
+
socket.connect(port, host);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function checkHttp(port, opts = {}) {
|
|
435
|
+
const path = opts.path ?? "/";
|
|
436
|
+
const host = opts.host ?? "127.0.0.1";
|
|
437
|
+
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
438
|
+
const accept = (code) => {
|
|
439
|
+
if (opts.expect === void 0) return code >= 200 && code < 300;
|
|
440
|
+
if (Array.isArray(opts.expect)) return opts.expect.includes(code);
|
|
441
|
+
return code === opts.expect;
|
|
442
|
+
};
|
|
443
|
+
return new Promise((resolve4) => {
|
|
444
|
+
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
445
|
+
const ok = typeof res.statusCode === "number" && accept(res.statusCode);
|
|
446
|
+
res.resume();
|
|
447
|
+
resolve4(ok);
|
|
448
|
+
});
|
|
449
|
+
req.on("error", () => resolve4(false));
|
|
450
|
+
req.on("timeout", () => {
|
|
451
|
+
req.destroy();
|
|
452
|
+
resolve4(false);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function checkHealth(port, hc) {
|
|
457
|
+
if (hc?.type === "http") {
|
|
458
|
+
return checkHttp(port, {
|
|
459
|
+
path: hc.path,
|
|
460
|
+
expect: hc.expect,
|
|
461
|
+
host: hc.host,
|
|
462
|
+
timeoutMs: hc.timeoutMs
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
466
|
+
}
|
|
467
|
+
function waitForPort(port, opts = {}) {
|
|
468
|
+
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
469
|
+
return new Promise((resolve4) => {
|
|
470
|
+
const start = Date.now();
|
|
471
|
+
const check = () => {
|
|
472
|
+
checkPort(port).then((ok) => {
|
|
473
|
+
if (ok) return resolve4(true);
|
|
474
|
+
if (Date.now() - start > timeout) return resolve4(false);
|
|
475
|
+
setTimeout(check, interval);
|
|
476
|
+
});
|
|
477
|
+
};
|
|
478
|
+
check();
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
function deriveHealth(isUp, currentStatus) {
|
|
482
|
+
if (currentStatus === "idle") return "idle";
|
|
483
|
+
if (isUp) return "up";
|
|
484
|
+
return currentStatus === "starting" ? "wait" : "down";
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/utils.ts
|
|
488
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
|
|
489
|
+
import { createHash } from "crypto";
|
|
490
|
+
import { join as join2 } from "path";
|
|
491
|
+
function parseEnvFile(filePath, baseEnv = {}) {
|
|
492
|
+
const env = { ...baseEnv };
|
|
493
|
+
if (!existsSync3(filePath)) return env;
|
|
494
|
+
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
495
|
+
const trimmed = line.trim();
|
|
496
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
497
|
+
const eqIdx = trimmed.indexOf("=");
|
|
498
|
+
if (eqIdx === -1) continue;
|
|
499
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
500
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
501
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
502
|
+
val = val.slice(1, -1);
|
|
503
|
+
}
|
|
504
|
+
if (!env[key]) env[key] = val;
|
|
505
|
+
}
|
|
506
|
+
return env;
|
|
507
|
+
}
|
|
508
|
+
function nextRamBannerVisibility(usagePct, previousVisible, highWatermark = 80, lowWatermark = 75) {
|
|
509
|
+
if (usagePct >= highWatermark) return true;
|
|
510
|
+
if (usagePct < lowWatermark) return false;
|
|
511
|
+
return previousVisible;
|
|
512
|
+
}
|
|
513
|
+
function redactSecrets(env) {
|
|
514
|
+
if (!env) return {};
|
|
515
|
+
const out = {};
|
|
516
|
+
for (const [k, v] of Object.entries(env)) {
|
|
517
|
+
out[k] = /secret|token|password|api[_-]?key|auth/i.test(k) ? "***" : v;
|
|
518
|
+
}
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
function detectLogLevel(line) {
|
|
522
|
+
const l = line.toLowerCase();
|
|
523
|
+
if (/\b(?:error|err|fail(?:ed|ure|ures|s)?|fatal|exception|crash(?:ed|es)?)\b/.test(l) || /❌|✗|⛔/.test(line)) return "error";
|
|
524
|
+
if (/\b(?:warn(?:ed|ing|s|ings)?|deprec)\b/.test(l) || /⚠/.test(line)) return "warn";
|
|
525
|
+
return "info";
|
|
526
|
+
}
|
|
527
|
+
function compileSearchPattern(term) {
|
|
528
|
+
if (!term) return null;
|
|
529
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(term);
|
|
530
|
+
if (slashed) {
|
|
531
|
+
const flags = slashed[2].includes("i") ? slashed[2] : slashed[2] + "i";
|
|
532
|
+
try {
|
|
533
|
+
const re = new RegExp(slashed[1], flags);
|
|
534
|
+
return { test: (l) => re.test(l), regex: re };
|
|
535
|
+
} catch {
|
|
536
|
+
const lower2 = term.toLowerCase();
|
|
537
|
+
return { test: (l) => l.toLowerCase().includes(lower2), invalid: true };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const lower = term.toLowerCase();
|
|
541
|
+
return { test: (l) => l.toLowerCase().includes(lower) };
|
|
542
|
+
}
|
|
543
|
+
function fmtUptime(ms) {
|
|
544
|
+
if (!ms || ms < 0) return "-";
|
|
545
|
+
const s = Math.floor(ms / 1e3);
|
|
546
|
+
if (s < 60) return `${s}s`;
|
|
547
|
+
const m = Math.floor(s / 60);
|
|
548
|
+
if (m < 60) return `${m}m${s % 60}s`;
|
|
549
|
+
const h = Math.floor(m / 60);
|
|
550
|
+
if (h < 24) return `${h}h${m % 60}m`;
|
|
551
|
+
const d = Math.floor(h / 24);
|
|
552
|
+
return `${d}d${h % 24}h`;
|
|
553
|
+
}
|
|
554
|
+
function needsInstall(fullCwd) {
|
|
555
|
+
const nm = join2(fullCwd, "node_modules");
|
|
556
|
+
if (!existsSync3(nm)) return true;
|
|
557
|
+
try {
|
|
558
|
+
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
559
|
+
const stampFile = join2(nm, ".install-stamp");
|
|
560
|
+
if (existsSync3(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
function writeInstallStamp(fullCwd) {
|
|
566
|
+
try {
|
|
567
|
+
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
568
|
+
writeFileSync(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
569
|
+
} catch {
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function sortServiceNames(names, sortMode, statsMap, procState) {
|
|
573
|
+
if (sortMode === "name") return names.slice().sort();
|
|
574
|
+
return names.slice().sort((a, b) => {
|
|
575
|
+
if (sortMode === "mem") {
|
|
576
|
+
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
577
|
+
}
|
|
578
|
+
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function groupByPhase(services) {
|
|
582
|
+
const phases = {};
|
|
583
|
+
for (const s of services) {
|
|
584
|
+
(phases[s.phase] ??= []).push(s);
|
|
585
|
+
}
|
|
586
|
+
return phases;
|
|
587
|
+
}
|
|
588
|
+
function buildProcessArgs(svc) {
|
|
589
|
+
const extra = svc.nodeArgs ?? [];
|
|
590
|
+
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
591
|
+
if (svc.cmd === "node") return [`--max-old-space-size=${svc.maxMem}`, ...extra, ...svc.args];
|
|
592
|
+
return [...extra, ...svc.args];
|
|
593
|
+
}
|
|
594
|
+
function buildProcessEnv(svc, baseEnv) {
|
|
595
|
+
const env = { ...baseEnv, ...svc.extraEnv ?? {} };
|
|
596
|
+
if (svc.maxMem && svc.cmd !== "node") {
|
|
597
|
+
const existing = env["NODE_OPTIONS"] ?? "";
|
|
598
|
+
const flag = `--max-old-space-size=${svc.maxMem}`;
|
|
599
|
+
if (!existing.includes("max-old-space-size")) {
|
|
600
|
+
env["NODE_OPTIONS"] = existing ? `${existing} ${flag}` : flag;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return env;
|
|
604
|
+
}
|
|
605
|
+
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
606
|
+
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
607
|
+
const cpuDelta = totalCpuSec - prevCpu;
|
|
608
|
+
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
609
|
+
}
|
|
610
|
+
var tagColors = [
|
|
611
|
+
"cyan",
|
|
612
|
+
"yellow",
|
|
613
|
+
"green",
|
|
614
|
+
"magenta",
|
|
615
|
+
"blue",
|
|
616
|
+
"red",
|
|
617
|
+
"#5faf5f",
|
|
618
|
+
"#d7af5f",
|
|
619
|
+
"#5f87d7",
|
|
620
|
+
"#af5faf",
|
|
621
|
+
"#5fd7d7",
|
|
622
|
+
"#d75f5f",
|
|
623
|
+
"white"
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
// src/orchestrator/subcommands.ts
|
|
627
|
+
var KNOWN = /* @__PURE__ */ new Set(["logs", "install", "status", "help"]);
|
|
628
|
+
function detectSubcommand(argv) {
|
|
629
|
+
const first = argv[0];
|
|
630
|
+
return first && KNOWN.has(first) ? first : null;
|
|
631
|
+
}
|
|
632
|
+
function logRoot(config, override) {
|
|
633
|
+
const root = override ?? join3(homedir(), ".devup", "logs");
|
|
634
|
+
return join3(root, sanitize(config.name));
|
|
635
|
+
}
|
|
636
|
+
function sanitize(name) {
|
|
637
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
638
|
+
}
|
|
639
|
+
async function runLogs(argv, opts) {
|
|
640
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
641
|
+
const follow = argv.includes("--follow") || argv.includes("-f");
|
|
642
|
+
const svcArg = argv.find((a) => !a.startsWith("-"));
|
|
643
|
+
if (!svcArg) {
|
|
644
|
+
out("usage: devup logs <service> [--follow]");
|
|
645
|
+
return 1;
|
|
646
|
+
}
|
|
647
|
+
const knownSvcs = opts.config.services.map((s) => s.name);
|
|
648
|
+
if (!knownSvcs.includes(svcArg)) {
|
|
649
|
+
out(`Unknown service "${svcArg}". Known: ${knownSvcs.join(", ")}`);
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
652
|
+
const file = join3(logRoot(opts.config, opts.logDir), `${sanitize(svcArg)}.log`);
|
|
653
|
+
if (!existsSync4(file)) {
|
|
654
|
+
out(`No log file yet for "${svcArg}" (${file})`);
|
|
655
|
+
return follow ? await followFile(file, out) : 1;
|
|
656
|
+
}
|
|
657
|
+
await streamFile(file, out);
|
|
658
|
+
if (!follow) return 0;
|
|
659
|
+
return await followFile(file, out, statSync(file).size);
|
|
660
|
+
}
|
|
661
|
+
async function streamFile(file, out) {
|
|
662
|
+
return new Promise((resolve4, reject) => {
|
|
663
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: "utf8" }) });
|
|
664
|
+
rl.on("line", (l) => out(l));
|
|
665
|
+
rl.on("close", () => resolve4());
|
|
666
|
+
rl.on("error", reject);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
async function followFile(file, out, startAt = 0) {
|
|
670
|
+
let pos = startAt;
|
|
671
|
+
while (!existsSync4(file)) await new Promise((r) => setTimeout(r, 500));
|
|
672
|
+
return new Promise((resolve4) => {
|
|
673
|
+
const tick = async () => {
|
|
674
|
+
const size = statSync(file).size;
|
|
675
|
+
if (size > pos) {
|
|
676
|
+
await new Promise((res) => {
|
|
677
|
+
const rl = createInterface({ input: createReadStream(file, { encoding: "utf8", start: pos, end: size - 1 }) });
|
|
678
|
+
rl.on("line", (l) => out(l));
|
|
679
|
+
rl.on("close", () => {
|
|
680
|
+
pos = size;
|
|
681
|
+
res();
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
} else if (size < pos) {
|
|
685
|
+
pos = 0;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
watchFile(file, { interval: 500 }, () => {
|
|
689
|
+
void tick();
|
|
690
|
+
});
|
|
691
|
+
process.once("SIGINT", () => {
|
|
692
|
+
unwatchFile(file);
|
|
693
|
+
resolve4(0);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
async function runInstall(opts) {
|
|
698
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
699
|
+
const concurrency = opts.concurrency ?? 4;
|
|
700
|
+
const items = opts.config.services.map((s) => ({ name: s.name, cwd: join3(opts.baseCwd, s.cwd) }));
|
|
701
|
+
const queue = [...items];
|
|
702
|
+
const failed = [];
|
|
703
|
+
let inFlight = 0;
|
|
704
|
+
await new Promise((resolve4) => {
|
|
705
|
+
const pump = () => {
|
|
706
|
+
while (inFlight < concurrency && queue.length) {
|
|
707
|
+
const item = queue.shift();
|
|
708
|
+
inFlight++;
|
|
709
|
+
installOne(item.cwd, opts.env).then((ok) => {
|
|
710
|
+
inFlight--;
|
|
711
|
+
if (ok) out(`\u2713 ${item.name}`);
|
|
712
|
+
else {
|
|
713
|
+
failed.push(item.name);
|
|
714
|
+
out(`\u2717 ${item.name}`);
|
|
715
|
+
}
|
|
716
|
+
if (queue.length === 0 && inFlight === 0) resolve4();
|
|
717
|
+
else pump();
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
pump();
|
|
722
|
+
});
|
|
723
|
+
if (failed.length) {
|
|
724
|
+
out(`
|
|
725
|
+
failed: ${failed.join(", ")}`);
|
|
726
|
+
return 1;
|
|
727
|
+
}
|
|
728
|
+
out(`
|
|
729
|
+
${items.length} services up to date`);
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
function installOne(cwd, env) {
|
|
733
|
+
if (!existsSync4(cwd)) return Promise.resolve(false);
|
|
734
|
+
if (!needsInstall(cwd)) return Promise.resolve(true);
|
|
735
|
+
return new Promise((resolve4) => {
|
|
736
|
+
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
737
|
+
const proc = spawn(command, ["install"], { cwd, env, stdio: ["ignore", "ignore", "pipe"] });
|
|
738
|
+
proc.on("close", (code) => {
|
|
739
|
+
if (code === 0) {
|
|
740
|
+
writeInstallStamp(cwd);
|
|
741
|
+
resolve4(true);
|
|
742
|
+
} else resolve4(false);
|
|
743
|
+
});
|
|
744
|
+
proc.on("error", () => resolve4(false));
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
async function runStatus(opts) {
|
|
748
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
749
|
+
out(`${opts.config.icon ?? "\u{1F4E6}"} ${opts.config.name} \u2014 ${opts.config.services.length} services`);
|
|
750
|
+
out("");
|
|
751
|
+
const maxLen = Math.max(...opts.config.services.map((s) => s.name.length), 12);
|
|
752
|
+
out(`${"Service".padEnd(maxLen)} ${"Port".padStart(5)} ${"Type".padEnd(4)} Health`);
|
|
753
|
+
out("-".repeat(maxLen + 24));
|
|
754
|
+
for (const svc of opts.config.services) {
|
|
755
|
+
const up = await checkHealth(svc.port, svc.healthCheck);
|
|
756
|
+
const health = up ? "\u2713 up" : "\u2717 down";
|
|
757
|
+
out(`${svc.name.padEnd(maxLen)} ${String(svc.port).padStart(5)} ${svc.type.padEnd(4)} ${health}`);
|
|
758
|
+
}
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
function runHelp(argv, opts = {}) {
|
|
762
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
763
|
+
const sub = argv[0];
|
|
764
|
+
if (sub === "logs") {
|
|
765
|
+
out("Usage: devup logs <service> [--follow|-f]");
|
|
766
|
+
out(" Print the persisted log file for a service (works without devup running).");
|
|
767
|
+
out(" --follow tails new lines as they are appended.");
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
if (sub === "install") {
|
|
771
|
+
out("Usage: devup install");
|
|
772
|
+
out(" Run `npm install` across every service.cwd in parallel (max 4 at a time).");
|
|
773
|
+
out(" Skips services whose .install-stamp matches package.json hash.");
|
|
774
|
+
return 0;
|
|
775
|
+
}
|
|
776
|
+
if (sub === "status") {
|
|
777
|
+
out("Usage: devup status");
|
|
778
|
+
out(" For each service, probes its health-check endpoint and prints up/down.");
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
out("Subcommands:");
|
|
782
|
+
out(" devup logs <service> [--follow] Read the persisted log file");
|
|
783
|
+
out(" devup install Concurrent npm install across services");
|
|
784
|
+
out(" devup status Health check every service in config");
|
|
785
|
+
out(" devup help [<subcommand>] Show detailed help for a subcommand");
|
|
786
|
+
out("");
|
|
787
|
+
out("No subcommand \u2192 launch the interactive TUI.");
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
347
791
|
// src/platform/detect.ts
|
|
348
792
|
async function detectPlatform() {
|
|
349
793
|
switch (process.platform) {
|
|
@@ -365,8 +809,8 @@ async function detectPlatform() {
|
|
|
365
809
|
}
|
|
366
810
|
|
|
367
811
|
// src/proxy-config/traefik.ts
|
|
368
|
-
import { existsSync as
|
|
369
|
-
import { dirname } from "path";
|
|
812
|
+
import { existsSync as existsSync5, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
813
|
+
import { dirname as dirname2 } from "path";
|
|
370
814
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
371
815
|
var TraefikProvider = class {
|
|
372
816
|
name = "traefik";
|
|
@@ -403,9 +847,9 @@ ${svcs.join("\n")}
|
|
|
403
847
|
`;
|
|
404
848
|
}
|
|
405
849
|
write(content, opts) {
|
|
406
|
-
const dir =
|
|
407
|
-
if (!
|
|
408
|
-
|
|
850
|
+
const dir = dirname2(opts.confPath);
|
|
851
|
+
if (!existsSync5(dir)) mkdirSync(dir, { recursive: true });
|
|
852
|
+
writeFileSync2(opts.confPath, content);
|
|
409
853
|
}
|
|
410
854
|
clear(opts) {
|
|
411
855
|
this.write(EMPTY_CONFIG, opts);
|
|
@@ -413,8 +857,8 @@ ${svcs.join("\n")}
|
|
|
413
857
|
};
|
|
414
858
|
|
|
415
859
|
// src/proxy-config/nginx.ts
|
|
416
|
-
import { existsSync as
|
|
417
|
-
import { dirname as
|
|
860
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
861
|
+
import { dirname as dirname3 } from "path";
|
|
418
862
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
419
863
|
var NginxProvider = class {
|
|
420
864
|
name = "nginx";
|
|
@@ -451,9 +895,9 @@ var NginxProvider = class {
|
|
|
451
895
|
return blocks.join("\n\n") + "\n";
|
|
452
896
|
}
|
|
453
897
|
write(content, opts) {
|
|
454
|
-
const dir =
|
|
455
|
-
if (!
|
|
456
|
-
|
|
898
|
+
const dir = dirname3(opts.confPath);
|
|
899
|
+
if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
|
|
900
|
+
writeFileSync3(opts.confPath, content);
|
|
457
901
|
}
|
|
458
902
|
clear(opts) {
|
|
459
903
|
this.write(EMPTY_CONFIG2, opts);
|
|
@@ -461,8 +905,8 @@ var NginxProvider = class {
|
|
|
461
905
|
};
|
|
462
906
|
|
|
463
907
|
// src/proxy-config/caddy.ts
|
|
464
|
-
import { existsSync as
|
|
465
|
-
import { dirname as
|
|
908
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
909
|
+
import { dirname as dirname4 } from "path";
|
|
466
910
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
467
911
|
var CaddyProvider = class {
|
|
468
912
|
name = "caddy";
|
|
@@ -485,9 +929,9 @@ var CaddyProvider = class {
|
|
|
485
929
|
return blocks.join("\n\n") + "\n";
|
|
486
930
|
}
|
|
487
931
|
write(content, opts) {
|
|
488
|
-
const dir =
|
|
489
|
-
if (!
|
|
490
|
-
|
|
932
|
+
const dir = dirname4(opts.confPath);
|
|
933
|
+
if (!existsSync7(dir)) mkdirSync3(dir, { recursive: true });
|
|
934
|
+
writeFileSync4(opts.confPath, content);
|
|
491
935
|
}
|
|
492
936
|
clear(opts) {
|
|
493
937
|
this.write(EMPTY_CONFIG3, opts);
|
|
@@ -509,201 +953,23 @@ function detectProxyProvider(name) {
|
|
|
509
953
|
return factory();
|
|
510
954
|
}
|
|
511
955
|
|
|
512
|
-
// src/utils.ts
|
|
513
|
-
import { existsSync as existsSync6, readFileSync, writeFileSync as writeFileSync4 } from "fs";
|
|
514
|
-
import { createHash } from "crypto";
|
|
515
|
-
import { join as join2 } from "path";
|
|
516
|
-
function parseEnvFile(filePath, baseEnv = {}) {
|
|
517
|
-
const env = { ...baseEnv };
|
|
518
|
-
if (!existsSync6(filePath)) return env;
|
|
519
|
-
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
520
|
-
const trimmed = line.trim();
|
|
521
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
522
|
-
const eqIdx = trimmed.indexOf("=");
|
|
523
|
-
if (eqIdx === -1) continue;
|
|
524
|
-
const key = trimmed.slice(0, eqIdx).trim();
|
|
525
|
-
let val = trimmed.slice(eqIdx + 1).trim();
|
|
526
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
527
|
-
val = val.slice(1, -1);
|
|
528
|
-
}
|
|
529
|
-
if (!env[key]) env[key] = val;
|
|
530
|
-
}
|
|
531
|
-
return env;
|
|
532
|
-
}
|
|
533
|
-
function fmtUptime(ms) {
|
|
534
|
-
if (!ms || ms < 0) return "-";
|
|
535
|
-
const s = Math.floor(ms / 1e3);
|
|
536
|
-
if (s < 60) return `${s}s`;
|
|
537
|
-
const m = Math.floor(s / 60);
|
|
538
|
-
if (m < 60) return `${m}m${s % 60}s`;
|
|
539
|
-
const h = Math.floor(m / 60);
|
|
540
|
-
if (h < 24) return `${h}h${m % 60}m`;
|
|
541
|
-
const d = Math.floor(h / 24);
|
|
542
|
-
return `${d}d${h % 24}h`;
|
|
543
|
-
}
|
|
544
|
-
function needsInstall(fullCwd) {
|
|
545
|
-
const nm = join2(fullCwd, "node_modules");
|
|
546
|
-
if (!existsSync6(nm)) return true;
|
|
547
|
-
try {
|
|
548
|
-
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
549
|
-
const stampFile = join2(nm, ".install-stamp");
|
|
550
|
-
if (existsSync6(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
551
|
-
} catch {
|
|
552
|
-
}
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
|
-
function writeInstallStamp(fullCwd) {
|
|
556
|
-
try {
|
|
557
|
-
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
558
|
-
writeFileSync4(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
559
|
-
} catch {
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
function sortServiceNames(names, sortMode, statsMap, procState) {
|
|
563
|
-
if (sortMode === "name") return names.slice().sort();
|
|
564
|
-
return names.slice().sort((a, b) => {
|
|
565
|
-
if (sortMode === "mem") {
|
|
566
|
-
return (parseFloat(statsMap[b]?.mem ?? "0") || 0) - (parseFloat(statsMap[a]?.mem ?? "0") || 0);
|
|
567
|
-
}
|
|
568
|
-
return (procState[b]?.errors ?? 0) - (procState[a]?.errors ?? 0);
|
|
569
|
-
});
|
|
570
|
-
}
|
|
571
|
-
function groupByPhase(services) {
|
|
572
|
-
const phases = {};
|
|
573
|
-
for (const s of services) {
|
|
574
|
-
(phases[s.phase] ??= []).push(s);
|
|
575
|
-
}
|
|
576
|
-
return phases;
|
|
577
|
-
}
|
|
578
|
-
function buildProcessArgs(svc) {
|
|
579
|
-
const extra = svc.nodeArgs ?? [];
|
|
580
|
-
if (!svc.maxMem) return [...extra, ...svc.args];
|
|
581
|
-
if (svc.cmd === "node") return [`--max-old-space-size=${svc.maxMem}`, ...extra, ...svc.args];
|
|
582
|
-
return [...extra, ...svc.args];
|
|
583
|
-
}
|
|
584
|
-
function buildProcessEnv(svc, baseEnv) {
|
|
585
|
-
const env = { ...baseEnv, ...svc.extraEnv ?? {} };
|
|
586
|
-
if (svc.maxMem && svc.cmd !== "node") {
|
|
587
|
-
const existing = env["NODE_OPTIONS"] ?? "";
|
|
588
|
-
const flag = `--max-old-space-size=${svc.maxMem}`;
|
|
589
|
-
if (!existing.includes("max-old-space-size")) {
|
|
590
|
-
env["NODE_OPTIONS"] = existing ? `${existing} ${flag}` : flag;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return env;
|
|
594
|
-
}
|
|
595
|
-
function calcCpuPercent(totalCpuSec, prevCpu, prevTime) {
|
|
596
|
-
const elapsed = (Date.now() - prevTime) / 1e3;
|
|
597
|
-
const cpuDelta = totalCpuSec - prevCpu;
|
|
598
|
-
return elapsed > 0 ? cpuDelta / elapsed * 100 : 0;
|
|
599
|
-
}
|
|
600
|
-
var tagColors = [
|
|
601
|
-
"cyan",
|
|
602
|
-
"yellow",
|
|
603
|
-
"green",
|
|
604
|
-
"magenta",
|
|
605
|
-
"blue",
|
|
606
|
-
"red",
|
|
607
|
-
"#5faf5f",
|
|
608
|
-
"#d7af5f",
|
|
609
|
-
"#5f87d7",
|
|
610
|
-
"#af5faf",
|
|
611
|
-
"#5fd7d7",
|
|
612
|
-
"#d75f5f",
|
|
613
|
-
"white"
|
|
614
|
-
];
|
|
615
|
-
|
|
616
956
|
// src/tui/App.tsx
|
|
617
|
-
import { useEffect as useEffect5, useState as
|
|
957
|
+
import { useEffect as useEffect5, useState as useState6, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
618
958
|
import { Box as Box6, Text as Text6, useStdout } from "ink";
|
|
619
959
|
|
|
620
960
|
// src/tui/hooks/useProcessManager.ts
|
|
621
961
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
622
962
|
|
|
623
963
|
// src/process/manager.ts
|
|
624
|
-
import { spawn as
|
|
625
|
-
import {
|
|
626
|
-
|
|
627
|
-
// src/process/health.ts
|
|
628
|
-
import net from "net";
|
|
629
|
-
import http from "http";
|
|
630
|
-
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
631
|
-
return new Promise((resolve3) => {
|
|
632
|
-
const socket = new net.Socket();
|
|
633
|
-
socket.setTimeout(timeoutMs);
|
|
634
|
-
socket.once("connect", () => {
|
|
635
|
-
socket.destroy();
|
|
636
|
-
resolve3(true);
|
|
637
|
-
});
|
|
638
|
-
socket.once("error", () => {
|
|
639
|
-
socket.destroy();
|
|
640
|
-
resolve3(false);
|
|
641
|
-
});
|
|
642
|
-
socket.once("timeout", () => {
|
|
643
|
-
socket.destroy();
|
|
644
|
-
resolve3(false);
|
|
645
|
-
});
|
|
646
|
-
socket.connect(port, host);
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
function checkHttp(port, opts = {}) {
|
|
650
|
-
const path = opts.path ?? "/";
|
|
651
|
-
const host = opts.host ?? "127.0.0.1";
|
|
652
|
-
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
653
|
-
const accept = (code) => {
|
|
654
|
-
if (opts.expect === void 0) return code >= 200 && code < 300;
|
|
655
|
-
if (Array.isArray(opts.expect)) return opts.expect.includes(code);
|
|
656
|
-
return code === opts.expect;
|
|
657
|
-
};
|
|
658
|
-
return new Promise((resolve3) => {
|
|
659
|
-
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
660
|
-
const ok = typeof res.statusCode === "number" && accept(res.statusCode);
|
|
661
|
-
res.resume();
|
|
662
|
-
resolve3(ok);
|
|
663
|
-
});
|
|
664
|
-
req.on("error", () => resolve3(false));
|
|
665
|
-
req.on("timeout", () => {
|
|
666
|
-
req.destroy();
|
|
667
|
-
resolve3(false);
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
function checkHealth(port, hc) {
|
|
672
|
-
if (hc?.type === "http") {
|
|
673
|
-
return checkHttp(port, {
|
|
674
|
-
path: hc.path,
|
|
675
|
-
expect: hc.expect,
|
|
676
|
-
host: hc.host,
|
|
677
|
-
timeoutMs: hc.timeoutMs
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
return checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
681
|
-
}
|
|
682
|
-
function waitForPort(port, opts = {}) {
|
|
683
|
-
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
684
|
-
return new Promise((resolve3) => {
|
|
685
|
-
const start = Date.now();
|
|
686
|
-
const check = () => {
|
|
687
|
-
checkPort(port).then((ok) => {
|
|
688
|
-
if (ok) return resolve3(true);
|
|
689
|
-
if (Date.now() - start > timeout) return resolve3(false);
|
|
690
|
-
setTimeout(check, interval);
|
|
691
|
-
});
|
|
692
|
-
};
|
|
693
|
-
check();
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
function deriveHealth(isUp, currentStatus) {
|
|
697
|
-
if (currentStatus === "idle") return "idle";
|
|
698
|
-
if (isUp) return "up";
|
|
699
|
-
return currentStatus === "starting" ? "wait" : "down";
|
|
700
|
-
}
|
|
964
|
+
import { spawn as spawn3 } from "child_process";
|
|
965
|
+
import { existsSync as existsSync9 } from "fs";
|
|
966
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
701
967
|
|
|
702
968
|
// src/process/installer.ts
|
|
703
|
-
import { spawn } from "child_process";
|
|
704
|
-
import { existsSync as
|
|
969
|
+
import { spawn as spawn2 } from "child_process";
|
|
970
|
+
import { existsSync as existsSync8 } from "fs";
|
|
705
971
|
function installService(cwd, env, onLog) {
|
|
706
|
-
if (!
|
|
972
|
+
if (!existsSync8(cwd)) {
|
|
707
973
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
708
974
|
return Promise.resolve(false);
|
|
709
975
|
}
|
|
@@ -712,9 +978,9 @@ function installService(cwd, env, onLog) {
|
|
|
712
978
|
return Promise.resolve(true);
|
|
713
979
|
}
|
|
714
980
|
onLog?.("\u{1F4E6} npm install...");
|
|
715
|
-
return new Promise((
|
|
981
|
+
return new Promise((resolve4) => {
|
|
716
982
|
const command = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
717
|
-
const proc =
|
|
983
|
+
const proc = spawn2(command, ["install"], { cwd, env, stdio: ["ignore", "ignore", "pipe"] });
|
|
718
984
|
let stderr = "";
|
|
719
985
|
proc.stderr?.on("data", (d) => {
|
|
720
986
|
stderr += d.toString();
|
|
@@ -722,16 +988,16 @@ function installService(cwd, env, onLog) {
|
|
|
722
988
|
proc.on("close", (code) => {
|
|
723
989
|
if (code !== 0) {
|
|
724
990
|
onLog?.(`\u26A0 npm install failed: ${stderr.split("\n")[0]}`);
|
|
725
|
-
|
|
991
|
+
resolve4(false);
|
|
726
992
|
} else {
|
|
727
993
|
writeInstallStamp(cwd);
|
|
728
994
|
onLog?.("\u2705 dependencies ready");
|
|
729
|
-
|
|
995
|
+
resolve4(true);
|
|
730
996
|
}
|
|
731
997
|
});
|
|
732
998
|
proc.on("error", (err) => {
|
|
733
999
|
onLog?.(`\u26A0 spawn error: ${err.message}`);
|
|
734
|
-
|
|
1000
|
+
resolve4(false);
|
|
735
1001
|
});
|
|
736
1002
|
});
|
|
737
1003
|
}
|
|
@@ -749,6 +1015,26 @@ function compileReadyPattern(pattern) {
|
|
|
749
1015
|
return null;
|
|
750
1016
|
}
|
|
751
1017
|
}
|
|
1018
|
+
function extractWatchPaths(args) {
|
|
1019
|
+
const watchFlags = /* @__PURE__ */ new Set(["--watch", "--watch-path"]);
|
|
1020
|
+
const out = [];
|
|
1021
|
+
for (let i = 0; i < args.length; i++) {
|
|
1022
|
+
const a = args[i];
|
|
1023
|
+
if (watchFlags.has(a)) {
|
|
1024
|
+
const v = args[i + 1];
|
|
1025
|
+
if (v && !v.startsWith("-")) {
|
|
1026
|
+
out.push(v);
|
|
1027
|
+
i++;
|
|
1028
|
+
}
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const eq = a.indexOf("=");
|
|
1032
|
+
if (eq > 0 && watchFlags.has(a.slice(0, eq))) {
|
|
1033
|
+
out.push(a.slice(eq + 1));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
752
1038
|
function lineBuffer(onLine) {
|
|
753
1039
|
let buf = "";
|
|
754
1040
|
return {
|
|
@@ -783,12 +1069,12 @@ var ProcessManager = class {
|
|
|
783
1069
|
this.events = opts.events;
|
|
784
1070
|
}
|
|
785
1071
|
async install(svc, colorIdx) {
|
|
786
|
-
const cwd =
|
|
1072
|
+
const cwd = join4(this.baseCwd, svc.cwd);
|
|
787
1073
|
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
788
1074
|
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
789
1075
|
}
|
|
790
1076
|
async start(svc, colorIdx, isRestart = false) {
|
|
791
|
-
const cwd =
|
|
1077
|
+
const cwd = join4(this.baseCwd, svc.cwd);
|
|
792
1078
|
if (svc.type === "api") {
|
|
793
1079
|
const occupied = await checkPort(svc.port);
|
|
794
1080
|
if (occupied && !isRestart) {
|
|
@@ -804,8 +1090,14 @@ var ProcessManager = class {
|
|
|
804
1090
|
}
|
|
805
1091
|
}
|
|
806
1092
|
const args = buildProcessArgs(svc);
|
|
1093
|
+
const missingWatchPaths = extractWatchPaths(args).filter((p) => !existsSync9(resolve3(cwd, p)));
|
|
1094
|
+
if (missingWatchPaths.length) {
|
|
1095
|
+
this.log(svc.name, `\u26A0 missing watch paths: ${missingWatchPaths.join(", ")}`, colorIdx);
|
|
1096
|
+
this.recordCrashedState(svc, colorIdx);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
807
1099
|
const env = buildProcessEnv(svc, this.env);
|
|
808
|
-
const proc =
|
|
1100
|
+
const proc = spawn3(svc.cmd, args, { cwd, env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
809
1101
|
const prev = this.state.get(svc.name);
|
|
810
1102
|
const state = {
|
|
811
1103
|
svc,
|
|
@@ -831,12 +1123,14 @@ var ProcessManager = class {
|
|
|
831
1123
|
this.events.onStateChange(svc.name, state);
|
|
832
1124
|
}
|
|
833
1125
|
};
|
|
1126
|
+
const errorRegex = compileReadyPattern(svc.errorPattern);
|
|
1127
|
+
const countsAsError = (line) => errorRegex ? errorRegex.test(line) : true;
|
|
834
1128
|
const stdoutBuf = lineBuffer((line) => {
|
|
835
1129
|
markReadyIfMatch(line);
|
|
836
1130
|
this.log(svc.name, line, colorIdx);
|
|
837
1131
|
});
|
|
838
1132
|
const stderrBuf = lineBuffer((line) => {
|
|
839
|
-
state.errors += 1;
|
|
1133
|
+
if (countsAsError(line)) state.errors += 1;
|
|
840
1134
|
markReadyIfMatch(line);
|
|
841
1135
|
this.log(svc.name, line, colorIdx);
|
|
842
1136
|
});
|
|
@@ -877,29 +1171,29 @@ var ProcessManager = class {
|
|
|
877
1171
|
}
|
|
878
1172
|
runPreBuild(svc, cwd, colorIdx) {
|
|
879
1173
|
this.log(svc.name, `\u{1F528} preBuild: ${svc.preBuild}`, colorIdx);
|
|
880
|
-
return new Promise((
|
|
1174
|
+
return new Promise((resolve4) => {
|
|
881
1175
|
const isWin = process.platform === "win32";
|
|
882
1176
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
883
1177
|
const shellFlag = isWin ? "/c" : "-c";
|
|
884
1178
|
const env = buildProcessEnv(svc, this.env);
|
|
885
|
-
const child =
|
|
1179
|
+
const child = spawn3(shell, [shellFlag, svc.preBuild], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
886
1180
|
const outBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
887
1181
|
const errBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
888
1182
|
child.stdout?.on("data", (d) => outBuf.push(d));
|
|
889
1183
|
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
890
1184
|
child.on("error", (err) => {
|
|
891
1185
|
this.log(svc.name, `[build] \u274C ${err.message}`, colorIdx);
|
|
892
|
-
|
|
1186
|
+
resolve4(false);
|
|
893
1187
|
});
|
|
894
1188
|
child.on("close", (code) => {
|
|
895
1189
|
outBuf.flush();
|
|
896
1190
|
errBuf.flush();
|
|
897
1191
|
if (code === 0) {
|
|
898
1192
|
this.log(svc.name, `[build] \u2705 done`, colorIdx);
|
|
899
|
-
|
|
1193
|
+
resolve4(true);
|
|
900
1194
|
} else {
|
|
901
1195
|
this.log(svc.name, `[build] \u274C exited with code ${code}`, colorIdx);
|
|
902
|
-
|
|
1196
|
+
resolve4(false);
|
|
903
1197
|
}
|
|
904
1198
|
});
|
|
905
1199
|
});
|
|
@@ -909,7 +1203,7 @@ var ProcessManager = class {
|
|
|
909
1203
|
const isWin = process.platform === "win32";
|
|
910
1204
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
911
1205
|
const shellFlag = isWin ? "/c" : "-c";
|
|
912
|
-
const child =
|
|
1206
|
+
const child = spawn3(shell, [shellFlag, svc.watchBuild], {
|
|
913
1207
|
cwd,
|
|
914
1208
|
env,
|
|
915
1209
|
detached: true,
|
|
@@ -971,6 +1265,10 @@ var ProcessManager = class {
|
|
|
971
1265
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
972
1266
|
continue;
|
|
973
1267
|
}
|
|
1268
|
+
const startPeriodMs = (st.svc.healthCheck?.startPeriod ?? 0) * 1e3;
|
|
1269
|
+
if (startPeriodMs > 0 && st.startedAt && Date.now() - st.startedAt < startPeriodMs) {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
974
1272
|
const isUp = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
975
1273
|
const prev = st.health;
|
|
976
1274
|
st.health = deriveHealth(isUp, st.status);
|
|
@@ -992,14 +1290,14 @@ var ProcessManager = class {
|
|
|
992
1290
|
}
|
|
993
1291
|
for (const st of this.state.values()) this.stopWatchProc(st);
|
|
994
1292
|
const waits = procs.map(
|
|
995
|
-
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((
|
|
1293
|
+
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((resolve4) => p.once("close", () => resolve4()))
|
|
996
1294
|
);
|
|
997
1295
|
let timedOut = false;
|
|
998
1296
|
await Promise.race([
|
|
999
1297
|
Promise.all(waits),
|
|
1000
|
-
new Promise((
|
|
1298
|
+
new Promise((resolve4) => setTimeout(() => {
|
|
1001
1299
|
timedOut = true;
|
|
1002
|
-
|
|
1300
|
+
resolve4();
|
|
1003
1301
|
}, grace))
|
|
1004
1302
|
]);
|
|
1005
1303
|
if (timedOut) {
|
|
@@ -1010,7 +1308,7 @@ var ProcessManager = class {
|
|
|
1010
1308
|
}
|
|
1011
1309
|
await Promise.race([
|
|
1012
1310
|
Promise.all(waits),
|
|
1013
|
-
new Promise((
|
|
1311
|
+
new Promise((resolve4) => setTimeout(resolve4, 1e3))
|
|
1014
1312
|
]);
|
|
1015
1313
|
}
|
|
1016
1314
|
}
|
|
@@ -1042,7 +1340,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1042
1340
|
events: {
|
|
1043
1341
|
onLog: (svcName, text, colorIdx) => {
|
|
1044
1342
|
sinkRef.current?.write(svcName, text);
|
|
1045
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1343
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1046
1344
|
if (pausedRef.current) {
|
|
1047
1345
|
pendingLogsRef.current.push(entry);
|
|
1048
1346
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1100,7 +1398,7 @@ function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
|
1100
1398
|
}, []);
|
|
1101
1399
|
const pushLog = useCallback((svcName, text, colorIdx = 0) => {
|
|
1102
1400
|
sinkRef.current?.write(svcName, text);
|
|
1103
|
-
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1401
|
+
const entry = { svcName, text, colorIdx, ts: Date.now(), level: detectLogLevel(text) };
|
|
1104
1402
|
if (pausedRef.current) {
|
|
1105
1403
|
pendingLogsRef.current.push(entry);
|
|
1106
1404
|
if (pendingLogsRef.current.length > 5e3) {
|
|
@@ -1173,8 +1471,11 @@ function useKeyBindings(opts) {
|
|
|
1173
1471
|
sortIdx: 0,
|
|
1174
1472
|
proxyEnabled: false,
|
|
1175
1473
|
logsScrollOffset: 0,
|
|
1176
|
-
statsScrollOffset: 0
|
|
1474
|
+
statsScrollOffset: 0,
|
|
1475
|
+
levelFilter: "all",
|
|
1476
|
+
verboseStats: false
|
|
1177
1477
|
});
|
|
1478
|
+
const LEVEL_CYCLE = ["all", "error", "warn"];
|
|
1178
1479
|
const setModal = useCallback2((modal) => setState((s) => ({ ...s, modal })), []);
|
|
1179
1480
|
const setFilter = useCallback2((f) => setState((s) => ({ ...s, logFilter: f, modal: "none" })), []);
|
|
1180
1481
|
const setSearch = useCallback2((t) => setState((s) => ({ ...s, searchTerm: t, modal: "none" })), []);
|
|
@@ -1196,14 +1497,15 @@ function useKeyBindings(opts) {
|
|
|
1196
1497
|
else if (input === "r") setModal("restart");
|
|
1197
1498
|
else if (input === "o") setModal("open");
|
|
1198
1499
|
else if (input === "/") setModal("search");
|
|
1199
|
-
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null }));
|
|
1500
|
+
else if (input === "a") setState((s) => ({ ...s, logFilter: null, searchTerm: null, levelFilter: "all" }));
|
|
1200
1501
|
else if (input === "p") setState((s) => ({ ...s, logsPaused: !s.logsPaused }));
|
|
1201
1502
|
else if (input === "t") setState((s) => ({ ...s, showTimestamps: !s.showTimestamps }));
|
|
1202
1503
|
else if (input === "s") setState((s) => ({ ...s, sortIdx: (s.sortIdx + 1) % SORT_MODES.length }));
|
|
1203
1504
|
else if (input === "T") {
|
|
1204
1505
|
opts.onToggleProxy();
|
|
1205
1506
|
setState((s) => ({ ...s, proxyEnabled: !s.proxyEnabled }));
|
|
1206
|
-
}
|
|
1507
|
+
} else if (input === "L") setState((s) => ({ ...s, levelFilter: LEVEL_CYCLE[(LEVEL_CYCLE.indexOf(s.levelFilter) + 1) % LEVEL_CYCLE.length] }));
|
|
1508
|
+
else if (input === "v") setState((s) => ({ ...s, verboseStats: !s.verboseStats }));
|
|
1207
1509
|
}, { isActive });
|
|
1208
1510
|
return {
|
|
1209
1511
|
...state,
|
|
@@ -1245,11 +1547,12 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1245
1547
|
}
|
|
1246
1548
|
|
|
1247
1549
|
// src/tui/LogsPanel.tsx
|
|
1248
|
-
import { useEffect as useEffect3 } from "react";
|
|
1550
|
+
import { useEffect as useEffect3, useMemo } from "react";
|
|
1249
1551
|
import { Box, Text } from "ink";
|
|
1250
1552
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1251
|
-
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
1252
|
-
const
|
|
1553
|
+
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll, levelFilter = "all" }) {
|
|
1554
|
+
const byService = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
1555
|
+
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
1253
1556
|
const contentHeight = Math.max(1, height - 2);
|
|
1254
1557
|
const totalLines = filtered.length;
|
|
1255
1558
|
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
@@ -1260,11 +1563,14 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1260
1563
|
useEffect3(() => {
|
|
1261
1564
|
resetScroll();
|
|
1262
1565
|
}, [filter, searchTerm, resetScroll]);
|
|
1566
|
+
const matcher = useMemo(() => compileSearchPattern(searchTerm), [searchTerm]);
|
|
1263
1567
|
const scrolled = effectiveOffset > 0;
|
|
1264
1568
|
const label = [
|
|
1265
1569
|
"Logs",
|
|
1266
1570
|
filter ? `[${filter}]` : "",
|
|
1267
1571
|
searchTerm ? `/${searchTerm}` : "",
|
|
1572
|
+
matcher?.invalid ? "(invalid regex)" : "",
|
|
1573
|
+
levelFilter !== "all" ? `[level: ${levelFilter}${levelFilter === "warn" ? "+error" : ""}]` : "",
|
|
1268
1574
|
paused ? "[PAUSED]" : "",
|
|
1269
1575
|
scrolled ? "[SCROLL]" : "",
|
|
1270
1576
|
`${filtered.length} lines`,
|
|
@@ -1280,7 +1586,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1280
1586
|
const color = tagColors[entry.colorIdx % tagColors.length];
|
|
1281
1587
|
const ts = showTimestamps ? new Date(entry.ts).toLocaleTimeString("en-GB") + " " : "";
|
|
1282
1588
|
const line = entry.text;
|
|
1283
|
-
const isMatch =
|
|
1589
|
+
const isMatch = matcher ? matcher.test(line) : false;
|
|
1284
1590
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
1285
1591
|
showTimestamps && /* @__PURE__ */ jsx(Text, { dimColor: true, children: ts }),
|
|
1286
1592
|
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
@@ -1296,7 +1602,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1296
1602
|
}
|
|
1297
1603
|
|
|
1298
1604
|
// src/tui/StatsPanel.tsx
|
|
1299
|
-
import { useEffect as useEffect4 } from "react";
|
|
1605
|
+
import { useEffect as useEffect4, useState as useState3 } from "react";
|
|
1300
1606
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
1301
1607
|
import os from "os";
|
|
1302
1608
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -1306,29 +1612,71 @@ var H = {
|
|
|
1306
1612
|
down: { c: "\u25CF", color: "red" },
|
|
1307
1613
|
idle: { c: "\u25CB", color: "blue" }
|
|
1308
1614
|
};
|
|
1309
|
-
|
|
1310
|
-
|
|
1615
|
+
var MAX_RESTARTS2 = 3;
|
|
1616
|
+
function isCrashLooped(st) {
|
|
1617
|
+
return st.status === "crashed" && st.restarts >= MAX_RESTARTS2;
|
|
1618
|
+
}
|
|
1619
|
+
function Row({ name, st, stat, ml, verbose }) {
|
|
1620
|
+
const looped = isCrashLooped(st);
|
|
1621
|
+
const indicator = looped ? /* @__PURE__ */ jsx2(Text2, { color: "red", bold: true, children: "\u2716" }) : /* @__PURE__ */ jsx2(Text2, { color: (H[st.health] ?? H["down"]).color, children: (H[st.health] ?? H["down"]).c });
|
|
1311
1622
|
const color = tagColors[st.colorIdx % tagColors.length];
|
|
1312
|
-
const sc = st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1623
|
+
const sc = looped ? "red" : st.status === "running" ? "green" : st.status === "starting" ? "yellow" : st.status === "idle" ? "blue" : "red";
|
|
1624
|
+
const statusLabel = looped ? "looping" : st.status;
|
|
1313
1625
|
const up = st.startedAt ? fmtUptime(Date.now() - st.startedAt) : "-";
|
|
1314
|
-
|
|
1315
|
-
/* @__PURE__ */
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1626
|
+
if (!verbose) {
|
|
1627
|
+
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1628
|
+
indicator,
|
|
1629
|
+
" ",
|
|
1630
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1631
|
+
" ",
|
|
1632
|
+
String(st.svc.port).padStart(5),
|
|
1633
|
+
" ",
|
|
1634
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1635
|
+
" ",
|
|
1636
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1637
|
+
" ",
|
|
1638
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1639
|
+
" ",
|
|
1640
|
+
String(st.errors).padStart(3),
|
|
1641
|
+
" ",
|
|
1642
|
+
String(st.restarts).padStart(3),
|
|
1643
|
+
" ",
|
|
1644
|
+
up.padStart(6)
|
|
1645
|
+
] });
|
|
1646
|
+
}
|
|
1647
|
+
const resolvedArgs = buildProcessArgs(st.svc).join(" ");
|
|
1648
|
+
const env = redactSecrets(st.svc.extraEnv);
|
|
1649
|
+
const envStr = Object.entries(env).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
1650
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1651
|
+
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1652
|
+
indicator,
|
|
1653
|
+
" ",
|
|
1654
|
+
/* @__PURE__ */ jsx2(Text2, { color, children: name.padEnd(ml) }),
|
|
1655
|
+
" ",
|
|
1656
|
+
String(st.svc.port).padStart(5),
|
|
1657
|
+
" ",
|
|
1658
|
+
/* @__PURE__ */ jsx2(Text2, { color: sc, bold: looped, children: statusLabel.padEnd(8) }),
|
|
1659
|
+
" ",
|
|
1660
|
+
(stat?.cpu ?? "-").padStart(6),
|
|
1661
|
+
" ",
|
|
1662
|
+
(stat?.mem ?? "-").padStart(8),
|
|
1663
|
+
" ",
|
|
1664
|
+
String(st.errors).padStart(3),
|
|
1665
|
+
" ",
|
|
1666
|
+
String(st.restarts).padStart(3),
|
|
1667
|
+
" ",
|
|
1668
|
+
up.padStart(6)
|
|
1669
|
+
] }),
|
|
1670
|
+
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1671
|
+
" cmd: ",
|
|
1672
|
+
st.svc.cmd,
|
|
1673
|
+
" ",
|
|
1674
|
+
resolvedArgs
|
|
1675
|
+
] }),
|
|
1676
|
+
envStr && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1677
|
+
" env: ",
|
|
1678
|
+
envStr
|
|
1679
|
+
] })
|
|
1332
1680
|
] });
|
|
1333
1681
|
}
|
|
1334
1682
|
function ColHeader({ ml }) {
|
|
@@ -1347,7 +1695,7 @@ function ColHeader({ ml }) {
|
|
|
1347
1695
|
"Up".padStart(6)
|
|
1348
1696
|
] });
|
|
1349
1697
|
}
|
|
1350
|
-
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
1698
|
+
function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scrollOffset, resetScroll, verbose = false }) {
|
|
1351
1699
|
const names = [...states.keys()];
|
|
1352
1700
|
const stObj = Object.fromEntries([...states].map(([k, v]) => [k, { errors: v.errors }]));
|
|
1353
1701
|
const statsObj = Object.fromEntries([...stats].map(([k, v]) => [k, v]));
|
|
@@ -1385,6 +1733,13 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1385
1733
|
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
1386
1734
|
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1387
1735
|
const scrolled = effectiveOffset > 0;
|
|
1736
|
+
const loopedCount = [...states.values()].filter(isCrashLooped).length;
|
|
1737
|
+
const ramPct = parseFloat(usedGB) / parseFloat(totalGB) * 100;
|
|
1738
|
+
const [ramBanner, setRamBanner] = useState3(false);
|
|
1739
|
+
useEffect4(() => {
|
|
1740
|
+
setRamBanner((prev) => nextRamBannerVisibility(ramPct, prev));
|
|
1741
|
+
}, [ramPct]);
|
|
1742
|
+
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) : [];
|
|
1388
1743
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
1389
1744
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1390
1745
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
@@ -1392,6 +1747,11 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1392
1747
|
positionInfo
|
|
1393
1748
|
] }),
|
|
1394
1749
|
scrolled && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " [SCROLL]" }),
|
|
1750
|
+
loopedCount > 0 && /* @__PURE__ */ jsxs2(Text2, { color: "red", bold: true, children: [
|
|
1751
|
+
" \u26A0 ",
|
|
1752
|
+
loopedCount,
|
|
1753
|
+
" need attention"
|
|
1754
|
+
] }),
|
|
1395
1755
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1396
1756
|
" System: ",
|
|
1397
1757
|
cpus,
|
|
@@ -1421,6 +1781,14 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1421
1781
|
sortMode
|
|
1422
1782
|
] })
|
|
1423
1783
|
] }),
|
|
1784
|
+
ramBanner && /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
1785
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "yellow", bold: true, children: [
|
|
1786
|
+
" \u26A0 RAM ",
|
|
1787
|
+
ramPct.toFixed(0),
|
|
1788
|
+
"% \u2014 top: "
|
|
1789
|
+
] }),
|
|
1790
|
+
/* @__PURE__ */ jsx2(Text2, { color: "yellow", children: topConsumers.map((c) => `${c.name} ${c.mb.toFixed(0)}MB`).join(", ") })
|
|
1791
|
+
] }),
|
|
1424
1792
|
/* @__PURE__ */ jsxs2(Box2, { flexGrow: 1, children: [
|
|
1425
1793
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
1426
1794
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "cyan", children: [
|
|
@@ -1429,7 +1797,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1429
1797
|
")"
|
|
1430
1798
|
] }),
|
|
1431
1799
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1432
|
-
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1800
|
+
visibleApis.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1433
1801
|
] }),
|
|
1434
1802
|
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: 1, children: Array.from({ length: contentHeight }, (_, i) => /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2502" }, i)) }),
|
|
1435
1803
|
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, flexBasis: 0, children: [
|
|
@@ -1439,7 +1807,7 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
1439
1807
|
")"
|
|
1440
1808
|
] }),
|
|
1441
1809
|
/* @__PURE__ */ jsx2(ColHeader, { ml }),
|
|
1442
|
-
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml }, n))
|
|
1810
|
+
visibleWebs.map((n) => /* @__PURE__ */ jsx2(Row, { name: n, st: states.get(n), stat: stats.get(n), ml, verbose }, n))
|
|
1443
1811
|
] })
|
|
1444
1812
|
] })
|
|
1445
1813
|
] });
|
|
@@ -1464,6 +1832,8 @@ function StatusBar() {
|
|
|
1464
1832
|
" Clear ",
|
|
1465
1833
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "f" }),
|
|
1466
1834
|
" Filter ",
|
|
1835
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "L" }),
|
|
1836
|
+
" Level ",
|
|
1467
1837
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "a" }),
|
|
1468
1838
|
" All ",
|
|
1469
1839
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "r" }),
|
|
@@ -1478,48 +1848,86 @@ function StatusBar() {
|
|
|
1478
1848
|
" Pause ",
|
|
1479
1849
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "t" }),
|
|
1480
1850
|
" Time ",
|
|
1851
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "v" }),
|
|
1852
|
+
" Verbose ",
|
|
1481
1853
|
/* @__PURE__ */ jsx3(Text3, { bold: true, children: "T" }),
|
|
1482
1854
|
" Proxy"
|
|
1483
1855
|
] }) });
|
|
1484
1856
|
}
|
|
1485
1857
|
|
|
1486
1858
|
// src/tui/ServiceList.tsx
|
|
1487
|
-
import { useState as
|
|
1859
|
+
import { useState as useState4, useMemo as useMemo2 } from "react";
|
|
1488
1860
|
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
1489
1861
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1490
1862
|
function ServiceList({ title, services, onSelect, onClose, filterType }) {
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1863
|
+
const allNames = useMemo2(
|
|
1864
|
+
() => [...services.keys()].filter((n) => !filterType || services.get(n).svc.type === filterType),
|
|
1865
|
+
[services, filterType]
|
|
1866
|
+
);
|
|
1867
|
+
const [idx, setIdx] = useState4(0);
|
|
1868
|
+
const [query, setQuery] = useState4("");
|
|
1869
|
+
const names = useMemo2(() => {
|
|
1870
|
+
if (!query) return allNames;
|
|
1871
|
+
const q = query.toLowerCase();
|
|
1872
|
+
return allNames.filter((n) => n.toLowerCase().includes(q));
|
|
1873
|
+
}, [allNames, query]);
|
|
1874
|
+
const clamped = Math.min(idx, Math.max(0, names.length - 1));
|
|
1493
1875
|
useInput2((input, key) => {
|
|
1494
|
-
if (key.escape)
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1876
|
+
if (key.escape) {
|
|
1877
|
+
if (query) setQuery("");
|
|
1878
|
+
else onClose();
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (key.return) {
|
|
1882
|
+
if (names[clamped]) onSelect(names[clamped]);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (key.upArrow) {
|
|
1886
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (key.downArrow) {
|
|
1890
|
+
setIdx((i) => Math.min(names.length - 1, i + 1));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
if (key.backspace || key.delete) {
|
|
1894
|
+
setQuery((q) => q.slice(0, -1));
|
|
1895
|
+
setIdx(0);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (input && !key.ctrl && !key.meta && input.length === 1) {
|
|
1899
|
+
setQuery((q) => q + input);
|
|
1900
|
+
setIdx(0);
|
|
1901
|
+
}
|
|
1499
1902
|
}, { isActive: process.stdin.isTTY ?? false });
|
|
1500
1903
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
1501
1904
|
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
|
|
1502
1905
|
" ",
|
|
1503
1906
|
title,
|
|
1504
|
-
" "
|
|
1907
|
+
" ",
|
|
1908
|
+
query && /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
1909
|
+
"[",
|
|
1910
|
+
query,
|
|
1911
|
+
"]"
|
|
1912
|
+
] })
|
|
1505
1913
|
] }),
|
|
1506
|
-
names.map((name, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i ===
|
|
1914
|
+
names.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " (no matches) " }) : names.map((name, i) => /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { color: i === clamped ? "cyan" : void 0, inverse: i === clamped, children: [
|
|
1507
1915
|
" ",
|
|
1508
1916
|
name,
|
|
1509
1917
|
" :",
|
|
1510
1918
|
services.get(name).svc.port,
|
|
1511
1919
|
" "
|
|
1512
1920
|
] }) }, name)),
|
|
1513
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 navigate Enter select Esc close" })
|
|
1921
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "type to filter \u2191\u2193 navigate Enter select Esc clear/close" })
|
|
1514
1922
|
] });
|
|
1515
1923
|
}
|
|
1516
1924
|
|
|
1517
1925
|
// src/tui/SearchInput.tsx
|
|
1518
|
-
import { useState as
|
|
1926
|
+
import { useState as useState5 } from "react";
|
|
1519
1927
|
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
1520
1928
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1521
1929
|
function SearchInput({ onSubmit, onClose }) {
|
|
1522
|
-
const [value, setValue] =
|
|
1930
|
+
const [value, setValue] = useState5("");
|
|
1523
1931
|
useInput3((input, key) => {
|
|
1524
1932
|
if (key.escape) onClose();
|
|
1525
1933
|
else if (key.return) onSubmit(value.trim() || null);
|
|
@@ -1647,8 +2055,8 @@ function createLazyProxy(opts) {
|
|
|
1647
2055
|
}
|
|
1648
2056
|
|
|
1649
2057
|
// src/process/external.ts
|
|
1650
|
-
import { spawn as
|
|
1651
|
-
import { join as
|
|
2058
|
+
import { spawn as spawn4 } from "child_process";
|
|
2059
|
+
import { join as join5 } from "path";
|
|
1652
2060
|
var DEFAULT_START_TIMEOUT_S = 60;
|
|
1653
2061
|
async function startExternals(externals, opts) {
|
|
1654
2062
|
const procs = [];
|
|
@@ -1681,16 +2089,16 @@ async function stopExternals(procs, platform, opts = {}) {
|
|
|
1681
2089
|
if (pid) platform.killTree(pid);
|
|
1682
2090
|
if (svc.stopCmd) {
|
|
1683
2091
|
opts.onLog?.(svc.name, `\u{1F9F9} ${svc.stopCmd}`);
|
|
1684
|
-
await new Promise((
|
|
2092
|
+
await new Promise((resolve4) => {
|
|
1685
2093
|
const isWin = process.platform === "win32";
|
|
1686
2094
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1687
2095
|
const flag = isWin ? "/c" : "-c";
|
|
1688
|
-
const cwd = svc.cwd ?
|
|
2096
|
+
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1689
2097
|
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1690
|
-
const child =
|
|
1691
|
-
child.on("close", () =>
|
|
1692
|
-
child.on("error", () =>
|
|
1693
|
-
setTimeout(() =>
|
|
2098
|
+
const child = spawn4(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
2099
|
+
child.on("close", () => resolve4());
|
|
2100
|
+
child.on("error", () => resolve4());
|
|
2101
|
+
setTimeout(() => resolve4(), 1e4);
|
|
1694
2102
|
});
|
|
1695
2103
|
}
|
|
1696
2104
|
} catch {
|
|
@@ -1702,10 +2110,10 @@ function spawnExternal(svc, opts) {
|
|
|
1702
2110
|
const isWin = process.platform === "win32";
|
|
1703
2111
|
const shell = isWin ? "cmd.exe" : "sh";
|
|
1704
2112
|
const flag = isWin ? "/c" : "-c";
|
|
1705
|
-
const cwd = svc.cwd ?
|
|
2113
|
+
const cwd = svc.cwd ? join5(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1706
2114
|
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1707
2115
|
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
1708
|
-
const child =
|
|
2116
|
+
const child = spawn4(shell, [flag, svc.cmd], {
|
|
1709
2117
|
cwd,
|
|
1710
2118
|
env,
|
|
1711
2119
|
detached: true,
|
|
@@ -1726,11 +2134,36 @@ async function waitHealthy(svc, timeoutMs) {
|
|
|
1726
2134
|
return false;
|
|
1727
2135
|
}
|
|
1728
2136
|
|
|
2137
|
+
// src/tui/tips.ts
|
|
2138
|
+
function pickTip(state) {
|
|
2139
|
+
if (state.crashLoopedCount > 0 && !state.shown.has("crashed")) {
|
|
2140
|
+
return { id: "crashed", message: "tip: press r to restart, or check the log of the failing service" };
|
|
2141
|
+
}
|
|
2142
|
+
if (state.totalLogs > 1e3 && !state.hasSearch && !state.shown.has("search")) {
|
|
2143
|
+
return { id: "search", message: "tip: press / to search in logs" };
|
|
2144
|
+
}
|
|
2145
|
+
if (state.totalLogs > 500 && !state.hasFilter && !state.shown.has("filter")) {
|
|
2146
|
+
return { id: "filter", message: "tip: press f to filter logs by service" };
|
|
2147
|
+
}
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
1729
2151
|
// src/tui/App.tsx
|
|
1730
2152
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2153
|
+
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
2154
|
+
if (proxyActive && proxyOpts) {
|
|
2155
|
+
const sub = proxyOpts.routes[name];
|
|
2156
|
+
if (sub !== void 0) {
|
|
2157
|
+
const host = sub ? `${sub}.${proxyOpts.domain}` : proxyOpts.domain;
|
|
2158
|
+
const scheme = proxyOpts.tls ? "https" : "http";
|
|
2159
|
+
return `${scheme}://${host}`;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return `http://localhost:${port}`;
|
|
2163
|
+
}
|
|
1731
2164
|
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
1732
2165
|
const { stdout } = useStdout();
|
|
1733
|
-
const [rows, setRows] =
|
|
2166
|
+
const [rows, setRows] = useState6(stdout?.rows ?? 40);
|
|
1734
2167
|
useEffect5(() => {
|
|
1735
2168
|
if (!stdout) return;
|
|
1736
2169
|
const onResize = () => setRows(stdout.rows ?? 40);
|
|
@@ -1743,9 +2176,11 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1743
2176
|
const statsHeight = rows - logsHeight - 2;
|
|
1744
2177
|
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
1745
2178
|
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
1746
|
-
const [booted, setBooted] =
|
|
2179
|
+
const [booted, setBooted] = useState6(false);
|
|
1747
2180
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
1748
2181
|
const externals = useRef3([]);
|
|
2182
|
+
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2183
|
+
const [activeTip, setActiveTip] = useState6(null);
|
|
1749
2184
|
const kb = useKeyBindings({
|
|
1750
2185
|
onQuit: () => {
|
|
1751
2186
|
void shutdown();
|
|
@@ -1771,6 +2206,21 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1771
2206
|
useEffect5(() => {
|
|
1772
2207
|
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
1773
2208
|
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
2209
|
+
useEffect5(() => {
|
|
2210
|
+
const tip = pickTip({
|
|
2211
|
+
totalLogs: pm.logs.length,
|
|
2212
|
+
hasSearch: !!kb.searchTerm,
|
|
2213
|
+
hasFilter: !!kb.logFilter,
|
|
2214
|
+
crashLoopedCount: [...pm.states.values()].filter(isCrashLooped).length,
|
|
2215
|
+
shown: shownTips.current
|
|
2216
|
+
});
|
|
2217
|
+
if (tip && tip.id !== activeTip) {
|
|
2218
|
+
shownTips.current.add(tip.id);
|
|
2219
|
+
setActiveTip(tip.message);
|
|
2220
|
+
const timer = setTimeout(() => setActiveTip(null), 12e3);
|
|
2221
|
+
return () => clearTimeout(timer);
|
|
2222
|
+
}
|
|
2223
|
+
}, [pm.logs.length, pm.states, kb.searchTerm, kb.logFilter, activeTip]);
|
|
1774
2224
|
useProxySync(proxyProvider, proxyOpts, pm.states, kb.proxyEnabled);
|
|
1775
2225
|
useEffect5(() => {
|
|
1776
2226
|
if (booted || !pm.manager) return;
|
|
@@ -1885,23 +2335,32 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1885
2335
|
}, [pm, kb]);
|
|
1886
2336
|
const handleOpenSelect = useCallback3((name) => {
|
|
1887
2337
|
const st = pm.states.get(name);
|
|
1888
|
-
if (st)
|
|
2338
|
+
if (st) {
|
|
2339
|
+
const url = buildServiceUrl(name, st.svc.port, cliArgs.proxy, proxyOpts);
|
|
2340
|
+
platform.openBrowser(url);
|
|
2341
|
+
}
|
|
1889
2342
|
kb.setModal("none");
|
|
1890
|
-
}, [pm, platform, kb]);
|
|
2343
|
+
}, [pm, platform, kb, cliArgs.proxy, proxyOpts]);
|
|
1891
2344
|
const icon = config.icon ?? "\u{1F4E6}";
|
|
1892
2345
|
const modeLabel = cliArgs.lazy && config.lazy ? "lazy" : "normal";
|
|
1893
2346
|
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height: rows, children: [
|
|
1894
|
-
/* @__PURE__ */
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2347
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
2348
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
|
|
2349
|
+
" ",
|
|
2350
|
+
icon,
|
|
2351
|
+
" ",
|
|
2352
|
+
config.name,
|
|
2353
|
+
" \u2014 devup \u2014 ",
|
|
2354
|
+
services.length,
|
|
2355
|
+
" services (",
|
|
2356
|
+
modeLabel,
|
|
2357
|
+
") "
|
|
2358
|
+
] }),
|
|
2359
|
+
activeTip && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2360
|
+
" \xB7 ",
|
|
2361
|
+
activeTip
|
|
2362
|
+
] })
|
|
2363
|
+
] }),
|
|
1905
2364
|
/* @__PURE__ */ jsx6(
|
|
1906
2365
|
LogsPanel,
|
|
1907
2366
|
{
|
|
@@ -1914,7 +2373,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1914
2373
|
height: logsHeight,
|
|
1915
2374
|
focused: kb.panel === "logs",
|
|
1916
2375
|
scrollOffset: kb.logsScrollOffset,
|
|
1917
|
-
resetScroll: kb.resetLogsScroll
|
|
2376
|
+
resetScroll: kb.resetLogsScroll,
|
|
2377
|
+
levelFilter: kb.levelFilter
|
|
1918
2378
|
}
|
|
1919
2379
|
),
|
|
1920
2380
|
/* @__PURE__ */ jsx6(
|
|
@@ -1927,7 +2387,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1927
2387
|
height: statsHeight,
|
|
1928
2388
|
focused: kb.panel === "stats",
|
|
1929
2389
|
scrollOffset: kb.statsScrollOffset,
|
|
1930
|
-
resetScroll: kb.resetStatsScroll
|
|
2390
|
+
resetScroll: kb.resetStatsScroll,
|
|
2391
|
+
verbose: kb.verboseStats
|
|
1931
2392
|
}
|
|
1932
2393
|
),
|
|
1933
2394
|
kb.modal === "filter" && /* @__PURE__ */ jsx6(ServiceList, { title: "Filter by service", services: pm.states, onSelect: handleFilterSelect, onClose: () => kb.setModal("none") }),
|
|
@@ -1939,23 +2400,23 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1939
2400
|
}
|
|
1940
2401
|
|
|
1941
2402
|
// src/process/log-sink.ts
|
|
1942
|
-
import { existsSync as
|
|
1943
|
-
import { join as
|
|
1944
|
-
import { homedir } from "os";
|
|
2403
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync4, renameSync, createWriteStream } from "fs";
|
|
2404
|
+
import { join as join6, dirname as dirname5 } from "path";
|
|
2405
|
+
import { homedir as homedir2 } from "os";
|
|
1945
2406
|
var LogSink = class {
|
|
1946
2407
|
dir;
|
|
1947
2408
|
rotateOnStart;
|
|
1948
2409
|
streams = /* @__PURE__ */ new Map();
|
|
1949
2410
|
seen = /* @__PURE__ */ new Set();
|
|
1950
2411
|
constructor(opts) {
|
|
1951
|
-
const root = opts.rootDir ??
|
|
1952
|
-
this.dir =
|
|
2412
|
+
const root = opts.rootDir ?? join6(homedir2(), ".devup", "logs");
|
|
2413
|
+
this.dir = join6(root, sanitize2(opts.projectName));
|
|
1953
2414
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1954
2415
|
mkdirSync4(this.dir, { recursive: true });
|
|
1955
2416
|
}
|
|
1956
2417
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1957
2418
|
pathFor(svcName) {
|
|
1958
|
-
return
|
|
2419
|
+
return join6(this.dir, `${sanitize2(svcName)}.log`);
|
|
1959
2420
|
}
|
|
1960
2421
|
write(svcName, line) {
|
|
1961
2422
|
const stream = this.streamFor(svcName);
|
|
@@ -1974,9 +2435,9 @@ var LogSink = class {
|
|
|
1974
2435
|
let s = this.streams.get(svcName);
|
|
1975
2436
|
if (s) return s;
|
|
1976
2437
|
const file = this.pathFor(svcName);
|
|
1977
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2438
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync10(file)) {
|
|
1978
2439
|
try {
|
|
1979
|
-
mkdirSync4(
|
|
2440
|
+
mkdirSync4(dirname5(file), { recursive: true });
|
|
1980
2441
|
renameSync(file, file + ".prev");
|
|
1981
2442
|
} catch {
|
|
1982
2443
|
}
|
|
@@ -1989,7 +2450,7 @@ var LogSink = class {
|
|
|
1989
2450
|
return s;
|
|
1990
2451
|
}
|
|
1991
2452
|
};
|
|
1992
|
-
function
|
|
2453
|
+
function sanitize2(name) {
|
|
1993
2454
|
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
1994
2455
|
}
|
|
1995
2456
|
|
|
@@ -2160,9 +2621,46 @@ function defineConfig(config) {
|
|
|
2160
2621
|
}
|
|
2161
2622
|
|
|
2162
2623
|
// src/index.ts
|
|
2624
|
+
function readVersion() {
|
|
2625
|
+
try {
|
|
2626
|
+
const here = dirname6(fileURLToPath2(import.meta.url));
|
|
2627
|
+
const pkgPath = join7(here, "..", "package.json");
|
|
2628
|
+
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "unknown";
|
|
2629
|
+
} catch {
|
|
2630
|
+
return "unknown";
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2163
2633
|
async function main() {
|
|
2634
|
+
const raw = process.argv.slice(2);
|
|
2635
|
+
if (raw.includes("-v") || raw.includes("--version")) {
|
|
2636
|
+
console.log(readVersion());
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (raw.includes("-h") || raw.includes("--help")) {
|
|
2640
|
+
console.log(USAGE);
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
const subcmd = detectSubcommand(raw);
|
|
2644
|
+
if (subcmd === "help") {
|
|
2645
|
+
process.exit(runHelp(raw.slice(1)));
|
|
2646
|
+
}
|
|
2164
2647
|
const cwd = process.cwd();
|
|
2165
|
-
const cliArgs = parseCliArgs(
|
|
2648
|
+
const cliArgs = parseCliArgs(raw);
|
|
2649
|
+
if (subcmd) {
|
|
2650
|
+
const subArgs = raw.slice(1);
|
|
2651
|
+
let cfgPath;
|
|
2652
|
+
try {
|
|
2653
|
+
cfgPath = findConfigFile(cwd, cliArgs.configPath);
|
|
2654
|
+
} catch (e) {
|
|
2655
|
+
console.error(`\u274C ${e.message}`);
|
|
2656
|
+
process.exit(1);
|
|
2657
|
+
}
|
|
2658
|
+
const cfg = await loadConfig(cfgPath);
|
|
2659
|
+
const subOpts = { config: cfg, baseCwd: cwd, env: process.env, logDir: cliArgs.logDir };
|
|
2660
|
+
if (subcmd === "logs") process.exit(await runLogs(subArgs, subOpts));
|
|
2661
|
+
if (subcmd === "install") process.exit(await runInstall(subOpts));
|
|
2662
|
+
if (subcmd === "status") process.exit(await runStatus(subOpts));
|
|
2663
|
+
}
|
|
2166
2664
|
let configPath;
|
|
2167
2665
|
try {
|
|
2168
2666
|
configPath = findConfigFile(cwd, cliArgs.configPath);
|
|
@@ -2189,7 +2687,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2189
2687
|
process.exit(1);
|
|
2190
2688
|
}
|
|
2191
2689
|
const platform = await detectPlatform();
|
|
2192
|
-
const envFile = config.envFile ?
|
|
2690
|
+
const envFile = config.envFile ? join7(cwd, config.envFile) : join7(cwd, ".env");
|
|
2193
2691
|
const env = parseEnvFile(envFile, process.env);
|
|
2194
2692
|
if (config.env) {
|
|
2195
2693
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -2206,7 +2704,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2206
2704
|
routes: config.proxy.routes,
|
|
2207
2705
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
2208
2706
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
2209
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
2707
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join7(homedir3(), ".traefik", "traefik_conf.yaml")
|
|
2210
2708
|
};
|
|
2211
2709
|
}
|
|
2212
2710
|
if (cliArgs.dryRun) {
|