@gachlab/devup 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/types.d.ts +5 -3
- package/dist/config/types.d.ts.map +1 -1
- package/dist/control-plane/socket-server.d.ts +6 -0
- package/dist/control-plane/socket-server.d.ts.map +1 -1
- package/dist/index.js +296 -85
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/daemon.d.ts.map +1 -1
- package/dist/orchestrator/subcommands.d.ts +6 -0
- package/dist/orchestrator/subcommands.d.ts.map +1 -1
- package/dist/process/health-poller.d.ts +3 -1
- package/dist/process/health-poller.d.ts.map +1 -1
- package/dist/process/health.d.ts +6 -2
- package/dist/process/health.d.ts.map +1 -1
- package/dist/process/log-sink.d.ts +6 -6
- package/dist/process/log-sink.d.ts.map +1 -1
- package/dist/process/spawner.d.ts +3 -6
- package/dist/process/spawner.d.ts.map +1 -1
- package/dist/process/types.d.ts +2 -0
- package/dist/process/types.d.ts.map +1 -1
- package/dist/tui/hooks/useControlPlane.d.ts +1 -1
- package/dist/tui/hooks/useControlPlane.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import { dirname as
|
|
6
|
+
import { readFileSync as readFileSync5, realpathSync } from "fs";
|
|
7
|
+
import { dirname as dirname8, join as join10 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
9
|
import { homedir as homedir5 } from "os";
|
|
10
10
|
|
|
11
11
|
// src/config/loader.ts
|
|
12
|
-
import { existsSync } from "fs";
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
13
13
|
import { readFile } from "fs/promises";
|
|
14
|
-
import { resolve, join } from "path";
|
|
14
|
+
import { resolve, join, dirname } from "path";
|
|
15
15
|
import { pathToFileURL } from "url";
|
|
16
16
|
var CONFIG_NAMES = [
|
|
17
17
|
"devup.config.ts",
|
|
@@ -33,7 +33,43 @@ function findConfigFile(cwd, explicit) {
|
|
|
33
33
|
Or use --config <path>`
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
|
+
function extractEnvFilePath(configPath, raw) {
|
|
37
|
+
if (configPath.endsWith(".json")) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw).envFile ?? null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const m = raw.match(/envFile\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
45
|
+
return m ? m[1] : null;
|
|
46
|
+
}
|
|
47
|
+
function loadEnvFile(envFilePath) {
|
|
48
|
+
if (!existsSync(envFilePath)) return;
|
|
49
|
+
try {
|
|
50
|
+
const content = readFileSync(envFilePath, "utf8");
|
|
51
|
+
for (const line of content.split("\n")) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
54
|
+
const eq = trimmed.indexOf("=");
|
|
55
|
+
if (eq < 1) continue;
|
|
56
|
+
const key = trimmed.slice(0, eq).trim();
|
|
57
|
+
const val = trimmed.slice(eq + 1).trim().replace(/^(['"`])(.*)\1$/, "$2");
|
|
58
|
+
if (!(key in process.env)) process.env[key] = val;
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
36
63
|
async function loadConfig(configPath) {
|
|
64
|
+
const configDir = dirname(configPath);
|
|
65
|
+
try {
|
|
66
|
+
const raw = readFileSync(configPath, "utf8");
|
|
67
|
+
const envFileRelPath = extractEnvFilePath(configPath, raw);
|
|
68
|
+
if (envFileRelPath) {
|
|
69
|
+
loadEnvFile(resolve(configDir, envFileRelPath));
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
37
73
|
if (configPath.endsWith(".json")) {
|
|
38
74
|
const raw = await readFile(configPath, "utf8");
|
|
39
75
|
return JSON.parse(raw);
|
|
@@ -439,9 +475,9 @@ function filterServices(services, args, config) {
|
|
|
439
475
|
|
|
440
476
|
// src/orchestrator/subcommands.ts
|
|
441
477
|
import { spawn as spawn5 } from "child_process";
|
|
442
|
-
import { createReadStream as createReadStream2, watchFile as watchFile2, unwatchFile as unwatchFile2, existsSync as existsSync11, statSync as
|
|
478
|
+
import { createReadStream as createReadStream2, watchFile as watchFile2, unwatchFile as unwatchFile2, existsSync as existsSync11, statSync as statSync3 } from "fs";
|
|
443
479
|
import { readFile as readFile2 } from "fs/promises";
|
|
444
|
-
import { join as join9, dirname as
|
|
480
|
+
import { join as join9, dirname as dirname4 } from "path";
|
|
445
481
|
import { fileURLToPath } from "url";
|
|
446
482
|
import { homedir as homedir4 } from "os";
|
|
447
483
|
import { createInterface as createInterface4 } from "readline";
|
|
@@ -496,27 +532,24 @@ function checkHttp(port, opts = {}) {
|
|
|
496
532
|
};
|
|
497
533
|
return new Promise((resolve4) => {
|
|
498
534
|
const req = http.get({ host, port, path, timeout: timeoutMs }, (res) => {
|
|
499
|
-
const
|
|
535
|
+
const code = res.statusCode ?? 0;
|
|
536
|
+
const ok = typeof res.statusCode === "number" && accept(code);
|
|
500
537
|
res.resume();
|
|
501
|
-
resolve4(ok);
|
|
538
|
+
resolve4(ok ? { ok: true } : { ok: false, reason: `HTTP ${code}` });
|
|
502
539
|
});
|
|
503
|
-
req.on("error", () => resolve4(false));
|
|
540
|
+
req.on("error", (e) => resolve4({ ok: false, reason: e.code ?? e.message }));
|
|
504
541
|
req.on("timeout", () => {
|
|
505
542
|
req.destroy();
|
|
506
|
-
resolve4(false);
|
|
543
|
+
resolve4({ ok: false, reason: `timed out after ${timeoutMs}ms` });
|
|
507
544
|
});
|
|
508
545
|
});
|
|
509
546
|
}
|
|
510
|
-
function checkHealth(port, hc) {
|
|
547
|
+
async function checkHealth(port, hc) {
|
|
511
548
|
if (hc?.type === "http") {
|
|
512
|
-
return checkHttp(port, {
|
|
513
|
-
path: hc.path,
|
|
514
|
-
expect: hc.expect,
|
|
515
|
-
host: hc.host,
|
|
516
|
-
timeoutMs: hc.timeoutMs
|
|
517
|
-
});
|
|
549
|
+
return checkHttp(port, { path: hc.path, expect: hc.expect, host: hc.host, timeoutMs: hc.timeoutMs });
|
|
518
550
|
}
|
|
519
|
-
|
|
551
|
+
const ok = await checkPort(port, "127.0.0.1", hc?.timeoutMs);
|
|
552
|
+
return ok ? { ok: true } : { ok: false, reason: `connection refused on :${port}` };
|
|
520
553
|
}
|
|
521
554
|
function waitForPort(port, opts = {}) {
|
|
522
555
|
const { timeout = 45e3, interval = 1e3 } = opts;
|
|
@@ -539,11 +572,11 @@ function deriveHealth(isUp, currentStatus) {
|
|
|
539
572
|
}
|
|
540
573
|
|
|
541
574
|
// src/utils/env.ts
|
|
542
|
-
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
575
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
543
576
|
function parseEnvFile(filePath, baseEnv = {}) {
|
|
544
577
|
const env = { ...baseEnv };
|
|
545
578
|
if (!existsSync3(filePath)) return env;
|
|
546
|
-
for (const line of
|
|
579
|
+
for (const line of readFileSync2(filePath, "utf8").split("\n")) {
|
|
547
580
|
const trimmed = line.trim();
|
|
548
581
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
549
582
|
const eqIdx = trimmed.indexOf("=");
|
|
@@ -606,23 +639,23 @@ function redactSecrets(env) {
|
|
|
606
639
|
}
|
|
607
640
|
|
|
608
641
|
// src/utils/install-stamp.ts
|
|
609
|
-
import { existsSync as existsSync4, readFileSync as
|
|
642
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
610
643
|
import { createHash } from "crypto";
|
|
611
644
|
import { join as join2 } from "path";
|
|
612
645
|
function needsInstall(fullCwd) {
|
|
613
646
|
const nm = join2(fullCwd, "node_modules");
|
|
614
647
|
if (!existsSync4(nm)) return true;
|
|
615
648
|
try {
|
|
616
|
-
const pkgHash = createHash("md5").update(
|
|
649
|
+
const pkgHash = createHash("md5").update(readFileSync3(join2(fullCwd, "package.json"))).digest("hex");
|
|
617
650
|
const stampFile = join2(nm, ".install-stamp");
|
|
618
|
-
if (existsSync4(stampFile) &&
|
|
651
|
+
if (existsSync4(stampFile) && readFileSync3(stampFile, "utf8") === pkgHash) return false;
|
|
619
652
|
} catch {
|
|
620
653
|
}
|
|
621
654
|
return true;
|
|
622
655
|
}
|
|
623
656
|
function writeInstallStamp(fullCwd) {
|
|
624
657
|
try {
|
|
625
|
-
const pkgHash = createHash("md5").update(
|
|
658
|
+
const pkgHash = createHash("md5").update(readFileSync3(join2(fullCwd, "package.json"))).digest("hex");
|
|
626
659
|
writeFileSync(join2(fullCwd, "node_modules", ".install-stamp"), pkgHash);
|
|
627
660
|
} catch {
|
|
628
661
|
}
|
|
@@ -703,7 +736,7 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
703
736
|
import { createServer } from "net";
|
|
704
737
|
import { createInterface } from "readline";
|
|
705
738
|
import { existsSync as existsSync5, unlinkSync, chmodSync, mkdirSync, statSync } from "fs";
|
|
706
|
-
import { dirname } from "path";
|
|
739
|
+
import { dirname as dirname2 } from "path";
|
|
707
740
|
import { join as join3 } from "path";
|
|
708
741
|
import { homedir } from "os";
|
|
709
742
|
function defaultSocketPath(projectName) {
|
|
@@ -712,7 +745,7 @@ function defaultSocketPath(projectName) {
|
|
|
712
745
|
}
|
|
713
746
|
async function startSocketServer(projectName, ctx, opts = {}) {
|
|
714
747
|
const path = opts.path ?? defaultSocketPath(projectName);
|
|
715
|
-
mkdirSync(
|
|
748
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
716
749
|
if (existsSync5(path)) {
|
|
717
750
|
try {
|
|
718
751
|
const st = statSync(path);
|
|
@@ -831,10 +864,12 @@ function serializeState(name, st) {
|
|
|
831
864
|
health: st.health,
|
|
832
865
|
port: st.svc.port,
|
|
833
866
|
type: st.svc.type,
|
|
867
|
+
phase: st.svc.phase,
|
|
834
868
|
errors: st.errors,
|
|
835
869
|
restarts: st.restarts,
|
|
836
870
|
pid: st.pid,
|
|
837
|
-
startedAt: st.startedAt
|
|
871
|
+
startedAt: st.startedAt,
|
|
872
|
+
crashLog: st.crashLog ?? null
|
|
838
873
|
};
|
|
839
874
|
}
|
|
840
875
|
function respond(socket, payload) {
|
|
@@ -851,6 +886,8 @@ async function dispatch(method, params, ctx) {
|
|
|
851
886
|
}
|
|
852
887
|
case "stats":
|
|
853
888
|
return await ctx.getStats();
|
|
889
|
+
case "info":
|
|
890
|
+
return ctx.getInfo();
|
|
854
891
|
case "restart": {
|
|
855
892
|
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
856
893
|
await ctx.restart(svc);
|
|
@@ -951,7 +988,7 @@ function openStream(socketPath, method, params, onFrame, onError) {
|
|
|
951
988
|
|
|
952
989
|
// src/orchestrator/daemon.ts
|
|
953
990
|
import { spawn as spawn4 } from "child_process";
|
|
954
|
-
import { writeFileSync as writeFileSync2, readFileSync as
|
|
991
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, existsSync as existsSync10, unlinkSync as unlinkSync2, mkdirSync as mkdirSync3, createReadStream } from "fs";
|
|
955
992
|
import { join as join8 } from "path";
|
|
956
993
|
import { homedir as homedir3, totalmem, freemem, cpus } from "os";
|
|
957
994
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -1057,6 +1094,8 @@ var MAX_RESTARTS = 3;
|
|
|
1057
1094
|
var BACKOFF_BASE_MS = 2e3;
|
|
1058
1095
|
|
|
1059
1096
|
// src/process/spawner.ts
|
|
1097
|
+
var CRASH_LOG_LINES = 20;
|
|
1098
|
+
var STARTUP_TIMEOUT_DEFAULT_MS = 45e3;
|
|
1060
1099
|
var Spawner = class {
|
|
1061
1100
|
baseCwd;
|
|
1062
1101
|
env;
|
|
@@ -1065,6 +1104,7 @@ var Spawner = class {
|
|
|
1065
1104
|
events;
|
|
1066
1105
|
lifecycle;
|
|
1067
1106
|
onCrash;
|
|
1107
|
+
startupTimers = /* @__PURE__ */ new Map();
|
|
1068
1108
|
constructor(opts) {
|
|
1069
1109
|
this.baseCwd = opts.baseCwd;
|
|
1070
1110
|
this.env = opts.env;
|
|
@@ -1076,6 +1116,7 @@ var Spawner = class {
|
|
|
1076
1116
|
}
|
|
1077
1117
|
async start(svc, colorIdx, isRestart = false) {
|
|
1078
1118
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1119
|
+
this.clearStartupTimer(svc.name);
|
|
1079
1120
|
if (svc.type === "api") {
|
|
1080
1121
|
const bindable = await isPortBindable(svc.port);
|
|
1081
1122
|
if (!bindable && !isRestart) {
|
|
@@ -1111,18 +1152,41 @@ var Spawner = class {
|
|
|
1111
1152
|
restarts: prev?.restarts ?? 0,
|
|
1112
1153
|
startedAt: Date.now(),
|
|
1113
1154
|
intentionalStop: false,
|
|
1114
|
-
colorIdx
|
|
1155
|
+
colorIdx,
|
|
1156
|
+
crashLog: null
|
|
1115
1157
|
};
|
|
1116
1158
|
this.state.set(svc.name, state);
|
|
1117
1159
|
this.procs.add(proc);
|
|
1118
1160
|
this.events.onStateChange(svc.name, state);
|
|
1119
1161
|
this.wireStdio(proc, svc, state, colorIdx);
|
|
1120
1162
|
this.wireCloseHandler(proc, svc, state, colorIdx);
|
|
1163
|
+
this.scheduleStartupTimeout(svc, state, colorIdx);
|
|
1121
1164
|
if (svc.watchBuild) {
|
|
1122
1165
|
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
1123
1166
|
}
|
|
1124
1167
|
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
1125
1168
|
}
|
|
1169
|
+
scheduleStartupTimeout(svc, state, colorIdx) {
|
|
1170
|
+
const timeoutMs = svc.healthCheck?.startupTimeoutMs ?? STARTUP_TIMEOUT_DEFAULT_MS;
|
|
1171
|
+
if (timeoutMs <= 0) return;
|
|
1172
|
+
const timer = setTimeout(() => {
|
|
1173
|
+
this.startupTimers.delete(svc.name);
|
|
1174
|
+
const current = this.state.get(svc.name);
|
|
1175
|
+
if (!current || current !== state || current.health === "up") return;
|
|
1176
|
+
current.status = "timeout";
|
|
1177
|
+
current.health = "down";
|
|
1178
|
+
this.log(svc.name, `\u23F1 startup timeout after ${timeoutMs / 1e3}s \u2014 service never became healthy`, colorIdx);
|
|
1179
|
+
this.events.onStateChange(svc.name, current);
|
|
1180
|
+
}, timeoutMs);
|
|
1181
|
+
this.startupTimers.set(svc.name, timer);
|
|
1182
|
+
}
|
|
1183
|
+
clearStartupTimer(name) {
|
|
1184
|
+
const t = this.startupTimers.get(name);
|
|
1185
|
+
if (t) {
|
|
1186
|
+
clearTimeout(t);
|
|
1187
|
+
this.startupTimers.delete(name);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1126
1190
|
wireStdio(proc, svc, state, colorIdx) {
|
|
1127
1191
|
const readyRegex = compileReadyPattern(svc.readyPattern);
|
|
1128
1192
|
const markReadyIfMatch = (line) => {
|
|
@@ -1130,29 +1194,37 @@ var Spawner = class {
|
|
|
1130
1194
|
if (readyRegex.test(line)) {
|
|
1131
1195
|
state.health = "up";
|
|
1132
1196
|
if (state.status === "starting") state.status = "running";
|
|
1197
|
+
this.clearStartupTimer(svc.name);
|
|
1133
1198
|
this.events.onStateChange(svc.name, state);
|
|
1134
1199
|
}
|
|
1135
1200
|
};
|
|
1136
1201
|
const errorRegex = compileReadyPattern(svc.errorPattern);
|
|
1137
1202
|
const countsAsError = (line) => errorRegex ? errorRegex.test(line) : true;
|
|
1203
|
+
const stderrBuf = [];
|
|
1138
1204
|
const stdoutBuf = lineBuffer((line) => {
|
|
1139
1205
|
markReadyIfMatch(line);
|
|
1140
1206
|
this.log(svc.name, line, colorIdx);
|
|
1141
1207
|
});
|
|
1142
|
-
const
|
|
1208
|
+
const stderrLineBuf = lineBuffer((line) => {
|
|
1143
1209
|
if (countsAsError(line)) state.errors += 1;
|
|
1144
1210
|
markReadyIfMatch(line);
|
|
1211
|
+
stderrBuf.push(line);
|
|
1212
|
+
if (stderrBuf.length > CRASH_LOG_LINES) stderrBuf.shift();
|
|
1145
1213
|
this.log(svc.name, line, colorIdx);
|
|
1146
1214
|
});
|
|
1147
1215
|
proc.stdout?.on("data", (d) => stdoutBuf.push(d));
|
|
1148
|
-
proc.stderr?.on("data", (d) =>
|
|
1216
|
+
proc.stderr?.on("data", (d) => stderrLineBuf.push(d));
|
|
1149
1217
|
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
1150
|
-
proc.stderr?.on("end", () =>
|
|
1218
|
+
proc.stderr?.on("end", () => {
|
|
1219
|
+
stderrLineBuf.flush();
|
|
1220
|
+
state._stderrBuf = stderrBuf;
|
|
1221
|
+
});
|
|
1151
1222
|
}
|
|
1152
1223
|
wireCloseHandler(proc, svc, state, colorIdx) {
|
|
1153
1224
|
proc.on("close", (code) => {
|
|
1154
1225
|
this.procs.delete(proc);
|
|
1155
1226
|
this.lifecycle.stopWatchProc(state);
|
|
1227
|
+
this.clearStartupTimer(svc.name);
|
|
1156
1228
|
if (state.intentionalStop) {
|
|
1157
1229
|
state.intentionalStop = false;
|
|
1158
1230
|
return;
|
|
@@ -1165,6 +1237,8 @@ var Spawner = class {
|
|
|
1165
1237
|
}
|
|
1166
1238
|
state.status = "crashed";
|
|
1167
1239
|
state.health = "down";
|
|
1240
|
+
const buf = state._stderrBuf;
|
|
1241
|
+
state.crashLog = buf && buf.length ? [...buf] : null;
|
|
1168
1242
|
this.log(svc.name, `\u274C exited with code ${code}`, colorIdx);
|
|
1169
1243
|
this.events.onStateChange(svc.name, state);
|
|
1170
1244
|
this.onCrash(svc, state, colorIdx);
|
|
@@ -1202,9 +1276,7 @@ var Spawner = class {
|
|
|
1202
1276
|
spawnWatchBuild(svc, cwd, env, colorIdx) {
|
|
1203
1277
|
this.log(svc.name, `\u{1F440} watchBuild: ${svc.watchBuild}`, colorIdx);
|
|
1204
1278
|
const isWin = process.platform === "win32";
|
|
1205
|
-
const
|
|
1206
|
-
const shellFlag = isWin ? "/c" : "-c";
|
|
1207
|
-
const child = spawn2(shell, [shellFlag, svc.watchBuild], {
|
|
1279
|
+
const child = spawn2(isWin ? "cmd.exe" : "sh", [isWin ? "/c" : "-c", svc.watchBuild], {
|
|
1208
1280
|
cwd,
|
|
1209
1281
|
env,
|
|
1210
1282
|
detached: true,
|
|
@@ -1217,8 +1289,6 @@ var Spawner = class {
|
|
|
1217
1289
|
child.on("error", (err) => this.log(svc.name, `[watch] \u274C ${err.message}`, colorIdx));
|
|
1218
1290
|
return child;
|
|
1219
1291
|
}
|
|
1220
|
-
/** Create a state entry in 'crashed' status without spawning a process
|
|
1221
|
-
* (used when preBuild fails or pre-flight checks fail). */
|
|
1222
1292
|
recordCrashedState(svc, colorIdx) {
|
|
1223
1293
|
const prev = this.state.get(svc.name);
|
|
1224
1294
|
this.state.set(svc.name, {
|
|
@@ -1231,7 +1301,8 @@ var Spawner = class {
|
|
|
1231
1301
|
restarts: prev?.restarts ?? 0,
|
|
1232
1302
|
startedAt: null,
|
|
1233
1303
|
intentionalStop: false,
|
|
1234
|
-
colorIdx
|
|
1304
|
+
colorIdx,
|
|
1305
|
+
crashLog: null
|
|
1235
1306
|
});
|
|
1236
1307
|
this.events.onStateChange(svc.name, this.state.get(svc.name));
|
|
1237
1308
|
}
|
|
@@ -1278,13 +1349,14 @@ var Restarter = class {
|
|
|
1278
1349
|
var HealthPoller = class {
|
|
1279
1350
|
state;
|
|
1280
1351
|
events;
|
|
1352
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
1281
1353
|
constructor(opts) {
|
|
1282
1354
|
this.state = opts.state;
|
|
1283
1355
|
this.events = opts.events;
|
|
1284
1356
|
}
|
|
1285
1357
|
async checkAll() {
|
|
1286
1358
|
for (const [name, st] of this.state) {
|
|
1287
|
-
if (!st.pid || st.status === "idle") {
|
|
1359
|
+
if (!st.pid || st.status === "idle" || st.status === "timeout") {
|
|
1288
1360
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
1289
1361
|
continue;
|
|
1290
1362
|
}
|
|
@@ -1292,10 +1364,22 @@ var HealthPoller = class {
|
|
|
1292
1364
|
if (startPeriodMs > 0 && st.startedAt && Date.now() - st.startedAt < startPeriodMs) {
|
|
1293
1365
|
continue;
|
|
1294
1366
|
}
|
|
1295
|
-
const
|
|
1367
|
+
const result = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
1368
|
+
const threshold = st.svc.healthCheck?.failureThreshold ?? 2;
|
|
1296
1369
|
const prev = st.health;
|
|
1297
|
-
|
|
1298
|
-
|
|
1370
|
+
if (result.ok) {
|
|
1371
|
+
this.failureCounts.delete(name);
|
|
1372
|
+
st.health = deriveHealth(true, st.status);
|
|
1373
|
+
if (st.health === "up" && st.status === "starting") st.status = "running";
|
|
1374
|
+
} else {
|
|
1375
|
+
const count = (this.failureCounts.get(name) ?? 0) + 1;
|
|
1376
|
+
this.failureCounts.set(name, count);
|
|
1377
|
+
if (count >= threshold) {
|
|
1378
|
+
const reason = result.reason ?? "probe failed";
|
|
1379
|
+
this.events.onLog(name, `[health] \u2717 ${name}: ${reason} (${count} consecutive failure${count > 1 ? "s" : ""})`, st.colorIdx);
|
|
1380
|
+
st.health = deriveHealth(false, st.status);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1299
1383
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
1300
1384
|
}
|
|
1301
1385
|
}
|
|
@@ -1441,44 +1525,82 @@ var ProcessManager = class {
|
|
|
1441
1525
|
};
|
|
1442
1526
|
|
|
1443
1527
|
// src/process/log-sink.ts
|
|
1444
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync2, renameSync, createWriteStream } from "fs";
|
|
1445
|
-
import { join as join6, dirname as
|
|
1528
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2, renameSync, createWriteStream, statSync as statSync2 } from "fs";
|
|
1529
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
1446
1530
|
import { homedir as homedir2 } from "os";
|
|
1531
|
+
var DEFAULT_MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
1447
1532
|
var LogSink = class {
|
|
1448
1533
|
dir;
|
|
1449
1534
|
rotateOnStart;
|
|
1535
|
+
maxLogBytes;
|
|
1450
1536
|
streams = /* @__PURE__ */ new Map();
|
|
1451
1537
|
seen = /* @__PURE__ */ new Set();
|
|
1538
|
+
bytesWritten = /* @__PURE__ */ new Map();
|
|
1452
1539
|
constructor(opts) {
|
|
1453
1540
|
const root = opts.rootDir ?? join6(homedir2(), ".devup", "logs");
|
|
1454
1541
|
this.dir = join6(root, sanitize(opts.projectName));
|
|
1455
1542
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1543
|
+
this.maxLogBytes = opts.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
1456
1544
|
mkdirSync2(this.dir, { recursive: true });
|
|
1457
1545
|
}
|
|
1458
|
-
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1459
1546
|
pathFor(svcName) {
|
|
1460
1547
|
return join6(this.dir, `${sanitize(svcName)}.log`);
|
|
1461
1548
|
}
|
|
1462
1549
|
write(svcName, line) {
|
|
1550
|
+
const entry = `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
1551
|
+
`;
|
|
1552
|
+
const bytes = Buffer.byteLength(entry, "utf8");
|
|
1553
|
+
const current = this.bytesWritten.get(svcName) ?? this.estimateCurrentSize(svcName);
|
|
1554
|
+
if (current + bytes > this.maxLogBytes) {
|
|
1555
|
+
this.rotateFile(svcName);
|
|
1556
|
+
}
|
|
1463
1557
|
const stream = this.streamFor(svcName);
|
|
1464
|
-
stream.write(
|
|
1465
|
-
|
|
1558
|
+
stream.write(entry, (err) => {
|
|
1559
|
+
if (err && svcName !== "devup") {
|
|
1560
|
+
this.streamFor("devup").write(
|
|
1561
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} \u26A0 log write error for "${svcName}": ${err.message}
|
|
1562
|
+
`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
});
|
|
1566
|
+
this.bytesWritten.set(svcName, (this.bytesWritten.get(svcName) ?? 0) + bytes);
|
|
1466
1567
|
}
|
|
1467
1568
|
async close() {
|
|
1468
|
-
const closes = [...this.streams.values()].map(
|
|
1469
|
-
(s) => new Promise((r) => s.end(() => r()))
|
|
1470
|
-
);
|
|
1569
|
+
const closes = [...this.streams.values()].map((s) => new Promise((r) => s.end(() => r())));
|
|
1471
1570
|
this.streams.clear();
|
|
1472
1571
|
this.seen.clear();
|
|
1572
|
+
this.bytesWritten.clear();
|
|
1473
1573
|
await Promise.all(closes);
|
|
1474
1574
|
}
|
|
1575
|
+
rotateFile(svcName) {
|
|
1576
|
+
const existing = this.streams.get(svcName);
|
|
1577
|
+
if (existing) {
|
|
1578
|
+
existing.destroy();
|
|
1579
|
+
this.streams.delete(svcName);
|
|
1580
|
+
}
|
|
1581
|
+
const file = this.pathFor(svcName);
|
|
1582
|
+
if (existsSync9(file)) {
|
|
1583
|
+
try {
|
|
1584
|
+
renameSync(file, file + ".prev");
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
this.bytesWritten.set(svcName, 0);
|
|
1589
|
+
}
|
|
1590
|
+
estimateCurrentSize(svcName) {
|
|
1591
|
+
try {
|
|
1592
|
+
return statSync2(this.pathFor(svcName)).size;
|
|
1593
|
+
} catch {
|
|
1594
|
+
return 0;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1475
1597
|
streamFor(svcName) {
|
|
1476
1598
|
let s = this.streams.get(svcName);
|
|
1477
1599
|
if (s) return s;
|
|
1478
1600
|
const file = this.pathFor(svcName);
|
|
1479
1601
|
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync9(file)) {
|
|
1480
1602
|
try {
|
|
1481
|
-
mkdirSync2(
|
|
1603
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
1482
1604
|
renameSync(file, file + ".prev");
|
|
1483
1605
|
} catch {
|
|
1484
1606
|
}
|
|
@@ -1586,7 +1708,7 @@ async function waitHealthy(svc, timeoutMs) {
|
|
|
1586
1708
|
const deadline = Date.now() + timeoutMs;
|
|
1587
1709
|
const port = svc.port;
|
|
1588
1710
|
while (Date.now() < deadline) {
|
|
1589
|
-
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
1711
|
+
if ((await checkHealth(port, svc.healthCheck)).ok) return true;
|
|
1590
1712
|
await new Promise((r) => setTimeout(r, 500));
|
|
1591
1713
|
}
|
|
1592
1714
|
return false;
|
|
@@ -1853,7 +1975,7 @@ function isDaemonRunning(projectName) {
|
|
|
1853
1975
|
if (!existsSync10(path)) return { pid: null, stale: false };
|
|
1854
1976
|
let pid;
|
|
1855
1977
|
try {
|
|
1856
|
-
pid = Number(
|
|
1978
|
+
pid = Number(readFileSync4(path, "utf8").trim());
|
|
1857
1979
|
if (!pid || !Number.isFinite(pid)) return { pid: null, stale: true };
|
|
1858
1980
|
} catch {
|
|
1859
1981
|
return { pid: null, stale: true };
|
|
@@ -2027,6 +2149,9 @@ async function daemonBody(opts) {
|
|
|
2027
2149
|
tls: proxyOpts.tls,
|
|
2028
2150
|
routes: proxyOpts.routes
|
|
2029
2151
|
};
|
|
2152
|
+
},
|
|
2153
|
+
getInfo() {
|
|
2154
|
+
return { project: projectName, profiles: config.profiles ?? {} };
|
|
2030
2155
|
}
|
|
2031
2156
|
}, { onLog: (msg) => writeDevupLog(msg) });
|
|
2032
2157
|
healthTimer = setInterval(() => {
|
|
@@ -2127,7 +2252,8 @@ async function bootLazy(mgr, services, lazyCfg, lazyTimeout, lazyProxies) {
|
|
|
2127
2252
|
restarts: 0,
|
|
2128
2253
|
startedAt: null,
|
|
2129
2254
|
intentionalStop: false,
|
|
2130
|
-
colorIdx: ci
|
|
2255
|
+
colorIdx: ci,
|
|
2256
|
+
crashLog: null
|
|
2131
2257
|
});
|
|
2132
2258
|
const proxy = createLazyProxy({
|
|
2133
2259
|
listenPort: svc.port,
|
|
@@ -2206,7 +2332,7 @@ async function runDetached(opts) {
|
|
|
2206
2332
|
const deadline = Date.now() + 9e4;
|
|
2207
2333
|
while (Date.now() < deadline) {
|
|
2208
2334
|
if (existsSync10(pidPath)) {
|
|
2209
|
-
const pid = Number(
|
|
2335
|
+
const pid = Number(readFileSync4(pidPath, "utf8").trim());
|
|
2210
2336
|
out("");
|
|
2211
2337
|
out(`\u{1F680} devup detached (PID ${pid})`);
|
|
2212
2338
|
out(" inspect: devup ctl status");
|
|
@@ -2215,7 +2341,7 @@ async function runDetached(opts) {
|
|
|
2215
2341
|
return 0;
|
|
2216
2342
|
}
|
|
2217
2343
|
if (existsSync10(errPath)) {
|
|
2218
|
-
const msg =
|
|
2344
|
+
const msg = readFileSync4(errPath, "utf8").trim();
|
|
2219
2345
|
out(`\u274C daemon boot failed: ${msg}`);
|
|
2220
2346
|
try {
|
|
2221
2347
|
unlinkSync2(errPath);
|
|
@@ -2288,7 +2414,7 @@ async function stopDaemon(projectName, opts = {}) {
|
|
|
2288
2414
|
}
|
|
2289
2415
|
|
|
2290
2416
|
// src/orchestrator/subcommands.ts
|
|
2291
|
-
var KNOWN = /* @__PURE__ */ new Set(["logs", "install", "status", "help", "ctl", "up", "down"]);
|
|
2417
|
+
var KNOWN = /* @__PURE__ */ new Set(["logs", "install", "status", "help", "ctl", "up", "down", "config"]);
|
|
2292
2418
|
function detectSubcommand(argv) {
|
|
2293
2419
|
const first = argv[0];
|
|
2294
2420
|
return first && KNOWN.has(first) ? first : null;
|
|
@@ -2320,7 +2446,7 @@ async function runLogs(argv, opts) {
|
|
|
2320
2446
|
}
|
|
2321
2447
|
await streamFile(file, out);
|
|
2322
2448
|
if (!follow) return 0;
|
|
2323
|
-
return await followFile(file, out,
|
|
2449
|
+
return await followFile(file, out, statSync3(file).size);
|
|
2324
2450
|
}
|
|
2325
2451
|
async function streamFile(file, out) {
|
|
2326
2452
|
return new Promise((resolve4, reject) => {
|
|
@@ -2335,7 +2461,7 @@ async function followFile(file, out, startAt = 0) {
|
|
|
2335
2461
|
while (!existsSync11(file)) await new Promise((r) => setTimeout(r, 500));
|
|
2336
2462
|
return new Promise((resolve4) => {
|
|
2337
2463
|
const tick = async () => {
|
|
2338
|
-
const size =
|
|
2464
|
+
const size = statSync3(file).size;
|
|
2339
2465
|
if (size > pos) {
|
|
2340
2466
|
await new Promise((res) => {
|
|
2341
2467
|
const rl = createInterface4({ input: createReadStream2(file, { encoding: "utf8", start: pos, end: size - 1 }) });
|
|
@@ -2416,7 +2542,7 @@ async function runStatus(opts) {
|
|
|
2416
2542
|
out(`${"Service".padEnd(maxLen)} ${"Port".padStart(5)} ${"Type".padEnd(4)} Health`);
|
|
2417
2543
|
out("-".repeat(maxLen + 24));
|
|
2418
2544
|
for (const svc of opts.config.services) {
|
|
2419
|
-
const up = await checkHealth(svc.port, svc.healthCheck);
|
|
2545
|
+
const { ok: up } = await checkHealth(svc.port, svc.healthCheck);
|
|
2420
2546
|
const health = up ? "\u2713 up" : "\u2717 down";
|
|
2421
2547
|
out(`${svc.name.padEnd(maxLen)} ${String(svc.port).padStart(5)} ${svc.type.padEnd(4)} ${health}`);
|
|
2422
2548
|
}
|
|
@@ -2459,15 +2585,6 @@ async function runCtl(argv, opts) {
|
|
|
2459
2585
|
out(`pong ts=${res.ts}`);
|
|
2460
2586
|
return 0;
|
|
2461
2587
|
}
|
|
2462
|
-
if (method === "status" && !follow) {
|
|
2463
|
-
const res = await sendRpc(socketPath, "status");
|
|
2464
|
-
if (!res.services.length) {
|
|
2465
|
-
out("(no services)");
|
|
2466
|
-
return 0;
|
|
2467
|
-
}
|
|
2468
|
-
fmtStatus(res.services, out);
|
|
2469
|
-
return 0;
|
|
2470
|
-
}
|
|
2471
2588
|
if (method === "status" && follow) {
|
|
2472
2589
|
return await new Promise((resolve4) => {
|
|
2473
2590
|
const abort = openStream(socketPath, "status.follow", {}, (frame) => {
|
|
@@ -2510,15 +2627,47 @@ async function runCtl(argv, opts) {
|
|
|
2510
2627
|
});
|
|
2511
2628
|
});
|
|
2512
2629
|
}
|
|
2630
|
+
if (method === "status" && !follow) {
|
|
2631
|
+
const json = argv.includes("--json");
|
|
2632
|
+
const res = await sendRpc(socketPath, "status");
|
|
2633
|
+
if (json) {
|
|
2634
|
+
out(JSON.stringify(res.services, null, 2));
|
|
2635
|
+
} else {
|
|
2636
|
+
if (!res.services.length) {
|
|
2637
|
+
out("(no services)");
|
|
2638
|
+
return 0;
|
|
2639
|
+
}
|
|
2640
|
+
fmtStatus(res.services, out);
|
|
2641
|
+
}
|
|
2642
|
+
return 0;
|
|
2643
|
+
}
|
|
2513
2644
|
if (method === "restart") {
|
|
2514
2645
|
const svc = argv[1];
|
|
2515
2646
|
if (!svc) {
|
|
2516
|
-
out("usage: devup ctl restart <service>");
|
|
2647
|
+
out("usage: devup ctl restart <service> [--wait] [--timeout <s>]");
|
|
2517
2648
|
return 1;
|
|
2518
2649
|
}
|
|
2650
|
+
const wait = argv.includes("--wait");
|
|
2651
|
+
const timeoutIdx = argv.indexOf("--timeout");
|
|
2652
|
+
const timeoutSec = timeoutIdx >= 0 ? Number(argv[timeoutIdx + 1] ?? 60) : 60;
|
|
2519
2653
|
await sendRpc(socketPath, "restart", { svc });
|
|
2520
|
-
|
|
2521
|
-
|
|
2654
|
+
if (!wait) {
|
|
2655
|
+
out(`\u2713 restart sent to ${svc}`);
|
|
2656
|
+
return 0;
|
|
2657
|
+
}
|
|
2658
|
+
out(`\u23F3 waiting for ${svc} to become healthy\u2026`);
|
|
2659
|
+
const deadline = Date.now() + timeoutSec * 1e3;
|
|
2660
|
+
while (Date.now() < deadline) {
|
|
2661
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2662
|
+
const status = await sendRpc(socketPath, "status");
|
|
2663
|
+
const row = status.services.find((s) => s.name === svc);
|
|
2664
|
+
if (row?.health === "up") {
|
|
2665
|
+
out(`\u2713 ${svc} is healthy`);
|
|
2666
|
+
return 0;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
out(`\u2717 ${svc} did not become healthy within ${timeoutSec}s`);
|
|
2670
|
+
return 1;
|
|
2522
2671
|
}
|
|
2523
2672
|
if (method === "stop") {
|
|
2524
2673
|
const svc = argv[1];
|
|
@@ -2537,6 +2686,63 @@ async function runCtl(argv, opts) {
|
|
|
2537
2686
|
return 1;
|
|
2538
2687
|
}
|
|
2539
2688
|
}
|
|
2689
|
+
async function runConfig(argv, opts) {
|
|
2690
|
+
const out = opts.out ?? ((l) => process.stdout.write(l + "\n"));
|
|
2691
|
+
const subcmd = argv[0];
|
|
2692
|
+
const json = argv.includes("--json");
|
|
2693
|
+
if (!subcmd || subcmd === "help") {
|
|
2694
|
+
out("Usage: devup config <subcommand>");
|
|
2695
|
+
out(" validate [--json] Validate the config and print errors/warnings");
|
|
2696
|
+
out(" show [--no-redact] Print the fully-resolved config as JSON");
|
|
2697
|
+
return 0;
|
|
2698
|
+
}
|
|
2699
|
+
let cfgPath;
|
|
2700
|
+
try {
|
|
2701
|
+
cfgPath = findConfigFile(opts.cwd, opts.configPath);
|
|
2702
|
+
} catch (e) {
|
|
2703
|
+
out(`\u274C ${e.message}`);
|
|
2704
|
+
return 1;
|
|
2705
|
+
}
|
|
2706
|
+
let config;
|
|
2707
|
+
try {
|
|
2708
|
+
config = await loadConfig(cfgPath);
|
|
2709
|
+
} catch (e) {
|
|
2710
|
+
out(`\u274C failed to load config: ${e.message}`);
|
|
2711
|
+
return 1;
|
|
2712
|
+
}
|
|
2713
|
+
if (subcmd === "validate") {
|
|
2714
|
+
const errors = validateConfig(config, opts.cwd);
|
|
2715
|
+
const warnings = collectWarnings(config);
|
|
2716
|
+
if (json) {
|
|
2717
|
+
out(JSON.stringify({ valid: errors.length === 0, errors: errors.map((e) => `${e.field}: ${e.message}`), warnings: warnings.map((w) => `${w.field}: ${w.message}`) }, null, 2));
|
|
2718
|
+
} else {
|
|
2719
|
+
if (errors.length) {
|
|
2720
|
+
out(formatValidationErrors(errors));
|
|
2721
|
+
}
|
|
2722
|
+
if (warnings.length) {
|
|
2723
|
+
out(formatValidationWarnings(warnings));
|
|
2724
|
+
}
|
|
2725
|
+
if (!errors.length) out(`\u2713 config is valid (${config.services.length} services${warnings.length ? `, ${warnings.length} warning${warnings.length > 1 ? "s" : ""}` : ""})`);
|
|
2726
|
+
}
|
|
2727
|
+
return errors.length ? 1 : 0;
|
|
2728
|
+
}
|
|
2729
|
+
if (subcmd === "show") {
|
|
2730
|
+
const noRedact = argv.includes("--no-redact");
|
|
2731
|
+
const resolved = noRedact ? config : redactConfig(config);
|
|
2732
|
+
out(JSON.stringify(resolved, null, 2));
|
|
2733
|
+
return 0;
|
|
2734
|
+
}
|
|
2735
|
+
out(`unknown config subcommand: ${subcmd}`);
|
|
2736
|
+
return 1;
|
|
2737
|
+
}
|
|
2738
|
+
function redactConfig(config) {
|
|
2739
|
+
const clone = JSON.parse(JSON.stringify(config));
|
|
2740
|
+
for (const svc of clone.services ?? []) {
|
|
2741
|
+
if (svc.extraEnv) svc.extraEnv = redactSecrets(svc.extraEnv);
|
|
2742
|
+
}
|
|
2743
|
+
if (clone.env) clone.env = redactSecrets(clone.env);
|
|
2744
|
+
return clone;
|
|
2745
|
+
}
|
|
2540
2746
|
async function runDown(opts) {
|
|
2541
2747
|
const out = opts.out ?? ((l) => process.stdout.write(l + "\n"));
|
|
2542
2748
|
return stopDaemon(opts.config.name, { out });
|
|
@@ -2735,7 +2941,7 @@ async function detectPlatform() {
|
|
|
2735
2941
|
|
|
2736
2942
|
// src/proxy-config/traefik.ts
|
|
2737
2943
|
import { existsSync as existsSync12, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2738
|
-
import { dirname as
|
|
2944
|
+
import { dirname as dirname5 } from "path";
|
|
2739
2945
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
2740
2946
|
var TraefikProvider = class {
|
|
2741
2947
|
name = "traefik";
|
|
@@ -2772,7 +2978,7 @@ ${svcs.join("\n")}
|
|
|
2772
2978
|
`;
|
|
2773
2979
|
}
|
|
2774
2980
|
write(content, opts) {
|
|
2775
|
-
const dir =
|
|
2981
|
+
const dir = dirname5(opts.confPath);
|
|
2776
2982
|
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
2777
2983
|
writeFileSync3(opts.confPath, content);
|
|
2778
2984
|
}
|
|
@@ -2783,7 +2989,7 @@ ${svcs.join("\n")}
|
|
|
2783
2989
|
|
|
2784
2990
|
// src/proxy-config/nginx.ts
|
|
2785
2991
|
import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2786
|
-
import { dirname as
|
|
2992
|
+
import { dirname as dirname6 } from "path";
|
|
2787
2993
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
2788
2994
|
var NginxProvider = class {
|
|
2789
2995
|
name = "nginx";
|
|
@@ -2820,7 +3026,7 @@ var NginxProvider = class {
|
|
|
2820
3026
|
return blocks.join("\n\n") + "\n";
|
|
2821
3027
|
}
|
|
2822
3028
|
write(content, opts) {
|
|
2823
|
-
const dir =
|
|
3029
|
+
const dir = dirname6(opts.confPath);
|
|
2824
3030
|
if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
|
|
2825
3031
|
writeFileSync4(opts.confPath, content);
|
|
2826
3032
|
}
|
|
@@ -2831,7 +3037,7 @@ var NginxProvider = class {
|
|
|
2831
3037
|
|
|
2832
3038
|
// src/proxy-config/caddy.ts
|
|
2833
3039
|
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2834
|
-
import { dirname as
|
|
3040
|
+
import { dirname as dirname7 } from "path";
|
|
2835
3041
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
2836
3042
|
var CaddyProvider = class {
|
|
2837
3043
|
name = "caddy";
|
|
@@ -2854,7 +3060,7 @@ var CaddyProvider = class {
|
|
|
2854
3060
|
return blocks.join("\n\n") + "\n";
|
|
2855
3061
|
}
|
|
2856
3062
|
write(content, opts) {
|
|
2857
|
-
const dir =
|
|
3063
|
+
const dir = dirname7(opts.confPath);
|
|
2858
3064
|
if (!existsSync14(dir)) mkdirSync6(dir, { recursive: true });
|
|
2859
3065
|
writeFileSync5(opts.confPath, content);
|
|
2860
3066
|
}
|
|
@@ -3147,7 +3353,7 @@ import { useEffect as useEffect5, useRef as useRef3 } from "react";
|
|
|
3147
3353
|
import { createInterface as createInterface5 } from "readline";
|
|
3148
3354
|
import { createReadStream as createReadStream3, existsSync as existsSync15 } from "fs";
|
|
3149
3355
|
import { totalmem as totalmem2, freemem as freemem2, cpus as cpus2 } from "os";
|
|
3150
|
-
function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy) {
|
|
3356
|
+
function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy, profiles) {
|
|
3151
3357
|
const handleRef = useRef3(null);
|
|
3152
3358
|
const prevCpuMap = useRef3(/* @__PURE__ */ new Map());
|
|
3153
3359
|
useEffect5(() => {
|
|
@@ -3222,6 +3428,9 @@ function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBu
|
|
|
3222
3428
|
tls: proxy.opts.tls,
|
|
3223
3429
|
routes: proxy.opts.routes
|
|
3224
3430
|
};
|
|
3431
|
+
},
|
|
3432
|
+
getInfo() {
|
|
3433
|
+
return { project: projectName, profiles };
|
|
3225
3434
|
}
|
|
3226
3435
|
}, { onLog: (msg) => pushLog("devup", msg, 12) });
|
|
3227
3436
|
handleRef.current = handle;
|
|
@@ -3233,7 +3442,7 @@ function useControlPlane(manager, projectName, logSink, pushLog, logBus, stateBu
|
|
|
3233
3442
|
void handle?.close();
|
|
3234
3443
|
handleRef.current = null;
|
|
3235
3444
|
};
|
|
3236
|
-
}, [manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy]);
|
|
3445
|
+
}, [manager, projectName, logSink, pushLog, logBus, stateBus, platform, proxy, profiles]);
|
|
3237
3446
|
return handleRef;
|
|
3238
3447
|
}
|
|
3239
3448
|
|
|
@@ -3565,7 +3774,8 @@ function useBootSequence(manager, config, services, cliArgs, platform, env, base
|
|
|
3565
3774
|
restarts: 0,
|
|
3566
3775
|
startedAt: null,
|
|
3567
3776
|
intentionalStop: false,
|
|
3568
|
-
colorIdx: ci
|
|
3777
|
+
colorIdx: ci,
|
|
3778
|
+
crashLog: null
|
|
3569
3779
|
};
|
|
3570
3780
|
mgr.state.set(svc.name, idleState);
|
|
3571
3781
|
const proxy = createLazyProxy({
|
|
@@ -3842,7 +4052,7 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
3842
4052
|
}
|
|
3843
4053
|
});
|
|
3844
4054
|
const proxyCtx = proxyProvider && proxyOpts ? { provider: proxyProvider, opts: proxyOpts } : null;
|
|
3845
|
-
const socketServer = useControlPlane(pm.manager, config.name, logSink, pm.pushLog, pm.logBus, pm.stateBus, platform, proxyCtx);
|
|
4055
|
+
const socketServer = useControlPlane(pm.manager, config.name, logSink, pm.pushLog, pm.logBus, pm.stateBus, platform, proxyCtx, config.profiles ?? {});
|
|
3846
4056
|
const shutdown = useCallback3(async () => {
|
|
3847
4057
|
lazyProxies.current.forEach((p) => p.destroy());
|
|
3848
4058
|
await socketServer.current?.close();
|
|
@@ -4099,7 +4309,7 @@ async function runOnce(opts) {
|
|
|
4099
4309
|
}
|
|
4100
4310
|
async function waitHealthy2(svc, deadline) {
|
|
4101
4311
|
while (Date.now() < deadline) {
|
|
4102
|
-
const ok = await checkHealth(svc.port, svc.healthCheck);
|
|
4312
|
+
const { ok } = await checkHealth(svc.port, svc.healthCheck);
|
|
4103
4313
|
if (ok) return true;
|
|
4104
4314
|
await new Promise((r) => setTimeout(r, 500));
|
|
4105
4315
|
}
|
|
@@ -4114,9 +4324,9 @@ function defineConfig(config) {
|
|
|
4114
4324
|
// src/index.ts
|
|
4115
4325
|
function readVersion() {
|
|
4116
4326
|
try {
|
|
4117
|
-
const here =
|
|
4327
|
+
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
4118
4328
|
const pkgPath = join10(here, "..", "package.json");
|
|
4119
|
-
return JSON.parse(
|
|
4329
|
+
return JSON.parse(readFileSync5(pkgPath, "utf8")).version ?? "unknown";
|
|
4120
4330
|
} catch {
|
|
4121
4331
|
return "unknown";
|
|
4122
4332
|
}
|
|
@@ -4153,6 +4363,7 @@ async function main() {
|
|
|
4153
4363
|
if (subcmd === "status") process.exit(await runStatus(subOpts));
|
|
4154
4364
|
if (subcmd === "ctl") process.exit(await runCtl(subArgs, subOpts));
|
|
4155
4365
|
if (subcmd === "down") process.exit(await runDown(subOpts));
|
|
4366
|
+
if (subcmd === "config") process.exit(await runConfig(subArgs, { cwd, configPath: cliArgs.configPath }));
|
|
4156
4367
|
}
|
|
4157
4368
|
let configPath;
|
|
4158
4369
|
try {
|