@gachlab/devup 0.10.1 → 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/index.js +284 -82
- package/dist/index.js.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/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);
|
|
@@ -835,7 +868,8 @@ function serializeState(name, st) {
|
|
|
835
868
|
errors: st.errors,
|
|
836
869
|
restarts: st.restarts,
|
|
837
870
|
pid: st.pid,
|
|
838
|
-
startedAt: st.startedAt
|
|
871
|
+
startedAt: st.startedAt,
|
|
872
|
+
crashLog: st.crashLog ?? null
|
|
839
873
|
};
|
|
840
874
|
}
|
|
841
875
|
function respond(socket, payload) {
|
|
@@ -954,7 +988,7 @@ function openStream(socketPath, method, params, onFrame, onError) {
|
|
|
954
988
|
|
|
955
989
|
// src/orchestrator/daemon.ts
|
|
956
990
|
import { spawn as spawn4 } from "child_process";
|
|
957
|
-
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";
|
|
958
992
|
import { join as join8 } from "path";
|
|
959
993
|
import { homedir as homedir3, totalmem, freemem, cpus } from "os";
|
|
960
994
|
import { setTimeout as sleep } from "timers/promises";
|
|
@@ -1060,6 +1094,8 @@ var MAX_RESTARTS = 3;
|
|
|
1060
1094
|
var BACKOFF_BASE_MS = 2e3;
|
|
1061
1095
|
|
|
1062
1096
|
// src/process/spawner.ts
|
|
1097
|
+
var CRASH_LOG_LINES = 20;
|
|
1098
|
+
var STARTUP_TIMEOUT_DEFAULT_MS = 45e3;
|
|
1063
1099
|
var Spawner = class {
|
|
1064
1100
|
baseCwd;
|
|
1065
1101
|
env;
|
|
@@ -1068,6 +1104,7 @@ var Spawner = class {
|
|
|
1068
1104
|
events;
|
|
1069
1105
|
lifecycle;
|
|
1070
1106
|
onCrash;
|
|
1107
|
+
startupTimers = /* @__PURE__ */ new Map();
|
|
1071
1108
|
constructor(opts) {
|
|
1072
1109
|
this.baseCwd = opts.baseCwd;
|
|
1073
1110
|
this.env = opts.env;
|
|
@@ -1079,6 +1116,7 @@ var Spawner = class {
|
|
|
1079
1116
|
}
|
|
1080
1117
|
async start(svc, colorIdx, isRestart = false) {
|
|
1081
1118
|
const cwd = join4(this.baseCwd, svc.cwd);
|
|
1119
|
+
this.clearStartupTimer(svc.name);
|
|
1082
1120
|
if (svc.type === "api") {
|
|
1083
1121
|
const bindable = await isPortBindable(svc.port);
|
|
1084
1122
|
if (!bindable && !isRestart) {
|
|
@@ -1114,18 +1152,41 @@ var Spawner = class {
|
|
|
1114
1152
|
restarts: prev?.restarts ?? 0,
|
|
1115
1153
|
startedAt: Date.now(),
|
|
1116
1154
|
intentionalStop: false,
|
|
1117
|
-
colorIdx
|
|
1155
|
+
colorIdx,
|
|
1156
|
+
crashLog: null
|
|
1118
1157
|
};
|
|
1119
1158
|
this.state.set(svc.name, state);
|
|
1120
1159
|
this.procs.add(proc);
|
|
1121
1160
|
this.events.onStateChange(svc.name, state);
|
|
1122
1161
|
this.wireStdio(proc, svc, state, colorIdx);
|
|
1123
1162
|
this.wireCloseHandler(proc, svc, state, colorIdx);
|
|
1163
|
+
this.scheduleStartupTimeout(svc, state, colorIdx);
|
|
1124
1164
|
if (svc.watchBuild) {
|
|
1125
1165
|
state.watchProc = this.spawnWatchBuild(svc, cwd, env, colorIdx);
|
|
1126
1166
|
}
|
|
1127
1167
|
this.log(svc.name, isRestart ? `\u{1F504} restarted (:${svc.port})` : `\u{1F680} started (:${svc.port})`, colorIdx);
|
|
1128
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
|
+
}
|
|
1129
1190
|
wireStdio(proc, svc, state, colorIdx) {
|
|
1130
1191
|
const readyRegex = compileReadyPattern(svc.readyPattern);
|
|
1131
1192
|
const markReadyIfMatch = (line) => {
|
|
@@ -1133,29 +1194,37 @@ var Spawner = class {
|
|
|
1133
1194
|
if (readyRegex.test(line)) {
|
|
1134
1195
|
state.health = "up";
|
|
1135
1196
|
if (state.status === "starting") state.status = "running";
|
|
1197
|
+
this.clearStartupTimer(svc.name);
|
|
1136
1198
|
this.events.onStateChange(svc.name, state);
|
|
1137
1199
|
}
|
|
1138
1200
|
};
|
|
1139
1201
|
const errorRegex = compileReadyPattern(svc.errorPattern);
|
|
1140
1202
|
const countsAsError = (line) => errorRegex ? errorRegex.test(line) : true;
|
|
1203
|
+
const stderrBuf = [];
|
|
1141
1204
|
const stdoutBuf = lineBuffer((line) => {
|
|
1142
1205
|
markReadyIfMatch(line);
|
|
1143
1206
|
this.log(svc.name, line, colorIdx);
|
|
1144
1207
|
});
|
|
1145
|
-
const
|
|
1208
|
+
const stderrLineBuf = lineBuffer((line) => {
|
|
1146
1209
|
if (countsAsError(line)) state.errors += 1;
|
|
1147
1210
|
markReadyIfMatch(line);
|
|
1211
|
+
stderrBuf.push(line);
|
|
1212
|
+
if (stderrBuf.length > CRASH_LOG_LINES) stderrBuf.shift();
|
|
1148
1213
|
this.log(svc.name, line, colorIdx);
|
|
1149
1214
|
});
|
|
1150
1215
|
proc.stdout?.on("data", (d) => stdoutBuf.push(d));
|
|
1151
|
-
proc.stderr?.on("data", (d) =>
|
|
1216
|
+
proc.stderr?.on("data", (d) => stderrLineBuf.push(d));
|
|
1152
1217
|
proc.stdout?.on("end", () => stdoutBuf.flush());
|
|
1153
|
-
proc.stderr?.on("end", () =>
|
|
1218
|
+
proc.stderr?.on("end", () => {
|
|
1219
|
+
stderrLineBuf.flush();
|
|
1220
|
+
state._stderrBuf = stderrBuf;
|
|
1221
|
+
});
|
|
1154
1222
|
}
|
|
1155
1223
|
wireCloseHandler(proc, svc, state, colorIdx) {
|
|
1156
1224
|
proc.on("close", (code) => {
|
|
1157
1225
|
this.procs.delete(proc);
|
|
1158
1226
|
this.lifecycle.stopWatchProc(state);
|
|
1227
|
+
this.clearStartupTimer(svc.name);
|
|
1159
1228
|
if (state.intentionalStop) {
|
|
1160
1229
|
state.intentionalStop = false;
|
|
1161
1230
|
return;
|
|
@@ -1168,6 +1237,8 @@ var Spawner = class {
|
|
|
1168
1237
|
}
|
|
1169
1238
|
state.status = "crashed";
|
|
1170
1239
|
state.health = "down";
|
|
1240
|
+
const buf = state._stderrBuf;
|
|
1241
|
+
state.crashLog = buf && buf.length ? [...buf] : null;
|
|
1171
1242
|
this.log(svc.name, `\u274C exited with code ${code}`, colorIdx);
|
|
1172
1243
|
this.events.onStateChange(svc.name, state);
|
|
1173
1244
|
this.onCrash(svc, state, colorIdx);
|
|
@@ -1205,9 +1276,7 @@ var Spawner = class {
|
|
|
1205
1276
|
spawnWatchBuild(svc, cwd, env, colorIdx) {
|
|
1206
1277
|
this.log(svc.name, `\u{1F440} watchBuild: ${svc.watchBuild}`, colorIdx);
|
|
1207
1278
|
const isWin = process.platform === "win32";
|
|
1208
|
-
const
|
|
1209
|
-
const shellFlag = isWin ? "/c" : "-c";
|
|
1210
|
-
const child = spawn2(shell, [shellFlag, svc.watchBuild], {
|
|
1279
|
+
const child = spawn2(isWin ? "cmd.exe" : "sh", [isWin ? "/c" : "-c", svc.watchBuild], {
|
|
1211
1280
|
cwd,
|
|
1212
1281
|
env,
|
|
1213
1282
|
detached: true,
|
|
@@ -1220,8 +1289,6 @@ var Spawner = class {
|
|
|
1220
1289
|
child.on("error", (err) => this.log(svc.name, `[watch] \u274C ${err.message}`, colorIdx));
|
|
1221
1290
|
return child;
|
|
1222
1291
|
}
|
|
1223
|
-
/** Create a state entry in 'crashed' status without spawning a process
|
|
1224
|
-
* (used when preBuild fails or pre-flight checks fail). */
|
|
1225
1292
|
recordCrashedState(svc, colorIdx) {
|
|
1226
1293
|
const prev = this.state.get(svc.name);
|
|
1227
1294
|
this.state.set(svc.name, {
|
|
@@ -1234,7 +1301,8 @@ var Spawner = class {
|
|
|
1234
1301
|
restarts: prev?.restarts ?? 0,
|
|
1235
1302
|
startedAt: null,
|
|
1236
1303
|
intentionalStop: false,
|
|
1237
|
-
colorIdx
|
|
1304
|
+
colorIdx,
|
|
1305
|
+
crashLog: null
|
|
1238
1306
|
});
|
|
1239
1307
|
this.events.onStateChange(svc.name, this.state.get(svc.name));
|
|
1240
1308
|
}
|
|
@@ -1281,13 +1349,14 @@ var Restarter = class {
|
|
|
1281
1349
|
var HealthPoller = class {
|
|
1282
1350
|
state;
|
|
1283
1351
|
events;
|
|
1352
|
+
failureCounts = /* @__PURE__ */ new Map();
|
|
1284
1353
|
constructor(opts) {
|
|
1285
1354
|
this.state = opts.state;
|
|
1286
1355
|
this.events = opts.events;
|
|
1287
1356
|
}
|
|
1288
1357
|
async checkAll() {
|
|
1289
1358
|
for (const [name, st] of this.state) {
|
|
1290
|
-
if (!st.pid || st.status === "idle") {
|
|
1359
|
+
if (!st.pid || st.status === "idle" || st.status === "timeout") {
|
|
1291
1360
|
st.health = st.status === "idle" ? "idle" : "down";
|
|
1292
1361
|
continue;
|
|
1293
1362
|
}
|
|
@@ -1295,10 +1364,22 @@ var HealthPoller = class {
|
|
|
1295
1364
|
if (startPeriodMs > 0 && st.startedAt && Date.now() - st.startedAt < startPeriodMs) {
|
|
1296
1365
|
continue;
|
|
1297
1366
|
}
|
|
1298
|
-
const
|
|
1367
|
+
const result = await checkHealth(st.svc.port, st.svc.healthCheck);
|
|
1368
|
+
const threshold = st.svc.healthCheck?.failureThreshold ?? 2;
|
|
1299
1369
|
const prev = st.health;
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
+
}
|
|
1302
1383
|
if (prev !== st.health) this.events.onStateChange(name, st);
|
|
1303
1384
|
}
|
|
1304
1385
|
}
|
|
@@ -1444,44 +1525,82 @@ var ProcessManager = class {
|
|
|
1444
1525
|
};
|
|
1445
1526
|
|
|
1446
1527
|
// src/process/log-sink.ts
|
|
1447
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync2, renameSync, createWriteStream } from "fs";
|
|
1448
|
-
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";
|
|
1449
1530
|
import { homedir as homedir2 } from "os";
|
|
1531
|
+
var DEFAULT_MAX_LOG_BYTES = 10 * 1024 * 1024;
|
|
1450
1532
|
var LogSink = class {
|
|
1451
1533
|
dir;
|
|
1452
1534
|
rotateOnStart;
|
|
1535
|
+
maxLogBytes;
|
|
1453
1536
|
streams = /* @__PURE__ */ new Map();
|
|
1454
1537
|
seen = /* @__PURE__ */ new Set();
|
|
1538
|
+
bytesWritten = /* @__PURE__ */ new Map();
|
|
1455
1539
|
constructor(opts) {
|
|
1456
1540
|
const root = opts.rootDir ?? join6(homedir2(), ".devup", "logs");
|
|
1457
1541
|
this.dir = join6(root, sanitize(opts.projectName));
|
|
1458
1542
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
1543
|
+
this.maxLogBytes = opts.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
1459
1544
|
mkdirSync2(this.dir, { recursive: true });
|
|
1460
1545
|
}
|
|
1461
|
-
/** Returns the file path for a service log (useful for tests / UI). */
|
|
1462
1546
|
pathFor(svcName) {
|
|
1463
1547
|
return join6(this.dir, `${sanitize(svcName)}.log`);
|
|
1464
1548
|
}
|
|
1465
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
|
+
}
|
|
1466
1557
|
const stream = this.streamFor(svcName);
|
|
1467
|
-
stream.write(
|
|
1468
|
-
|
|
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);
|
|
1469
1567
|
}
|
|
1470
1568
|
async close() {
|
|
1471
|
-
const closes = [...this.streams.values()].map(
|
|
1472
|
-
(s) => new Promise((r) => s.end(() => r()))
|
|
1473
|
-
);
|
|
1569
|
+
const closes = [...this.streams.values()].map((s) => new Promise((r) => s.end(() => r())));
|
|
1474
1570
|
this.streams.clear();
|
|
1475
1571
|
this.seen.clear();
|
|
1572
|
+
this.bytesWritten.clear();
|
|
1476
1573
|
await Promise.all(closes);
|
|
1477
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
|
+
}
|
|
1478
1597
|
streamFor(svcName) {
|
|
1479
1598
|
let s = this.streams.get(svcName);
|
|
1480
1599
|
if (s) return s;
|
|
1481
1600
|
const file = this.pathFor(svcName);
|
|
1482
1601
|
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync9(file)) {
|
|
1483
1602
|
try {
|
|
1484
|
-
mkdirSync2(
|
|
1603
|
+
mkdirSync2(dirname3(file), { recursive: true });
|
|
1485
1604
|
renameSync(file, file + ".prev");
|
|
1486
1605
|
} catch {
|
|
1487
1606
|
}
|
|
@@ -1589,7 +1708,7 @@ async function waitHealthy(svc, timeoutMs) {
|
|
|
1589
1708
|
const deadline = Date.now() + timeoutMs;
|
|
1590
1709
|
const port = svc.port;
|
|
1591
1710
|
while (Date.now() < deadline) {
|
|
1592
|
-
if (await checkHealth(port, svc.healthCheck)) return true;
|
|
1711
|
+
if ((await checkHealth(port, svc.healthCheck)).ok) return true;
|
|
1593
1712
|
await new Promise((r) => setTimeout(r, 500));
|
|
1594
1713
|
}
|
|
1595
1714
|
return false;
|
|
@@ -1856,7 +1975,7 @@ function isDaemonRunning(projectName) {
|
|
|
1856
1975
|
if (!existsSync10(path)) return { pid: null, stale: false };
|
|
1857
1976
|
let pid;
|
|
1858
1977
|
try {
|
|
1859
|
-
pid = Number(
|
|
1978
|
+
pid = Number(readFileSync4(path, "utf8").trim());
|
|
1860
1979
|
if (!pid || !Number.isFinite(pid)) return { pid: null, stale: true };
|
|
1861
1980
|
} catch {
|
|
1862
1981
|
return { pid: null, stale: true };
|
|
@@ -2133,7 +2252,8 @@ async function bootLazy(mgr, services, lazyCfg, lazyTimeout, lazyProxies) {
|
|
|
2133
2252
|
restarts: 0,
|
|
2134
2253
|
startedAt: null,
|
|
2135
2254
|
intentionalStop: false,
|
|
2136
|
-
colorIdx: ci
|
|
2255
|
+
colorIdx: ci,
|
|
2256
|
+
crashLog: null
|
|
2137
2257
|
});
|
|
2138
2258
|
const proxy = createLazyProxy({
|
|
2139
2259
|
listenPort: svc.port,
|
|
@@ -2212,7 +2332,7 @@ async function runDetached(opts) {
|
|
|
2212
2332
|
const deadline = Date.now() + 9e4;
|
|
2213
2333
|
while (Date.now() < deadline) {
|
|
2214
2334
|
if (existsSync10(pidPath)) {
|
|
2215
|
-
const pid = Number(
|
|
2335
|
+
const pid = Number(readFileSync4(pidPath, "utf8").trim());
|
|
2216
2336
|
out("");
|
|
2217
2337
|
out(`\u{1F680} devup detached (PID ${pid})`);
|
|
2218
2338
|
out(" inspect: devup ctl status");
|
|
@@ -2221,7 +2341,7 @@ async function runDetached(opts) {
|
|
|
2221
2341
|
return 0;
|
|
2222
2342
|
}
|
|
2223
2343
|
if (existsSync10(errPath)) {
|
|
2224
|
-
const msg =
|
|
2344
|
+
const msg = readFileSync4(errPath, "utf8").trim();
|
|
2225
2345
|
out(`\u274C daemon boot failed: ${msg}`);
|
|
2226
2346
|
try {
|
|
2227
2347
|
unlinkSync2(errPath);
|
|
@@ -2294,7 +2414,7 @@ async function stopDaemon(projectName, opts = {}) {
|
|
|
2294
2414
|
}
|
|
2295
2415
|
|
|
2296
2416
|
// src/orchestrator/subcommands.ts
|
|
2297
|
-
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"]);
|
|
2298
2418
|
function detectSubcommand(argv) {
|
|
2299
2419
|
const first = argv[0];
|
|
2300
2420
|
return first && KNOWN.has(first) ? first : null;
|
|
@@ -2326,7 +2446,7 @@ async function runLogs(argv, opts) {
|
|
|
2326
2446
|
}
|
|
2327
2447
|
await streamFile(file, out);
|
|
2328
2448
|
if (!follow) return 0;
|
|
2329
|
-
return await followFile(file, out,
|
|
2449
|
+
return await followFile(file, out, statSync3(file).size);
|
|
2330
2450
|
}
|
|
2331
2451
|
async function streamFile(file, out) {
|
|
2332
2452
|
return new Promise((resolve4, reject) => {
|
|
@@ -2341,7 +2461,7 @@ async function followFile(file, out, startAt = 0) {
|
|
|
2341
2461
|
while (!existsSync11(file)) await new Promise((r) => setTimeout(r, 500));
|
|
2342
2462
|
return new Promise((resolve4) => {
|
|
2343
2463
|
const tick = async () => {
|
|
2344
|
-
const size =
|
|
2464
|
+
const size = statSync3(file).size;
|
|
2345
2465
|
if (size > pos) {
|
|
2346
2466
|
await new Promise((res) => {
|
|
2347
2467
|
const rl = createInterface4({ input: createReadStream2(file, { encoding: "utf8", start: pos, end: size - 1 }) });
|
|
@@ -2422,7 +2542,7 @@ async function runStatus(opts) {
|
|
|
2422
2542
|
out(`${"Service".padEnd(maxLen)} ${"Port".padStart(5)} ${"Type".padEnd(4)} Health`);
|
|
2423
2543
|
out("-".repeat(maxLen + 24));
|
|
2424
2544
|
for (const svc of opts.config.services) {
|
|
2425
|
-
const up = await checkHealth(svc.port, svc.healthCheck);
|
|
2545
|
+
const { ok: up } = await checkHealth(svc.port, svc.healthCheck);
|
|
2426
2546
|
const health = up ? "\u2713 up" : "\u2717 down";
|
|
2427
2547
|
out(`${svc.name.padEnd(maxLen)} ${String(svc.port).padStart(5)} ${svc.type.padEnd(4)} ${health}`);
|
|
2428
2548
|
}
|
|
@@ -2465,15 +2585,6 @@ async function runCtl(argv, opts) {
|
|
|
2465
2585
|
out(`pong ts=${res.ts}`);
|
|
2466
2586
|
return 0;
|
|
2467
2587
|
}
|
|
2468
|
-
if (method === "status" && !follow) {
|
|
2469
|
-
const res = await sendRpc(socketPath, "status");
|
|
2470
|
-
if (!res.services.length) {
|
|
2471
|
-
out("(no services)");
|
|
2472
|
-
return 0;
|
|
2473
|
-
}
|
|
2474
|
-
fmtStatus(res.services, out);
|
|
2475
|
-
return 0;
|
|
2476
|
-
}
|
|
2477
2588
|
if (method === "status" && follow) {
|
|
2478
2589
|
return await new Promise((resolve4) => {
|
|
2479
2590
|
const abort = openStream(socketPath, "status.follow", {}, (frame) => {
|
|
@@ -2516,15 +2627,47 @@ async function runCtl(argv, opts) {
|
|
|
2516
2627
|
});
|
|
2517
2628
|
});
|
|
2518
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
|
+
}
|
|
2519
2644
|
if (method === "restart") {
|
|
2520
2645
|
const svc = argv[1];
|
|
2521
2646
|
if (!svc) {
|
|
2522
|
-
out("usage: devup ctl restart <service>");
|
|
2647
|
+
out("usage: devup ctl restart <service> [--wait] [--timeout <s>]");
|
|
2523
2648
|
return 1;
|
|
2524
2649
|
}
|
|
2650
|
+
const wait = argv.includes("--wait");
|
|
2651
|
+
const timeoutIdx = argv.indexOf("--timeout");
|
|
2652
|
+
const timeoutSec = timeoutIdx >= 0 ? Number(argv[timeoutIdx + 1] ?? 60) : 60;
|
|
2525
2653
|
await sendRpc(socketPath, "restart", { svc });
|
|
2526
|
-
|
|
2527
|
-
|
|
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;
|
|
2528
2671
|
}
|
|
2529
2672
|
if (method === "stop") {
|
|
2530
2673
|
const svc = argv[1];
|
|
@@ -2543,6 +2686,63 @@ async function runCtl(argv, opts) {
|
|
|
2543
2686
|
return 1;
|
|
2544
2687
|
}
|
|
2545
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
|
+
}
|
|
2546
2746
|
async function runDown(opts) {
|
|
2547
2747
|
const out = opts.out ?? ((l) => process.stdout.write(l + "\n"));
|
|
2548
2748
|
return stopDaemon(opts.config.name, { out });
|
|
@@ -2741,7 +2941,7 @@ async function detectPlatform() {
|
|
|
2741
2941
|
|
|
2742
2942
|
// src/proxy-config/traefik.ts
|
|
2743
2943
|
import { existsSync as existsSync12, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
2744
|
-
import { dirname as
|
|
2944
|
+
import { dirname as dirname5 } from "path";
|
|
2745
2945
|
var EMPTY_CONFIG = "http:\n routers: {}\n services: {}\n";
|
|
2746
2946
|
var TraefikProvider = class {
|
|
2747
2947
|
name = "traefik";
|
|
@@ -2778,7 +2978,7 @@ ${svcs.join("\n")}
|
|
|
2778
2978
|
`;
|
|
2779
2979
|
}
|
|
2780
2980
|
write(content, opts) {
|
|
2781
|
-
const dir =
|
|
2981
|
+
const dir = dirname5(opts.confPath);
|
|
2782
2982
|
if (!existsSync12(dir)) mkdirSync4(dir, { recursive: true });
|
|
2783
2983
|
writeFileSync3(opts.confPath, content);
|
|
2784
2984
|
}
|
|
@@ -2789,7 +2989,7 @@ ${svcs.join("\n")}
|
|
|
2789
2989
|
|
|
2790
2990
|
// src/proxy-config/nginx.ts
|
|
2791
2991
|
import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
2792
|
-
import { dirname as
|
|
2992
|
+
import { dirname as dirname6 } from "path";
|
|
2793
2993
|
var EMPTY_CONFIG2 = "# devup: no healthy services\n";
|
|
2794
2994
|
var NginxProvider = class {
|
|
2795
2995
|
name = "nginx";
|
|
@@ -2826,7 +3026,7 @@ var NginxProvider = class {
|
|
|
2826
3026
|
return blocks.join("\n\n") + "\n";
|
|
2827
3027
|
}
|
|
2828
3028
|
write(content, opts) {
|
|
2829
|
-
const dir =
|
|
3029
|
+
const dir = dirname6(opts.confPath);
|
|
2830
3030
|
if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
|
|
2831
3031
|
writeFileSync4(opts.confPath, content);
|
|
2832
3032
|
}
|
|
@@ -2837,7 +3037,7 @@ var NginxProvider = class {
|
|
|
2837
3037
|
|
|
2838
3038
|
// src/proxy-config/caddy.ts
|
|
2839
3039
|
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
2840
|
-
import { dirname as
|
|
3040
|
+
import { dirname as dirname7 } from "path";
|
|
2841
3041
|
var EMPTY_CONFIG3 = "# devup: no healthy services\n";
|
|
2842
3042
|
var CaddyProvider = class {
|
|
2843
3043
|
name = "caddy";
|
|
@@ -2860,7 +3060,7 @@ var CaddyProvider = class {
|
|
|
2860
3060
|
return blocks.join("\n\n") + "\n";
|
|
2861
3061
|
}
|
|
2862
3062
|
write(content, opts) {
|
|
2863
|
-
const dir =
|
|
3063
|
+
const dir = dirname7(opts.confPath);
|
|
2864
3064
|
if (!existsSync14(dir)) mkdirSync6(dir, { recursive: true });
|
|
2865
3065
|
writeFileSync5(opts.confPath, content);
|
|
2866
3066
|
}
|
|
@@ -3574,7 +3774,8 @@ function useBootSequence(manager, config, services, cliArgs, platform, env, base
|
|
|
3574
3774
|
restarts: 0,
|
|
3575
3775
|
startedAt: null,
|
|
3576
3776
|
intentionalStop: false,
|
|
3577
|
-
colorIdx: ci
|
|
3777
|
+
colorIdx: ci,
|
|
3778
|
+
crashLog: null
|
|
3578
3779
|
};
|
|
3579
3780
|
mgr.state.set(svc.name, idleState);
|
|
3580
3781
|
const proxy = createLazyProxy({
|
|
@@ -4108,7 +4309,7 @@ async function runOnce(opts) {
|
|
|
4108
4309
|
}
|
|
4109
4310
|
async function waitHealthy2(svc, deadline) {
|
|
4110
4311
|
while (Date.now() < deadline) {
|
|
4111
|
-
const ok = await checkHealth(svc.port, svc.healthCheck);
|
|
4312
|
+
const { ok } = await checkHealth(svc.port, svc.healthCheck);
|
|
4112
4313
|
if (ok) return true;
|
|
4113
4314
|
await new Promise((r) => setTimeout(r, 500));
|
|
4114
4315
|
}
|
|
@@ -4123,9 +4324,9 @@ function defineConfig(config) {
|
|
|
4123
4324
|
// src/index.ts
|
|
4124
4325
|
function readVersion() {
|
|
4125
4326
|
try {
|
|
4126
|
-
const here =
|
|
4327
|
+
const here = dirname8(fileURLToPath2(import.meta.url));
|
|
4127
4328
|
const pkgPath = join10(here, "..", "package.json");
|
|
4128
|
-
return JSON.parse(
|
|
4329
|
+
return JSON.parse(readFileSync5(pkgPath, "utf8")).version ?? "unknown";
|
|
4129
4330
|
} catch {
|
|
4130
4331
|
return "unknown";
|
|
4131
4332
|
}
|
|
@@ -4162,6 +4363,7 @@ async function main() {
|
|
|
4162
4363
|
if (subcmd === "status") process.exit(await runStatus(subOpts));
|
|
4163
4364
|
if (subcmd === "ctl") process.exit(await runCtl(subArgs, subOpts));
|
|
4164
4365
|
if (subcmd === "down") process.exit(await runDown(subOpts));
|
|
4366
|
+
if (subcmd === "config") process.exit(await runConfig(subArgs, { cwd, configPath: cliArgs.configPath }));
|
|
4165
4367
|
}
|
|
4166
4368
|
let configPath;
|
|
4167
4369
|
try {
|