@gachlab/devup 0.1.1 → 0.2.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 +82 -0
- package/README.md +87 -11
- package/dist/config/cli.d.ts +5 -0
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/types.d.ts +13 -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 +642 -179
- package/dist/index.js.map +1 -1
- package/dist/lazy/proxy.d.ts.map +1 -1
- package/dist/orchestrator/dry-run.d.ts +15 -0
- package/dist/orchestrator/dry-run.d.ts.map +1 -0
- package/dist/orchestrator/once.d.ts +19 -0
- package/dist/orchestrator/once.d.ts.map +1 -0
- package/dist/process/health.d.ts +10 -1
- package/dist/process/health.d.ts.map +1 -1
- package/dist/process/installer.d.ts +0 -10
- package/dist/process/installer.d.ts.map +1 -1
- package/dist/process/log-sink.d.ts +23 -0
- package/dist/process/log-sink.d.ts.map +1 -0
- package/dist/process/manager.d.ts +5 -3
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/proxy-config/caddy.d.ts +10 -0
- package/dist/proxy-config/caddy.d.ts.map +1 -0
- package/dist/proxy-config/detect.d.ts.map +1 -1
- package/dist/proxy-config/nginx.d.ts +10 -0
- package/dist/proxy-config/nginx.d.ts.map +1 -0
- package/dist/tui/App.d.ts +3 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/dist/tui/StatsPanel.d.ts.map +1 -1
- package/dist/tui/hooks/useKeyBindings.d.ts.map +1 -1
- package/dist/tui/hooks/useProcessManager.d.ts +6 -3
- package/dist/tui/hooks/useProcessManager.d.ts.map +1 -1
- package/dist/tui/hooks/useProxySync.d.ts.map +1 -1
- package/dist/utils.d.ts +0 -5
- package/dist/utils.d.ts.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import { join as
|
|
7
|
-
import { homedir } from "os";
|
|
6
|
+
import { join as join5 } from "path";
|
|
7
|
+
import { homedir as homedir2 } from "os";
|
|
8
8
|
|
|
9
9
|
// src/config/loader.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -48,6 +48,30 @@ async function loadConfig(configPath) {
|
|
|
48
48
|
// src/config/validator.ts
|
|
49
49
|
import { existsSync as existsSync2 } from "fs";
|
|
50
50
|
import { resolve as resolve2 } from "path";
|
|
51
|
+
|
|
52
|
+
// src/lazy/classifier.ts
|
|
53
|
+
var LAZY_PORT_OFFSET = 1e4;
|
|
54
|
+
function classifyServices(services, config) {
|
|
55
|
+
const alwaysOnSet = new Set(config?.alwaysOn ?? []);
|
|
56
|
+
const alwaysOn = [];
|
|
57
|
+
const lazy = [];
|
|
58
|
+
for (const svc of services) {
|
|
59
|
+
if (alwaysOnSet.has(svc.name)) alwaysOn.push(svc);
|
|
60
|
+
else lazy.push(svc);
|
|
61
|
+
}
|
|
62
|
+
return { alwaysOn, lazy };
|
|
63
|
+
}
|
|
64
|
+
function getLazyRealPort(originalPort) {
|
|
65
|
+
return originalPort + LAZY_PORT_OFFSET;
|
|
66
|
+
}
|
|
67
|
+
function rewriteServicePort(svc) {
|
|
68
|
+
const realPort = getLazyRealPort(svc.port);
|
|
69
|
+
const args = svc.args.map((a) => a === String(svc.port) ? String(realPort) : a);
|
|
70
|
+
const extraEnv = { ...svc.extraEnv, PORT_OVERRIDE: String(realPort) };
|
|
71
|
+
return { ...svc, port: realPort, args, extraEnv, realPort, originalPort: svc.port };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/config/validator.ts
|
|
51
75
|
function validateConfig(config, cwd) {
|
|
52
76
|
const errors = [];
|
|
53
77
|
if (!config.name?.trim()) {
|
|
@@ -88,6 +112,15 @@ function validateConfig(config, cwd) {
|
|
|
88
112
|
if (svc.cwd && !existsSync2(resolve2(cwd, svc.cwd))) {
|
|
89
113
|
errors.push({ field: `services[${svc.name}].cwd`, message: `Directory not found: ${svc.cwd}` });
|
|
90
114
|
}
|
|
115
|
+
if (svc.healthCheck) {
|
|
116
|
+
const hc = svc.healthCheck;
|
|
117
|
+
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
118
|
+
errors.push({ field: `services[${svc.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type} (must be "tcp" or "http")` });
|
|
119
|
+
}
|
|
120
|
+
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
121
|
+
errors.push({ field: `services[${svc.name}].healthCheck.path`, message: `healthCheck.path must start with "/": got "${hc.path}"` });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
91
124
|
}
|
|
92
125
|
if (config.lazy?.alwaysOn) {
|
|
93
126
|
for (const ref of config.lazy.alwaysOn) {
|
|
@@ -96,6 +129,22 @@ function validateConfig(config, cwd) {
|
|
|
96
129
|
}
|
|
97
130
|
}
|
|
98
131
|
}
|
|
132
|
+
if (config.lazy) {
|
|
133
|
+
const alwaysOn = new Set(config.lazy.alwaysOn ?? []);
|
|
134
|
+
const portToSvc = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const svc of config.services) portToSvc.set(svc.port, svc.name);
|
|
136
|
+
for (const svc of config.services) {
|
|
137
|
+
if (alwaysOn.has(svc.name)) continue;
|
|
138
|
+
const realPort = svc.port + LAZY_PORT_OFFSET;
|
|
139
|
+
const conflict = portToSvc.get(realPort);
|
|
140
|
+
if (conflict && conflict !== svc.name) {
|
|
141
|
+
errors.push({
|
|
142
|
+
field: `services[${svc.name}].port`,
|
|
143
|
+
message: `Lazy real port ${realPort} (= ${svc.port}+${LAZY_PORT_OFFSET}) collides with service ${conflict}`
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
99
148
|
if (config.proxy?.routes) {
|
|
100
149
|
for (const ref of Object.keys(config.proxy.routes)) {
|
|
101
150
|
if (!names.has(ref)) {
|
|
@@ -111,6 +160,7 @@ function formatValidationErrors(errors) {
|
|
|
111
160
|
|
|
112
161
|
// src/config/cli.ts
|
|
113
162
|
var DEFAULT_LAZY_TIMEOUT = 10;
|
|
163
|
+
var DEFAULT_ONCE_TIMEOUT = 90;
|
|
114
164
|
function parseCliArgs(argv) {
|
|
115
165
|
const args = {
|
|
116
166
|
skip: [],
|
|
@@ -118,7 +168,11 @@ function parseCliArgs(argv) {
|
|
|
118
168
|
lazyTimeout: DEFAULT_LAZY_TIMEOUT,
|
|
119
169
|
proxy: false,
|
|
120
170
|
proxyTls: true,
|
|
121
|
-
proxyEntrypoint: "websecure"
|
|
171
|
+
proxyEntrypoint: "websecure",
|
|
172
|
+
dryRun: false,
|
|
173
|
+
once: false,
|
|
174
|
+
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
175
|
+
logFile: true
|
|
122
176
|
};
|
|
123
177
|
for (let i = 0; i < argv.length; i++) {
|
|
124
178
|
const arg = argv[i];
|
|
@@ -171,6 +225,23 @@ function parseCliArgs(argv) {
|
|
|
171
225
|
args.proxyEntrypoint = next ?? "websecure";
|
|
172
226
|
i++;
|
|
173
227
|
break;
|
|
228
|
+
case "--dry-run":
|
|
229
|
+
args.dryRun = true;
|
|
230
|
+
break;
|
|
231
|
+
case "--once":
|
|
232
|
+
args.once = true;
|
|
233
|
+
break;
|
|
234
|
+
case "--once-timeout":
|
|
235
|
+
args.onceTimeout = parseInt(next ?? "", 10) || DEFAULT_ONCE_TIMEOUT;
|
|
236
|
+
i++;
|
|
237
|
+
break;
|
|
238
|
+
case "--no-log-file":
|
|
239
|
+
args.logFile = false;
|
|
240
|
+
break;
|
|
241
|
+
case "--log-dir":
|
|
242
|
+
args.logDir = next;
|
|
243
|
+
i++;
|
|
244
|
+
break;
|
|
174
245
|
}
|
|
175
246
|
}
|
|
176
247
|
return args;
|
|
@@ -268,9 +339,93 @@ ${svcs.join("\n")}
|
|
|
268
339
|
}
|
|
269
340
|
};
|
|
270
341
|
|
|
342
|
+
// src/proxy-config/nginx.ts
|
|
343
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
344
|
+
import { dirname as dirname2 } from "path";
|
|
345
|
+
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
346
|
+
var NginxProvider = class {
|
|
347
|
+
name = "nginx";
|
|
348
|
+
generate(services, opts) {
|
|
349
|
+
const blocks = [];
|
|
350
|
+
for (const [name, st] of services) {
|
|
351
|
+
if (st.health !== "up") continue;
|
|
352
|
+
const sub = opts.routes[name];
|
|
353
|
+
if (sub === void 0) continue;
|
|
354
|
+
const serverName = sub ? `${sub}.${opts.domain}` : opts.domain;
|
|
355
|
+
const port = st.realPort ?? st.port;
|
|
356
|
+
const listen = opts.tls ? "443 ssl" : "80";
|
|
357
|
+
const tlsBlock = opts.tls ? ` ssl_certificate /etc/nginx/certs/${serverName}.crt;
|
|
358
|
+
ssl_certificate_key /etc/nginx/certs/${serverName}.key;
|
|
359
|
+
` : "";
|
|
360
|
+
blocks.push(
|
|
361
|
+
`server {
|
|
362
|
+
listen ${listen};
|
|
363
|
+
server_name ${serverName};
|
|
364
|
+
` + tlsBlock + ` location / {
|
|
365
|
+
proxy_pass http://${opts.host}:${port};
|
|
366
|
+
proxy_set_header Host $host;
|
|
367
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
368
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
369
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
370
|
+
proxy_http_version 1.1;
|
|
371
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
372
|
+
proxy_set_header Connection "upgrade";
|
|
373
|
+
}
|
|
374
|
+
}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (!blocks.length) return EMPTY_CONFIG2;
|
|
378
|
+
return blocks.join("\n\n") + "\n";
|
|
379
|
+
}
|
|
380
|
+
write(content, opts) {
|
|
381
|
+
const dir = dirname2(opts.confPath);
|
|
382
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
383
|
+
writeFileSync2(opts.confPath, content);
|
|
384
|
+
}
|
|
385
|
+
clear(opts) {
|
|
386
|
+
this.write(EMPTY_CONFIG2, opts);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/proxy-config/caddy.ts
|
|
391
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
392
|
+
import { dirname as dirname3 } from "path";
|
|
393
|
+
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
394
|
+
var CaddyProvider = class {
|
|
395
|
+
name = "caddy";
|
|
396
|
+
generate(services, opts) {
|
|
397
|
+
const blocks = [];
|
|
398
|
+
for (const [name, st] of services) {
|
|
399
|
+
if (st.health !== "up") continue;
|
|
400
|
+
const sub = opts.routes[name];
|
|
401
|
+
if (sub === void 0) continue;
|
|
402
|
+
const host = sub ? `${sub}.${opts.domain}` : opts.domain;
|
|
403
|
+
const port = st.realPort ?? st.port;
|
|
404
|
+
const siteAddr = opts.tls ? host : `http://${host}`;
|
|
405
|
+
blocks.push(
|
|
406
|
+
`${siteAddr} {
|
|
407
|
+
reverse_proxy ${opts.host}:${port}
|
|
408
|
+
}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (!blocks.length) return EMPTY_CONFIG3;
|
|
412
|
+
return blocks.join("\n\n") + "\n";
|
|
413
|
+
}
|
|
414
|
+
write(content, opts) {
|
|
415
|
+
const dir = dirname3(opts.confPath);
|
|
416
|
+
if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
|
|
417
|
+
writeFileSync3(opts.confPath, content);
|
|
418
|
+
}
|
|
419
|
+
clear(opts) {
|
|
420
|
+
this.write(EMPTY_CONFIG3, opts);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
271
424
|
// src/proxy-config/detect.ts
|
|
272
425
|
var providers = {
|
|
273
|
-
traefik: () => new TraefikProvider()
|
|
426
|
+
traefik: () => new TraefikProvider(),
|
|
427
|
+
nginx: () => new NginxProvider(),
|
|
428
|
+
caddy: () => new CaddyProvider()
|
|
274
429
|
};
|
|
275
430
|
function detectProxyProvider(name) {
|
|
276
431
|
const factory = providers[name];
|
|
@@ -282,12 +437,12 @@ function detectProxyProvider(name) {
|
|
|
282
437
|
}
|
|
283
438
|
|
|
284
439
|
// src/utils.ts
|
|
285
|
-
import { existsSync as
|
|
440
|
+
import { existsSync as existsSync6, readFileSync, writeFileSync as writeFileSync4 } from "fs";
|
|
286
441
|
import { createHash } from "crypto";
|
|
287
442
|
import { join as join2 } from "path";
|
|
288
443
|
function parseEnvFile(filePath, baseEnv = {}) {
|
|
289
444
|
const env = { ...baseEnv };
|
|
290
|
-
if (!
|
|
445
|
+
if (!existsSync6(filePath)) return env;
|
|
291
446
|
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
292
447
|
const trimmed = line.trim();
|
|
293
448
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -309,15 +464,17 @@ function fmtUptime(ms) {
|
|
|
309
464
|
const m = Math.floor(s / 60);
|
|
310
465
|
if (m < 60) return `${m}m${s % 60}s`;
|
|
311
466
|
const h = Math.floor(m / 60);
|
|
312
|
-
return `${h}h${m % 60}m`;
|
|
467
|
+
if (h < 24) return `${h}h${m % 60}m`;
|
|
468
|
+
const d = Math.floor(h / 24);
|
|
469
|
+
return `${d}d${h % 24}h`;
|
|
313
470
|
}
|
|
314
471
|
function needsInstall(fullCwd) {
|
|
315
472
|
const nm = join2(fullCwd, "node_modules");
|
|
316
|
-
if (!
|
|
473
|
+
if (!existsSync6(nm)) return true;
|
|
317
474
|
try {
|
|
318
475
|
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
319
476
|
const stampFile = join2(nm, ".install-stamp");
|
|
320
|
-
if (
|
|
477
|
+
if (existsSync6(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
321
478
|
} catch {
|
|
322
479
|
}
|
|
323
480
|
return true;
|
|
@@ -325,7 +482,7 @@ function needsInstall(fullCwd) {
|
|
|
325
482
|
function writeInstallStamp(fullCwd) {
|
|
326
483
|
try {
|
|
327
484
|
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
328
|
-
|
|
485
|
+
writeFileSync4(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
329
486
|
} catch {
|
|
330
487
|
}
|
|
331
488
|
}
|
|
@@ -396,10 +553,11 @@ import { join as join3 } from "path";
|
|
|
396
553
|
|
|
397
554
|
// src/process/health.ts
|
|
398
555
|
import net from "net";
|
|
399
|
-
|
|
556
|
+
import http from "http";
|
|
557
|
+
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
400
558
|
return new Promise((resolve3) => {
|
|
401
559
|
const socket = new net.Socket();
|
|
402
|
-
socket.setTimeout(
|
|
560
|
+
socket.setTimeout(timeoutMs);
|
|
403
561
|
socket.once("connect", () => {
|
|
404
562
|
socket.destroy();
|
|
405
563
|
resolve3(true);
|
|
@@ -415,6 +573,39 @@ function checkPort(port, host = "127.0.0.1") {
|
|
|
415
573
|
socket.connect(port, host);
|
|
416
574
|
});
|
|
417
575
|
}
|
|
576
|
+
function checkHttp(port, opts = {}) {
|
|
577
|
+
const path = opts.path ?? "/";
|
|
578
|
+
const host = opts.host ?? "127.0.0.1";
|
|
579
|
+
const timeoutMs = opts.timeoutMs ?? 2e3;
|
|
580
|
+
const accept = (code) => {
|
|
581
|
+
if (opts.expect === void 0) return code >= 200 && code < 300;
|
|
582
|
+
if (Array.isArray(opts.expect)) return opts.expect.includes(code);
|
|
583
|
+
return code === opts.expect;
|
|
584
|
+
};
|
|
585
|
+
return new Promise((resolve3) => {
|
|
586
|
+
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
587
|
+
const ok = typeof res.statusCode === "number" && accept(res.statusCode);
|
|
588
|
+
res.resume();
|
|
589
|
+
resolve3(ok);
|
|
590
|
+
});
|
|
591
|
+
req.on("error", () => resolve3(false));
|
|
592
|
+
req.on("timeout", () => {
|
|
593
|
+
req.destroy();
|
|
594
|
+
resolve3(false);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function checkHealth(port, hc) {
|
|
599
|
+
if (hc?.type === "http") {
|
|
600
|
+
return checkHttp(port, {
|
|
601
|
+
path: hc.path,
|
|
602
|
+
expect: hc.expect,
|
|
603
|
+
host: hc.host,
|
|
604
|
+
timeoutMs: hc.timeoutMs
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
608
|
+
}
|
|
418
609
|
function waitForPort(port, opts = {}) {
|
|
419
610
|
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
420
611
|
return new Promise((resolve3) => {
|
|
@@ -437,9 +628,9 @@ function deriveHealth(isUp, currentStatus) {
|
|
|
437
628
|
|
|
438
629
|
// src/process/installer.ts
|
|
439
630
|
import { spawn } from "child_process";
|
|
440
|
-
import { existsSync as
|
|
631
|
+
import { existsSync as existsSync7 } from "fs";
|
|
441
632
|
function installService(cwd, env, onLog) {
|
|
442
|
-
if (!
|
|
633
|
+
if (!existsSync7(cwd)) {
|
|
443
634
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
444
635
|
return Promise.resolve(false);
|
|
445
636
|
}
|
|
@@ -475,6 +666,26 @@ function installService(cwd, env, onLog) {
|
|
|
475
666
|
// src/process/manager.ts
|
|
476
667
|
var MAX_RESTARTS = 3;
|
|
477
668
|
var BACKOFF_BASE_MS = 2e3;
|
|
669
|
+
function lineBuffer(onLine) {
|
|
670
|
+
let buf = "";
|
|
671
|
+
return {
|
|
672
|
+
push(chunk) {
|
|
673
|
+
buf += chunk.toString();
|
|
674
|
+
let idx;
|
|
675
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
676
|
+
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
677
|
+
buf = buf.slice(idx + 1);
|
|
678
|
+
if (line.length) onLine(line);
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
flush() {
|
|
682
|
+
if (buf.length) {
|
|
683
|
+
onLine(buf);
|
|
684
|
+
buf = "";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
478
689
|
var ProcessManager = class {
|
|
479
690
|
state = /* @__PURE__ */ new Map();
|
|
480
691
|
procs = /* @__PURE__ */ new Set();
|
|
@@ -488,9 +699,10 @@ var ProcessManager = class {
|
|
|
488
699
|
this.platform = opts.platform;
|
|
489
700
|
this.events = opts.events;
|
|
490
701
|
}
|
|
491
|
-
async install(svc) {
|
|
702
|
+
async install(svc, colorIdx) {
|
|
492
703
|
const cwd = join3(this.baseCwd, svc.cwd);
|
|
493
|
-
|
|
704
|
+
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
705
|
+
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
494
706
|
}
|
|
495
707
|
async start(svc, colorIdx, isRestart = false) {
|
|
496
708
|
const cwd = join3(this.baseCwd, svc.cwd);
|
|
@@ -520,11 +732,15 @@ var ProcessManager = class {
|
|
|
520
732
|
this.state.set(svc.name, state);
|
|
521
733
|
this.procs.add(proc);
|
|
522
734
|
this.events.onStateChange(svc.name, state);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
state.errors +=
|
|
526
|
-
this.log(svc.name,
|
|
735
|
+
const stdoutBuf = lineBuffer((line) => this.log(svc.name, line, colorIdx));
|
|
736
|
+
const stderrBuf = lineBuffer((line) => {
|
|
737
|
+
state.errors += 1;
|
|
738
|
+
this.log(svc.name, line, colorIdx);
|
|
527
739
|
});
|
|
740
|
+
proc.stdout?.on("data", (d) => stdoutBuf.push(d));
|
|
741
|
+
proc.stderr?.on("data", (d) => stderrBuf.push(d));
|
|
742
|
+
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
743
|
+
proc.stderr?.on("end", () => stderrBuf.flush());
|
|
528
744
|
proc.on("close", (code) => {
|
|
529
745
|
this.procs.delete(proc);
|
|
530
746
|
if (state.intentionalStop) {
|
|
@@ -562,7 +778,7 @@ var ProcessManager = class {
|
|
|
562
778
|
const st = this.state.get(name);
|
|
563
779
|
if (!st) return;
|
|
564
780
|
this.stop(name);
|
|
565
|
-
st.restarts
|
|
781
|
+
st.restarts = 0;
|
|
566
782
|
const delay = st.proc ? 1500 : 100;
|
|
567
783
|
await new Promise((r) => setTimeout(r, delay));
|
|
568
784
|
await this.start(st.svc, st.colorIdx, true);
|
|
@@ -574,39 +790,65 @@ var ProcessManager = class {
|
|
|
574
790
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
575
791
|
continue;
|
|
576
792
|
}
|
|
577
|
-
const
|
|
578
|
-
const isUp = await checkPort(port);
|
|
793
|
+
const isUp = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
579
794
|
const prev = st.health;
|
|
580
795
|
st.health = deriveHealth(isUp, st.status);
|
|
581
796
|
if (st.health === "up" && st.status === "starting") st.status = "running";
|
|
582
797
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
583
798
|
}
|
|
584
799
|
}
|
|
585
|
-
cleanup() {
|
|
586
|
-
|
|
800
|
+
async cleanup(opts = {}) {
|
|
801
|
+
const grace = opts.gracePeriodMs ?? 3e3;
|
|
802
|
+
const procs = [...this.procs];
|
|
803
|
+
if (!procs.length) return;
|
|
804
|
+
for (const proc of procs) {
|
|
805
|
+
const st = this.findStateByProc(proc);
|
|
806
|
+
if (st) st.intentionalStop = true;
|
|
587
807
|
if (proc.pid) this.platform.killTree(proc.pid);
|
|
588
808
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
809
|
+
const waits = procs.map(
|
|
810
|
+
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((resolve3) => p.once("close", () => resolve3()))
|
|
811
|
+
);
|
|
812
|
+
let timedOut = false;
|
|
813
|
+
await Promise.race([
|
|
814
|
+
Promise.all(waits),
|
|
815
|
+
new Promise((resolve3) => setTimeout(() => {
|
|
816
|
+
timedOut = true;
|
|
817
|
+
resolve3();
|
|
818
|
+
}, grace))
|
|
819
|
+
]);
|
|
820
|
+
if (timedOut) {
|
|
821
|
+
for (const proc of procs) {
|
|
822
|
+
if (proc.pid && proc.exitCode === null && proc.signalCode === null) {
|
|
823
|
+
this.platform.killTree(proc.pid, "SIGKILL");
|
|
824
|
+
}
|
|
592
825
|
}
|
|
593
|
-
|
|
826
|
+
await Promise.race([
|
|
827
|
+
Promise.all(waits),
|
|
828
|
+
new Promise((resolve3) => setTimeout(resolve3, 1e3))
|
|
829
|
+
]);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
findStateByProc(proc) {
|
|
833
|
+
for (const st of this.state.values()) if (st.proc === proc) return st;
|
|
834
|
+
return void 0;
|
|
594
835
|
}
|
|
595
836
|
log(name, text, colorIdx) {
|
|
596
837
|
this.events.onLog(name, text, colorIdx);
|
|
597
838
|
}
|
|
598
|
-
getColorIdx(name) {
|
|
599
|
-
return this.state.get(name)?.colorIdx ?? 0;
|
|
600
|
-
}
|
|
601
839
|
};
|
|
602
840
|
|
|
603
841
|
// src/tui/hooks/useProcessManager.ts
|
|
604
|
-
function useProcessManager(platform, baseCwd, env) {
|
|
842
|
+
function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
605
843
|
const [states, setStates] = useState(/* @__PURE__ */ new Map());
|
|
606
844
|
const [logs, setLogs] = useState([]);
|
|
607
845
|
const [stats, setStats] = useState(/* @__PURE__ */ new Map());
|
|
608
846
|
const mgrRef = useRef(null);
|
|
609
847
|
const prevCpu = useRef(/* @__PURE__ */ new Map());
|
|
848
|
+
const pausedRef = useRef(false);
|
|
849
|
+
const pendingLogsRef = useRef([]);
|
|
850
|
+
const sinkRef = useRef(logSink);
|
|
851
|
+
sinkRef.current = logSink;
|
|
610
852
|
useEffect(() => {
|
|
611
853
|
const mgr2 = new ProcessManager({
|
|
612
854
|
baseCwd,
|
|
@@ -614,9 +856,17 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
614
856
|
platform,
|
|
615
857
|
events: {
|
|
616
858
|
onLog: (svcName, text, colorIdx) => {
|
|
617
|
-
|
|
859
|
+
sinkRef.current?.write(svcName, text);
|
|
860
|
+
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
861
|
+
if (pausedRef.current) {
|
|
862
|
+
pendingLogsRef.current.push(entry);
|
|
863
|
+
if (pendingLogsRef.current.length > 5e3) {
|
|
864
|
+
pendingLogsRef.current = pendingLogsRef.current.slice(-5e3);
|
|
865
|
+
}
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
618
868
|
setLogs((prev) => {
|
|
619
|
-
const next =
|
|
869
|
+
const next = prev.concat(entry);
|
|
620
870
|
return next.length > 5e3 ? next.slice(-5e3) : next;
|
|
621
871
|
});
|
|
622
872
|
},
|
|
@@ -659,6 +909,21 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
659
909
|
return () => clearInterval(id);
|
|
660
910
|
}, [platform]);
|
|
661
911
|
const mgr = mgrRef.current;
|
|
912
|
+
const clearLogs = useCallback(() => {
|
|
913
|
+
pendingLogsRef.current = [];
|
|
914
|
+
setLogs([]);
|
|
915
|
+
}, []);
|
|
916
|
+
const setPaused = useCallback((paused) => {
|
|
917
|
+
pausedRef.current = paused;
|
|
918
|
+
if (!paused && pendingLogsRef.current.length) {
|
|
919
|
+
const flush = pendingLogsRef.current;
|
|
920
|
+
pendingLogsRef.current = [];
|
|
921
|
+
setLogs((prev) => {
|
|
922
|
+
const next = prev.concat(flush);
|
|
923
|
+
return next.length > 5e3 ? next.slice(-5e3) : next;
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}, []);
|
|
662
927
|
return {
|
|
663
928
|
states,
|
|
664
929
|
logs,
|
|
@@ -666,8 +931,10 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
666
931
|
start: useCallback((svc, colorIdx) => mgr?.start(svc, colorIdx), [mgr]),
|
|
667
932
|
stop: useCallback((name) => mgr?.stop(name), [mgr]),
|
|
668
933
|
restart: useCallback((name) => mgr?.restart(name), [mgr]),
|
|
669
|
-
install: useCallback((svc) => mgr?.install(svc), [mgr]),
|
|
934
|
+
install: useCallback((svc, colorIdx) => mgr?.install(svc, colorIdx), [mgr]),
|
|
670
935
|
cleanup: useCallback(() => mgr?.cleanup(), [mgr]),
|
|
936
|
+
clearLogs,
|
|
937
|
+
setPaused,
|
|
671
938
|
manager: mgr
|
|
672
939
|
};
|
|
673
940
|
}
|
|
@@ -676,6 +943,24 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
676
943
|
import { useInput } from "ink";
|
|
677
944
|
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
678
945
|
var SORT_MODES = ["name", "mem", "errors"];
|
|
946
|
+
function scrollBy(setState, delta) {
|
|
947
|
+
setState((s) => {
|
|
948
|
+
if (s.panel === "logs") {
|
|
949
|
+
const next2 = s.logsScrollOffset - delta;
|
|
950
|
+
return { ...s, logsScrollOffset: Math.max(0, next2) };
|
|
951
|
+
}
|
|
952
|
+
const next = s.statsScrollOffset + delta;
|
|
953
|
+
return { ...s, statsScrollOffset: Math.max(0, next) };
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
function scrollTo(setState, target) {
|
|
957
|
+
setState((s) => {
|
|
958
|
+
if (s.panel === "logs") {
|
|
959
|
+
return { ...s, logsScrollOffset: target === "top" ? Number.MAX_SAFE_INTEGER : 0 };
|
|
960
|
+
}
|
|
961
|
+
return { ...s, statsScrollOffset: target === "top" ? 0 : Number.MAX_SAFE_INTEGER };
|
|
962
|
+
});
|
|
963
|
+
}
|
|
679
964
|
function useKeyBindings(opts) {
|
|
680
965
|
const [state, setState] = useState2({
|
|
681
966
|
panel: "logs",
|
|
@@ -696,6 +981,14 @@ function useKeyBindings(opts) {
|
|
|
696
981
|
useInput((input, key) => {
|
|
697
982
|
if (state.modal !== "none") return;
|
|
698
983
|
if (input === "q" || key.ctrl && input === "c") opts.onQuit();
|
|
984
|
+
else if (key.ctrl && input === "a") scrollTo(setState, "top");
|
|
985
|
+
else if (key.ctrl && input === "e") scrollTo(setState, "bottom");
|
|
986
|
+
else if (key.ctrl && input === "b") scrollBy(setState, -10);
|
|
987
|
+
else if (key.ctrl && input === "f") scrollBy(setState, 10);
|
|
988
|
+
else if (key.upArrow) scrollBy(setState, -1);
|
|
989
|
+
else if (key.downArrow) scrollBy(setState, 1);
|
|
990
|
+
else if (input === "[") scrollBy(setState, -10);
|
|
991
|
+
else if (input === "]") scrollBy(setState, 10);
|
|
699
992
|
else if (input === "c") opts.onClearLogs();
|
|
700
993
|
else if (key.tab) setState((s) => ({ ...s, panel: s.panel === "logs" ? "stats" : "logs" }));
|
|
701
994
|
else if (input === "f") setModal("filter");
|
|
@@ -709,60 +1002,6 @@ function useKeyBindings(opts) {
|
|
|
709
1002
|
else if (input === "T") {
|
|
710
1003
|
opts.onToggleProxy();
|
|
711
1004
|
setState((s) => ({ ...s, proxyEnabled: !s.proxyEnabled }));
|
|
712
|
-
} else if (key.upArrow) {
|
|
713
|
-
setState((s) => {
|
|
714
|
-
if (s.panel === "logs") {
|
|
715
|
-
return { ...s, logsScrollOffset: Math.max(0, s.logsScrollOffset - 1) };
|
|
716
|
-
} else if (s.panel === "stats") {
|
|
717
|
-
return { ...s, statsScrollOffset: Math.max(0, s.statsScrollOffset - 1) };
|
|
718
|
-
}
|
|
719
|
-
return s;
|
|
720
|
-
});
|
|
721
|
-
} else if (key.downArrow) {
|
|
722
|
-
setState((s) => {
|
|
723
|
-
if (s.panel === "logs") {
|
|
724
|
-
return { ...s, logsScrollOffset: s.logsScrollOffset + 1 };
|
|
725
|
-
} else if (s.panel === "stats") {
|
|
726
|
-
return { ...s, statsScrollOffset: s.statsScrollOffset + 1 };
|
|
727
|
-
}
|
|
728
|
-
return s;
|
|
729
|
-
});
|
|
730
|
-
} else if (input === "[" || key.ctrl && input === "b") {
|
|
731
|
-
setState((s) => {
|
|
732
|
-
if (s.panel === "logs") {
|
|
733
|
-
return { ...s, logsScrollOffset: Math.max(0, s.logsScrollOffset - 10) };
|
|
734
|
-
} else if (s.panel === "stats") {
|
|
735
|
-
return { ...s, statsScrollOffset: Math.max(0, s.statsScrollOffset - 10) };
|
|
736
|
-
}
|
|
737
|
-
return s;
|
|
738
|
-
});
|
|
739
|
-
} else if (input === "]" || key.ctrl && input === "f") {
|
|
740
|
-
setState((s) => {
|
|
741
|
-
if (s.panel === "logs") {
|
|
742
|
-
return { ...s, logsScrollOffset: s.logsScrollOffset + 10 };
|
|
743
|
-
} else if (s.panel === "stats") {
|
|
744
|
-
return { ...s, statsScrollOffset: s.statsScrollOffset + 10 };
|
|
745
|
-
}
|
|
746
|
-
return s;
|
|
747
|
-
});
|
|
748
|
-
} else if (key.ctrl && input === "a") {
|
|
749
|
-
setState((s) => {
|
|
750
|
-
if (s.panel === "logs") {
|
|
751
|
-
return { ...s, logsScrollOffset: 0 };
|
|
752
|
-
} else if (s.panel === "stats") {
|
|
753
|
-
return { ...s, statsScrollOffset: 0 };
|
|
754
|
-
}
|
|
755
|
-
return s;
|
|
756
|
-
});
|
|
757
|
-
} else if (key.ctrl && input === "e") {
|
|
758
|
-
setState((s) => {
|
|
759
|
-
if (s.panel === "logs") {
|
|
760
|
-
return { ...s, logsScrollOffset: Number.MAX_SAFE_INTEGER };
|
|
761
|
-
} else if (s.panel === "stats") {
|
|
762
|
-
return { ...s, statsScrollOffset: Number.MAX_SAFE_INTEGER };
|
|
763
|
-
}
|
|
764
|
-
return s;
|
|
765
|
-
});
|
|
766
1005
|
}
|
|
767
1006
|
}, { isActive });
|
|
768
1007
|
return {
|
|
@@ -780,26 +1019,28 @@ function useKeyBindings(opts) {
|
|
|
780
1019
|
// src/tui/hooks/useProxySync.ts
|
|
781
1020
|
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
782
1021
|
function useProxySync(provider, opts, states, enabled) {
|
|
783
|
-
const
|
|
1022
|
+
const statesRef = useRef2(states);
|
|
1023
|
+
const lastContentRef = useRef2(null);
|
|
1024
|
+
statesRef.current = states;
|
|
784
1025
|
useEffect2(() => {
|
|
785
|
-
if (!provider || !opts || !enabled)
|
|
786
|
-
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
1026
|
+
if (!provider || !opts || !enabled) return;
|
|
789
1027
|
const sync = () => {
|
|
790
1028
|
const svcStates = /* @__PURE__ */ new Map();
|
|
791
|
-
for (const [name, st] of
|
|
1029
|
+
for (const [name, st] of statesRef.current) {
|
|
792
1030
|
svcStates.set(name, { port: st.svc.port, health: st.health, realPort: st.svc.realPort });
|
|
793
1031
|
}
|
|
794
1032
|
const content = provider.generate(svcStates, opts);
|
|
1033
|
+
if (content === lastContentRef.current) return;
|
|
1034
|
+
lastContentRef.current = content;
|
|
795
1035
|
provider.write(content, opts);
|
|
796
1036
|
};
|
|
797
1037
|
sync();
|
|
798
|
-
|
|
1038
|
+
const id = setInterval(sync, 3e3);
|
|
799
1039
|
return () => {
|
|
800
|
-
|
|
1040
|
+
clearInterval(id);
|
|
1041
|
+
lastContentRef.current = null;
|
|
801
1042
|
};
|
|
802
|
-
}, [provider, opts, enabled
|
|
1043
|
+
}, [provider, opts, enabled]);
|
|
803
1044
|
}
|
|
804
1045
|
|
|
805
1046
|
// src/tui/LogsPanel.tsx
|
|
@@ -808,27 +1049,25 @@ import { Box, Text } from "ink";
|
|
|
808
1049
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
809
1050
|
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
810
1051
|
const filtered = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
811
|
-
const contentHeight = height - 2;
|
|
1052
|
+
const contentHeight = Math.max(1, height - 2);
|
|
812
1053
|
const totalLines = filtered.length;
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
} else {
|
|
819
|
-
startIndex = Math.max(0, totalLines - contentHeight);
|
|
820
|
-
}
|
|
821
|
-
const visible = filtered.slice(startIndex, startIndex + contentHeight);
|
|
1054
|
+
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
1055
|
+
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxOffset : Math.min(scrollOffset, maxOffset);
|
|
1056
|
+
const startIndex = Math.max(0, totalLines - contentHeight - effectiveOffset);
|
|
1057
|
+
const endIndex = Math.min(startIndex + contentHeight, totalLines);
|
|
1058
|
+
const visible = filtered.slice(startIndex, endIndex);
|
|
822
1059
|
useEffect3(() => {
|
|
823
1060
|
resetScroll();
|
|
824
1061
|
}, [filter, searchTerm, resetScroll]);
|
|
1062
|
+
const scrolled = effectiveOffset > 0;
|
|
825
1063
|
const label = [
|
|
826
1064
|
"Logs",
|
|
827
1065
|
filter ? `[${filter}]` : "",
|
|
828
1066
|
searchTerm ? `/${searchTerm}` : "",
|
|
829
1067
|
paused ? "[PAUSED]" : "",
|
|
1068
|
+
scrolled ? "[SCROLL]" : "",
|
|
830
1069
|
`${filtered.length} lines`,
|
|
831
|
-
focused ? `(${startIndex + 1}-${
|
|
1070
|
+
focused && totalLines > 0 ? `(${startIndex + 1}-${endIndex}/${totalLines})` : ""
|
|
832
1071
|
].filter(Boolean).join(" ");
|
|
833
1072
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "cyan" : "gray", height, children: [
|
|
834
1073
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
@@ -931,33 +1170,29 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
931
1170
|
}
|
|
932
1171
|
const stackMem = totalMemMB >= 1024 ? (totalMemMB / 1024).toFixed(2) + " GB" : totalMemMB.toFixed(1) + " MB";
|
|
933
1172
|
const ml = maxNameLen;
|
|
934
|
-
const contentHeight = height - 2;
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
} else if (scrollOffset > 0) {
|
|
943
|
-
apiStartIndex = Math.min(scrollOffset, maxApiRows);
|
|
944
|
-
webStartIndex = Math.min(scrollOffset, maxWebRows);
|
|
945
|
-
}
|
|
946
|
-
const visibleApis = apis.slice(apiStartIndex, apiStartIndex + contentHeight - 2);
|
|
947
|
-
const visibleWebs = webs.slice(webStartIndex, webStartIndex + contentHeight - 2);
|
|
1173
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1174
|
+
const rowsPerCol = Math.max(1, contentHeight - 2);
|
|
1175
|
+
const maxRows = Math.max(0, Math.max(apis.length, webs.length) - rowsPerCol);
|
|
1176
|
+
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxRows : Math.min(scrollOffset, maxRows);
|
|
1177
|
+
const apiStartIndex = Math.min(effectiveOffset, Math.max(0, apis.length - rowsPerCol));
|
|
1178
|
+
const webStartIndex = Math.min(effectiveOffset, Math.max(0, webs.length - rowsPerCol));
|
|
1179
|
+
const visibleApis = apis.slice(apiStartIndex, apiStartIndex + rowsPerCol);
|
|
1180
|
+
const visibleWebs = webs.slice(webStartIndex, webStartIndex + rowsPerCol);
|
|
948
1181
|
useEffect4(() => {
|
|
949
1182
|
resetScroll();
|
|
950
1183
|
}, [sortMode, resetScroll]);
|
|
951
|
-
const
|
|
952
|
-
const positionInfo = focused ? `(${
|
|
1184
|
+
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
1185
|
+
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1186
|
+
const scrolled = effectiveOffset > 0;
|
|
953
1187
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
954
1188
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
955
1189
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
956
1190
|
" Stats ",
|
|
957
1191
|
positionInfo
|
|
958
1192
|
] }),
|
|
1193
|
+
scrolled && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " [SCROLL]" }),
|
|
959
1194
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
960
|
-
"System: ",
|
|
1195
|
+
" System: ",
|
|
961
1196
|
cpus,
|
|
962
1197
|
"c Load ",
|
|
963
1198
|
load,
|
|
@@ -1097,59 +1332,58 @@ function SearchInput({ onSubmit, onClose }) {
|
|
|
1097
1332
|
] });
|
|
1098
1333
|
}
|
|
1099
1334
|
|
|
1100
|
-
// src/lazy/classifier.ts
|
|
1101
|
-
var LAZY_PORT_OFFSET = 1e4;
|
|
1102
|
-
function classifyServices(services, config) {
|
|
1103
|
-
const alwaysOnSet = new Set(config?.alwaysOn ?? []);
|
|
1104
|
-
const alwaysOn = [];
|
|
1105
|
-
const lazy = [];
|
|
1106
|
-
for (const svc of services) {
|
|
1107
|
-
if (alwaysOnSet.has(svc.name)) alwaysOn.push(svc);
|
|
1108
|
-
else lazy.push(svc);
|
|
1109
|
-
}
|
|
1110
|
-
return { alwaysOn, lazy };
|
|
1111
|
-
}
|
|
1112
|
-
function getLazyRealPort(originalPort) {
|
|
1113
|
-
return originalPort + LAZY_PORT_OFFSET;
|
|
1114
|
-
}
|
|
1115
|
-
function rewriteServicePort(svc) {
|
|
1116
|
-
const realPort = getLazyRealPort(svc.port);
|
|
1117
|
-
const args = svc.args.map((a) => a === String(svc.port) ? String(realPort) : a);
|
|
1118
|
-
const extraEnv = { ...svc.extraEnv, PORT_OVERRIDE: String(realPort) };
|
|
1119
|
-
return { ...svc, port: realPort, args, extraEnv, realPort, originalPort: svc.port };
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
1335
|
// src/lazy/proxy.ts
|
|
1123
1336
|
import net2 from "net";
|
|
1124
1337
|
function createLazyProxy(opts) {
|
|
1125
1338
|
const { listenPort, targetPort, timeoutMin, onDemandStart, onIdleStop, isAlive, onLog } = opts;
|
|
1126
1339
|
let idleTimer = null;
|
|
1340
|
+
let lastActivity = Date.now();
|
|
1127
1341
|
let starting = false;
|
|
1128
1342
|
let serviceReady = false;
|
|
1129
1343
|
let pendingConns = [];
|
|
1130
|
-
|
|
1344
|
+
const activeConns = /* @__PURE__ */ new Set();
|
|
1345
|
+
function bumpActivity() {
|
|
1346
|
+
lastActivity = Date.now();
|
|
1347
|
+
}
|
|
1348
|
+
function scheduleIdleCheck() {
|
|
1131
1349
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1132
|
-
if (timeoutMin
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1350
|
+
if (timeoutMin <= 0) return;
|
|
1351
|
+
const periodMs = timeoutMin * 6e4;
|
|
1352
|
+
idleTimer = setTimeout(() => {
|
|
1353
|
+
const elapsed = Date.now() - lastActivity;
|
|
1354
|
+
if (activeConns.size > 0 || elapsed < periodMs) {
|
|
1355
|
+
scheduleIdleCheck();
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
serviceReady = false;
|
|
1359
|
+
onLog?.(`\u{1F4A4} idle ${timeoutMin}min \u2014 stopping`);
|
|
1360
|
+
onIdleStop();
|
|
1361
|
+
}, periodMs);
|
|
1139
1362
|
}
|
|
1140
1363
|
function pipeToTarget(client) {
|
|
1141
1364
|
const target = net2.createConnection({ port: targetPort, host: "127.0.0.1", allowHalfOpen: true });
|
|
1365
|
+
activeConns.add(client);
|
|
1366
|
+
const cleanup = () => {
|
|
1367
|
+
activeConns.delete(client);
|
|
1368
|
+
bumpActivity();
|
|
1369
|
+
};
|
|
1142
1370
|
target.on("error", () => {
|
|
1143
1371
|
client.destroy();
|
|
1372
|
+
cleanup();
|
|
1144
1373
|
});
|
|
1145
1374
|
client.on("error", () => {
|
|
1146
1375
|
target.destroy();
|
|
1376
|
+
cleanup();
|
|
1147
1377
|
});
|
|
1378
|
+
client.on("close", cleanup);
|
|
1379
|
+
target.on("close", cleanup);
|
|
1148
1380
|
target.on("connect", () => {
|
|
1149
1381
|
target.on("data", (chunk) => {
|
|
1382
|
+
bumpActivity();
|
|
1150
1383
|
if (!client.destroyed) client.write(chunk);
|
|
1151
1384
|
});
|
|
1152
1385
|
client.on("data", (chunk) => {
|
|
1386
|
+
bumpActivity();
|
|
1153
1387
|
if (!target.destroyed) target.write(chunk);
|
|
1154
1388
|
});
|
|
1155
1389
|
target.on("end", () => {
|
|
@@ -1161,7 +1395,7 @@ function createLazyProxy(opts) {
|
|
|
1161
1395
|
});
|
|
1162
1396
|
}
|
|
1163
1397
|
async function handleConnection(client) {
|
|
1164
|
-
|
|
1398
|
+
bumpActivity();
|
|
1165
1399
|
client.on("error", () => {
|
|
1166
1400
|
});
|
|
1167
1401
|
if (serviceReady && isAlive()) {
|
|
@@ -1175,9 +1409,10 @@ function createLazyProxy(opts) {
|
|
|
1175
1409
|
if (starting) return;
|
|
1176
1410
|
starting = true;
|
|
1177
1411
|
onLog?.("\u26A1 on-demand start");
|
|
1412
|
+
let ok = false;
|
|
1178
1413
|
try {
|
|
1179
1414
|
await onDemandStart();
|
|
1180
|
-
|
|
1415
|
+
ok = await waitForPort(targetPort, { timeout: 45e3, interval: 500 });
|
|
1181
1416
|
if (ok) serviceReady = true;
|
|
1182
1417
|
else onLog?.("\u26A0 timeout waiting for service");
|
|
1183
1418
|
} catch (e) {
|
|
@@ -1185,19 +1420,26 @@ function createLazyProxy(opts) {
|
|
|
1185
1420
|
}
|
|
1186
1421
|
starting = false;
|
|
1187
1422
|
const conns = pendingConns.splice(0);
|
|
1423
|
+
if (!ok) {
|
|
1424
|
+
for (const conn of conns) {
|
|
1425
|
+
if (!conn.destroyed) conn.destroy();
|
|
1426
|
+
}
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1188
1429
|
for (const conn of conns) {
|
|
1189
1430
|
if (!conn.destroyed) pipeToTarget(conn);
|
|
1190
1431
|
}
|
|
1191
1432
|
}
|
|
1192
1433
|
const server = net2.createServer({ allowHalfOpen: true }, (socket) => handleConnection(socket));
|
|
1193
1434
|
server.listen(listenPort, "0.0.0.0");
|
|
1194
|
-
|
|
1435
|
+
scheduleIdleCheck();
|
|
1195
1436
|
return {
|
|
1196
1437
|
server,
|
|
1197
|
-
resetTimer,
|
|
1438
|
+
resetTimer: bumpActivity,
|
|
1198
1439
|
destroy: () => {
|
|
1199
1440
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1200
1441
|
pendingConns.forEach((s) => s.destroy());
|
|
1442
|
+
activeConns.forEach((s) => s.destroy());
|
|
1201
1443
|
server.close();
|
|
1202
1444
|
}
|
|
1203
1445
|
};
|
|
@@ -1205,26 +1447,40 @@ function createLazyProxy(opts) {
|
|
|
1205
1447
|
|
|
1206
1448
|
// src/tui/App.tsx
|
|
1207
1449
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1208
|
-
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts }) {
|
|
1450
|
+
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
1209
1451
|
const { stdout } = useStdout();
|
|
1210
|
-
const rows = stdout?.rows ?? 40;
|
|
1452
|
+
const [rows, setRows] = useState5(stdout?.rows ?? 40);
|
|
1453
|
+
useEffect5(() => {
|
|
1454
|
+
if (!stdout) return;
|
|
1455
|
+
const onResize = () => setRows(stdout.rows ?? 40);
|
|
1456
|
+
stdout.on("resize", onResize);
|
|
1457
|
+
return () => {
|
|
1458
|
+
stdout.off("resize", onResize);
|
|
1459
|
+
};
|
|
1460
|
+
}, [stdout]);
|
|
1211
1461
|
const logsHeight = Math.floor(rows * 0.65);
|
|
1212
1462
|
const statsHeight = rows - logsHeight - 2;
|
|
1213
1463
|
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
1214
|
-
const pm = useProcessManager(platform, baseCwd, env);
|
|
1464
|
+
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
1215
1465
|
const [booted, setBooted] = useState5(false);
|
|
1216
1466
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
1217
1467
|
const kb = useKeyBindings({
|
|
1218
1468
|
onQuit: () => {
|
|
1219
|
-
|
|
1220
|
-
pm.cleanup();
|
|
1221
|
-
process.exit(0);
|
|
1222
|
-
},
|
|
1223
|
-
onClearLogs: () => {
|
|
1469
|
+
void shutdown();
|
|
1224
1470
|
},
|
|
1471
|
+
onClearLogs: pm.clearLogs,
|
|
1225
1472
|
onToggleProxy: () => {
|
|
1226
1473
|
}
|
|
1227
1474
|
});
|
|
1475
|
+
const shutdown = useCallback3(async () => {
|
|
1476
|
+
lazyProxies.current.forEach((p) => p.destroy());
|
|
1477
|
+
await pm.cleanup();
|
|
1478
|
+
await logSink?.close();
|
|
1479
|
+
process.exit(0);
|
|
1480
|
+
}, [pm, logSink]);
|
|
1481
|
+
useEffect5(() => {
|
|
1482
|
+
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
1483
|
+
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
1228
1484
|
useProxySync(proxyProvider, proxyOpts, pm.states, kb.proxyEnabled);
|
|
1229
1485
|
useEffect5(() => {
|
|
1230
1486
|
if (booted || !pm.manager) return;
|
|
@@ -1240,8 +1496,9 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1240
1496
|
for (const num of Object.keys(aoPhases).map(Number).sort((a, b) => a - b)) {
|
|
1241
1497
|
const svcs = aoPhases[num];
|
|
1242
1498
|
for (const svc of svcs) {
|
|
1243
|
-
|
|
1244
|
-
await mgr.
|
|
1499
|
+
const ci = colorIdx++;
|
|
1500
|
+
await mgr.install(svc, ci);
|
|
1501
|
+
await mgr.start(svc, ci);
|
|
1245
1502
|
}
|
|
1246
1503
|
const apis = svcs.filter((s) => s.type === "api");
|
|
1247
1504
|
if (apis.length) await Promise.all(apis.map((s) => waitForPort(s.port, { timeout: 45e3 })));
|
|
@@ -1271,7 +1528,7 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1271
1528
|
targetPort: rewritten.realPort,
|
|
1272
1529
|
timeoutMin: lazyTimeout,
|
|
1273
1530
|
onDemandStart: async () => {
|
|
1274
|
-
await mgr.install(rewritten);
|
|
1531
|
+
await mgr.install(rewritten, ci);
|
|
1275
1532
|
await mgr.start(rewritten, ci);
|
|
1276
1533
|
const ok = await waitForPort(rewritten.realPort, { timeout: 45e3 });
|
|
1277
1534
|
const st = mgr.state.get(svc.name);
|
|
@@ -1304,8 +1561,9 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1304
1561
|
for (const num of Object.keys(phases).map(Number).sort((a, b) => a - b)) {
|
|
1305
1562
|
const svcs = phases[num];
|
|
1306
1563
|
for (const svc of svcs) {
|
|
1307
|
-
|
|
1308
|
-
await mgr.
|
|
1564
|
+
const ci = colorIdx++;
|
|
1565
|
+
await mgr.install(svc, ci);
|
|
1566
|
+
await mgr.start(svc, ci);
|
|
1309
1567
|
}
|
|
1310
1568
|
const apis = svcs.filter((s) => s.type === "api");
|
|
1311
1569
|
if (apis.length) await Promise.all(apis.map((s) => waitForPort(s.port, { timeout: 45e3 })));
|
|
@@ -1377,6 +1635,189 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1377
1635
|
] });
|
|
1378
1636
|
}
|
|
1379
1637
|
|
|
1638
|
+
// src/process/log-sink.ts
|
|
1639
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync, createWriteStream } from "fs";
|
|
1640
|
+
import { join as join4, dirname as dirname4 } from "path";
|
|
1641
|
+
import { homedir } from "os";
|
|
1642
|
+
var LogSink = class {
|
|
1643
|
+
dir;
|
|
1644
|
+
rotateOnStart;
|
|
1645
|
+
streams = /* @__PURE__ */ new Map();
|
|
1646
|
+
seen = /* @__PURE__ */ new Set();
|
|
1647
|
+
constructor(opts) {
|
|
1648
|
+
const root = opts.rootDir ?? join4(homedir(), ".devup", "logs");
|
|
1649
|
+
this.dir = join4(root, sanitize(opts.projectName));
|
|
1650
|
+
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1651
|
+
mkdirSync4(this.dir, { recursive: true });
|
|
1652
|
+
}
|
|
1653
|
+
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1654
|
+
pathFor(svcName) {
|
|
1655
|
+
return join4(this.dir, `${sanitize(svcName)}.log`);
|
|
1656
|
+
}
|
|
1657
|
+
write(svcName, line) {
|
|
1658
|
+
const stream = this.streamFor(svcName);
|
|
1659
|
+
stream.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
1660
|
+
`);
|
|
1661
|
+
}
|
|
1662
|
+
async close() {
|
|
1663
|
+
const closes = [...this.streams.values()].map(
|
|
1664
|
+
(s) => new Promise((r) => s.end(() => r()))
|
|
1665
|
+
);
|
|
1666
|
+
this.streams.clear();
|
|
1667
|
+
this.seen.clear();
|
|
1668
|
+
await Promise.all(closes);
|
|
1669
|
+
}
|
|
1670
|
+
streamFor(svcName) {
|
|
1671
|
+
let s = this.streams.get(svcName);
|
|
1672
|
+
if (s) return s;
|
|
1673
|
+
const file = this.pathFor(svcName);
|
|
1674
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync8(file)) {
|
|
1675
|
+
try {
|
|
1676
|
+
mkdirSync4(dirname4(file), { recursive: true });
|
|
1677
|
+
renameSync(file, file + ".prev");
|
|
1678
|
+
} catch {
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
this.seen.add(svcName);
|
|
1682
|
+
s = createWriteStream(file, { flags: "a" });
|
|
1683
|
+
s.on("error", () => {
|
|
1684
|
+
});
|
|
1685
|
+
this.streams.set(svcName, s);
|
|
1686
|
+
return s;
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
function sanitize(name) {
|
|
1690
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/orchestrator/dry-run.ts
|
|
1694
|
+
function renderDryRun(opts) {
|
|
1695
|
+
const { config, services, cliArgs, env, proxyProvider, proxyOpts } = opts;
|
|
1696
|
+
const lines = [];
|
|
1697
|
+
lines.push(`Project: ${config.icon ?? "\u{1F4E6}"} ${config.name}`);
|
|
1698
|
+
lines.push(`Mode: ${cliArgs.lazy && config.lazy ? "lazy" : "normal"}`);
|
|
1699
|
+
lines.push(`Services: ${services.length}`);
|
|
1700
|
+
lines.push("");
|
|
1701
|
+
const lazyMode = cliArgs.lazy && !!config.lazy;
|
|
1702
|
+
let alwaysOn = services;
|
|
1703
|
+
let lazy = [];
|
|
1704
|
+
if (lazyMode) {
|
|
1705
|
+
const c = classifyServices(services, config.lazy);
|
|
1706
|
+
alwaysOn = c.alwaysOn;
|
|
1707
|
+
lazy = c.lazy;
|
|
1708
|
+
}
|
|
1709
|
+
const phases = groupByPhase(alwaysOn);
|
|
1710
|
+
const phaseNums = Object.keys(phases).map(Number).sort((a, b) => a - b);
|
|
1711
|
+
for (const num of phaseNums) {
|
|
1712
|
+
lines.push(`Phase ${num}:`);
|
|
1713
|
+
for (const svc of phases[num]) {
|
|
1714
|
+
lines.push(formatService(svc, env, false));
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (lazy.length) {
|
|
1718
|
+
lines.push("");
|
|
1719
|
+
lines.push("Lazy (on-demand):");
|
|
1720
|
+
for (const svc of lazy) {
|
|
1721
|
+
const rewritten = rewriteServicePort(svc);
|
|
1722
|
+
lines.push(formatService(rewritten, env, true));
|
|
1723
|
+
lines.push(` proxy :${svc.port} \u2192 :${getLazyRealPort(svc.port)} (idle timeout ${cliArgs.lazyTimeout}m)`);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (proxyProvider && proxyOpts) {
|
|
1727
|
+
lines.push("");
|
|
1728
|
+
lines.push(`Proxy: ${proxyProvider.name} \u2192 ${proxyOpts.confPath}`);
|
|
1729
|
+
const svcStates = /* @__PURE__ */ new Map();
|
|
1730
|
+
for (const svc of services) {
|
|
1731
|
+
const real = lazyMode && !alwaysOn.includes(svc) ? getLazyRealPort(svc.port) : void 0;
|
|
1732
|
+
svcStates.set(svc.name, { port: svc.port, health: "up", realPort: real });
|
|
1733
|
+
}
|
|
1734
|
+
const content = proxyProvider.generate(svcStates, proxyOpts);
|
|
1735
|
+
lines.push("");
|
|
1736
|
+
lines.push("--- generated config ---");
|
|
1737
|
+
lines.push(content);
|
|
1738
|
+
}
|
|
1739
|
+
return lines.join("\n");
|
|
1740
|
+
}
|
|
1741
|
+
function formatService(svc, env, isLazy) {
|
|
1742
|
+
const args = buildProcessArgs(svc);
|
|
1743
|
+
const cmdLine = [svc.cmd, ...args].join(" ");
|
|
1744
|
+
const built = buildProcessEnv(svc, env);
|
|
1745
|
+
const extraEnv = Object.keys(svc.extraEnv ?? {}).length ? " env=" + Object.entries(svc.extraEnv).map(([k, v]) => `${k}=${v}`).join(" ") : "";
|
|
1746
|
+
const memTag = svc.maxMem ? ` mem=${svc.maxMem}MB` : "";
|
|
1747
|
+
const hc = svc.healthCheck;
|
|
1748
|
+
const hcTag = hc?.type === "http" ? ` health=http ${hc.path ?? "/"}` : "";
|
|
1749
|
+
const lazyTag = isLazy ? " [lazy]" : "";
|
|
1750
|
+
void built;
|
|
1751
|
+
return ` - ${svc.name.padEnd(20)} (${svc.type}) :${svc.port} ${cmdLine}${memTag}${hcTag}${lazyTag}${extraEnv}`;
|
|
1752
|
+
}
|
|
1753
|
+
function runDryRun(opts) {
|
|
1754
|
+
console.log(renderDryRun(opts));
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
// src/orchestrator/once.ts
|
|
1758
|
+
async function runOnce(opts) {
|
|
1759
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
1760
|
+
const { config, services, cliArgs, platform, env, baseCwd, logSink } = opts;
|
|
1761
|
+
const mgr = new ProcessManager({
|
|
1762
|
+
baseCwd,
|
|
1763
|
+
env,
|
|
1764
|
+
platform,
|
|
1765
|
+
events: {
|
|
1766
|
+
onLog: (svc, text) => {
|
|
1767
|
+
logSink?.write(svc, text);
|
|
1768
|
+
out(`[${svc}] ${text}`);
|
|
1769
|
+
},
|
|
1770
|
+
onStateChange: () => {
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
const phases = groupByPhase(services);
|
|
1775
|
+
const phaseNums = Object.keys(phases).map(Number).sort((a, b) => a - b);
|
|
1776
|
+
const apiNames = services.filter((s) => s.type === "api").map((s) => s.name);
|
|
1777
|
+
const deadline = Date.now() + cliArgs.onceTimeout * 1e3;
|
|
1778
|
+
let colorIdx = 0;
|
|
1779
|
+
for (const num of phaseNums) {
|
|
1780
|
+
out(`\u25B6 phase ${num}`);
|
|
1781
|
+
for (const svc of phases[num]) {
|
|
1782
|
+
const ci = colorIdx++;
|
|
1783
|
+
const installed = await mgr.install(svc, ci);
|
|
1784
|
+
if (!installed) {
|
|
1785
|
+
out(`\u2717 install failed for ${svc.name}`);
|
|
1786
|
+
await mgr.cleanup();
|
|
1787
|
+
return 1;
|
|
1788
|
+
}
|
|
1789
|
+
await mgr.start(svc, ci);
|
|
1790
|
+
}
|
|
1791
|
+
const apis = phases[num].filter((s) => s.type === "api");
|
|
1792
|
+
for (const api of apis) {
|
|
1793
|
+
const ok = await waitHealthy(api, deadline);
|
|
1794
|
+
if (!ok) {
|
|
1795
|
+
out(`\u2717 ${api.name} did not become healthy within ${cliArgs.onceTimeout}s`);
|
|
1796
|
+
await mgr.cleanup();
|
|
1797
|
+
return 1;
|
|
1798
|
+
}
|
|
1799
|
+
out(`\u2713 ${api.name} ready`);
|
|
1800
|
+
const st = mgr.state.get(api.name);
|
|
1801
|
+
if (st) {
|
|
1802
|
+
st.status = "running";
|
|
1803
|
+
st.health = "up";
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const summary = `ready: ${apiNames.length} APIs in ${((cliArgs.onceTimeout * 1e3 - (deadline - Date.now())) / 1e3).toFixed(1)}s`;
|
|
1808
|
+
out(summary);
|
|
1809
|
+
await mgr.cleanup();
|
|
1810
|
+
return 0;
|
|
1811
|
+
}
|
|
1812
|
+
async function waitHealthy(svc, deadline) {
|
|
1813
|
+
while (Date.now() < deadline) {
|
|
1814
|
+
const ok = await checkHealth(svc.port, svc.healthCheck);
|
|
1815
|
+
if (ok) return true;
|
|
1816
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1817
|
+
}
|
|
1818
|
+
return false;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1380
1821
|
// src/config/types.ts
|
|
1381
1822
|
function defineConfig(config) {
|
|
1382
1823
|
return config;
|
|
@@ -1406,7 +1847,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
1406
1847
|
process.exit(1);
|
|
1407
1848
|
}
|
|
1408
1849
|
const platform = await detectPlatform();
|
|
1409
|
-
const envFile = config.envFile ?
|
|
1850
|
+
const envFile = config.envFile ? join5(cwd, config.envFile) : join5(cwd, ".env");
|
|
1410
1851
|
const env = parseEnvFile(envFile, process.env);
|
|
1411
1852
|
if (config.env) {
|
|
1412
1853
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -1423,12 +1864,33 @@ ${formatValidationErrors(errors)}`);
|
|
|
1423
1864
|
routes: config.proxy.routes,
|
|
1424
1865
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
1425
1866
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
1426
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
1867
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join5(homedir2(), ".traefik", "traefik_conf.yaml")
|
|
1427
1868
|
};
|
|
1428
1869
|
}
|
|
1870
|
+
if (cliArgs.dryRun) {
|
|
1871
|
+
runDryRun({ config, services, cliArgs, env, baseCwd: cwd, proxyProvider, proxyOpts });
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
let logSink = null;
|
|
1875
|
+
if (cliArgs.logFile) {
|
|
1876
|
+
logSink = new LogSink({ projectName: config.name, rootDir: cliArgs.logDir });
|
|
1877
|
+
}
|
|
1878
|
+
if (cliArgs.once) {
|
|
1879
|
+
const code = await runOnce({
|
|
1880
|
+
config,
|
|
1881
|
+
services,
|
|
1882
|
+
cliArgs,
|
|
1883
|
+
platform,
|
|
1884
|
+
env,
|
|
1885
|
+
baseCwd: cwd,
|
|
1886
|
+
logSink
|
|
1887
|
+
});
|
|
1888
|
+
await logSink?.close();
|
|
1889
|
+
process.exit(code);
|
|
1890
|
+
}
|
|
1429
1891
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
1430
1892
|
const { waitUntilExit } = render(
|
|
1431
|
-
|
|
1893
|
+
React7.createElement(App, {
|
|
1432
1894
|
config,
|
|
1433
1895
|
services,
|
|
1434
1896
|
cliArgs,
|
|
@@ -1436,7 +1898,8 @@ ${formatValidationErrors(errors)}`);
|
|
|
1436
1898
|
env,
|
|
1437
1899
|
baseCwd: cwd,
|
|
1438
1900
|
proxyProvider,
|
|
1439
|
-
proxyOpts
|
|
1901
|
+
proxyOpts,
|
|
1902
|
+
logSink
|
|
1440
1903
|
}),
|
|
1441
1904
|
{ exitOnCtrlC: false, patchConsole: isInteractive, interactive: isInteractive }
|
|
1442
1905
|
);
|