@gachlab/devup 0.1.1 → 0.3.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 +112 -0
- package/README.md +185 -13
- package/dist/config/cli.d.ts +8 -2
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/types.d.ts +40 -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 +987 -182
- 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/external.d.ts +30 -0
- package/dist/process/external.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 +12 -3
- package/dist/process/manager.d.ts.map +1 -1
- package/dist/process/types.d.ts +2 -0
- package/dist/process/types.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 +7 -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 join6 } 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,34 @@ 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.readyPattern !== void 0) {
|
|
116
|
+
if (typeof svc.readyPattern !== "string" || !svc.readyPattern.length) {
|
|
117
|
+
errors.push({ field: `services[${svc.name}].readyPattern`, message: `readyPattern must be a non-empty string` });
|
|
118
|
+
} else {
|
|
119
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(svc.readyPattern);
|
|
120
|
+
try {
|
|
121
|
+
if (slashed) new RegExp(slashed[1], slashed[2] || "i");
|
|
122
|
+
else new RegExp(svc.readyPattern, "i");
|
|
123
|
+
} catch (e) {
|
|
124
|
+
errors.push({ field: `services[${svc.name}].readyPattern`, message: `Invalid regex: ${e.message}` });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (svc.preBuild !== void 0 && (typeof svc.preBuild !== "string" || !svc.preBuild.trim())) {
|
|
129
|
+
errors.push({ field: `services[${svc.name}].preBuild`, message: `preBuild must be a non-empty string` });
|
|
130
|
+
}
|
|
131
|
+
if (svc.watchBuild !== void 0 && (typeof svc.watchBuild !== "string" || !svc.watchBuild.trim())) {
|
|
132
|
+
errors.push({ field: `services[${svc.name}].watchBuild`, message: `watchBuild must be a non-empty string` });
|
|
133
|
+
}
|
|
134
|
+
if (svc.healthCheck) {
|
|
135
|
+
const hc = svc.healthCheck;
|
|
136
|
+
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
137
|
+
errors.push({ field: `services[${svc.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type} (must be "tcp" or "http")` });
|
|
138
|
+
}
|
|
139
|
+
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
140
|
+
errors.push({ field: `services[${svc.name}].healthCheck.path`, message: `healthCheck.path must start with "/": got "${hc.path}"` });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
91
143
|
}
|
|
92
144
|
if (config.lazy?.alwaysOn) {
|
|
93
145
|
for (const ref of config.lazy.alwaysOn) {
|
|
@@ -96,6 +148,50 @@ function validateConfig(config, cwd) {
|
|
|
96
148
|
}
|
|
97
149
|
}
|
|
98
150
|
}
|
|
151
|
+
if (config.lazy) {
|
|
152
|
+
const alwaysOn = new Set(config.lazy.alwaysOn ?? []);
|
|
153
|
+
const portToSvc = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const svc of config.services) portToSvc.set(svc.port, svc.name);
|
|
155
|
+
for (const svc of config.services) {
|
|
156
|
+
if (alwaysOn.has(svc.name)) continue;
|
|
157
|
+
const realPort = svc.port + LAZY_PORT_OFFSET;
|
|
158
|
+
const conflict = portToSvc.get(realPort);
|
|
159
|
+
if (conflict && conflict !== svc.name) {
|
|
160
|
+
errors.push({
|
|
161
|
+
field: `services[${svc.name}].port`,
|
|
162
|
+
message: `Lazy real port ${realPort} (= ${svc.port}+${LAZY_PORT_OFFSET}) collides with service ${conflict}`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (config.external) {
|
|
168
|
+
const extNames = /* @__PURE__ */ new Set();
|
|
169
|
+
for (const ext of config.external) {
|
|
170
|
+
if (!ext.name?.trim()) {
|
|
171
|
+
errors.push({ field: "external[].name", message: "External service name is required" });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (extNames.has(ext.name)) {
|
|
175
|
+
errors.push({ field: `external[${ext.name}].name`, message: `Duplicate external name: ${ext.name}` });
|
|
176
|
+
}
|
|
177
|
+
extNames.add(ext.name);
|
|
178
|
+
if (!ext.cmd?.trim()) {
|
|
179
|
+
errors.push({ field: `external[${ext.name}].cmd`, message: "cmd is required" });
|
|
180
|
+
}
|
|
181
|
+
if (ext.healthCheck) {
|
|
182
|
+
const hc = ext.healthCheck;
|
|
183
|
+
if (hc.type !== "tcp" && hc.type !== "http") {
|
|
184
|
+
errors.push({ field: `external[${ext.name}].healthCheck.type`, message: `Invalid healthCheck.type: ${hc.type}` });
|
|
185
|
+
}
|
|
186
|
+
if ((hc.type === "tcp" || hc.type === "http") && (typeof ext.port !== "number" || ext.port <= 0)) {
|
|
187
|
+
errors.push({ field: `external[${ext.name}].port`, message: `port is required when healthCheck is set (got ${ext.port})` });
|
|
188
|
+
}
|
|
189
|
+
if (hc.type === "http" && hc.path && !hc.path.startsWith("/")) {
|
|
190
|
+
errors.push({ field: `external[${ext.name}].healthCheck.path`, message: `must start with "/"` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
99
195
|
if (config.proxy?.routes) {
|
|
100
196
|
for (const ref of Object.keys(config.proxy.routes)) {
|
|
101
197
|
if (!names.has(ref)) {
|
|
@@ -103,6 +199,19 @@ function validateConfig(config, cwd) {
|
|
|
103
199
|
}
|
|
104
200
|
}
|
|
105
201
|
}
|
|
202
|
+
if (config.profiles) {
|
|
203
|
+
for (const [profile, svcNames] of Object.entries(config.profiles)) {
|
|
204
|
+
if (!Array.isArray(svcNames) || !svcNames.length) {
|
|
205
|
+
errors.push({ field: `profiles.${profile}`, message: `Profile "${profile}" must be a non-empty array of service names` });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
for (const ref of svcNames) {
|
|
209
|
+
if (!names.has(ref)) {
|
|
210
|
+
errors.push({ field: `profiles.${profile}`, message: `Unknown service: ${ref}` });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
106
215
|
return errors;
|
|
107
216
|
}
|
|
108
217
|
function formatValidationErrors(errors) {
|
|
@@ -111,6 +220,7 @@ function formatValidationErrors(errors) {
|
|
|
111
220
|
|
|
112
221
|
// src/config/cli.ts
|
|
113
222
|
var DEFAULT_LAZY_TIMEOUT = 10;
|
|
223
|
+
var DEFAULT_ONCE_TIMEOUT = 90;
|
|
114
224
|
function parseCliArgs(argv) {
|
|
115
225
|
const args = {
|
|
116
226
|
skip: [],
|
|
@@ -118,7 +228,11 @@ function parseCliArgs(argv) {
|
|
|
118
228
|
lazyTimeout: DEFAULT_LAZY_TIMEOUT,
|
|
119
229
|
proxy: false,
|
|
120
230
|
proxyTls: true,
|
|
121
|
-
proxyEntrypoint: "websecure"
|
|
231
|
+
proxyEntrypoint: "websecure",
|
|
232
|
+
dryRun: false,
|
|
233
|
+
once: false,
|
|
234
|
+
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
235
|
+
logFile: true
|
|
122
236
|
};
|
|
123
237
|
for (let i = 0; i < argv.length; i++) {
|
|
124
238
|
const arg = argv[i];
|
|
@@ -140,6 +254,10 @@ function parseCliArgs(argv) {
|
|
|
140
254
|
args.services = next?.split(",");
|
|
141
255
|
i++;
|
|
142
256
|
break;
|
|
257
|
+
case "--profile":
|
|
258
|
+
args.profile = next;
|
|
259
|
+
i++;
|
|
260
|
+
break;
|
|
143
261
|
case "--lazy":
|
|
144
262
|
args.lazy = true;
|
|
145
263
|
break;
|
|
@@ -171,13 +289,39 @@ function parseCliArgs(argv) {
|
|
|
171
289
|
args.proxyEntrypoint = next ?? "websecure";
|
|
172
290
|
i++;
|
|
173
291
|
break;
|
|
292
|
+
case "--dry-run":
|
|
293
|
+
args.dryRun = true;
|
|
294
|
+
break;
|
|
295
|
+
case "--once":
|
|
296
|
+
args.once = true;
|
|
297
|
+
break;
|
|
298
|
+
case "--once-timeout":
|
|
299
|
+
args.onceTimeout = parseInt(next ?? "", 10) || DEFAULT_ONCE_TIMEOUT;
|
|
300
|
+
i++;
|
|
301
|
+
break;
|
|
302
|
+
case "--no-log-file":
|
|
303
|
+
args.logFile = false;
|
|
304
|
+
break;
|
|
305
|
+
case "--log-dir":
|
|
306
|
+
args.logDir = next;
|
|
307
|
+
i++;
|
|
308
|
+
break;
|
|
174
309
|
}
|
|
175
310
|
}
|
|
176
311
|
return args;
|
|
177
312
|
}
|
|
178
|
-
function filterServices(services, args) {
|
|
313
|
+
function filterServices(services, args, config) {
|
|
179
314
|
let result = services;
|
|
180
|
-
if (args.
|
|
315
|
+
if (args.profile) {
|
|
316
|
+
const profileNames = config?.profiles?.[args.profile];
|
|
317
|
+
if (!profileNames) {
|
|
318
|
+
const available = Object.keys(config?.profiles ?? {});
|
|
319
|
+
const hint = available.length ? `Available: ${available.join(", ")}` : "No profiles defined in config.";
|
|
320
|
+
throw new Error(`Unknown profile: "${args.profile}". ${hint}`);
|
|
321
|
+
}
|
|
322
|
+
const set = new Set(profileNames);
|
|
323
|
+
result = result.filter((s) => set.has(s.name));
|
|
324
|
+
} else if (args.services) {
|
|
181
325
|
const explicit = new Set(args.services);
|
|
182
326
|
result = result.filter((s) => explicit.has(s.name));
|
|
183
327
|
} else if (args.only) {
|
|
@@ -268,9 +412,93 @@ ${svcs.join("\n")}
|
|
|
268
412
|
}
|
|
269
413
|
};
|
|
270
414
|
|
|
415
|
+
// src/proxy-config/nginx.ts
|
|
416
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
417
|
+
import { dirname as dirname2 } from "path";
|
|
418
|
+
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
419
|
+
var NginxProvider = class {
|
|
420
|
+
name = "nginx";
|
|
421
|
+
generate(services, opts) {
|
|
422
|
+
const blocks = [];
|
|
423
|
+
for (const [name, st] of services) {
|
|
424
|
+
if (st.health !== "up") continue;
|
|
425
|
+
const sub = opts.routes[name];
|
|
426
|
+
if (sub === void 0) continue;
|
|
427
|
+
const serverName = sub ? `${sub}.${opts.domain}` : opts.domain;
|
|
428
|
+
const port = st.realPort ?? st.port;
|
|
429
|
+
const listen = opts.tls ? "443 ssl" : "80";
|
|
430
|
+
const tlsBlock = opts.tls ? ` ssl_certificate /etc/nginx/certs/${serverName}.crt;
|
|
431
|
+
ssl_certificate_key /etc/nginx/certs/${serverName}.key;
|
|
432
|
+
` : "";
|
|
433
|
+
blocks.push(
|
|
434
|
+
`server {
|
|
435
|
+
listen ${listen};
|
|
436
|
+
server_name ${serverName};
|
|
437
|
+
` + tlsBlock + ` location / {
|
|
438
|
+
proxy_pass http://${opts.host}:${port};
|
|
439
|
+
proxy_set_header Host $host;
|
|
440
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
441
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
442
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
443
|
+
proxy_http_version 1.1;
|
|
444
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
445
|
+
proxy_set_header Connection "upgrade";
|
|
446
|
+
}
|
|
447
|
+
}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (!blocks.length) return EMPTY_CONFIG2;
|
|
451
|
+
return blocks.join("\n\n") + "\n";
|
|
452
|
+
}
|
|
453
|
+
write(content, opts) {
|
|
454
|
+
const dir = dirname2(opts.confPath);
|
|
455
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
456
|
+
writeFileSync2(opts.confPath, content);
|
|
457
|
+
}
|
|
458
|
+
clear(opts) {
|
|
459
|
+
this.write(EMPTY_CONFIG2, opts);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/proxy-config/caddy.ts
|
|
464
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
465
|
+
import { dirname as dirname3 } from "path";
|
|
466
|
+
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
467
|
+
var CaddyProvider = class {
|
|
468
|
+
name = "caddy";
|
|
469
|
+
generate(services, opts) {
|
|
470
|
+
const blocks = [];
|
|
471
|
+
for (const [name, st] of services) {
|
|
472
|
+
if (st.health !== "up") continue;
|
|
473
|
+
const sub = opts.routes[name];
|
|
474
|
+
if (sub === void 0) continue;
|
|
475
|
+
const host = sub ? `${sub}.${opts.domain}` : opts.domain;
|
|
476
|
+
const port = st.realPort ?? st.port;
|
|
477
|
+
const siteAddr = opts.tls ? host : `http://${host}`;
|
|
478
|
+
blocks.push(
|
|
479
|
+
`${siteAddr} {
|
|
480
|
+
reverse_proxy ${opts.host}:${port}
|
|
481
|
+
}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
if (!blocks.length) return EMPTY_CONFIG3;
|
|
485
|
+
return blocks.join("\n\n") + "\n";
|
|
486
|
+
}
|
|
487
|
+
write(content, opts) {
|
|
488
|
+
const dir = dirname3(opts.confPath);
|
|
489
|
+
if (!existsSync5(dir)) mkdirSync3(dir, { recursive: true });
|
|
490
|
+
writeFileSync3(opts.confPath, content);
|
|
491
|
+
}
|
|
492
|
+
clear(opts) {
|
|
493
|
+
this.write(EMPTY_CONFIG3, opts);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
271
497
|
// src/proxy-config/detect.ts
|
|
272
498
|
var providers = {
|
|
273
|
-
traefik: () => new TraefikProvider()
|
|
499
|
+
traefik: () => new TraefikProvider(),
|
|
500
|
+
nginx: () => new NginxProvider(),
|
|
501
|
+
caddy: () => new CaddyProvider()
|
|
274
502
|
};
|
|
275
503
|
function detectProxyProvider(name) {
|
|
276
504
|
const factory = providers[name];
|
|
@@ -282,12 +510,12 @@ function detectProxyProvider(name) {
|
|
|
282
510
|
}
|
|
283
511
|
|
|
284
512
|
// src/utils.ts
|
|
285
|
-
import { existsSync as
|
|
513
|
+
import { existsSync as existsSync6, readFileSync, writeFileSync as writeFileSync4 } from "fs";
|
|
286
514
|
import { createHash } from "crypto";
|
|
287
515
|
import { join as join2 } from "path";
|
|
288
516
|
function parseEnvFile(filePath, baseEnv = {}) {
|
|
289
517
|
const env = { ...baseEnv };
|
|
290
|
-
if (!
|
|
518
|
+
if (!existsSync6(filePath)) return env;
|
|
291
519
|
for (const line of readFileSync(filePath, "utf8").split("\n")) {
|
|
292
520
|
const trimmed = line.trim();
|
|
293
521
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -309,15 +537,17 @@ function fmtUptime(ms) {
|
|
|
309
537
|
const m = Math.floor(s / 60);
|
|
310
538
|
if (m < 60) return `${m}m${s % 60}s`;
|
|
311
539
|
const h = Math.floor(m / 60);
|
|
312
|
-
return `${h}h${m % 60}m`;
|
|
540
|
+
if (h < 24) return `${h}h${m % 60}m`;
|
|
541
|
+
const d = Math.floor(h / 24);
|
|
542
|
+
return `${d}d${h % 24}h`;
|
|
313
543
|
}
|
|
314
544
|
function needsInstall(fullCwd) {
|
|
315
545
|
const nm = join2(fullCwd, "node_modules");
|
|
316
|
-
if (!
|
|
546
|
+
if (!existsSync6(nm)) return true;
|
|
317
547
|
try {
|
|
318
548
|
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
319
549
|
const stampFile = join2(nm, ".install-stamp");
|
|
320
|
-
if (
|
|
550
|
+
if (existsSync6(stampFile) && readFileSync(stampFile, "utf8") === pkgHash) return false;
|
|
321
551
|
} catch {
|
|
322
552
|
}
|
|
323
553
|
return true;
|
|
@@ -325,7 +555,7 @@ function needsInstall(fullCwd) {
|
|
|
325
555
|
function writeInstallStamp(fullCwd) {
|
|
326
556
|
try {
|
|
327
557
|
const pkgHash = createHash("md5").update(readFileSync(join2(fullCwd, "package.json"))).digest("hex");
|
|
328
|
-
|
|
558
|
+
writeFileSync4(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
329
559
|
} catch {
|
|
330
560
|
}
|
|
331
561
|
}
|
|
@@ -396,10 +626,11 @@ import { join as join3 } from "path";
|
|
|
396
626
|
|
|
397
627
|
// src/process/health.ts
|
|
398
628
|
import net from "net";
|
|
399
|
-
|
|
629
|
+
import http from "http";
|
|
630
|
+
function checkPort(port, host = "127.0.0.1", timeoutMs = 2e3) {
|
|
400
631
|
return new Promise((resolve3) => {
|
|
401
632
|
const socket = new net.Socket();
|
|
402
|
-
socket.setTimeout(
|
|
633
|
+
socket.setTimeout(timeoutMs);
|
|
403
634
|
socket.once("connect", () => {
|
|
404
635
|
socket.destroy();
|
|
405
636
|
resolve3(true);
|
|
@@ -415,6 +646,39 @@ function checkPort(port, host = "127.0.0.1") {
|
|
|
415
646
|
socket.connect(port, host);
|
|
416
647
|
});
|
|
417
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
|
+
}
|
|
418
682
|
function waitForPort(port, opts = {}) {
|
|
419
683
|
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
420
684
|
return new Promise((resolve3) => {
|
|
@@ -437,9 +701,9 @@ function deriveHealth(isUp, currentStatus) {
|
|
|
437
701
|
|
|
438
702
|
// src/process/installer.ts
|
|
439
703
|
import { spawn } from "child_process";
|
|
440
|
-
import { existsSync as
|
|
704
|
+
import { existsSync as existsSync7 } from "fs";
|
|
441
705
|
function installService(cwd, env, onLog) {
|
|
442
|
-
if (!
|
|
706
|
+
if (!existsSync7(cwd)) {
|
|
443
707
|
onLog?.(`\u26A0 directory not found: ${cwd}`);
|
|
444
708
|
return Promise.resolve(false);
|
|
445
709
|
}
|
|
@@ -475,6 +739,36 @@ function installService(cwd, env, onLog) {
|
|
|
475
739
|
// src/process/manager.ts
|
|
476
740
|
var MAX_RESTARTS = 3;
|
|
477
741
|
var BACKOFF_BASE_MS = 2e3;
|
|
742
|
+
function compileReadyPattern(pattern) {
|
|
743
|
+
if (!pattern) return null;
|
|
744
|
+
const slashed = /^\/(.+)\/([gimsuy]*)$/.exec(pattern);
|
|
745
|
+
try {
|
|
746
|
+
if (slashed) return new RegExp(slashed[1], slashed[2] || "i");
|
|
747
|
+
return new RegExp(pattern, "i");
|
|
748
|
+
} catch {
|
|
749
|
+
return null;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function lineBuffer(onLine) {
|
|
753
|
+
let buf = "";
|
|
754
|
+
return {
|
|
755
|
+
push(chunk) {
|
|
756
|
+
buf += chunk.toString();
|
|
757
|
+
let idx;
|
|
758
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
759
|
+
const line = buf.slice(0, idx).replace(/\r$/, "");
|
|
760
|
+
buf = buf.slice(idx + 1);
|
|
761
|
+
if (line.length) onLine(line);
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
flush() {
|
|
765
|
+
if (buf.length) {
|
|
766
|
+
onLine(buf);
|
|
767
|
+
buf = "";
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
}
|
|
478
772
|
var ProcessManager = class {
|
|
479
773
|
state = /* @__PURE__ */ new Map();
|
|
480
774
|
procs = /* @__PURE__ */ new Set();
|
|
@@ -488,9 +782,10 @@ var ProcessManager = class {
|
|
|
488
782
|
this.platform = opts.platform;
|
|
489
783
|
this.events = opts.events;
|
|
490
784
|
}
|
|
491
|
-
async install(svc) {
|
|
785
|
+
async install(svc, colorIdx) {
|
|
492
786
|
const cwd = join3(this.baseCwd, svc.cwd);
|
|
493
|
-
|
|
787
|
+
const idx = colorIdx ?? this.state.get(svc.name)?.colorIdx ?? 0;
|
|
788
|
+
return installService(cwd, this.env, (msg) => this.log(svc.name, msg, idx));
|
|
494
789
|
}
|
|
495
790
|
async start(svc, colorIdx, isRestart = false) {
|
|
496
791
|
const cwd = join3(this.baseCwd, svc.cwd);
|
|
@@ -501,6 +796,13 @@ var ProcessManager = class {
|
|
|
501
796
|
return;
|
|
502
797
|
}
|
|
503
798
|
}
|
|
799
|
+
if (svc.preBuild) {
|
|
800
|
+
const built = await this.runPreBuild(svc, cwd, colorIdx);
|
|
801
|
+
if (!built) {
|
|
802
|
+
this.recordCrashedState(svc, colorIdx);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
504
806
|
const args = buildProcessArgs(svc);
|
|
505
807
|
const env = buildProcessEnv(svc, this.env);
|
|
506
808
|
const proc = spawn2(svc.cmd, args, { cwd, env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -520,13 +822,31 @@ var ProcessManager = class {
|
|
|
520
822
|
this.state.set(svc.name, state);
|
|
521
823
|
this.procs.add(proc);
|
|
522
824
|
this.events.onStateChange(svc.name, state);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
state.
|
|
526
|
-
|
|
825
|
+
const readyRegex = compileReadyPattern(svc.readyPattern);
|
|
826
|
+
const markReadyIfMatch = (line) => {
|
|
827
|
+
if (!readyRegex || state.health === "up") return;
|
|
828
|
+
if (readyRegex.test(line)) {
|
|
829
|
+
state.health = "up";
|
|
830
|
+
if (state.status === "starting") state.status = "running";
|
|
831
|
+
this.events.onStateChange(svc.name, state);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
const stdoutBuf = lineBuffer((line) => {
|
|
835
|
+
markReadyIfMatch(line);
|
|
836
|
+
this.log(svc.name, line, colorIdx);
|
|
837
|
+
});
|
|
838
|
+
const stderrBuf = lineBuffer((line) => {
|
|
839
|
+
state.errors += 1;
|
|
840
|
+
markReadyIfMatch(line);
|
|
841
|
+
this.log(svc.name, line, colorIdx);
|
|
527
842
|
});
|
|
843
|
+
proc.stdout?.on("data", (d) => stdoutBuf.push(d));
|
|
844
|
+
proc.stderr?.on("data", (d) => stderrBuf.push(d));
|
|
845
|
+
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
846
|
+
proc.stderr?.on("end", () => stderrBuf.flush());
|
|
528
847
|
proc.on("close", (code) => {
|
|
529
848
|
this.procs.delete(proc);
|
|
849
|
+
this.stopWatchProc(state);
|
|
530
850
|
if (state.intentionalStop) {
|
|
531
851
|
state.intentionalStop = false;
|
|
532
852
|
return;
|
|
@@ -550,19 +870,96 @@ var ProcessManager = class {
|
|
|
550
870
|
this.log(svc.name, "\u26D4 max restarts reached", colorIdx);
|
|
551
871
|
}
|
|
552
872
|
});
|
|
873
|
+
if (svc.watchBuild) {
|
|
874
|
+
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
875
|
+
}
|
|
553
876
|
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
554
877
|
}
|
|
878
|
+
runPreBuild(svc, cwd, colorIdx) {
|
|
879
|
+
this.log(svc.name, `\u{1F528} preBuild: ${svc.preBuild}`, colorIdx);
|
|
880
|
+
return new Promise((resolve3) => {
|
|
881
|
+
const isWin = process.platform === "win32";
|
|
882
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
883
|
+
const shellFlag = isWin ? "/c" : "-c";
|
|
884
|
+
const env = buildProcessEnv(svc, this.env);
|
|
885
|
+
const child = spawn2(shell, [shellFlag, svc.preBuild], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
886
|
+
const outBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
887
|
+
const errBuf = lineBuffer((line) => this.log(svc.name, `[build] ${line}`, colorIdx));
|
|
888
|
+
child.stdout?.on("data", (d) => outBuf.push(d));
|
|
889
|
+
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
890
|
+
child.on("error", (err) => {
|
|
891
|
+
this.log(svc.name, `[build] \u274C ${err.message}`, colorIdx);
|
|
892
|
+
resolve3(false);
|
|
893
|
+
});
|
|
894
|
+
child.on("close", (code) => {
|
|
895
|
+
outBuf.flush();
|
|
896
|
+
errBuf.flush();
|
|
897
|
+
if (code === 0) {
|
|
898
|
+
this.log(svc.name, `[build] \u2705 done`, colorIdx);
|
|
899
|
+
resolve3(true);
|
|
900
|
+
} else {
|
|
901
|
+
this.log(svc.name, `[build] \u274C exited with code ${code}`, colorIdx);
|
|
902
|
+
resolve3(false);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
spawnWatchBuild(svc, cwd, env, colorIdx) {
|
|
908
|
+
this.log(svc.name, `\u{1F440} watchBuild: ${svc.watchBuild}`, colorIdx);
|
|
909
|
+
const isWin = process.platform === "win32";
|
|
910
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
911
|
+
const shellFlag = isWin ? "/c" : "-c";
|
|
912
|
+
const child = spawn2(shell, [shellFlag, svc.watchBuild], {
|
|
913
|
+
cwd,
|
|
914
|
+
env,
|
|
915
|
+
detached: true,
|
|
916
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
917
|
+
});
|
|
918
|
+
const outBuf = lineBuffer((line) => this.log(svc.name, `[watch] ${line}`, colorIdx));
|
|
919
|
+
const errBuf = lineBuffer((line) => this.log(svc.name, `[watch] ${line}`, colorIdx));
|
|
920
|
+
child.stdout?.on("data", (d) => outBuf.push(d));
|
|
921
|
+
child.stderr?.on("data", (d) => errBuf.push(d));
|
|
922
|
+
child.on("error", (err) => this.log(svc.name, `[watch] \u274C ${err.message}`, colorIdx));
|
|
923
|
+
return child;
|
|
924
|
+
}
|
|
925
|
+
/** Create a state entry in 'crashed' status without spawning a process (used when preBuild fails). */
|
|
926
|
+
recordCrashedState(svc, colorIdx) {
|
|
927
|
+
const prev = this.state.get(svc.name);
|
|
928
|
+
this.state.set(svc.name, {
|
|
929
|
+
svc,
|
|
930
|
+
proc: null,
|
|
931
|
+
pid: null,
|
|
932
|
+
status: "crashed",
|
|
933
|
+
health: "down",
|
|
934
|
+
errors: prev?.errors ?? 0,
|
|
935
|
+
restarts: prev?.restarts ?? 0,
|
|
936
|
+
startedAt: null,
|
|
937
|
+
intentionalStop: false,
|
|
938
|
+
colorIdx
|
|
939
|
+
});
|
|
940
|
+
this.events.onStateChange(svc.name, this.state.get(svc.name));
|
|
941
|
+
}
|
|
555
942
|
stop(name) {
|
|
556
943
|
const st = this.state.get(name);
|
|
557
944
|
if (!st?.proc || !st.pid) return;
|
|
558
945
|
st.intentionalStop = true;
|
|
559
946
|
this.platform.killTree(st.pid);
|
|
947
|
+
this.stopWatchProc(st);
|
|
948
|
+
}
|
|
949
|
+
stopWatchProc(state) {
|
|
950
|
+
const wp = state.watchProc;
|
|
951
|
+
if (!wp || !wp.pid) return;
|
|
952
|
+
try {
|
|
953
|
+
this.platform.killTree(wp.pid);
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
state.watchProc = null;
|
|
560
957
|
}
|
|
561
958
|
async restart(name) {
|
|
562
959
|
const st = this.state.get(name);
|
|
563
960
|
if (!st) return;
|
|
564
961
|
this.stop(name);
|
|
565
|
-
st.restarts
|
|
962
|
+
st.restarts = 0;
|
|
566
963
|
const delay = st.proc ? 1500 : 100;
|
|
567
964
|
await new Promise((r) => setTimeout(r, delay));
|
|
568
965
|
await this.start(st.svc, st.colorIdx, true);
|
|
@@ -574,39 +971,69 @@ var ProcessManager = class {
|
|
|
574
971
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
575
972
|
continue;
|
|
576
973
|
}
|
|
577
|
-
const
|
|
578
|
-
const isUp = await checkPort(port);
|
|
974
|
+
const isUp = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
579
975
|
const prev = st.health;
|
|
580
976
|
st.health = deriveHealth(isUp, st.status);
|
|
581
977
|
if (st.health === "up" && st.status === "starting") st.status = "running";
|
|
582
978
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
583
979
|
}
|
|
584
980
|
}
|
|
585
|
-
cleanup() {
|
|
586
|
-
|
|
981
|
+
async cleanup(opts = {}) {
|
|
982
|
+
const grace = opts.gracePeriodMs ?? 3e3;
|
|
983
|
+
const procs = [...this.procs];
|
|
984
|
+
if (!procs.length) return;
|
|
985
|
+
for (const proc of procs) {
|
|
986
|
+
const st = this.findStateByProc(proc);
|
|
987
|
+
if (st) {
|
|
988
|
+
st.intentionalStop = true;
|
|
989
|
+
this.stopWatchProc(st);
|
|
990
|
+
}
|
|
587
991
|
if (proc.pid) this.platform.killTree(proc.pid);
|
|
588
992
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
993
|
+
for (const st of this.state.values()) this.stopWatchProc(st);
|
|
994
|
+
const waits = procs.map(
|
|
995
|
+
(p) => p.exitCode !== null || p.signalCode !== null ? Promise.resolve() : new Promise((resolve3) => p.once("close", () => resolve3()))
|
|
996
|
+
);
|
|
997
|
+
let timedOut = false;
|
|
998
|
+
await Promise.race([
|
|
999
|
+
Promise.all(waits),
|
|
1000
|
+
new Promise((resolve3) => setTimeout(() => {
|
|
1001
|
+
timedOut = true;
|
|
1002
|
+
resolve3();
|
|
1003
|
+
}, grace))
|
|
1004
|
+
]);
|
|
1005
|
+
if (timedOut) {
|
|
1006
|
+
for (const proc of procs) {
|
|
1007
|
+
if (proc.pid && proc.exitCode === null && proc.signalCode === null) {
|
|
1008
|
+
this.platform.killTree(proc.pid, "SIGKILL");
|
|
1009
|
+
}
|
|
592
1010
|
}
|
|
593
|
-
|
|
1011
|
+
await Promise.race([
|
|
1012
|
+
Promise.all(waits),
|
|
1013
|
+
new Promise((resolve3) => setTimeout(resolve3, 1e3))
|
|
1014
|
+
]);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
findStateByProc(proc) {
|
|
1018
|
+
for (const st of this.state.values()) if (st.proc === proc) return st;
|
|
1019
|
+
return void 0;
|
|
594
1020
|
}
|
|
595
1021
|
log(name, text, colorIdx) {
|
|
596
1022
|
this.events.onLog(name, text, colorIdx);
|
|
597
1023
|
}
|
|
598
|
-
getColorIdx(name) {
|
|
599
|
-
return this.state.get(name)?.colorIdx ?? 0;
|
|
600
|
-
}
|
|
601
1024
|
};
|
|
602
1025
|
|
|
603
1026
|
// src/tui/hooks/useProcessManager.ts
|
|
604
|
-
function useProcessManager(platform, baseCwd, env) {
|
|
1027
|
+
function useProcessManager(platform, baseCwd, env, logSink = null) {
|
|
605
1028
|
const [states, setStates] = useState(/* @__PURE__ */ new Map());
|
|
606
1029
|
const [logs, setLogs] = useState([]);
|
|
607
1030
|
const [stats, setStats] = useState(/* @__PURE__ */ new Map());
|
|
608
1031
|
const mgrRef = useRef(null);
|
|
609
1032
|
const prevCpu = useRef(/* @__PURE__ */ new Map());
|
|
1033
|
+
const pausedRef = useRef(false);
|
|
1034
|
+
const pendingLogsRef = useRef([]);
|
|
1035
|
+
const sinkRef = useRef(logSink);
|
|
1036
|
+
sinkRef.current = logSink;
|
|
610
1037
|
useEffect(() => {
|
|
611
1038
|
const mgr2 = new ProcessManager({
|
|
612
1039
|
baseCwd,
|
|
@@ -614,9 +1041,17 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
614
1041
|
platform,
|
|
615
1042
|
events: {
|
|
616
1043
|
onLog: (svcName, text, colorIdx) => {
|
|
617
|
-
|
|
1044
|
+
sinkRef.current?.write(svcName, text);
|
|
1045
|
+
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1046
|
+
if (pausedRef.current) {
|
|
1047
|
+
pendingLogsRef.current.push(entry);
|
|
1048
|
+
if (pendingLogsRef.current.length > 5e3) {
|
|
1049
|
+
pendingLogsRef.current = pendingLogsRef.current.slice(-5e3);
|
|
1050
|
+
}
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
618
1053
|
setLogs((prev) => {
|
|
619
|
-
const next =
|
|
1054
|
+
const next = prev.concat(entry);
|
|
620
1055
|
return next.length > 5e3 ? next.slice(-5e3) : next;
|
|
621
1056
|
});
|
|
622
1057
|
},
|
|
@@ -659,6 +1094,36 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
659
1094
|
return () => clearInterval(id);
|
|
660
1095
|
}, [platform]);
|
|
661
1096
|
const mgr = mgrRef.current;
|
|
1097
|
+
const clearLogs = useCallback(() => {
|
|
1098
|
+
pendingLogsRef.current = [];
|
|
1099
|
+
setLogs([]);
|
|
1100
|
+
}, []);
|
|
1101
|
+
const pushLog = useCallback((svcName, text, colorIdx = 0) => {
|
|
1102
|
+
sinkRef.current?.write(svcName, text);
|
|
1103
|
+
const entry = { svcName, text, colorIdx, ts: Date.now() };
|
|
1104
|
+
if (pausedRef.current) {
|
|
1105
|
+
pendingLogsRef.current.push(entry);
|
|
1106
|
+
if (pendingLogsRef.current.length > 5e3) {
|
|
1107
|
+
pendingLogsRef.current = pendingLogsRef.current.slice(-5e3);
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
setLogs((prev) => {
|
|
1112
|
+
const next = prev.concat(entry);
|
|
1113
|
+
return next.length > 5e3 ? next.slice(-5e3) : next;
|
|
1114
|
+
});
|
|
1115
|
+
}, []);
|
|
1116
|
+
const setPaused = useCallback((paused) => {
|
|
1117
|
+
pausedRef.current = paused;
|
|
1118
|
+
if (!paused && pendingLogsRef.current.length) {
|
|
1119
|
+
const flush = pendingLogsRef.current;
|
|
1120
|
+
pendingLogsRef.current = [];
|
|
1121
|
+
setLogs((prev) => {
|
|
1122
|
+
const next = prev.concat(flush);
|
|
1123
|
+
return next.length > 5e3 ? next.slice(-5e3) : next;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
}, []);
|
|
662
1127
|
return {
|
|
663
1128
|
states,
|
|
664
1129
|
logs,
|
|
@@ -666,8 +1131,11 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
666
1131
|
start: useCallback((svc, colorIdx) => mgr?.start(svc, colorIdx), [mgr]),
|
|
667
1132
|
stop: useCallback((name) => mgr?.stop(name), [mgr]),
|
|
668
1133
|
restart: useCallback((name) => mgr?.restart(name), [mgr]),
|
|
669
|
-
install: useCallback((svc) => mgr?.install(svc), [mgr]),
|
|
1134
|
+
install: useCallback((svc, colorIdx) => mgr?.install(svc, colorIdx), [mgr]),
|
|
670
1135
|
cleanup: useCallback(() => mgr?.cleanup(), [mgr]),
|
|
1136
|
+
clearLogs,
|
|
1137
|
+
setPaused,
|
|
1138
|
+
pushLog,
|
|
671
1139
|
manager: mgr
|
|
672
1140
|
};
|
|
673
1141
|
}
|
|
@@ -676,6 +1144,24 @@ function useProcessManager(platform, baseCwd, env) {
|
|
|
676
1144
|
import { useInput } from "ink";
|
|
677
1145
|
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
678
1146
|
var SORT_MODES = ["name", "mem", "errors"];
|
|
1147
|
+
function scrollBy(setState, delta) {
|
|
1148
|
+
setState((s) => {
|
|
1149
|
+
if (s.panel === "logs") {
|
|
1150
|
+
const next2 = s.logsScrollOffset - delta;
|
|
1151
|
+
return { ...s, logsScrollOffset: Math.max(0, next2) };
|
|
1152
|
+
}
|
|
1153
|
+
const next = s.statsScrollOffset + delta;
|
|
1154
|
+
return { ...s, statsScrollOffset: Math.max(0, next) };
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
function scrollTo(setState, target) {
|
|
1158
|
+
setState((s) => {
|
|
1159
|
+
if (s.panel === "logs") {
|
|
1160
|
+
return { ...s, logsScrollOffset: target === "top" ? Number.MAX_SAFE_INTEGER : 0 };
|
|
1161
|
+
}
|
|
1162
|
+
return { ...s, statsScrollOffset: target === "top" ? 0 : Number.MAX_SAFE_INTEGER };
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
679
1165
|
function useKeyBindings(opts) {
|
|
680
1166
|
const [state, setState] = useState2({
|
|
681
1167
|
panel: "logs",
|
|
@@ -696,6 +1182,14 @@ function useKeyBindings(opts) {
|
|
|
696
1182
|
useInput((input, key) => {
|
|
697
1183
|
if (state.modal !== "none") return;
|
|
698
1184
|
if (input === "q" || key.ctrl && input === "c") opts.onQuit();
|
|
1185
|
+
else if (key.ctrl && input === "a") scrollTo(setState, "top");
|
|
1186
|
+
else if (key.ctrl && input === "e") scrollTo(setState, "bottom");
|
|
1187
|
+
else if (key.ctrl && input === "b") scrollBy(setState, -10);
|
|
1188
|
+
else if (key.ctrl && input === "f") scrollBy(setState, 10);
|
|
1189
|
+
else if (key.upArrow) scrollBy(setState, -1);
|
|
1190
|
+
else if (key.downArrow) scrollBy(setState, 1);
|
|
1191
|
+
else if (input === "[") scrollBy(setState, -10);
|
|
1192
|
+
else if (input === "]") scrollBy(setState, 10);
|
|
699
1193
|
else if (input === "c") opts.onClearLogs();
|
|
700
1194
|
else if (key.tab) setState((s) => ({ ...s, panel: s.panel === "logs" ? "stats" : "logs" }));
|
|
701
1195
|
else if (input === "f") setModal("filter");
|
|
@@ -709,60 +1203,6 @@ function useKeyBindings(opts) {
|
|
|
709
1203
|
else if (input === "T") {
|
|
710
1204
|
opts.onToggleProxy();
|
|
711
1205
|
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
1206
|
}
|
|
767
1207
|
}, { isActive });
|
|
768
1208
|
return {
|
|
@@ -780,26 +1220,28 @@ function useKeyBindings(opts) {
|
|
|
780
1220
|
// src/tui/hooks/useProxySync.ts
|
|
781
1221
|
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
782
1222
|
function useProxySync(provider, opts, states, enabled) {
|
|
783
|
-
const
|
|
1223
|
+
const statesRef = useRef2(states);
|
|
1224
|
+
const lastContentRef = useRef2(null);
|
|
1225
|
+
statesRef.current = states;
|
|
784
1226
|
useEffect2(() => {
|
|
785
|
-
if (!provider || !opts || !enabled)
|
|
786
|
-
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
1227
|
+
if (!provider || !opts || !enabled) return;
|
|
789
1228
|
const sync = () => {
|
|
790
1229
|
const svcStates = /* @__PURE__ */ new Map();
|
|
791
|
-
for (const [name, st] of
|
|
1230
|
+
for (const [name, st] of statesRef.current) {
|
|
792
1231
|
svcStates.set(name, { port: st.svc.port, health: st.health, realPort: st.svc.realPort });
|
|
793
1232
|
}
|
|
794
1233
|
const content = provider.generate(svcStates, opts);
|
|
1234
|
+
if (content === lastContentRef.current) return;
|
|
1235
|
+
lastContentRef.current = content;
|
|
795
1236
|
provider.write(content, opts);
|
|
796
1237
|
};
|
|
797
1238
|
sync();
|
|
798
|
-
|
|
1239
|
+
const id = setInterval(sync, 3e3);
|
|
799
1240
|
return () => {
|
|
800
|
-
|
|
1241
|
+
clearInterval(id);
|
|
1242
|
+
lastContentRef.current = null;
|
|
801
1243
|
};
|
|
802
|
-
}, [provider, opts, enabled
|
|
1244
|
+
}, [provider, opts, enabled]);
|
|
803
1245
|
}
|
|
804
1246
|
|
|
805
1247
|
// src/tui/LogsPanel.tsx
|
|
@@ -808,27 +1250,25 @@ import { Box, Text } from "ink";
|
|
|
808
1250
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
809
1251
|
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll }) {
|
|
810
1252
|
const filtered = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
811
|
-
const contentHeight = height - 2;
|
|
1253
|
+
const contentHeight = Math.max(1, height - 2);
|
|
812
1254
|
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);
|
|
1255
|
+
const maxOffset = Math.max(0, totalLines - contentHeight);
|
|
1256
|
+
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxOffset : Math.min(scrollOffset, maxOffset);
|
|
1257
|
+
const startIndex = Math.max(0, totalLines - contentHeight - effectiveOffset);
|
|
1258
|
+
const endIndex = Math.min(startIndex + contentHeight, totalLines);
|
|
1259
|
+
const visible = filtered.slice(startIndex, endIndex);
|
|
822
1260
|
useEffect3(() => {
|
|
823
1261
|
resetScroll();
|
|
824
1262
|
}, [filter, searchTerm, resetScroll]);
|
|
1263
|
+
const scrolled = effectiveOffset > 0;
|
|
825
1264
|
const label = [
|
|
826
1265
|
"Logs",
|
|
827
1266
|
filter ? `[${filter}]` : "",
|
|
828
1267
|
searchTerm ? `/${searchTerm}` : "",
|
|
829
1268
|
paused ? "[PAUSED]" : "",
|
|
1269
|
+
scrolled ? "[SCROLL]" : "",
|
|
830
1270
|
`${filtered.length} lines`,
|
|
831
|
-
focused ? `(${startIndex + 1}-${
|
|
1271
|
+
focused && totalLines > 0 ? `(${startIndex + 1}-${endIndex}/${totalLines})` : ""
|
|
832
1272
|
].filter(Boolean).join(" ");
|
|
833
1273
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "cyan" : "gray", height, children: [
|
|
834
1274
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
@@ -931,33 +1371,29 @@ function StatsPanel({ states, stats, sortMode, maxNameLen, height, focused, scro
|
|
|
931
1371
|
}
|
|
932
1372
|
const stackMem = totalMemMB >= 1024 ? (totalMemMB / 1024).toFixed(2) + " GB" : totalMemMB.toFixed(1) + " MB";
|
|
933
1373
|
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);
|
|
1374
|
+
const contentHeight = Math.max(1, height - 2);
|
|
1375
|
+
const rowsPerCol = Math.max(1, contentHeight - 2);
|
|
1376
|
+
const maxRows = Math.max(0, Math.max(apis.length, webs.length) - rowsPerCol);
|
|
1377
|
+
const effectiveOffset = scrollOffset === Number.MAX_SAFE_INTEGER ? maxRows : Math.min(scrollOffset, maxRows);
|
|
1378
|
+
const apiStartIndex = Math.min(effectiveOffset, Math.max(0, apis.length - rowsPerCol));
|
|
1379
|
+
const webStartIndex = Math.min(effectiveOffset, Math.max(0, webs.length - rowsPerCol));
|
|
1380
|
+
const visibleApis = apis.slice(apiStartIndex, apiStartIndex + rowsPerCol);
|
|
1381
|
+
const visibleWebs = webs.slice(webStartIndex, webStartIndex + rowsPerCol);
|
|
948
1382
|
useEffect4(() => {
|
|
949
1383
|
resetScroll();
|
|
950
1384
|
}, [sortMode, resetScroll]);
|
|
951
|
-
const
|
|
952
|
-
const positionInfo = focused ? `(${
|
|
1385
|
+
const totalRowsLong = Math.max(apis.length, webs.length);
|
|
1386
|
+
const positionInfo = focused && totalRowsLong > 0 ? `(${effectiveOffset + 1}-${Math.min(effectiveOffset + rowsPerCol, totalRowsLong)}/${totalRowsLong})` : "";
|
|
1387
|
+
const scrolled = effectiveOffset > 0;
|
|
953
1388
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: focused ? "green" : "gray", height, children: [
|
|
954
1389
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
955
1390
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, color: "green", children: [
|
|
956
1391
|
" Stats ",
|
|
957
1392
|
positionInfo
|
|
958
1393
|
] }),
|
|
1394
|
+
scrolled && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " [SCROLL]" }),
|
|
959
1395
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
960
|
-
"System: ",
|
|
1396
|
+
" System: ",
|
|
961
1397
|
cpus,
|
|
962
1398
|
"c Load ",
|
|
963
1399
|
load,
|
|
@@ -1097,59 +1533,58 @@ function SearchInput({ onSubmit, onClose }) {
|
|
|
1097
1533
|
] });
|
|
1098
1534
|
}
|
|
1099
1535
|
|
|
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
1536
|
// src/lazy/proxy.ts
|
|
1123
1537
|
import net2 from "net";
|
|
1124
1538
|
function createLazyProxy(opts) {
|
|
1125
1539
|
const { listenPort, targetPort, timeoutMin, onDemandStart, onIdleStop, isAlive, onLog } = opts;
|
|
1126
1540
|
let idleTimer = null;
|
|
1541
|
+
let lastActivity = Date.now();
|
|
1127
1542
|
let starting = false;
|
|
1128
1543
|
let serviceReady = false;
|
|
1129
1544
|
let pendingConns = [];
|
|
1130
|
-
|
|
1545
|
+
const activeConns = /* @__PURE__ */ new Set();
|
|
1546
|
+
function bumpActivity() {
|
|
1547
|
+
lastActivity = Date.now();
|
|
1548
|
+
}
|
|
1549
|
+
function scheduleIdleCheck() {
|
|
1131
1550
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1132
|
-
if (timeoutMin
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1551
|
+
if (timeoutMin <= 0) return;
|
|
1552
|
+
const periodMs = timeoutMin * 6e4;
|
|
1553
|
+
idleTimer = setTimeout(() => {
|
|
1554
|
+
const elapsed = Date.now() - lastActivity;
|
|
1555
|
+
if (activeConns.size > 0 || elapsed < periodMs) {
|
|
1556
|
+
scheduleIdleCheck();
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
serviceReady = false;
|
|
1560
|
+
onLog?.(`\u{1F4A4} idle ${timeoutMin}min \u2014 stopping`);
|
|
1561
|
+
onIdleStop();
|
|
1562
|
+
}, periodMs);
|
|
1139
1563
|
}
|
|
1140
1564
|
function pipeToTarget(client) {
|
|
1141
1565
|
const target = net2.createConnection({ port: targetPort, host: "127.0.0.1", allowHalfOpen: true });
|
|
1566
|
+
activeConns.add(client);
|
|
1567
|
+
const cleanup = () => {
|
|
1568
|
+
activeConns.delete(client);
|
|
1569
|
+
bumpActivity();
|
|
1570
|
+
};
|
|
1142
1571
|
target.on("error", () => {
|
|
1143
1572
|
client.destroy();
|
|
1573
|
+
cleanup();
|
|
1144
1574
|
});
|
|
1145
1575
|
client.on("error", () => {
|
|
1146
1576
|
target.destroy();
|
|
1577
|
+
cleanup();
|
|
1147
1578
|
});
|
|
1579
|
+
client.on("close", cleanup);
|
|
1580
|
+
target.on("close", cleanup);
|
|
1148
1581
|
target.on("connect", () => {
|
|
1149
1582
|
target.on("data", (chunk) => {
|
|
1583
|
+
bumpActivity();
|
|
1150
1584
|
if (!client.destroyed) client.write(chunk);
|
|
1151
1585
|
});
|
|
1152
1586
|
client.on("data", (chunk) => {
|
|
1587
|
+
bumpActivity();
|
|
1153
1588
|
if (!target.destroyed) target.write(chunk);
|
|
1154
1589
|
});
|
|
1155
1590
|
target.on("end", () => {
|
|
@@ -1161,7 +1596,7 @@ function createLazyProxy(opts) {
|
|
|
1161
1596
|
});
|
|
1162
1597
|
}
|
|
1163
1598
|
async function handleConnection(client) {
|
|
1164
|
-
|
|
1599
|
+
bumpActivity();
|
|
1165
1600
|
client.on("error", () => {
|
|
1166
1601
|
});
|
|
1167
1602
|
if (serviceReady && isAlive()) {
|
|
@@ -1175,9 +1610,10 @@ function createLazyProxy(opts) {
|
|
|
1175
1610
|
if (starting) return;
|
|
1176
1611
|
starting = true;
|
|
1177
1612
|
onLog?.("\u26A1 on-demand start");
|
|
1613
|
+
let ok = false;
|
|
1178
1614
|
try {
|
|
1179
1615
|
await onDemandStart();
|
|
1180
|
-
|
|
1616
|
+
ok = await waitForPort(targetPort, { timeout: 45e3, interval: 500 });
|
|
1181
1617
|
if (ok) serviceReady = true;
|
|
1182
1618
|
else onLog?.("\u26A0 timeout waiting for service");
|
|
1183
1619
|
} catch (e) {
|
|
@@ -1185,46 +1621,156 @@ function createLazyProxy(opts) {
|
|
|
1185
1621
|
}
|
|
1186
1622
|
starting = false;
|
|
1187
1623
|
const conns = pendingConns.splice(0);
|
|
1624
|
+
if (!ok) {
|
|
1625
|
+
for (const conn of conns) {
|
|
1626
|
+
if (!conn.destroyed) conn.destroy();
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1188
1630
|
for (const conn of conns) {
|
|
1189
1631
|
if (!conn.destroyed) pipeToTarget(conn);
|
|
1190
1632
|
}
|
|
1191
1633
|
}
|
|
1192
1634
|
const server = net2.createServer({ allowHalfOpen: true }, (socket) => handleConnection(socket));
|
|
1193
1635
|
server.listen(listenPort, "0.0.0.0");
|
|
1194
|
-
|
|
1636
|
+
scheduleIdleCheck();
|
|
1195
1637
|
return {
|
|
1196
1638
|
server,
|
|
1197
|
-
resetTimer,
|
|
1639
|
+
resetTimer: bumpActivity,
|
|
1198
1640
|
destroy: () => {
|
|
1199
1641
|
if (idleTimer) clearTimeout(idleTimer);
|
|
1200
1642
|
pendingConns.forEach((s) => s.destroy());
|
|
1643
|
+
activeConns.forEach((s) => s.destroy());
|
|
1201
1644
|
server.close();
|
|
1202
1645
|
}
|
|
1203
1646
|
};
|
|
1204
1647
|
}
|
|
1205
1648
|
|
|
1649
|
+
// src/process/external.ts
|
|
1650
|
+
import { spawn as spawn3 } from "child_process";
|
|
1651
|
+
import { join as join4 } from "path";
|
|
1652
|
+
var DEFAULT_START_TIMEOUT_S = 60;
|
|
1653
|
+
async function startExternals(externals, opts) {
|
|
1654
|
+
const procs = [];
|
|
1655
|
+
const failed = [];
|
|
1656
|
+
for (const svc of externals) {
|
|
1657
|
+
const proc = spawnExternal(svc, opts);
|
|
1658
|
+
procs.push({ svc, proc, pid: proc.pid ?? null });
|
|
1659
|
+
if (!svc.healthCheck) {
|
|
1660
|
+
opts.onLog?.(svc.name, "\u2705 started (no healthCheck)");
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (svc.healthCheck.type === "tcp" && !svc.port) {
|
|
1664
|
+
opts.onLog?.(svc.name, "\u26A0 tcp healthCheck requires `port` \u2014 skipping wait");
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const timeoutMs = (svc.startTimeout ?? DEFAULT_START_TIMEOUT_S) * 1e3;
|
|
1668
|
+
const ok = await waitHealthy(svc, timeoutMs);
|
|
1669
|
+
if (ok) {
|
|
1670
|
+
opts.onLog?.(svc.name, "\u2705 healthy");
|
|
1671
|
+
} else {
|
|
1672
|
+
opts.onLog?.(svc.name, `\u274C never became healthy (timeout ${timeoutMs / 1e3}s)`);
|
|
1673
|
+
failed.push(svc.name);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return { procs, allHealthy: failed.length === 0, failed };
|
|
1677
|
+
}
|
|
1678
|
+
async function stopExternals(procs, platform, opts = {}) {
|
|
1679
|
+
for (const { svc, proc, pid } of procs) {
|
|
1680
|
+
try {
|
|
1681
|
+
if (pid) platform.killTree(pid);
|
|
1682
|
+
if (svc.stopCmd) {
|
|
1683
|
+
opts.onLog?.(svc.name, `\u{1F9F9} ${svc.stopCmd}`);
|
|
1684
|
+
await new Promise((resolve3) => {
|
|
1685
|
+
const isWin = process.platform === "win32";
|
|
1686
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
1687
|
+
const flag = isWin ? "/c" : "-c";
|
|
1688
|
+
const cwd = svc.cwd ? join4(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1689
|
+
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1690
|
+
const child = spawn3(shell, [flag, svc.stopCmd], { cwd, env, stdio: "ignore" });
|
|
1691
|
+
child.on("close", () => resolve3());
|
|
1692
|
+
child.on("error", () => resolve3());
|
|
1693
|
+
setTimeout(() => resolve3(), 1e4);
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
} catch {
|
|
1697
|
+
}
|
|
1698
|
+
void proc;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function spawnExternal(svc, opts) {
|
|
1702
|
+
const isWin = process.platform === "win32";
|
|
1703
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
1704
|
+
const flag = isWin ? "/c" : "-c";
|
|
1705
|
+
const cwd = svc.cwd ? join4(opts.baseCwd, svc.cwd) : opts.baseCwd;
|
|
1706
|
+
const env = { ...opts.env, ...svc.extraEnv ?? {} };
|
|
1707
|
+
opts.onLog?.(svc.name, `\u{1F680} ${svc.cmd}`);
|
|
1708
|
+
const child = spawn3(shell, [flag, svc.cmd], {
|
|
1709
|
+
cwd,
|
|
1710
|
+
env,
|
|
1711
|
+
detached: true,
|
|
1712
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1713
|
+
});
|
|
1714
|
+
child.stdout?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
1715
|
+
child.stderr?.on("data", (d) => opts.onLog?.(svc.name, d.toString().trimEnd()));
|
|
1716
|
+
child.on("error", (err) => opts.onLog?.(svc.name, `\u274C spawn error: ${err.message}`));
|
|
1717
|
+
return child;
|
|
1718
|
+
}
|
|
1719
|
+
async function waitHealthy(svc, timeoutMs) {
|
|
1720
|
+
const deadline = Date.now() + timeoutMs;
|
|
1721
|
+
const port = svc.port;
|
|
1722
|
+
while (Date.now() < deadline) {
|
|
1723
|
+
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
1724
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1725
|
+
}
|
|
1726
|
+
return false;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1206
1729
|
// src/tui/App.tsx
|
|
1207
1730
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1208
|
-
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts }) {
|
|
1731
|
+
function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider, proxyOpts, logSink }) {
|
|
1209
1732
|
const { stdout } = useStdout();
|
|
1210
|
-
const rows = stdout?.rows ?? 40;
|
|
1733
|
+
const [rows, setRows] = useState5(stdout?.rows ?? 40);
|
|
1734
|
+
useEffect5(() => {
|
|
1735
|
+
if (!stdout) return;
|
|
1736
|
+
const onResize = () => setRows(stdout.rows ?? 40);
|
|
1737
|
+
stdout.on("resize", onResize);
|
|
1738
|
+
return () => {
|
|
1739
|
+
stdout.off("resize", onResize);
|
|
1740
|
+
};
|
|
1741
|
+
}, [stdout]);
|
|
1211
1742
|
const logsHeight = Math.floor(rows * 0.65);
|
|
1212
1743
|
const statsHeight = rows - logsHeight - 2;
|
|
1213
1744
|
const maxNameLen = Math.max(...services.map((s) => s.name.length), 10);
|
|
1214
|
-
const pm = useProcessManager(platform, baseCwd, env);
|
|
1745
|
+
const pm = useProcessManager(platform, baseCwd, env, logSink);
|
|
1215
1746
|
const [booted, setBooted] = useState5(false);
|
|
1216
1747
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
1748
|
+
const externals = useRef3([]);
|
|
1217
1749
|
const kb = useKeyBindings({
|
|
1218
1750
|
onQuit: () => {
|
|
1219
|
-
|
|
1220
|
-
pm.cleanup();
|
|
1221
|
-
process.exit(0);
|
|
1222
|
-
},
|
|
1223
|
-
onClearLogs: () => {
|
|
1751
|
+
void shutdown();
|
|
1224
1752
|
},
|
|
1753
|
+
onClearLogs: pm.clearLogs,
|
|
1225
1754
|
onToggleProxy: () => {
|
|
1226
1755
|
}
|
|
1227
1756
|
});
|
|
1757
|
+
const shutdown = useCallback3(async () => {
|
|
1758
|
+
lazyProxies.current.forEach((p) => p.destroy());
|
|
1759
|
+
await pm.cleanup();
|
|
1760
|
+
if (externals.current.length) {
|
|
1761
|
+
await stopExternals(externals.current, platform, {
|
|
1762
|
+
baseCwd,
|
|
1763
|
+
env,
|
|
1764
|
+
onLog: (svc, msg) => pm.pushLog(`ext:${svc}`, msg, 12)
|
|
1765
|
+
});
|
|
1766
|
+
externals.current = [];
|
|
1767
|
+
}
|
|
1768
|
+
await logSink?.close();
|
|
1769
|
+
process.exit(0);
|
|
1770
|
+
}, [pm, logSink, platform, baseCwd, env]);
|
|
1771
|
+
useEffect5(() => {
|
|
1772
|
+
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
1773
|
+
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
1228
1774
|
useProxySync(proxyProvider, proxyOpts, pm.states, kb.proxyEnabled);
|
|
1229
1775
|
useEffect5(() => {
|
|
1230
1776
|
if (booted || !pm.manager) return;
|
|
@@ -1233,6 +1779,19 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1233
1779
|
(async () => {
|
|
1234
1780
|
const lazyMode = cliArgs.lazy;
|
|
1235
1781
|
const lazyTimeout = cliArgs.lazyTimeout;
|
|
1782
|
+
if (config.external?.length) {
|
|
1783
|
+
const result = await startExternals(config.external, {
|
|
1784
|
+
baseCwd,
|
|
1785
|
+
env,
|
|
1786
|
+
platform,
|
|
1787
|
+
onLog: (svc, msg) => pm.pushLog(`ext:${svc}`, msg, 12)
|
|
1788
|
+
});
|
|
1789
|
+
externals.current = result.procs;
|
|
1790
|
+
if (!result.allHealthy) {
|
|
1791
|
+
pm.pushLog("devup", `\u274C external(s) failed: ${result.failed.join(", ")}. Aborting boot.`, 5);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1236
1795
|
if (lazyMode && config.lazy) {
|
|
1237
1796
|
const { alwaysOn, lazy } = classifyServices(services, config.lazy);
|
|
1238
1797
|
const aoPhases = groupByPhase(alwaysOn);
|
|
@@ -1240,8 +1799,9 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1240
1799
|
for (const num of Object.keys(aoPhases).map(Number).sort((a, b) => a - b)) {
|
|
1241
1800
|
const svcs = aoPhases[num];
|
|
1242
1801
|
for (const svc of svcs) {
|
|
1243
|
-
|
|
1244
|
-
await mgr.
|
|
1802
|
+
const ci = colorIdx++;
|
|
1803
|
+
await mgr.install(svc, ci);
|
|
1804
|
+
await mgr.start(svc, ci);
|
|
1245
1805
|
}
|
|
1246
1806
|
const apis = svcs.filter((s) => s.type === "api");
|
|
1247
1807
|
if (apis.length) await Promise.all(apis.map((s) => waitForPort(s.port, { timeout: 45e3 })));
|
|
@@ -1271,7 +1831,7 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1271
1831
|
targetPort: rewritten.realPort,
|
|
1272
1832
|
timeoutMin: lazyTimeout,
|
|
1273
1833
|
onDemandStart: async () => {
|
|
1274
|
-
await mgr.install(rewritten);
|
|
1834
|
+
await mgr.install(rewritten, ci);
|
|
1275
1835
|
await mgr.start(rewritten, ci);
|
|
1276
1836
|
const ok = await waitForPort(rewritten.realPort, { timeout: 45e3 });
|
|
1277
1837
|
const st = mgr.state.get(svc.name);
|
|
@@ -1304,8 +1864,9 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1304
1864
|
for (const num of Object.keys(phases).map(Number).sort((a, b) => a - b)) {
|
|
1305
1865
|
const svcs = phases[num];
|
|
1306
1866
|
for (const svc of svcs) {
|
|
1307
|
-
|
|
1308
|
-
await mgr.
|
|
1867
|
+
const ci = colorIdx++;
|
|
1868
|
+
await mgr.install(svc, ci);
|
|
1869
|
+
await mgr.start(svc, ci);
|
|
1309
1870
|
}
|
|
1310
1871
|
const apis = svcs.filter((s) => s.type === "api");
|
|
1311
1872
|
if (apis.length) await Promise.all(apis.map((s) => waitForPort(s.port, { timeout: 45e3 })));
|
|
@@ -1377,6 +1938,222 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
1377
1938
|
] });
|
|
1378
1939
|
}
|
|
1379
1940
|
|
|
1941
|
+
// src/process/log-sink.ts
|
|
1942
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, renameSync, createWriteStream } from "fs";
|
|
1943
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1944
|
+
import { homedir } from "os";
|
|
1945
|
+
var LogSink = class {
|
|
1946
|
+
dir;
|
|
1947
|
+
rotateOnStart;
|
|
1948
|
+
streams = /* @__PURE__ */ new Map();
|
|
1949
|
+
seen = /* @__PURE__ */ new Set();
|
|
1950
|
+
constructor(opts) {
|
|
1951
|
+
const root = opts.rootDir ?? join5(homedir(), ".devup", "logs");
|
|
1952
|
+
this.dir = join5(root, sanitize(opts.projectName));
|
|
1953
|
+
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1954
|
+
mkdirSync4(this.dir, { recursive: true });
|
|
1955
|
+
}
|
|
1956
|
+
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1957
|
+
pathFor(svcName) {
|
|
1958
|
+
return join5(this.dir, `${sanitize(svcName)}.log`);
|
|
1959
|
+
}
|
|
1960
|
+
write(svcName, line) {
|
|
1961
|
+
const stream = this.streamFor(svcName);
|
|
1962
|
+
stream.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
1963
|
+
`);
|
|
1964
|
+
}
|
|
1965
|
+
async close() {
|
|
1966
|
+
const closes = [...this.streams.values()].map(
|
|
1967
|
+
(s) => new Promise((r) => s.end(() => r()))
|
|
1968
|
+
);
|
|
1969
|
+
this.streams.clear();
|
|
1970
|
+
this.seen.clear();
|
|
1971
|
+
await Promise.all(closes);
|
|
1972
|
+
}
|
|
1973
|
+
streamFor(svcName) {
|
|
1974
|
+
let s = this.streams.get(svcName);
|
|
1975
|
+
if (s) return s;
|
|
1976
|
+
const file = this.pathFor(svcName);
|
|
1977
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync8(file)) {
|
|
1978
|
+
try {
|
|
1979
|
+
mkdirSync4(dirname4(file), { recursive: true });
|
|
1980
|
+
renameSync(file, file + ".prev");
|
|
1981
|
+
} catch {
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
this.seen.add(svcName);
|
|
1985
|
+
s = createWriteStream(file, { flags: "a" });
|
|
1986
|
+
s.on("error", () => {
|
|
1987
|
+
});
|
|
1988
|
+
this.streams.set(svcName, s);
|
|
1989
|
+
return s;
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
function sanitize(name) {
|
|
1993
|
+
return name.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "") || "devup";
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/orchestrator/dry-run.ts
|
|
1997
|
+
function renderDryRun(opts) {
|
|
1998
|
+
const { config, services, cliArgs, env, proxyProvider, proxyOpts } = opts;
|
|
1999
|
+
const lines = [];
|
|
2000
|
+
lines.push(`Project: ${config.icon ?? "\u{1F4E6}"} ${config.name}`);
|
|
2001
|
+
lines.push(`Mode: ${cliArgs.lazy && config.lazy ? "lazy" : "normal"}`);
|
|
2002
|
+
if (cliArgs.profile) lines.push(`Profile: ${cliArgs.profile}`);
|
|
2003
|
+
lines.push(`Services: ${services.length}`);
|
|
2004
|
+
lines.push("");
|
|
2005
|
+
if (config.external?.length) {
|
|
2006
|
+
lines.push(`Externals (${config.external.length}):`);
|
|
2007
|
+
for (const ext of config.external) {
|
|
2008
|
+
const hc = ext.healthCheck;
|
|
2009
|
+
const hcTag = hc ? ` health=${hc.type}${hc.type === "http" ? " " + (hc.path ?? "/") : ""} :${ext.port ?? "?"}` : "";
|
|
2010
|
+
lines.push(` - ${ext.name.padEnd(20)} ${ext.cmd}${hcTag}`);
|
|
2011
|
+
}
|
|
2012
|
+
lines.push("");
|
|
2013
|
+
}
|
|
2014
|
+
const lazyMode = cliArgs.lazy && !!config.lazy;
|
|
2015
|
+
let alwaysOn = services;
|
|
2016
|
+
let lazy = [];
|
|
2017
|
+
if (lazyMode) {
|
|
2018
|
+
const c = classifyServices(services, config.lazy);
|
|
2019
|
+
alwaysOn = c.alwaysOn;
|
|
2020
|
+
lazy = c.lazy;
|
|
2021
|
+
}
|
|
2022
|
+
const phases = groupByPhase(alwaysOn);
|
|
2023
|
+
const phaseNums = Object.keys(phases).map(Number).sort((a, b) => a - b);
|
|
2024
|
+
for (const num of phaseNums) {
|
|
2025
|
+
lines.push(`Phase ${num}:`);
|
|
2026
|
+
for (const svc of phases[num]) {
|
|
2027
|
+
lines.push(formatService(svc, env, false));
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
if (lazy.length) {
|
|
2031
|
+
lines.push("");
|
|
2032
|
+
lines.push("Lazy (on-demand):");
|
|
2033
|
+
for (const svc of lazy) {
|
|
2034
|
+
const rewritten = rewriteServicePort(svc);
|
|
2035
|
+
lines.push(formatService(rewritten, env, true));
|
|
2036
|
+
lines.push(` proxy :${svc.port} \u2192 :${getLazyRealPort(svc.port)} (idle timeout ${cliArgs.lazyTimeout}m)`);
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
if (proxyProvider && proxyOpts) {
|
|
2040
|
+
lines.push("");
|
|
2041
|
+
lines.push(`Proxy: ${proxyProvider.name} \u2192 ${proxyOpts.confPath}`);
|
|
2042
|
+
const svcStates = /* @__PURE__ */ new Map();
|
|
2043
|
+
for (const svc of services) {
|
|
2044
|
+
const real = lazyMode && !alwaysOn.includes(svc) ? getLazyRealPort(svc.port) : void 0;
|
|
2045
|
+
svcStates.set(svc.name, { port: svc.port, health: "up", realPort: real });
|
|
2046
|
+
}
|
|
2047
|
+
const content = proxyProvider.generate(svcStates, proxyOpts);
|
|
2048
|
+
lines.push("");
|
|
2049
|
+
lines.push("--- generated config ---");
|
|
2050
|
+
lines.push(content);
|
|
2051
|
+
}
|
|
2052
|
+
return lines.join("\n");
|
|
2053
|
+
}
|
|
2054
|
+
function formatService(svc, env, isLazy) {
|
|
2055
|
+
const args = buildProcessArgs(svc);
|
|
2056
|
+
const cmdLine = [svc.cmd, ...args].join(" ");
|
|
2057
|
+
const built = buildProcessEnv(svc, env);
|
|
2058
|
+
const extraEnv = Object.keys(svc.extraEnv ?? {}).length ? " env=" + Object.entries(svc.extraEnv).map(([k, v]) => `${k}=${v}`).join(" ") : "";
|
|
2059
|
+
const memTag = svc.maxMem ? ` mem=${svc.maxMem}MB` : "";
|
|
2060
|
+
const hc = svc.healthCheck;
|
|
2061
|
+
const hcTag = hc?.type === "http" ? ` health=http ${hc.path ?? "/"}` : "";
|
|
2062
|
+
const lazyTag = isLazy ? " [lazy]" : "";
|
|
2063
|
+
void built;
|
|
2064
|
+
return ` - ${svc.name.padEnd(20)} (${svc.type}) :${svc.port} ${cmdLine}${memTag}${hcTag}${lazyTag}${extraEnv}`;
|
|
2065
|
+
}
|
|
2066
|
+
function runDryRun(opts) {
|
|
2067
|
+
console.log(renderDryRun(opts));
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/orchestrator/once.ts
|
|
2071
|
+
async function runOnce(opts) {
|
|
2072
|
+
const out = opts.out ?? ((l) => console.log(l));
|
|
2073
|
+
const { config, services, cliArgs, platform, env, baseCwd, logSink } = opts;
|
|
2074
|
+
const mgr = new ProcessManager({
|
|
2075
|
+
baseCwd,
|
|
2076
|
+
env,
|
|
2077
|
+
platform,
|
|
2078
|
+
events: {
|
|
2079
|
+
onLog: (svc, text) => {
|
|
2080
|
+
logSink?.write(svc, text);
|
|
2081
|
+
out(`[${svc}] ${text}`);
|
|
2082
|
+
},
|
|
2083
|
+
onStateChange: () => {
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
let externals = [];
|
|
2088
|
+
if (config.external?.length) {
|
|
2089
|
+
out(`\u25B6 externals (${config.external.length})`);
|
|
2090
|
+
const result = await startExternals(config.external, {
|
|
2091
|
+
baseCwd,
|
|
2092
|
+
env,
|
|
2093
|
+
platform,
|
|
2094
|
+
onLog: (svc, msg) => {
|
|
2095
|
+
logSink?.write(`ext:${svc}`, msg);
|
|
2096
|
+
out(`[ext:${svc}] ${msg}`);
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
externals = result.procs;
|
|
2100
|
+
if (!result.allHealthy) {
|
|
2101
|
+
out(`\u2717 externals failed: ${result.failed.join(", ")}`);
|
|
2102
|
+
await stopExternals(externals, platform, { baseCwd, env });
|
|
2103
|
+
await mgr.cleanup();
|
|
2104
|
+
return 1;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
const phases = groupByPhase(services);
|
|
2108
|
+
const phaseNums = Object.keys(phases).map(Number).sort((a, b) => a - b);
|
|
2109
|
+
const apiNames = services.filter((s) => s.type === "api").map((s) => s.name);
|
|
2110
|
+
const deadline = Date.now() + cliArgs.onceTimeout * 1e3;
|
|
2111
|
+
let colorIdx = 0;
|
|
2112
|
+
for (const num of phaseNums) {
|
|
2113
|
+
out(`\u25B6 phase ${num}`);
|
|
2114
|
+
for (const svc of phases[num]) {
|
|
2115
|
+
const ci = colorIdx++;
|
|
2116
|
+
const installed = await mgr.install(svc, ci);
|
|
2117
|
+
if (!installed) {
|
|
2118
|
+
out(`\u2717 install failed for ${svc.name}`);
|
|
2119
|
+
await mgr.cleanup();
|
|
2120
|
+
await stopExternals(externals, platform, { baseCwd, env });
|
|
2121
|
+
return 1;
|
|
2122
|
+
}
|
|
2123
|
+
await mgr.start(svc, ci);
|
|
2124
|
+
}
|
|
2125
|
+
const apis = phases[num].filter((s) => s.type === "api");
|
|
2126
|
+
for (const api of apis) {
|
|
2127
|
+
const ok = await waitHealthy2(api, deadline);
|
|
2128
|
+
if (!ok) {
|
|
2129
|
+
out(`\u2717 ${api.name} did not become healthy within ${cliArgs.onceTimeout}s`);
|
|
2130
|
+
await mgr.cleanup();
|
|
2131
|
+
await stopExternals(externals, platform, { baseCwd, env });
|
|
2132
|
+
return 1;
|
|
2133
|
+
}
|
|
2134
|
+
out(`\u2713 ${api.name} ready`);
|
|
2135
|
+
const st = mgr.state.get(api.name);
|
|
2136
|
+
if (st) {
|
|
2137
|
+
st.status = "running";
|
|
2138
|
+
st.health = "up";
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const summary = `ready: ${apiNames.length} APIs in ${((cliArgs.onceTimeout * 1e3 - (deadline - Date.now())) / 1e3).toFixed(1)}s`;
|
|
2143
|
+
out(summary);
|
|
2144
|
+
await mgr.cleanup();
|
|
2145
|
+
await stopExternals(externals, platform, { baseCwd, env });
|
|
2146
|
+
return 0;
|
|
2147
|
+
}
|
|
2148
|
+
async function waitHealthy2(svc, deadline) {
|
|
2149
|
+
while (Date.now() < deadline) {
|
|
2150
|
+
const ok = await checkHealth(svc.port, svc.healthCheck);
|
|
2151
|
+
if (ok) return true;
|
|
2152
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2153
|
+
}
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
|
|
1380
2157
|
// src/config/types.ts
|
|
1381
2158
|
function defineConfig(config) {
|
|
1382
2159
|
return config;
|
|
@@ -1400,13 +2177,19 @@ async function main() {
|
|
|
1400
2177
|
${formatValidationErrors(errors)}`);
|
|
1401
2178
|
process.exit(1);
|
|
1402
2179
|
}
|
|
1403
|
-
|
|
2180
|
+
let services;
|
|
2181
|
+
try {
|
|
2182
|
+
services = filterServices(config.services, cliArgs, config);
|
|
2183
|
+
} catch (e) {
|
|
2184
|
+
console.error(`\u274C ${e.message}`);
|
|
2185
|
+
process.exit(1);
|
|
2186
|
+
}
|
|
1404
2187
|
if (!services.length) {
|
|
1405
2188
|
console.error("\u274C No services to run after filtering");
|
|
1406
2189
|
process.exit(1);
|
|
1407
2190
|
}
|
|
1408
2191
|
const platform = await detectPlatform();
|
|
1409
|
-
const envFile = config.envFile ?
|
|
2192
|
+
const envFile = config.envFile ? join6(cwd, config.envFile) : join6(cwd, ".env");
|
|
1410
2193
|
const env = parseEnvFile(envFile, process.env);
|
|
1411
2194
|
if (config.env) {
|
|
1412
2195
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -1423,12 +2206,33 @@ ${formatValidationErrors(errors)}`);
|
|
|
1423
2206
|
routes: config.proxy.routes,
|
|
1424
2207
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
1425
2208
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
1426
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
2209
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join6(homedir2(), ".traefik", "traefik_conf.yaml")
|
|
1427
2210
|
};
|
|
1428
2211
|
}
|
|
2212
|
+
if (cliArgs.dryRun) {
|
|
2213
|
+
runDryRun({ config, services, cliArgs, env, baseCwd: cwd, proxyProvider, proxyOpts });
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
let logSink = null;
|
|
2217
|
+
if (cliArgs.logFile) {
|
|
2218
|
+
logSink = new LogSink({ projectName: config.name, rootDir: cliArgs.logDir });
|
|
2219
|
+
}
|
|
2220
|
+
if (cliArgs.once) {
|
|
2221
|
+
const code = await runOnce({
|
|
2222
|
+
config,
|
|
2223
|
+
services,
|
|
2224
|
+
cliArgs,
|
|
2225
|
+
platform,
|
|
2226
|
+
env,
|
|
2227
|
+
baseCwd: cwd,
|
|
2228
|
+
logSink
|
|
2229
|
+
});
|
|
2230
|
+
await logSink?.close();
|
|
2231
|
+
process.exit(code);
|
|
2232
|
+
}
|
|
1429
2233
|
const isInteractive = process.stdin.isTTY ?? false;
|
|
1430
2234
|
const { waitUntilExit } = render(
|
|
1431
|
-
|
|
2235
|
+
React7.createElement(App, {
|
|
1432
2236
|
config,
|
|
1433
2237
|
services,
|
|
1434
2238
|
cliArgs,
|
|
@@ -1436,7 +2240,8 @@ ${formatValidationErrors(errors)}`);
|
|
|
1436
2240
|
env,
|
|
1437
2241
|
baseCwd: cwd,
|
|
1438
2242
|
proxyProvider,
|
|
1439
|
-
proxyOpts
|
|
2243
|
+
proxyOpts,
|
|
2244
|
+
logSink
|
|
1440
2245
|
}),
|
|
1441
2246
|
{ exitOnCtrlC: false, patchConsole: isInteractive, interactive: isInteractive }
|
|
1442
2247
|
);
|