@tridha643/hestia 1.2.0 → 1.3.1
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/README.md +3 -2
- package/dist/assets/portless/dist/cli.js +4 -3
- package/dist/assets/portless/hestia-hardening.patch +7 -6
- package/dist/assets/portless/provenance.json +1 -1
- package/dist/cli.js +935 -378
- package/dist/cli.js.map +32 -28
- package/dist/daemon.js +624 -225
- package/dist/daemon.js.map +21 -19
- package/package.json +1 -1
- package/skills/hestia/SKILL.md +12 -1
package/dist/cli.js
CHANGED
|
@@ -7254,7 +7254,7 @@ function parsePidfile(source, path) {
|
|
|
7254
7254
|
if (pidfile.schemaVersion !== undefined && pidfile.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
7255
7255
|
throw new HestiaError("state-corrupt", `invalid pidfile ${path}: unsupported schemaVersion`, { path });
|
|
7256
7256
|
}
|
|
7257
|
-
if (typeof pidfile.name !== "string" || !Number.
|
|
7257
|
+
if (typeof pidfile.name !== "string" || !Number.isSafeInteger(pidfile.pid) || pidfile.pid <= 0 || typeof pidfile.startTime !== "string" || typeof pidfile.logPath !== "string") {
|
|
7258
7258
|
throw new HestiaError("state-corrupt", `invalid pidfile ${path}: missing process identity`, { path });
|
|
7259
7259
|
}
|
|
7260
7260
|
if (pidfile.schemaVersion === STATE_SCHEMA_VERSION && typeof pidfile.specFingerprint !== "string") {
|
|
@@ -7272,12 +7272,25 @@ function removePidfile(worktreeRoot, name) {
|
|
|
7272
7272
|
rmSync2(pidfilePath(worktreeRoot, name), { force: true });
|
|
7273
7273
|
}
|
|
7274
7274
|
function listPidfiles(dir) {
|
|
7275
|
+
const scan = scanPidfiles(dir);
|
|
7276
|
+
if (scan.errors[0] !== undefined)
|
|
7277
|
+
throw scan.errors[0];
|
|
7278
|
+
return scan.pidfiles;
|
|
7279
|
+
}
|
|
7280
|
+
function scanPidfiles(dir) {
|
|
7275
7281
|
if (!existsSync2(dir))
|
|
7276
|
-
return [];
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7282
|
+
return { pidfiles: [], errors: [] };
|
|
7283
|
+
const pidfiles = [];
|
|
7284
|
+
const errors2 = [];
|
|
7285
|
+
for (const file of readdirSync(dir).filter((candidate) => candidate.endsWith(".json"))) {
|
|
7286
|
+
const path = join2(dir, file);
|
|
7287
|
+
try {
|
|
7288
|
+
pidfiles.push(parsePidfile(readFileSync2(path, "utf8"), path));
|
|
7289
|
+
} catch (error) {
|
|
7290
|
+
errors2.push(error);
|
|
7291
|
+
}
|
|
7292
|
+
}
|
|
7293
|
+
return { pidfiles, errors: errors2 };
|
|
7281
7294
|
}
|
|
7282
7295
|
function procSpecFingerprint(spec) {
|
|
7283
7296
|
const env = Object.entries(spec.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
@@ -7290,23 +7303,28 @@ function procSpecFingerprint(spec) {
|
|
|
7290
7303
|
backend: spec.backend ?? "proc",
|
|
7291
7304
|
resolver: spec.varlock ? "varlock" : "direct",
|
|
7292
7305
|
inspectorPort: spec.inspectorPort ?? null,
|
|
7306
|
+
healthPath: spec.healthPath ?? null,
|
|
7293
7307
|
configPath: spec.configPath ?? null,
|
|
7294
7308
|
originService: spec.originService ?? null,
|
|
7295
7309
|
originEndpoint: spec.originEndpoint ?? null
|
|
7296
7310
|
});
|
|
7297
7311
|
return createHash3("sha256").update(canonical).digest("hex");
|
|
7298
7312
|
}
|
|
7299
|
-
function
|
|
7313
|
+
function probeProcessStartTime(pid, env) {
|
|
7300
7314
|
try {
|
|
7301
7315
|
const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
7302
7316
|
encoding: "utf8",
|
|
7303
7317
|
env
|
|
7304
7318
|
}).trim();
|
|
7305
|
-
return out === "" ? null : out;
|
|
7319
|
+
return { status: "ok", value: out === "" ? null : out };
|
|
7306
7320
|
} catch {
|
|
7307
|
-
return
|
|
7321
|
+
return { status: "error" };
|
|
7308
7322
|
}
|
|
7309
7323
|
}
|
|
7324
|
+
function readProcessStartTime(pid, env) {
|
|
7325
|
+
const result = probeProcessStartTime(pid, env);
|
|
7326
|
+
return result.status === "ok" ? result.value : null;
|
|
7327
|
+
}
|
|
7310
7328
|
function startTimeOf(pid) {
|
|
7311
7329
|
return readProcessStartTime(pid, { ...process.env, LC_ALL: "C", LANG: "C" });
|
|
7312
7330
|
}
|
|
@@ -7357,30 +7375,40 @@ function plausibleProcessLocales(canonicalStartTime, recordedStartTime) {
|
|
|
7357
7375
|
});
|
|
7358
7376
|
return [...new Set(plausible)];
|
|
7359
7377
|
}
|
|
7360
|
-
function
|
|
7378
|
+
function probeLegacyLocalizedStartTime(pid, canonicalStartTime, recordedStartTime) {
|
|
7361
7379
|
const cacheKey = `${pid}\x00${canonicalStartTime}\x00${recordedStartTime}`;
|
|
7362
7380
|
const cached = legacyStartTimeMatches.get(cacheKey);
|
|
7363
7381
|
if (cached !== undefined)
|
|
7364
|
-
return cached;
|
|
7382
|
+
return cached ? "live" : "dead";
|
|
7365
7383
|
let matched = false;
|
|
7384
|
+
let readFailed = false;
|
|
7366
7385
|
for (const locale of plausibleProcessLocales(canonicalStartTime, recordedStartTime)) {
|
|
7367
|
-
const
|
|
7386
|
+
const read = probeProcessStartTime(pid, {
|
|
7368
7387
|
...process.env,
|
|
7369
7388
|
LC_ALL: locale,
|
|
7370
7389
|
LANG: locale
|
|
7371
7390
|
});
|
|
7372
|
-
if (
|
|
7391
|
+
if (read.status === "error") {
|
|
7392
|
+
readFailed = true;
|
|
7393
|
+
continue;
|
|
7394
|
+
}
|
|
7395
|
+
if (read.value !== null && processStartTimeMatches(read.value, recordedStartTime)) {
|
|
7373
7396
|
matched = true;
|
|
7374
7397
|
break;
|
|
7375
7398
|
}
|
|
7376
7399
|
}
|
|
7400
|
+
if (!matched && readFailed)
|
|
7401
|
+
return "unknown";
|
|
7377
7402
|
if (legacyStartTimeMatches.size >= 512) {
|
|
7378
7403
|
const oldest = legacyStartTimeMatches.keys().next().value;
|
|
7379
7404
|
if (oldest !== undefined)
|
|
7380
7405
|
legacyStartTimeMatches.delete(oldest);
|
|
7381
7406
|
}
|
|
7382
7407
|
legacyStartTimeMatches.set(cacheKey, matched);
|
|
7383
|
-
return matched;
|
|
7408
|
+
return matched ? "live" : "dead";
|
|
7409
|
+
}
|
|
7410
|
+
function legacyLocalizedStartTimeMatches(pid, canonicalStartTime, recordedStartTime) {
|
|
7411
|
+
return probeLegacyLocalizedStartTime(pid, canonicalStartTime, recordedStartTime) === "live";
|
|
7384
7412
|
}
|
|
7385
7413
|
function processStartTimeMatches(current, recorded) {
|
|
7386
7414
|
if (current === recorded)
|
|
@@ -7402,6 +7430,27 @@ function isLive(pf) {
|
|
|
7402
7430
|
return true;
|
|
7403
7431
|
return current !== null && legacyLocalizedStartTimeMatches(pf.pid, current, pf.startTime);
|
|
7404
7432
|
}
|
|
7433
|
+
function probeProcessIdentity(pf) {
|
|
7434
|
+
try {
|
|
7435
|
+
process.kill(pf.pid, 0);
|
|
7436
|
+
} catch (error) {
|
|
7437
|
+
return error.code === "ESRCH" ? "dead" : "unknown";
|
|
7438
|
+
}
|
|
7439
|
+
let current;
|
|
7440
|
+
try {
|
|
7441
|
+
current = execFileSync("ps", ["-o", "lstart=", "-p", String(pf.pid)], {
|
|
7442
|
+
encoding: "utf8",
|
|
7443
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" }
|
|
7444
|
+
}).trim();
|
|
7445
|
+
} catch {
|
|
7446
|
+
return "unknown";
|
|
7447
|
+
}
|
|
7448
|
+
if (current === "")
|
|
7449
|
+
return "dead";
|
|
7450
|
+
if (processStartTimeMatches(current, pf.startTime))
|
|
7451
|
+
return "live";
|
|
7452
|
+
return probeLegacyLocalizedStartTime(pf.pid, current, pf.startTime);
|
|
7453
|
+
}
|
|
7405
7454
|
var installedProcessLocales, legacyStartTimeMatches, legacyMacosProcessLocales;
|
|
7406
7455
|
var init_pidfile = __esm(() => {
|
|
7407
7456
|
init_src();
|
|
@@ -7529,6 +7578,13 @@ function readMirrorState(project) {
|
|
|
7529
7578
|
chmodSync2(p, 384);
|
|
7530
7579
|
return record;
|
|
7531
7580
|
}
|
|
7581
|
+
function readMirrorStateSafe(project) {
|
|
7582
|
+
try {
|
|
7583
|
+
return { status: "ok", record: readMirrorState(project) };
|
|
7584
|
+
} catch (error) {
|
|
7585
|
+
return { status: "error", error };
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7532
7588
|
function clearState(worktreeRoot, project) {
|
|
7533
7589
|
const p = statePath(worktreeRoot);
|
|
7534
7590
|
if (existsSync3(p))
|
|
@@ -7738,6 +7794,26 @@ var init_override = __esm(() => {
|
|
|
7738
7794
|
// packages/engine/src/repository-config.ts
|
|
7739
7795
|
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
7740
7796
|
import { join as join5 } from "path";
|
|
7797
|
+
function parseEnvironment(value, path) {
|
|
7798
|
+
if (value === undefined)
|
|
7799
|
+
return {};
|
|
7800
|
+
const environment = {};
|
|
7801
|
+
for (const [name, rawValue] of Object.entries(table(value, path))) {
|
|
7802
|
+
if (!ENVIRONMENT_KEY.test(name))
|
|
7803
|
+
throw new Error(`${path} has invalid environment key ${JSON.stringify(name)}`);
|
|
7804
|
+
if (typeof rawValue === "string") {
|
|
7805
|
+
environment[name] = rawValue;
|
|
7806
|
+
continue;
|
|
7807
|
+
}
|
|
7808
|
+
const fileValue = table(rawValue, `${path}.${name}`);
|
|
7809
|
+
knownKeys(fileValue, ["file"], `${path}.${name}`);
|
|
7810
|
+
if (typeof fileValue.file !== "string" || !/^\.hestia\//.test(fileValue.file) || fileValue.file.includes("..")) {
|
|
7811
|
+
throw new Error(`${path}.${name}.file must be a path below .hestia/`);
|
|
7812
|
+
}
|
|
7813
|
+
environment[name] = { file: fileValue.file };
|
|
7814
|
+
}
|
|
7815
|
+
return environment;
|
|
7816
|
+
}
|
|
7741
7817
|
function emptyConfig() {
|
|
7742
7818
|
return { version: 1, workloads: {} };
|
|
7743
7819
|
}
|
|
@@ -7797,7 +7873,19 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
7797
7873
|
if (!WORKLOAD_NAME.test(name))
|
|
7798
7874
|
throw new Error(`invalid workload name ${JSON.stringify(name)}`);
|
|
7799
7875
|
const workload = table(rawWorkload, `workloads.${name}`);
|
|
7800
|
-
knownKeys(workload, [
|
|
7876
|
+
knownKeys(workload, [
|
|
7877
|
+
"source",
|
|
7878
|
+
"compose_service",
|
|
7879
|
+
"dockerfile",
|
|
7880
|
+
"command",
|
|
7881
|
+
"wrangler_config",
|
|
7882
|
+
"port",
|
|
7883
|
+
"cwd",
|
|
7884
|
+
"varlock",
|
|
7885
|
+
"health_path",
|
|
7886
|
+
"env",
|
|
7887
|
+
"endpoints"
|
|
7888
|
+
], `workloads.${name}`);
|
|
7801
7889
|
if (!["compose", "dockerfile", "proc", "wrangler"].includes(workload.source)) {
|
|
7802
7890
|
throw new Error(`workloads.${name}.source must be compose, dockerfile, proc, or wrangler`);
|
|
7803
7891
|
}
|
|
@@ -7813,6 +7901,18 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
7813
7901
|
throw new Error(`workloads.${name}.${key} must be a string`);
|
|
7814
7902
|
}
|
|
7815
7903
|
}
|
|
7904
|
+
if (workload.cwd !== undefined && typeof workload.cwd !== "string") {
|
|
7905
|
+
throw new Error(`workloads.${name}.cwd must be a string`);
|
|
7906
|
+
}
|
|
7907
|
+
if (workload.varlock !== undefined && typeof workload.varlock !== "boolean") {
|
|
7908
|
+
throw new Error(`workloads.${name}.varlock must be a boolean`);
|
|
7909
|
+
}
|
|
7910
|
+
if (workload.health_path !== undefined && (typeof workload.health_path !== "string" || !workload.health_path.startsWith("/"))) {
|
|
7911
|
+
throw new Error(`workloads.${name}.health_path must start with /`);
|
|
7912
|
+
}
|
|
7913
|
+
if (workload.health_path !== undefined && workload.port === "none") {
|
|
7914
|
+
throw new Error(`workloads.${name}.health_path requires a port`);
|
|
7915
|
+
}
|
|
7816
7916
|
if (sourceKind === "compose" && typeof workload.compose_service !== "string") {
|
|
7817
7917
|
throw new Error(`workloads.${name}.compose_service is required for compose`);
|
|
7818
7918
|
}
|
|
@@ -7826,6 +7926,10 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
7826
7926
|
command: workload.command,
|
|
7827
7927
|
wranglerConfig: workload.wrangler_config,
|
|
7828
7928
|
port: workload.port,
|
|
7929
|
+
cwd: workload.cwd,
|
|
7930
|
+
varlock: workload.varlock,
|
|
7931
|
+
healthPath: workload.health_path,
|
|
7932
|
+
env: parseEnvironment(workload.env, `workloads.${name}.env`),
|
|
7829
7933
|
endpoints: parseEndpoints(workload.endpoints, `workloads.${name}.endpoints`)
|
|
7830
7934
|
};
|
|
7831
7935
|
}
|
|
@@ -7870,6 +7974,17 @@ function renderRepositoryWorkloadConfig(config) {
|
|
|
7870
7974
|
lines.push(`wrangler_config = ${quote(workload.wranglerConfig)}`);
|
|
7871
7975
|
if (workload.port !== undefined)
|
|
7872
7976
|
lines.push(`port = ${quote(workload.port)}`);
|
|
7977
|
+
if (workload.cwd !== undefined)
|
|
7978
|
+
lines.push(`cwd = ${quote(workload.cwd)}`);
|
|
7979
|
+
if (workload.varlock !== undefined)
|
|
7980
|
+
lines.push(`varlock = ${workload.varlock}`);
|
|
7981
|
+
if (workload.healthPath !== undefined)
|
|
7982
|
+
lines.push(`health_path = ${quote(workload.healthPath)}`);
|
|
7983
|
+
for (const key of Object.keys(workload.env).sort()) {
|
|
7984
|
+
const value = workload.env[key];
|
|
7985
|
+
const rendered = typeof value === "string" ? quote(value) : `{ file = ${quote(value.file)} }`;
|
|
7986
|
+
lines.push(`env.${quote(key)} = ${rendered}`);
|
|
7987
|
+
}
|
|
7873
7988
|
lines.push("");
|
|
7874
7989
|
for (const alias of Object.keys(workload.endpoints).sort()) {
|
|
7875
7990
|
const endpoint = workload.endpoints[alias];
|
|
@@ -7883,12 +7998,13 @@ function renderRepositoryWorkloadConfig(config) {
|
|
|
7883
7998
|
`).trimEnd()}
|
|
7884
7999
|
`;
|
|
7885
8000
|
}
|
|
7886
|
-
var WORKLOAD_NAME, BINDING;
|
|
8001
|
+
var WORKLOAD_NAME, BINDING, ENVIRONMENT_KEY;
|
|
7887
8002
|
var init_repository_config = __esm(() => {
|
|
7888
8003
|
init_src();
|
|
7889
8004
|
init_state();
|
|
7890
8005
|
WORKLOAD_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
7891
8006
|
BINDING = /^(?:main|[1-9][0-9]{0,4})\/(?:tcp|udp)$/;
|
|
8007
|
+
ENVIRONMENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7892
8008
|
});
|
|
7893
8009
|
|
|
7894
8010
|
// packages/engine/src/discovery.ts
|
|
@@ -7926,7 +8042,12 @@ function sameEndpoint(left, right) {
|
|
|
7926
8042
|
return left.binding === right.binding && left.kind === right.kind && left.local === right.local;
|
|
7927
8043
|
}
|
|
7928
8044
|
function cloneWorkload(workload) {
|
|
7929
|
-
return {
|
|
8045
|
+
return {
|
|
8046
|
+
...workload,
|
|
8047
|
+
command: workload.command?.slice(),
|
|
8048
|
+
env: { ...workload.env },
|
|
8049
|
+
endpoints: { ...workload.endpoints }
|
|
8050
|
+
};
|
|
7930
8051
|
}
|
|
7931
8052
|
function mergeConfigLayers(repository, machine) {
|
|
7932
8053
|
const workloads = {};
|
|
@@ -7952,6 +8073,10 @@ function mergeConfigLayers(repository, machine) {
|
|
|
7952
8073
|
conflicts.push(`machine workload ${name} cannot replace committed source ${committed.source} with ${overlay.source}`);
|
|
7953
8074
|
continue;
|
|
7954
8075
|
}
|
|
8076
|
+
committed.cwd = overlay.cwd ?? committed.cwd;
|
|
8077
|
+
committed.varlock = overlay.varlock ?? committed.varlock;
|
|
8078
|
+
committed.healthPath = overlay.healthPath ?? committed.healthPath;
|
|
8079
|
+
committed.env = { ...committed.env, ...overlay.env };
|
|
7955
8080
|
for (const [alias, endpoint] of Object.entries(overlay.endpoints)) {
|
|
7956
8081
|
const existing = committed.endpoints[alias];
|
|
7957
8082
|
if (existing !== undefined && !sameEndpoint(existing, endpoint)) {
|
|
@@ -8244,7 +8369,7 @@ async function composeUp(ctx, services) {
|
|
|
8244
8369
|
async function composeDown(ctx, destroy) {
|
|
8245
8370
|
const rest = ["down", "--remove-orphans"];
|
|
8246
8371
|
if (destroy)
|
|
8247
|
-
rest.push("-v");
|
|
8372
|
+
rest.push("-v", "--rmi", "local");
|
|
8248
8373
|
await docker(ctx, rest);
|
|
8249
8374
|
}
|
|
8250
8375
|
async function composePs(ctx) {
|
|
@@ -8373,7 +8498,7 @@ function allocatePort() {
|
|
|
8373
8498
|
});
|
|
8374
8499
|
});
|
|
8375
8500
|
}
|
|
8376
|
-
function
|
|
8501
|
+
function tryPsSnapshot() {
|
|
8377
8502
|
try {
|
|
8378
8503
|
const out = execFileSync2("ps", ["-A", "-o", "pid=,pgid=,ppid="], {
|
|
8379
8504
|
encoding: "utf8",
|
|
@@ -8388,11 +8513,10 @@ function psSnapshot() {
|
|
|
8388
8513
|
}
|
|
8389
8514
|
return rows;
|
|
8390
8515
|
} catch {
|
|
8391
|
-
return
|
|
8516
|
+
return null;
|
|
8392
8517
|
}
|
|
8393
8518
|
}
|
|
8394
|
-
function
|
|
8395
|
-
const rows = psSnapshot();
|
|
8519
|
+
function processTreeFromSnapshot(rootPid, rows) {
|
|
8396
8520
|
const byParent = new Map;
|
|
8397
8521
|
for (const r of rows) {
|
|
8398
8522
|
const list = byParent.get(r.ppid) ?? [];
|
|
@@ -8415,6 +8539,9 @@ function processTree(rootPid) {
|
|
|
8415
8539
|
}
|
|
8416
8540
|
return tree;
|
|
8417
8541
|
}
|
|
8542
|
+
function processTree(rootPid) {
|
|
8543
|
+
return processTreeFromSnapshot(rootPid, tryPsSnapshot() ?? []);
|
|
8544
|
+
}
|
|
8418
8545
|
function parseLsof(out) {
|
|
8419
8546
|
const listeners = [];
|
|
8420
8547
|
let pid = 0;
|
|
@@ -8450,12 +8577,12 @@ async function allListeners() {
|
|
|
8450
8577
|
return parseLsof(stdout);
|
|
8451
8578
|
} catch (err) {
|
|
8452
8579
|
const e = err;
|
|
8453
|
-
if (typeof e.stdout === "string" && e.code
|
|
8580
|
+
if (typeof e.stdout === "string" && e.code === 1 && (e.stderr ?? "").trim() === "") {
|
|
8454
8581
|
tool = "lsof";
|
|
8455
8582
|
return parseLsof(e.stdout);
|
|
8456
8583
|
}
|
|
8457
|
-
if (
|
|
8458
|
-
|
|
8584
|
+
if (e.code !== "ENOENT")
|
|
8585
|
+
throw err;
|
|
8459
8586
|
}
|
|
8460
8587
|
}
|
|
8461
8588
|
try {
|
|
@@ -8464,16 +8591,24 @@ async function allListeners() {
|
|
|
8464
8591
|
});
|
|
8465
8592
|
tool = "ss";
|
|
8466
8593
|
return parseSs(stdout);
|
|
8467
|
-
} catch {
|
|
8594
|
+
} catch (error) {
|
|
8595
|
+
if (error.code !== "ENOENT")
|
|
8596
|
+
throw error;
|
|
8468
8597
|
throw new HestiaError("ownership-tool-missing", "neither lsof nor ss is available \u2014 cannot verify port ownership " + "(required: tools like next dev silently bind a different port when " + "theirs is taken, so a bare listen-check would report the wrong port healthy)");
|
|
8469
8598
|
}
|
|
8470
8599
|
}
|
|
8471
8600
|
async function inspectPort(rootPid, port) {
|
|
8472
|
-
const
|
|
8601
|
+
const firstSnapshot = tryPsSnapshot();
|
|
8602
|
+
if (firstSnapshot === null)
|
|
8603
|
+
throw new Error("Port ownership process-tree inspection failed: ps unavailable");
|
|
8604
|
+
const members = new Set(processTreeFromSnapshot(rootPid, firstSnapshot).map((r) => r.pid));
|
|
8473
8605
|
const listeners = await allListeners();
|
|
8474
8606
|
const owner = listeners.find((l) => l.port === port);
|
|
8475
8607
|
if (owner !== undefined && !members.has(owner.pid)) {
|
|
8476
|
-
|
|
8608
|
+
const secondSnapshot = tryPsSnapshot();
|
|
8609
|
+
if (secondSnapshot === null)
|
|
8610
|
+
throw new Error("Port ownership process-tree recheck failed: ps unavailable");
|
|
8611
|
+
for (const member of processTreeFromSnapshot(rootPid, secondSnapshot))
|
|
8477
8612
|
members.add(member.pid);
|
|
8478
8613
|
}
|
|
8479
8614
|
return {
|
|
@@ -8543,19 +8678,32 @@ var init_shutdown = __esm(() => {
|
|
|
8543
8678
|
|
|
8544
8679
|
// packages/engine/src/proc/resolver.ts
|
|
8545
8680
|
import { existsSync as existsSync6 } from "fs";
|
|
8546
|
-
import { join as join8 } from "path";
|
|
8547
|
-
function detectVarlock(worktreeRoot) {
|
|
8548
|
-
const
|
|
8549
|
-
const
|
|
8550
|
-
|
|
8681
|
+
import { dirname as dirname3, join as join8, resolve as resolve2 } from "path";
|
|
8682
|
+
function detectVarlock(worktreeRoot, workingDirectory = worktreeRoot) {
|
|
8683
|
+
const root = resolve2(worktreeRoot);
|
|
8684
|
+
const cwd = resolve2(workingDirectory);
|
|
8685
|
+
if (!existsSync6(join8(cwd, ".env.schema")))
|
|
8686
|
+
return null;
|
|
8687
|
+
let directory = cwd;
|
|
8688
|
+
for (;; ) {
|
|
8689
|
+
const bin = join8(directory, "node_modules", ".bin", "varlock");
|
|
8690
|
+
if (existsSync6(bin))
|
|
8691
|
+
return bin;
|
|
8692
|
+
if (directory === root)
|
|
8693
|
+
return null;
|
|
8694
|
+
const parent = dirname3(directory);
|
|
8695
|
+
if (parent === directory || !parent.startsWith(root))
|
|
8696
|
+
return null;
|
|
8697
|
+
directory = parent;
|
|
8698
|
+
}
|
|
8551
8699
|
}
|
|
8552
8700
|
function wrapWithVarlock(bin, argv) {
|
|
8553
8701
|
return [bin, "run", "--no-redact-stdout", "--", ...argv];
|
|
8554
8702
|
}
|
|
8555
|
-
function requireVarlock(worktreeRoot) {
|
|
8556
|
-
const bin = detectVarlock(worktreeRoot);
|
|
8703
|
+
function requireVarlock(worktreeRoot, workingDirectory = worktreeRoot) {
|
|
8704
|
+
const bin = detectVarlock(worktreeRoot, workingDirectory);
|
|
8557
8705
|
if (bin === null) {
|
|
8558
|
-
throw new HestiaError("varlock-missing", `--varlock requires
|
|
8706
|
+
throw new HestiaError("varlock-missing", `--varlock requires ${join8(workingDirectory, ".env.schema")} and a local ` + `node_modules/.bin/varlock between that directory and the worktree root`);
|
|
8559
8707
|
}
|
|
8560
8708
|
return bin;
|
|
8561
8709
|
}
|
|
@@ -8582,7 +8730,7 @@ function spawnOnce(argv, cwd, env, logPath, attempt) {
|
|
|
8582
8730
|
closeSync2(openProcAttemptLog(logPath, attempt));
|
|
8583
8731
|
const relaySpec = Buffer.from(JSON.stringify({ argv, cwd, env, logPath })).toString("base64");
|
|
8584
8732
|
const relayFd = openSync2(logPath, "a", 384);
|
|
8585
|
-
return new Promise((
|
|
8733
|
+
return new Promise((resolve3, reject) => {
|
|
8586
8734
|
const child = spawn(process.execPath, [PROC_RELAY_ENTRYPOINT], {
|
|
8587
8735
|
cwd,
|
|
8588
8736
|
env: { ...process.env, [RELAY_SPEC_ENV]: relaySpec },
|
|
@@ -8596,7 +8744,7 @@ function spawnOnce(argv, cwd, env, logPath, attempt) {
|
|
|
8596
8744
|
child.once("spawn", () => {
|
|
8597
8745
|
closeSync2(relayFd);
|
|
8598
8746
|
child.unref();
|
|
8599
|
-
|
|
8747
|
+
resolve3(child.pid);
|
|
8600
8748
|
});
|
|
8601
8749
|
});
|
|
8602
8750
|
}
|
|
@@ -8613,18 +8761,22 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
8613
8761
|
const cwd = spec.cwd ? join9(worktreeRoot, spec.cwd) : worktreeRoot;
|
|
8614
8762
|
const wantPort = spec.port !== "none";
|
|
8615
8763
|
const readyTimeoutMs = spec.readyTimeoutMs ?? READY_TIMEOUT_MS;
|
|
8616
|
-
const varlockBin = spec.varlock ? requireVarlock(worktreeRoot) : null;
|
|
8764
|
+
const varlockBin = spec.varlock ? requireVarlock(worktreeRoot, cwd) : null;
|
|
8617
8765
|
let lastFailure = "";
|
|
8618
8766
|
for (let attempt = 1;attempt <= MAX_ATTEMPTS; attempt++) {
|
|
8619
8767
|
const port = wantPort ? await allocatePort() : undefined;
|
|
8620
8768
|
let argv = port === undefined ? spec.argv : substitutePort(spec.argv, port);
|
|
8621
8769
|
if (varlockBin !== null)
|
|
8622
8770
|
argv = wrapWithVarlock(varlockBin, argv);
|
|
8771
|
+
const resolvedSpecEnvironment = port === undefined ? spec.env : Object.fromEntries(Object.entries(spec.env ?? {}).map(([name, value]) => [
|
|
8772
|
+
name,
|
|
8773
|
+
substitutePort([value], port)[0]
|
|
8774
|
+
]));
|
|
8623
8775
|
const env = {
|
|
8624
8776
|
...process.env,
|
|
8625
8777
|
...stackEnv,
|
|
8626
8778
|
...port !== undefined ? { PORT: String(port), [`HESTIA_${envKey(spec.name)}_PORT`]: String(port) } : {},
|
|
8627
|
-
...
|
|
8779
|
+
...resolvedSpecEnvironment
|
|
8628
8780
|
};
|
|
8629
8781
|
const pid = await spawnOnce(argv, cwd, env, logPath, attempt);
|
|
8630
8782
|
const startTime = startTimeOf(pid) ?? "";
|
|
@@ -8644,7 +8796,7 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
8644
8796
|
};
|
|
8645
8797
|
writePidfile(worktreeRoot, pf);
|
|
8646
8798
|
mirrorPidfile2?.(pf);
|
|
8647
|
-
const outcome = await waitUntilReady(pf, port, readyTimeoutMs);
|
|
8799
|
+
const outcome = await waitUntilReady(pf, port, readyTimeoutMs, spec.healthPath);
|
|
8648
8800
|
const record = {
|
|
8649
8801
|
name: spec.name,
|
|
8650
8802
|
backend: pf.backend,
|
|
@@ -8680,7 +8832,7 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
8680
8832
|
continue;
|
|
8681
8833
|
case "timeout": {
|
|
8682
8834
|
record.state = "unhealthy";
|
|
8683
|
-
const bound = outcome.memberPorts.length > 0 ? `it is listening on port(s) ${outcome.memberPorts.join(", ")} instead` : `it never opened a listening socket (if it isn't a server, use --no-port)`;
|
|
8835
|
+
const bound = outcome.healthPending ? `it owned port ${port}, but ${spec.healthPath} never returned a 2xx response` : outcome.memberPorts.length > 0 ? `it is listening on port(s) ${outcome.memberPorts.join(", ")} instead` : `it never opened a listening socket (if it isn't a server, use --no-port)`;
|
|
8684
8836
|
return {
|
|
8685
8837
|
record,
|
|
8686
8838
|
pidfile: pf,
|
|
@@ -8691,26 +8843,41 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
8691
8843
|
}
|
|
8692
8844
|
throw new HestiaError("port-allocation-failed", `could not hold a port for "${spec.name}" after ${MAX_ATTEMPTS} attempts (${lastFailure})`);
|
|
8693
8845
|
}
|
|
8694
|
-
async function waitUntilReady(pf, port, timeoutMs) {
|
|
8846
|
+
async function waitUntilReady(pf, port, timeoutMs, healthPath) {
|
|
8695
8847
|
const deadline = Date.now() + timeoutMs;
|
|
8696
8848
|
if (port === undefined) {
|
|
8697
8849
|
await new Promise((r) => setTimeout(r, NO_PORT_GRACE_MS));
|
|
8698
8850
|
return isLive(pf) ? { kind: "ready" } : { kind: "exited" };
|
|
8699
8851
|
}
|
|
8700
8852
|
let memberPorts = [];
|
|
8853
|
+
let ownershipProven = false;
|
|
8701
8854
|
while (Date.now() < deadline) {
|
|
8702
8855
|
if (!isLive(pf))
|
|
8703
8856
|
return { kind: "exited" };
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8857
|
+
if (!ownershipProven) {
|
|
8858
|
+
const view = await inspectPort(pf.pgid, port);
|
|
8859
|
+
memberPorts = view.memberPorts;
|
|
8860
|
+
if (view.ownerIsMember)
|
|
8861
|
+
ownershipProven = true;
|
|
8862
|
+
else if (view.owner !== undefined)
|
|
8863
|
+
return { kind: "stolen", byPid: view.owner.pid };
|
|
8864
|
+
}
|
|
8865
|
+
if (ownershipProven) {
|
|
8866
|
+
if (healthPath === undefined)
|
|
8867
|
+
return { kind: "ready" };
|
|
8868
|
+
try {
|
|
8869
|
+
const response = await fetch(`http://127.0.0.1:${port}${healthPath}`);
|
|
8870
|
+
if (response.ok)
|
|
8871
|
+
return { kind: "ready" };
|
|
8872
|
+
} catch {}
|
|
8710
8873
|
}
|
|
8711
8874
|
await new Promise((r) => setTimeout(r, POLL_MS2));
|
|
8712
8875
|
}
|
|
8713
|
-
return {
|
|
8876
|
+
return {
|
|
8877
|
+
kind: "timeout",
|
|
8878
|
+
memberPorts,
|
|
8879
|
+
healthPending: healthPath !== undefined && ownershipProven
|
|
8880
|
+
};
|
|
8714
8881
|
}
|
|
8715
8882
|
var READY_TIMEOUT_MS = 60000, NO_PORT_GRACE_MS = 2000, POLL_MS2 = 300, MAX_ATTEMPTS = 3, PROC_RESTART_SENTINEL = `--- hestia: proc restarted (port stolen) ---
|
|
8716
8883
|
`, RELAY_SPEC_ENV = "HESTIA_PROC_RELAY_SPEC", PROC_RELAY_ENTRYPOINT, PORT_TOKEN = "{port}", ESCAPED = "{{port}}", SENTINEL = "\x00";
|
|
@@ -9106,12 +9273,12 @@ class SlotLedger {
|
|
|
9106
9273
|
return { live, reserved, warnings };
|
|
9107
9274
|
}
|
|
9108
9275
|
}
|
|
9109
|
-
var DEFAULT_MAX_STACKS = 5, RESERVATION_TTL_MS = 60000, DOCKER_PROBE_TIMEOUT_MS = 5000, dockerProbe = (project) => new Promise((
|
|
9276
|
+
var DEFAULT_MAX_STACKS = 5, RESERVATION_TTL_MS = 60000, DOCKER_PROBE_TIMEOUT_MS = 5000, dockerProbe = (project) => new Promise((resolve3) => {
|
|
9110
9277
|
execFile5("docker", ["ps", "-q", "--filter", `label=${LABELS.stack}=${project}`], { timeout: DOCKER_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
9111
9278
|
if (err)
|
|
9112
|
-
|
|
9279
|
+
resolve3(null);
|
|
9113
9280
|
else
|
|
9114
|
-
|
|
9281
|
+
resolve3(stdout.trim() === "" ? "dead" : "live");
|
|
9115
9282
|
});
|
|
9116
9283
|
});
|
|
9117
9284
|
var init_slots = __esm(() => {
|
|
@@ -9312,7 +9479,7 @@ var init_client = __esm(() => {
|
|
|
9312
9479
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync6 } from "fs";
|
|
9313
9480
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9314
9481
|
import { homedir as homedir2 } from "os";
|
|
9315
|
-
import { dirname as
|
|
9482
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
9316
9483
|
import { fileURLToPath } from "url";
|
|
9317
9484
|
function launchAgentsDir() {
|
|
9318
9485
|
return process.env.HESTIA_LAUNCHD_DIR ?? join13(homedir2(), "Library", "LaunchAgents");
|
|
@@ -9321,7 +9488,7 @@ function plistPath() {
|
|
|
9321
9488
|
return join13(launchAgentsDir(), `${LAUNCHD_LABEL}.plist`);
|
|
9322
9489
|
}
|
|
9323
9490
|
function daemonMainPath() {
|
|
9324
|
-
const bundled = join13(
|
|
9491
|
+
const bundled = join13(dirname4(fileURLToPath(import.meta.url)), "daemon.js");
|
|
9325
9492
|
if (existsSync11(bundled))
|
|
9326
9493
|
return bundled;
|
|
9327
9494
|
return fileURLToPath(new URL("./main.ts", import.meta.url));
|
|
@@ -9424,7 +9591,7 @@ function installLaunchd(opts) {
|
|
|
9424
9591
|
return { plist, warnings };
|
|
9425
9592
|
}
|
|
9426
9593
|
mkdirSync6(launchAgentsDir(), { recursive: true });
|
|
9427
|
-
mkdirSync6(
|
|
9594
|
+
mkdirSync6(dirname4(input.logPath), { recursive: true });
|
|
9428
9595
|
const target = plistPath();
|
|
9429
9596
|
const previous = existsSync11(target) ? readFileSync11(target, "utf8") : null;
|
|
9430
9597
|
const previouslyBootstrapped = isBootstrapped();
|
|
@@ -9510,14 +9677,14 @@ class Admission {
|
|
|
9510
9677
|
const first = await this.#locked(() => this.#try(identity2, holder));
|
|
9511
9678
|
if (first.granted || waitMs <= 0)
|
|
9512
9679
|
return first;
|
|
9513
|
-
return new Promise((
|
|
9680
|
+
return new Promise((resolve3) => {
|
|
9514
9681
|
const waiter = {
|
|
9515
9682
|
identity: identity2,
|
|
9516
9683
|
holder,
|
|
9517
|
-
resolve:
|
|
9684
|
+
resolve: resolve3,
|
|
9518
9685
|
timer: setTimeout(() => {
|
|
9519
9686
|
this.#queue = this.#queue.filter((candidate) => candidate !== waiter);
|
|
9520
|
-
|
|
9687
|
+
resolve3({ granted: false, live: first.live });
|
|
9521
9688
|
}, waitMs)
|
|
9522
9689
|
};
|
|
9523
9690
|
this.#queue.push(waiter);
|
|
@@ -9579,11 +9746,11 @@ class Admission {
|
|
|
9579
9746
|
return { granted: false, live: occupancy.live };
|
|
9580
9747
|
}
|
|
9581
9748
|
async#pump() {
|
|
9582
|
-
if (this.#queue.length === 0)
|
|
9583
|
-
return;
|
|
9584
9749
|
const { maxStacks } = resolveMaxStacks();
|
|
9585
9750
|
const occupancy = await this.ledger.occupancy();
|
|
9586
9751
|
this.#cache(occupancy);
|
|
9752
|
+
if (this.#queue.length === 0)
|
|
9753
|
+
return;
|
|
9587
9754
|
let used = occupancy.live.length + occupancy.reserved.length;
|
|
9588
9755
|
const granted = [];
|
|
9589
9756
|
for (const waiter of this.#queue) {
|
|
@@ -9621,7 +9788,7 @@ class Admission {
|
|
|
9621
9788
|
};
|
|
9622
9789
|
}
|
|
9623
9790
|
}
|
|
9624
|
-
var HESTIAD_PROTOCOL_VERSION =
|
|
9791
|
+
var HESTIAD_PROTOCOL_VERSION = 6, MAX_JSON_BODY_BYTES;
|
|
9625
9792
|
var init_routes = __esm(() => {
|
|
9626
9793
|
init_src();
|
|
9627
9794
|
init_state();
|
|
@@ -9758,13 +9925,13 @@ function portlessPayloadProvenanceIsCurrent(value) {
|
|
|
9758
9925
|
const provenance = value;
|
|
9759
9926
|
return provenance.package === "portless" && provenance.version === HESTIA_PORTLESS_VERSION && provenance.patchSha256 === HESTIA_PORTLESS_PATCH_SHA256;
|
|
9760
9927
|
}
|
|
9761
|
-
var HESTIA_PORTLESS_VERSION = "0.15.1", HESTIA_PORTLESS_PATCH_SHA256 = "
|
|
9928
|
+
var HESTIA_PORTLESS_VERSION = "0.15.1", HESTIA_PORTLESS_PATCH_SHA256 = "7d969a7e8bc556d0e866accc836b06e237f567b68d3d7fb7eaed5b0378fbc2d6";
|
|
9762
9929
|
|
|
9763
9930
|
// packages/engine/src/router/portless-adapter.ts
|
|
9764
9931
|
import { execFile as execFile6, spawnSync } from "child_process";
|
|
9765
9932
|
import { existsSync as existsSync12, readFileSync as readFileSync12, rmSync as rmSync7, statSync } from "fs";
|
|
9766
9933
|
import { promisify as promisify5 } from "util";
|
|
9767
|
-
import { dirname as
|
|
9934
|
+
import { dirname as dirname5, join as join15 } from "path";
|
|
9768
9935
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9769
9936
|
import { createConnection } from "net";
|
|
9770
9937
|
import { tmpdir } from "os";
|
|
@@ -9775,12 +9942,12 @@ function hestiaPortlessAliasesPath() {
|
|
|
9775
9942
|
return join15(hestiaHome(), "router", "portless", "aliases.json");
|
|
9776
9943
|
}
|
|
9777
9944
|
function portlessCliPath() {
|
|
9778
|
-
const moduleDirectory =
|
|
9945
|
+
const moduleDirectory = dirname5(fileURLToPath2(import.meta.url));
|
|
9779
9946
|
const bundled = join15(moduleDirectory, "assets", "portless", "dist", "cli.js");
|
|
9780
|
-
if (existsSync12(bundled) && portlessPayloadRootIsCurrent(
|
|
9947
|
+
if (existsSync12(bundled) && portlessPayloadRootIsCurrent(dirname5(dirname5(bundled))))
|
|
9781
9948
|
return bundled;
|
|
9782
9949
|
const workspaceBuild = join15(moduleDirectory, "..", "..", "..", "..", "dist", "assets", "portless", "dist", "cli.js");
|
|
9783
|
-
if (existsSync12(workspaceBuild) && portlessPayloadRootIsCurrent(
|
|
9950
|
+
if (existsSync12(workspaceBuild) && portlessPayloadRootIsCurrent(dirname5(dirname5(workspaceBuild)))) {
|
|
9784
9951
|
return workspaceBuild;
|
|
9785
9952
|
}
|
|
9786
9953
|
throw new HestiaError("router-version-unsupported", "current hardened Portless payload is unavailable; run bun run build and retry");
|
|
@@ -9904,11 +10071,11 @@ async function runRootCommand(command, args, interactive, optional = false) {
|
|
|
9904
10071
|
}
|
|
9905
10072
|
}
|
|
9906
10073
|
async function prepareHardenedPortlessPayloadAt(installDir, interactive) {
|
|
9907
|
-
const packageRoot =
|
|
10074
|
+
const packageRoot = dirname5(dirname5(portlessCliPath()));
|
|
9908
10075
|
const portlessDir = join15(installDir, "portless");
|
|
9909
10076
|
const bunPath = join15(installDir, "bun");
|
|
9910
|
-
const provenanceSource = join15(
|
|
9911
|
-
ensureDir(
|
|
10077
|
+
const provenanceSource = join15(dirname5(hestiaPortlessAliasesPath()), "payload-provenance.json");
|
|
10078
|
+
ensureDir(dirname5(provenanceSource));
|
|
9912
10079
|
writeAtomicJsonFile(provenanceSource, expectedPortlessPayloadProvenance());
|
|
9913
10080
|
await runRootCommand("mkdir", ["-p", installDir], interactive);
|
|
9914
10081
|
await runRootCommand("rm", ["-rf", portlessDir], interactive);
|
|
@@ -9942,7 +10109,7 @@ async function rollbackHardenedPortlessInstall(interactive) {
|
|
|
9942
10109
|
await removeHestiaPortlessCa(interactive);
|
|
9943
10110
|
} catch {}
|
|
9944
10111
|
await runRootCommand("rm", ["-rf", HESTIA_ROUTER_INSTALL_DIR], interactive, true);
|
|
9945
|
-
rmSync7(
|
|
10112
|
+
rmSync7(dirname5(hestiaPortlessAliasesPath()), { recursive: true, force: true });
|
|
9946
10113
|
}
|
|
9947
10114
|
async function trustHestiaPortlessCa(interactive) {
|
|
9948
10115
|
await runRootCommand("env", [
|
|
@@ -10008,11 +10175,11 @@ function requireNonInteractivePrivilege() {
|
|
|
10008
10175
|
}
|
|
10009
10176
|
}
|
|
10010
10177
|
async function loopbackPortIsBusy(port) {
|
|
10011
|
-
return new Promise((
|
|
10178
|
+
return new Promise((resolve3) => {
|
|
10012
10179
|
const socket = createConnection({ host: "127.0.0.1", port });
|
|
10013
10180
|
const finish = (busy) => {
|
|
10014
10181
|
socket.destroy();
|
|
10015
|
-
|
|
10182
|
+
resolve3(busy);
|
|
10016
10183
|
};
|
|
10017
10184
|
socket.setTimeout(500, () => finish(false));
|
|
10018
10185
|
socket.once("connect", () => finish(true));
|
|
@@ -10023,7 +10190,7 @@ async function waitForInstalledHestiaRouter(timeoutMs = 1e4) {
|
|
|
10023
10190
|
const deadline = Date.now() + timeoutMs;
|
|
10024
10191
|
let status = await readHestiaRouterStatus();
|
|
10025
10192
|
while (Date.now() < deadline && !(status.installed && status.running && status.trusted)) {
|
|
10026
|
-
await new Promise((
|
|
10193
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
10027
10194
|
status = await readHestiaRouterStatus();
|
|
10028
10195
|
}
|
|
10029
10196
|
if (!(status.installed && status.running && status.trusted)) {
|
|
@@ -10035,7 +10202,7 @@ async function waitForRunningHestiaRouter(timeoutMs = 1e4) {
|
|
|
10035
10202
|
const deadline = Date.now() + timeoutMs;
|
|
10036
10203
|
let status = await readHestiaRouterStatus();
|
|
10037
10204
|
while (Date.now() < deadline && !(status.installed && status.running)) {
|
|
10038
|
-
await new Promise((
|
|
10205
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
10039
10206
|
status = await readHestiaRouterStatus();
|
|
10040
10207
|
}
|
|
10041
10208
|
if (!(status.installed && status.running)) {
|
|
@@ -10147,7 +10314,7 @@ async function installHestiaRouter(interactive) {
|
|
|
10147
10314
|
} else {
|
|
10148
10315
|
requireNonInteractivePrivilege();
|
|
10149
10316
|
}
|
|
10150
|
-
ensureDir(
|
|
10317
|
+
ensureDir(dirname5(hestiaPortlessAliasesPath()));
|
|
10151
10318
|
if (!existsSync12(hestiaPortlessAliasesPath())) {
|
|
10152
10319
|
writeAtomicJsonFile(hestiaPortlessAliasesPath(), []);
|
|
10153
10320
|
}
|
|
@@ -10182,7 +10349,7 @@ async function uninstallHestiaRouter(interactive) {
|
|
|
10182
10349
|
if (ownership === "hestia") {
|
|
10183
10350
|
await runRootCommand("rm", ["-rf", HESTIA_ROUTER_INSTALL_DIR], interactive);
|
|
10184
10351
|
}
|
|
10185
|
-
rmSync7(
|
|
10352
|
+
rmSync7(dirname5(hestiaPortlessAliasesPath()), { recursive: true, force: true });
|
|
10186
10353
|
} catch (error) {
|
|
10187
10354
|
const detail = error.stderr ?? error.message;
|
|
10188
10355
|
throw new HestiaError("router-unreachable", `Router uninstall failed: ${detail.trim()}`);
|
|
@@ -10354,11 +10521,93 @@ var init_shared = __esm(() => {
|
|
|
10354
10521
|
PATH_SEGMENT_RE = /^[A-Za-z0-9._~!$&'()*+,;=:@%-]+$/;
|
|
10355
10522
|
});
|
|
10356
10523
|
|
|
10357
|
-
// packages/engine/src/router/
|
|
10524
|
+
// packages/engine/src/router/origin-liveness.ts
|
|
10358
10525
|
import { execFile as execFile7 } from "child_process";
|
|
10526
|
+
import { promisify as promisify6 } from "util";
|
|
10527
|
+
async function probeDockerOrigin(target, port) {
|
|
10528
|
+
try {
|
|
10529
|
+
const service = target.service;
|
|
10530
|
+
const args = [
|
|
10531
|
+
"ps",
|
|
10532
|
+
"--no-trunc",
|
|
10533
|
+
"--format",
|
|
10534
|
+
"{{.ID}}\t{{.Ports}}",
|
|
10535
|
+
"--filter",
|
|
10536
|
+
`label=dev.hestia.stack=${target.project}`,
|
|
10537
|
+
"--filter",
|
|
10538
|
+
`label=com.docker.compose.service=${service.name}`
|
|
10539
|
+
];
|
|
10540
|
+
const { stdout } = await pexec6("docker", args, { timeout: 2000 });
|
|
10541
|
+
const matches = stdout.split(`
|
|
10542
|
+
`).some((line) => {
|
|
10543
|
+
const [id, ports = ""] = line.split("\t");
|
|
10544
|
+
const identityMatches = service.containerId === undefined || id?.startsWith(service.containerId);
|
|
10545
|
+
return identityMatches && new RegExp(`(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0):${port}->`).test(ports);
|
|
10546
|
+
});
|
|
10547
|
+
return matches ? "live" : "dead";
|
|
10548
|
+
} catch {
|
|
10549
|
+
return "unknown";
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
async function probeProcOrigin(service, port) {
|
|
10553
|
+
if (service.pid === undefined || service.startTime === undefined)
|
|
10554
|
+
return "dead";
|
|
10555
|
+
if (!Number.isSafeInteger(service.pid) || service.pid <= 0 || typeof service.startTime !== "string") {
|
|
10556
|
+
throw new Error("Route origin has an invalid process identity");
|
|
10557
|
+
}
|
|
10558
|
+
const identity2 = probeProcessIdentity({ pid: service.pid, startTime: service.startTime });
|
|
10559
|
+
if (identity2 !== "live")
|
|
10560
|
+
return identity2;
|
|
10561
|
+
try {
|
|
10562
|
+
return (await inspectPort(service.pid, port)).ownerIsMember ? "live" : "dead";
|
|
10563
|
+
} catch {
|
|
10564
|
+
return "unknown";
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10567
|
+
async function probeLocalRouterTarget(target) {
|
|
10568
|
+
const service = target.service;
|
|
10569
|
+
const port = service?.publishedPort;
|
|
10570
|
+
if (service === undefined || port === undefined || service.backend === "tunnel")
|
|
10571
|
+
return "dead";
|
|
10572
|
+
return service.backend === "docker" ? await probeDockerOrigin(target, port) : await probeProcOrigin(service, port);
|
|
10573
|
+
}
|
|
10574
|
+
async function verifyLocalRouterTarget(target) {
|
|
10575
|
+
const port = target.service?.publishedPort;
|
|
10576
|
+
try {
|
|
10577
|
+
return port !== undefined && await probeLocalRouterTarget(target) === "live" ? port : null;
|
|
10578
|
+
} catch {
|
|
10579
|
+
return null;
|
|
10580
|
+
}
|
|
10581
|
+
}
|
|
10582
|
+
async function verifyStackServiceOrigin(record, service, publishedPort = service.publishedPort) {
|
|
10583
|
+
return await verifyLocalRouterTarget({
|
|
10584
|
+
project: record.project,
|
|
10585
|
+
service: { ...service, publishedPort }
|
|
10586
|
+
}) !== null;
|
|
10587
|
+
}
|
|
10588
|
+
function resolveSharedContractOrigin(mirror, contractService) {
|
|
10589
|
+
const endpoint = mirror.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === contractService);
|
|
10590
|
+
const service = mirror.services.find((candidate) => candidate.name === (endpoint?.workload ?? endpoint?.name));
|
|
10591
|
+
const binding = service?.bindings?.find((candidate) => `${candidate.target}/${candidate.protocol}` === endpoint?.binding);
|
|
10592
|
+
const publishedPort = binding?.publishedPort ?? service?.publishedPort;
|
|
10593
|
+
return service === undefined || service.backend === "tunnel" || publishedPort === undefined ? undefined : { ...service, publishedPort };
|
|
10594
|
+
}
|
|
10595
|
+
async function probeSharedHolderOrigin(mirror, contractService) {
|
|
10596
|
+
const service = resolveSharedContractOrigin(mirror, contractService);
|
|
10597
|
+
if (service === undefined)
|
|
10598
|
+
return "unknown";
|
|
10599
|
+
return await probeLocalRouterTarget({ project: mirror.project, service });
|
|
10600
|
+
}
|
|
10601
|
+
var pexec6;
|
|
10602
|
+
var init_origin_liveness = __esm(() => {
|
|
10603
|
+
init_pidfile();
|
|
10604
|
+
init_ports();
|
|
10605
|
+
pexec6 = promisify6(execFile7);
|
|
10606
|
+
});
|
|
10607
|
+
|
|
10608
|
+
// packages/engine/src/router/local-http-router.ts
|
|
10359
10609
|
import { Agent, createServer as createServer2, request as httpRequest } from "http";
|
|
10360
10610
|
import { createConnection as createConnection2, createServer as createNetServer } from "net";
|
|
10361
|
-
import { promisify as promisify6 } from "util";
|
|
10362
10611
|
import { existsSync as existsSync14, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
10363
10612
|
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as rmSync9 } from "fs";
|
|
10364
10613
|
import { tmpdir as tmpdir2 } from "os";
|
|
@@ -10430,55 +10679,6 @@ function forwardedHeaders(request, originPort) {
|
|
|
10430
10679
|
headers["x-forwarded-for"] = remote;
|
|
10431
10680
|
return headers;
|
|
10432
10681
|
}
|
|
10433
|
-
async function verifyDockerRouteTarget(target, port) {
|
|
10434
|
-
try {
|
|
10435
|
-
const service = target.service;
|
|
10436
|
-
const args = [
|
|
10437
|
-
"ps",
|
|
10438
|
-
"--no-trunc",
|
|
10439
|
-
"--format",
|
|
10440
|
-
"{{.ID}}\t{{.Ports}}",
|
|
10441
|
-
"--filter",
|
|
10442
|
-
`label=dev.hestia.stack=${target.project}`,
|
|
10443
|
-
"--filter",
|
|
10444
|
-
`label=com.docker.compose.service=${service.name}`
|
|
10445
|
-
];
|
|
10446
|
-
const { stdout } = await pexec6("docker", args, { timeout: 2000 });
|
|
10447
|
-
return stdout.split(`
|
|
10448
|
-
`).some((line) => {
|
|
10449
|
-
const [id, ports = ""] = line.split("\t");
|
|
10450
|
-
const identityMatches = service.containerId === undefined || id?.startsWith(service.containerId);
|
|
10451
|
-
return identityMatches && new RegExp(`(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0):${port}->`).test(ports);
|
|
10452
|
-
});
|
|
10453
|
-
} catch {
|
|
10454
|
-
return false;
|
|
10455
|
-
}
|
|
10456
|
-
}
|
|
10457
|
-
async function verifyLocalRouterTarget(target) {
|
|
10458
|
-
const service = target.service;
|
|
10459
|
-
const port = service?.publishedPort;
|
|
10460
|
-
if (service === undefined || port === undefined || service.backend === "tunnel")
|
|
10461
|
-
return null;
|
|
10462
|
-
if (service.backend === "docker") {
|
|
10463
|
-
return await verifyDockerRouteTarget(target, port) ? port : null;
|
|
10464
|
-
}
|
|
10465
|
-
if (service.pid === undefined || service.startTime === undefined)
|
|
10466
|
-
return null;
|
|
10467
|
-
if (!isLive({ pid: service.pid, startTime: service.startTime }))
|
|
10468
|
-
return null;
|
|
10469
|
-
try {
|
|
10470
|
-
return (await inspectPort(service.pid, port)).ownerIsMember ? port : null;
|
|
10471
|
-
} catch {
|
|
10472
|
-
return null;
|
|
10473
|
-
}
|
|
10474
|
-
}
|
|
10475
|
-
async function verifyStackServiceOrigin(record, service, publishedPort = service.publishedPort) {
|
|
10476
|
-
return await verifyLocalRouterTarget({
|
|
10477
|
-
hostname: "",
|
|
10478
|
-
project: record.project,
|
|
10479
|
-
service: { ...service, publishedPort }
|
|
10480
|
-
}) !== null;
|
|
10481
|
-
}
|
|
10482
10682
|
function targetIdentity(target) {
|
|
10483
10683
|
const service = target.service;
|
|
10484
10684
|
return [
|
|
@@ -10566,27 +10766,27 @@ class HestiaLocalHttpRouter {
|
|
|
10566
10766
|
#sharedRoutes = new Map;
|
|
10567
10767
|
#timer;
|
|
10568
10768
|
async start() {
|
|
10569
|
-
await new Promise((
|
|
10769
|
+
await new Promise((resolve3, reject) => {
|
|
10570
10770
|
this.#server.once("error", reject);
|
|
10571
10771
|
this.#server.listen(0, "127.0.0.1", () => {
|
|
10572
10772
|
this.#server.off("error", reject);
|
|
10573
|
-
|
|
10773
|
+
resolve3();
|
|
10574
10774
|
});
|
|
10575
10775
|
});
|
|
10576
10776
|
prepareGatewaySocket(this.socketPath);
|
|
10577
|
-
await new Promise((
|
|
10777
|
+
await new Promise((resolve3, reject) => {
|
|
10578
10778
|
this.#unixServer.once("error", reject);
|
|
10579
10779
|
this.#unixServer.listen(this.socketPath, () => {
|
|
10580
10780
|
this.#unixServer.off("error", reject);
|
|
10581
10781
|
chmodSync5(this.socketPath, 384);
|
|
10582
|
-
|
|
10782
|
+
resolve3();
|
|
10583
10783
|
});
|
|
10584
10784
|
});
|
|
10585
|
-
await new Promise((
|
|
10785
|
+
await new Promise((resolve3, reject) => {
|
|
10586
10786
|
this.#frontServer.once("error", reject);
|
|
10587
10787
|
this.#frontServer.listen(0, "127.0.0.1", () => {
|
|
10588
10788
|
this.#frontServer.off("error", reject);
|
|
10589
|
-
|
|
10789
|
+
resolve3();
|
|
10590
10790
|
});
|
|
10591
10791
|
});
|
|
10592
10792
|
await this.refreshRoutes();
|
|
@@ -10675,14 +10875,12 @@ class HestiaLocalHttpRouter {
|
|
|
10675
10875
|
let target;
|
|
10676
10876
|
if (holder !== undefined) {
|
|
10677
10877
|
const stack = records.find((record) => record.project === holder.project);
|
|
10678
|
-
const
|
|
10679
|
-
const service = stack?.services.find((candidate2) => candidate2.name === (endpoint?.workload ?? endpoint?.name));
|
|
10680
|
-
const binding = service?.bindings?.find((candidate2) => `${candidate2.target}/${candidate2.protocol}` === endpoint?.binding);
|
|
10681
|
-
const publishedPort = binding?.publishedPort ?? service?.publishedPort;
|
|
10878
|
+
const service = stack === undefined ? undefined : resolveSharedContractOrigin(stack, holder.service);
|
|
10682
10879
|
const candidate = {
|
|
10683
10880
|
hostname,
|
|
10684
10881
|
project: holder.project,
|
|
10685
|
-
|
|
10882
|
+
sharedHolder: holder.project,
|
|
10883
|
+
service
|
|
10686
10884
|
};
|
|
10687
10885
|
const previous = this.#sharedRoutes.get(hostname)?.find((entry) => entry.name === shared.name)?.target;
|
|
10688
10886
|
target = previous !== undefined && targetIdentity(previous) === targetIdentity(candidate) ? previous : candidate;
|
|
@@ -10784,7 +10982,10 @@ Connection: close\r
|
|
|
10784
10982
|
};
|
|
10785
10983
|
}
|
|
10786
10984
|
if (entry.target?.service?.publishedPort === undefined) {
|
|
10787
|
-
return {
|
|
10985
|
+
return {
|
|
10986
|
+
status: 503,
|
|
10987
|
+
message: `Hestia route origin unavailable \u2014 held by ${entry.target?.project ?? "unknown"}`
|
|
10988
|
+
};
|
|
10788
10989
|
}
|
|
10789
10990
|
return { target: entry.target };
|
|
10790
10991
|
}
|
|
@@ -10806,7 +11007,8 @@ Connection: close\r
|
|
|
10806
11007
|
const target = match.target;
|
|
10807
11008
|
const port = target.service.publishedPort;
|
|
10808
11009
|
if (await verifyLocalRouterTarget(target) !== port) {
|
|
10809
|
-
|
|
11010
|
+
const suffix = target.sharedHolder === undefined ? "" : ` \u2014 held by ${target.sharedHolder}`;
|
|
11011
|
+
return sendRouterResponse(response, 503, `Hestia route origin unavailable${suffix}`);
|
|
10810
11012
|
}
|
|
10811
11013
|
const proxy = httpRequest({
|
|
10812
11014
|
hostname: "127.0.0.1",
|
|
@@ -10828,7 +11030,8 @@ Connection: close\r
|
|
|
10828
11030
|
proxy.on("error", (error) => {
|
|
10829
11031
|
if (!response.headersSent) {
|
|
10830
11032
|
const ownershipFailure = error instanceof OriginOwnershipError;
|
|
10831
|
-
|
|
11033
|
+
const suffix = target.sharedHolder === undefined ? "" : ` \u2014 held by ${target.sharedHolder}`;
|
|
11034
|
+
sendRouterResponse(response, ownershipFailure ? 503 : 502, ownershipFailure ? `Hestia route origin unavailable${suffix}` : "Hestia route origin failed");
|
|
10832
11035
|
} else
|
|
10833
11036
|
response.destroy();
|
|
10834
11037
|
});
|
|
@@ -10905,16 +11108,14 @@ Connection: close\r
|
|
|
10905
11108
|
socket.once("error", () => originSocket.destroy());
|
|
10906
11109
|
}
|
|
10907
11110
|
}
|
|
10908
|
-
var
|
|
11111
|
+
var ROUTE_REFRESH_MS = 1000, MAX_MIRROR_BYTES, MAX_REQUEST_HEADER_BYTES, HOP_BY_HOP_HEADERS, OriginOwnershipError;
|
|
10909
11112
|
var init_local_http_router = __esm(() => {
|
|
10910
11113
|
init_src();
|
|
10911
|
-
init_pidfile();
|
|
10912
|
-
init_ports();
|
|
10913
11114
|
init_state();
|
|
10914
11115
|
init_router_config();
|
|
10915
11116
|
init_portless_adapter();
|
|
10916
11117
|
init_shared();
|
|
10917
|
-
|
|
11118
|
+
init_origin_liveness();
|
|
10918
11119
|
MAX_MIRROR_BYTES = 2 * 1024 * 1024;
|
|
10919
11120
|
MAX_REQUEST_HEADER_BYTES = 64 * 1024;
|
|
10920
11121
|
HOP_BY_HOP_HEADERS = new Set([
|
|
@@ -10931,31 +11132,73 @@ var init_local_http_router = __esm(() => {
|
|
|
10931
11132
|
};
|
|
10932
11133
|
});
|
|
10933
11134
|
|
|
11135
|
+
// packages/engine/src/configured-workload-environment.ts
|
|
11136
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
11137
|
+
import { join as join18 } from "path";
|
|
11138
|
+
function endpointTemplateValue(endpoint, field) {
|
|
11139
|
+
const value = endpoint[field];
|
|
11140
|
+
if (value === undefined) {
|
|
11141
|
+
throw new HestiaError("config-invalid", `endpoint template requests ${endpoint.name}.${field}, but that endpoint has no ${field}`);
|
|
11142
|
+
}
|
|
11143
|
+
return String(value);
|
|
11144
|
+
}
|
|
11145
|
+
function resolveEndpointTemplate(template, record) {
|
|
11146
|
+
const resolved = template.replace(ENDPOINT_TEMPLATE, (_match, endpointName, field) => {
|
|
11147
|
+
const endpoint = record.endpoints.find((candidate) => candidate.name === endpointName);
|
|
11148
|
+
if (endpoint === undefined) {
|
|
11149
|
+
throw new HestiaError("config-invalid", `endpoint template references ${JSON.stringify(endpointName)} before that endpoint is available`);
|
|
11150
|
+
}
|
|
11151
|
+
return endpointTemplateValue(endpoint, field);
|
|
11152
|
+
});
|
|
11153
|
+
if (resolved.includes("${endpoint:")) {
|
|
11154
|
+
throw new HestiaError("config-invalid", `malformed endpoint template ${JSON.stringify(template)}`);
|
|
11155
|
+
}
|
|
11156
|
+
return resolved;
|
|
11157
|
+
}
|
|
11158
|
+
function readIgnoredEnvironmentFile(worktreeRoot, file) {
|
|
11159
|
+
try {
|
|
11160
|
+
return readFileSync15(join18(worktreeRoot, file), "utf8").replace(/\r?\n$/, "");
|
|
11161
|
+
} catch (error) {
|
|
11162
|
+
throw new HestiaError("config-invalid", `cannot read configured environment file ${file}: ${error.message}`, { path: join18(worktreeRoot, file) });
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
function resolveConfiguredEnvironment(worktreeRoot, configured, record) {
|
|
11166
|
+
return Object.fromEntries(Object.entries(configured).map(([name, value]) => {
|
|
11167
|
+
const raw = typeof value === "string" ? value : readIgnoredEnvironmentFile(worktreeRoot, value.file);
|
|
11168
|
+
return [name, resolveEndpointTemplate(raw, record)];
|
|
11169
|
+
}));
|
|
11170
|
+
}
|
|
11171
|
+
var ENDPOINT_TEMPLATE;
|
|
11172
|
+
var init_configured_workload_environment = __esm(() => {
|
|
11173
|
+
init_src();
|
|
11174
|
+
ENDPOINT_TEMPLATE = /\$\{endpoint:([^}]+)\.(host|port|url)\}/g;
|
|
11175
|
+
});
|
|
11176
|
+
|
|
10934
11177
|
// packages/engine/src/wrangler/adapter.ts
|
|
10935
|
-
import { existsSync as existsSync15, readFileSync as
|
|
11178
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync2 } from "fs";
|
|
10936
11179
|
import { homedir as homedir3 } from "os";
|
|
10937
|
-
import { dirname as
|
|
11180
|
+
import { dirname as dirname6, join as join19, relative as relative3 } from "path";
|
|
10938
11181
|
import { connect } from "net";
|
|
10939
11182
|
function privateRegistryDir(worktreeRoot) {
|
|
10940
|
-
return
|
|
11183
|
+
return join19(worktreeRoot, ".hestia", "wrangler-registry");
|
|
10941
11184
|
}
|
|
10942
11185
|
function globalRegistryDir() {
|
|
10943
|
-
return process.env.WRANGLER_REGISTRY_PATH ??
|
|
11186
|
+
return process.env.WRANGLER_REGISTRY_PATH ?? join19(homedir3(), ".wrangler", "config", "registry");
|
|
10944
11187
|
}
|
|
10945
11188
|
function tcpConnectable(address, timeoutMs = 500) {
|
|
10946
11189
|
const [host, portStr] = address.split(":");
|
|
10947
11190
|
const port = Number(portStr);
|
|
10948
11191
|
if (!host || !Number.isFinite(port))
|
|
10949
11192
|
return Promise.resolve(false);
|
|
10950
|
-
return new Promise((
|
|
11193
|
+
return new Promise((resolve3) => {
|
|
10951
11194
|
const sock = connect({ host, port, timeout: timeoutMs });
|
|
10952
11195
|
sock.once("connect", () => {
|
|
10953
11196
|
sock.destroy();
|
|
10954
|
-
|
|
11197
|
+
resolve3(true);
|
|
10955
11198
|
});
|
|
10956
11199
|
const dead = () => {
|
|
10957
11200
|
sock.destroy();
|
|
10958
|
-
|
|
11201
|
+
resolve3(false);
|
|
10959
11202
|
};
|
|
10960
11203
|
sock.once("error", dead);
|
|
10961
11204
|
sock.once("timeout", dead);
|
|
@@ -10968,14 +11211,14 @@ async function preflightForeignSessions(workers) {
|
|
|
10968
11211
|
for (const w of workers) {
|
|
10969
11212
|
if (w.name === null)
|
|
10970
11213
|
continue;
|
|
10971
|
-
const entryPath =
|
|
11214
|
+
const entryPath = join19(dir, w.name);
|
|
10972
11215
|
if (!existsSync15(entryPath))
|
|
10973
11216
|
continue;
|
|
10974
11217
|
const freshMs = Date.now() - statSync2(entryPath).mtimeMs;
|
|
10975
11218
|
let live = freshMs < HEARTBEAT_FRESH_MS;
|
|
10976
11219
|
if (!live) {
|
|
10977
11220
|
try {
|
|
10978
|
-
const entry = JSON.parse(
|
|
11221
|
+
const entry = JSON.parse(readFileSync16(entryPath, "utf8"));
|
|
10979
11222
|
live = entry.debugPortAddress ? await tcpConnectable(entry.debugPortAddress) : false;
|
|
10980
11223
|
} catch {
|
|
10981
11224
|
live = false;
|
|
@@ -10996,14 +11239,14 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
10996
11239
|
throw new HestiaError("no-workers-found", opts.filter.length > 0 ? `no wrangler configs match --workers=${opts.filter.join(",")}` : `no wrangler.{jsonc,json,toml} found in ${worktreeRoot}`);
|
|
10997
11240
|
}
|
|
10998
11241
|
const wranglerBinFor = (configPath) => {
|
|
10999
|
-
let dir =
|
|
11242
|
+
let dir = dirname6(configPath);
|
|
11000
11243
|
for (;; ) {
|
|
11001
|
-
const candidate =
|
|
11244
|
+
const candidate = join19(dir, "node_modules", ".bin", "wrangler");
|
|
11002
11245
|
if (existsSync15(candidate))
|
|
11003
11246
|
return candidate;
|
|
11004
11247
|
if (dir === worktreeRoot)
|
|
11005
11248
|
return null;
|
|
11006
|
-
const parent =
|
|
11249
|
+
const parent = dirname6(dir);
|
|
11007
11250
|
if (parent === dir)
|
|
11008
11251
|
return null;
|
|
11009
11252
|
dir = parent;
|
|
@@ -11035,6 +11278,7 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11035
11278
|
throw new HestiaError("wrangler-missing", `no local wrangler for ${relative3(worktreeRoot, w.configPath)} ` + `(searched node_modules/.bin from the config dir up to the worktree ` + `root) \u2014 hestia never uses a global install`);
|
|
11036
11279
|
}
|
|
11037
11280
|
const inspectorPort = await allocatePort();
|
|
11281
|
+
const workerCwd = dirname6(w.configPath);
|
|
11038
11282
|
specs.push({
|
|
11039
11283
|
name: w.name,
|
|
11040
11284
|
argv: [
|
|
@@ -11052,9 +11296,10 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11052
11296
|
WRANGLER_REGISTRY_PATH: registry,
|
|
11053
11297
|
MINIFLARE_REGISTRY_PATH: registry
|
|
11054
11298
|
},
|
|
11299
|
+
cwd: relative3(worktreeRoot, workerCwd),
|
|
11055
11300
|
port: "auto",
|
|
11056
11301
|
signal: "int",
|
|
11057
|
-
varlock: opts.varlock,
|
|
11302
|
+
varlock: opts.varlock && detectVarlock(worktreeRoot, workerCwd) !== null,
|
|
11058
11303
|
backend: "wrangler",
|
|
11059
11304
|
inspectorPort,
|
|
11060
11305
|
configPath: w.configPath
|
|
@@ -11066,6 +11311,7 @@ var HEARTBEAT_FRESH_MS = 60000;
|
|
|
11066
11311
|
var init_adapter = __esm(() => {
|
|
11067
11312
|
init_src();
|
|
11068
11313
|
init_ports();
|
|
11314
|
+
init_resolver();
|
|
11069
11315
|
init_discover();
|
|
11070
11316
|
});
|
|
11071
11317
|
|
|
@@ -11105,12 +11351,12 @@ import { execFile as execFile8 } from "child_process";
|
|
|
11105
11351
|
import { promisify as promisify7 } from "util";
|
|
11106
11352
|
import { existsSync as existsSync17 } from "fs";
|
|
11107
11353
|
import { homedir as homedir4 } from "os";
|
|
11108
|
-
import { join as
|
|
11354
|
+
import { join as join20 } from "path";
|
|
11109
11355
|
function cloudflaredHome() {
|
|
11110
|
-
return process.env.HESTIA_CLOUDFLARED_HOME ??
|
|
11356
|
+
return process.env.HESTIA_CLOUDFLARED_HOME ?? join20(homedir4(), ".cloudflared");
|
|
11111
11357
|
}
|
|
11112
11358
|
function credFilePath(uuid) {
|
|
11113
|
-
return process.env.TUNNEL_CRED_FILE ??
|
|
11359
|
+
return process.env.TUNNEL_CRED_FILE ?? join20(cloudflaredHome(), `${uuid}.json`);
|
|
11114
11360
|
}
|
|
11115
11361
|
async function run(args, timeoutMs = 30000) {
|
|
11116
11362
|
try {
|
|
@@ -11157,8 +11403,8 @@ var init_cloudflared = __esm(() => {
|
|
|
11157
11403
|
|
|
11158
11404
|
// packages/engine/src/tunnel/ingress.ts
|
|
11159
11405
|
import { createHash as createHash6 } from "crypto";
|
|
11160
|
-
import { existsSync as existsSync18, readFileSync as
|
|
11161
|
-
import { join as
|
|
11406
|
+
import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
|
|
11407
|
+
import { join as join21 } from "path";
|
|
11162
11408
|
function hostnameFor(tunnelName, branch, service, zone) {
|
|
11163
11409
|
const label = `${slug(tunnelName)}-${slug(branch)}-${slug(service)}`;
|
|
11164
11410
|
if (label.length <= LABEL_MAX)
|
|
@@ -11176,12 +11422,12 @@ function inferZone(baseRules) {
|
|
|
11176
11422
|
return zones.size === 1 ? [...zones][0] : undefined;
|
|
11177
11423
|
}
|
|
11178
11424
|
function importBaseRules(uuid, name) {
|
|
11179
|
-
const p =
|
|
11425
|
+
const p = join21(cloudflaredHome(), "config.yml");
|
|
11180
11426
|
if (!existsSync18(p))
|
|
11181
11427
|
return [];
|
|
11182
11428
|
let parsed;
|
|
11183
11429
|
try {
|
|
11184
|
-
parsed = $parse(
|
|
11430
|
+
parsed = $parse(readFileSync17(p, "utf8"));
|
|
11185
11431
|
} catch {
|
|
11186
11432
|
return [];
|
|
11187
11433
|
}
|
|
@@ -11251,9 +11497,9 @@ var init_ingress = __esm(() => {
|
|
|
11251
11497
|
|
|
11252
11498
|
// packages/engine/src/tunnel/orphans.ts
|
|
11253
11499
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
11254
|
-
import { join as
|
|
11500
|
+
import { join as join22 } from "path";
|
|
11255
11501
|
function hestiaTunnelMarker(uuid) {
|
|
11256
|
-
return
|
|
11502
|
+
return join22(hestiaHome(), "tunnel", uuid);
|
|
11257
11503
|
}
|
|
11258
11504
|
function parseLocalHestiaConnectors(psOutput, marker) {
|
|
11259
11505
|
const rows = [];
|
|
@@ -11376,18 +11622,18 @@ var POLL_MS4 = 300;
|
|
|
11376
11622
|
import {
|
|
11377
11623
|
existsSync as existsSync19,
|
|
11378
11624
|
mkdirSync as mkdirSync8,
|
|
11379
|
-
readFileSync as
|
|
11625
|
+
readFileSync as readFileSync18,
|
|
11380
11626
|
readdirSync as readdirSync7
|
|
11381
11627
|
} from "fs";
|
|
11382
|
-
import { join as
|
|
11628
|
+
import { join as join23 } from "path";
|
|
11383
11629
|
function tunnelDir(uuid) {
|
|
11384
|
-
return
|
|
11630
|
+
return join23(hestiaHome(), "tunnel", uuid);
|
|
11385
11631
|
}
|
|
11386
11632
|
function configPath(uuid) {
|
|
11387
|
-
return
|
|
11633
|
+
return join23(tunnelDir(uuid), "config.yml");
|
|
11388
11634
|
}
|
|
11389
11635
|
function adoptedMarker(uuid) {
|
|
11390
|
-
return
|
|
11636
|
+
return join23(tunnelDir(uuid), "adopted.json");
|
|
11391
11637
|
}
|
|
11392
11638
|
function isAdopted(uuid) {
|
|
11393
11639
|
return existsSync19(adoptedMarker(uuid));
|
|
@@ -11398,7 +11644,7 @@ function readAdopted(uuid) {
|
|
|
11398
11644
|
return null;
|
|
11399
11645
|
let marker;
|
|
11400
11646
|
try {
|
|
11401
|
-
marker = JSON.parse(
|
|
11647
|
+
marker = JSON.parse(readFileSync18(p, "utf8"));
|
|
11402
11648
|
} catch {
|
|
11403
11649
|
marker = {};
|
|
11404
11650
|
}
|
|
@@ -11406,14 +11652,14 @@ function readAdopted(uuid) {
|
|
|
11406
11652
|
return { uuid, name: marker.name, credFile: marker.credFile, reconstructed: false };
|
|
11407
11653
|
}
|
|
11408
11654
|
let name;
|
|
11409
|
-
const stacksDir =
|
|
11655
|
+
const stacksDir = join23(hestiaHome(), "stacks");
|
|
11410
11656
|
if (existsSync19(stacksDir)) {
|
|
11411
11657
|
for (const project of readdirSync7(stacksDir)) {
|
|
11412
|
-
const sp =
|
|
11658
|
+
const sp = join23(stacksDir, project, "stack.json");
|
|
11413
11659
|
if (!existsSync19(sp))
|
|
11414
11660
|
continue;
|
|
11415
11661
|
try {
|
|
11416
|
-
const record = parseStackRecord(
|
|
11662
|
+
const record = parseStackRecord(readFileSync18(sp, "utf8"), sp);
|
|
11417
11663
|
if (record.tunnel?.uuid === uuid) {
|
|
11418
11664
|
name = record.tunnel.name;
|
|
11419
11665
|
break;
|
|
@@ -11429,24 +11675,24 @@ function readAdopted(uuid) {
|
|
|
11429
11675
|
};
|
|
11430
11676
|
}
|
|
11431
11677
|
function listAdopted() {
|
|
11432
|
-
const root =
|
|
11678
|
+
const root = join23(hestiaHome(), "tunnel");
|
|
11433
11679
|
if (!existsSync19(root))
|
|
11434
11680
|
return [];
|
|
11435
11681
|
return readdirSync7(root).filter((uuid) => isAdopted(uuid));
|
|
11436
11682
|
}
|
|
11437
11683
|
function collectDynamicRules(uuid) {
|
|
11438
|
-
const stacksDir =
|
|
11684
|
+
const stacksDir = join23(hestiaHome(), "stacks");
|
|
11439
11685
|
const rules = [];
|
|
11440
11686
|
const dropped = [];
|
|
11441
11687
|
if (!existsSync19(stacksDir))
|
|
11442
11688
|
return { rules, dropped };
|
|
11443
11689
|
for (const project of readdirSync7(stacksDir)) {
|
|
11444
|
-
const p =
|
|
11690
|
+
const p = join23(stacksDir, project, "stack.json");
|
|
11445
11691
|
if (!existsSync19(p))
|
|
11446
11692
|
continue;
|
|
11447
11693
|
let record;
|
|
11448
11694
|
try {
|
|
11449
|
-
record = parseStackRecord(
|
|
11695
|
+
record = parseStackRecord(readFileSync18(p, "utf8"), p);
|
|
11450
11696
|
} catch {
|
|
11451
11697
|
continue;
|
|
11452
11698
|
}
|
|
@@ -11490,7 +11736,7 @@ async function reconcileTunnel(ref, opts) {
|
|
|
11490
11736
|
if (reapedGroups > 0) {
|
|
11491
11737
|
warnings.push(`reaped ${reapedGroups} orphan hestia connector process group(s) for this tunnel`);
|
|
11492
11738
|
}
|
|
11493
|
-
const currentConfig = existsSync19(cfgPath) ?
|
|
11739
|
+
const currentConfig = existsSync19(cfgPath) ? readFileSync18(cfgPath, "utf8") : null;
|
|
11494
11740
|
if (live && currentConfig === nextConfig) {
|
|
11495
11741
|
return { restarted: false, metricsPort: pf.port, ready: true, warnings };
|
|
11496
11742
|
}
|
|
@@ -11657,7 +11903,7 @@ async function* composeLogLines(project, service, options = {}) {
|
|
|
11657
11903
|
args.push("--follow");
|
|
11658
11904
|
args.push(service);
|
|
11659
11905
|
const child = spawn3("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
11660
|
-
const completion = new Promise((
|
|
11906
|
+
const completion = new Promise((resolve3) => child.once("close", resolve3));
|
|
11661
11907
|
let stderr = "";
|
|
11662
11908
|
let spawnErrorMessage;
|
|
11663
11909
|
child.stderr.setEncoding("utf8");
|
|
@@ -11747,12 +11993,12 @@ function readLastLineEventsBeforeOffset(path, count, endOffset) {
|
|
|
11747
11993
|
function waitForLogPoll(signal) {
|
|
11748
11994
|
if (signal?.aborted)
|
|
11749
11995
|
return Promise.resolve();
|
|
11750
|
-
return new Promise((
|
|
11996
|
+
return new Promise((resolve3) => {
|
|
11751
11997
|
const timer = setTimeout(done, LOG_POLL_MS);
|
|
11752
11998
|
function done() {
|
|
11753
11999
|
clearTimeout(timer);
|
|
11754
12000
|
signal?.removeEventListener("abort", done);
|
|
11755
|
-
|
|
12001
|
+
resolve3();
|
|
11756
12002
|
}
|
|
11757
12003
|
signal?.addEventListener("abort", done, { once: true });
|
|
11758
12004
|
});
|
|
@@ -11862,10 +12108,10 @@ class BoundedLogMergeQueue {
|
|
|
11862
12108
|
}
|
|
11863
12109
|
async push(item, signal) {
|
|
11864
12110
|
while (!this.#closed && !signal?.aborted && this.#items.length >= this.capacity) {
|
|
11865
|
-
await new Promise((
|
|
12111
|
+
await new Promise((resolve3) => {
|
|
11866
12112
|
const done = () => {
|
|
11867
12113
|
signal?.removeEventListener("abort", done);
|
|
11868
|
-
|
|
12114
|
+
resolve3();
|
|
11869
12115
|
};
|
|
11870
12116
|
this.#capacityWaiters.push(done);
|
|
11871
12117
|
signal?.addEventListener("abort", done, { once: true });
|
|
@@ -11888,7 +12134,7 @@ class BoundedLogMergeQueue {
|
|
|
11888
12134
|
}
|
|
11889
12135
|
if (this.#closed)
|
|
11890
12136
|
return Promise.resolve({ done: true });
|
|
11891
|
-
return new Promise((
|
|
12137
|
+
return new Promise((resolve3) => this.#waiters.push(resolve3));
|
|
11892
12138
|
}
|
|
11893
12139
|
close() {
|
|
11894
12140
|
if (this.#closed)
|
|
@@ -12006,12 +12252,46 @@ var init_stream = __esm(() => {
|
|
|
12006
12252
|
});
|
|
12007
12253
|
|
|
12008
12254
|
// packages/engine/src/daemon/shared-arbiter.ts
|
|
12255
|
+
function probeMirrorProcessOccupancy(mirror) {
|
|
12256
|
+
const identities = [];
|
|
12257
|
+
let malformed = false;
|
|
12258
|
+
for (const service of [...mirror.services, ...mirror.auxiliary ?? []].filter((candidate) => candidate.backend === "proc" || candidate.backend === "wrangler")) {
|
|
12259
|
+
if (service.pid === undefined && service.startTime === undefined)
|
|
12260
|
+
continue;
|
|
12261
|
+
if (!Number.isSafeInteger(service.pid) || service.pid <= 0 || typeof service.startTime !== "string") {
|
|
12262
|
+
malformed = true;
|
|
12263
|
+
continue;
|
|
12264
|
+
}
|
|
12265
|
+
identities.push({ pid: service.pid, startTime: service.startTime });
|
|
12266
|
+
}
|
|
12267
|
+
const pidfileScan = scanPidfiles(mirrorProcsDir(mirror.project));
|
|
12268
|
+
malformed ||= pidfileScan.errors.length > 0;
|
|
12269
|
+
identities.push(...pidfileScan.pidfiles.filter((pidfile) => pidfile.backend !== "tunnel").map((pidfile) => ({ pid: pidfile.pid, startTime: pidfile.startTime })));
|
|
12270
|
+
let unknown = false;
|
|
12271
|
+
for (const identityRecord of identities) {
|
|
12272
|
+
const identity2 = probeProcessIdentity(identityRecord);
|
|
12273
|
+
if (identity2 === "live")
|
|
12274
|
+
return "live";
|
|
12275
|
+
if (identity2 === "unknown")
|
|
12276
|
+
unknown = true;
|
|
12277
|
+
}
|
|
12278
|
+
if (unknown)
|
|
12279
|
+
return "unknown";
|
|
12280
|
+
if (malformed)
|
|
12281
|
+
throw new Error("Shared holder mirror has an invalid process identity");
|
|
12282
|
+
return "dead";
|
|
12283
|
+
}
|
|
12284
|
+
|
|
12009
12285
|
class SharedArbiter {
|
|
12010
|
-
onChange;
|
|
12011
12286
|
#mutex = Promise.resolve();
|
|
12012
12287
|
#polls = new Map;
|
|
12013
|
-
|
|
12014
|
-
|
|
12288
|
+
#deadOriginStrikes = new Map;
|
|
12289
|
+
#onChange;
|
|
12290
|
+
#probeHolderOrigin;
|
|
12291
|
+
constructor(onChangeOrOptions) {
|
|
12292
|
+
const options = typeof onChangeOrOptions === "function" ? { onChange: onChangeOrOptions } : onChangeOrOptions;
|
|
12293
|
+
this.#onChange = options?.onChange;
|
|
12294
|
+
this.#probeHolderOrigin = options?.probeHolderOrigin ?? probeSharedHolderOrigin;
|
|
12015
12295
|
}
|
|
12016
12296
|
#locked(fn) {
|
|
12017
12297
|
const next = this.#mutex.then(fn, fn);
|
|
@@ -12020,7 +12300,7 @@ class SharedArbiter {
|
|
|
12020
12300
|
}
|
|
12021
12301
|
async#notify() {
|
|
12022
12302
|
try {
|
|
12023
|
-
await this
|
|
12303
|
+
await this.#onChange?.();
|
|
12024
12304
|
} catch {}
|
|
12025
12305
|
}
|
|
12026
12306
|
#result(record, granted) {
|
|
@@ -12043,7 +12323,8 @@ class SharedArbiter {
|
|
|
12043
12323
|
delete next.holder;
|
|
12044
12324
|
return next;
|
|
12045
12325
|
}
|
|
12046
|
-
|
|
12326
|
+
const mirror = readMirrorStateSafe(head.project);
|
|
12327
|
+
if (mirror.status === "ok" && mirror.record === null)
|
|
12047
12328
|
continue;
|
|
12048
12329
|
return {
|
|
12049
12330
|
...record,
|
|
@@ -12101,11 +12382,11 @@ class SharedArbiter {
|
|
|
12101
12382
|
}
|
|
12102
12383
|
if (waitMs <= 0)
|
|
12103
12384
|
return this.#result(outcome.record, false);
|
|
12104
|
-
return new Promise((
|
|
12385
|
+
return new Promise((resolve3) => {
|
|
12105
12386
|
const key = `${name}\x00${requester.project}`;
|
|
12106
12387
|
const polls = this.#polls.get(key) ?? [];
|
|
12107
12388
|
const poll = {
|
|
12108
|
-
resolve:
|
|
12389
|
+
resolve: resolve3,
|
|
12109
12390
|
timer: setTimeout(() => {
|
|
12110
12391
|
const remaining = (this.#polls.get(key) ?? []).filter((candidate) => candidate !== poll);
|
|
12111
12392
|
if (remaining.length === 0)
|
|
@@ -12113,7 +12394,7 @@ class SharedArbiter {
|
|
|
12113
12394
|
else
|
|
12114
12395
|
this.#polls.set(key, remaining);
|
|
12115
12396
|
const record = readSharedHostname(name);
|
|
12116
|
-
|
|
12397
|
+
resolve3(record === null ? { granted: false, queued: [] } : this.#result(record, record.holder?.project === requester.project));
|
|
12117
12398
|
}, waitMs)
|
|
12118
12399
|
};
|
|
12119
12400
|
polls.push(poll);
|
|
@@ -12195,47 +12476,150 @@ class SharedArbiter {
|
|
|
12195
12476
|
}
|
|
12196
12477
|
async sweep(occupied) {
|
|
12197
12478
|
let changed = false;
|
|
12479
|
+
const releases = [];
|
|
12198
12480
|
for (const record of listSharedHostnames()) {
|
|
12199
|
-
const
|
|
12200
|
-
const
|
|
12201
|
-
|
|
12481
|
+
const holder = record.holder;
|
|
12482
|
+
const holderMirror = holder === undefined ? undefined : readMirrorStateSafe(holder.project);
|
|
12483
|
+
const deadWaiterProjects = new Set((record.queue ?? []).flatMap((waiter) => {
|
|
12484
|
+
const mirror = readMirrorStateSafe(waiter.project);
|
|
12485
|
+
return mirror.status === "ok" && mirror.record === null ? [waiter.project] : [];
|
|
12486
|
+
}));
|
|
12487
|
+
const deadWaiters = (record.queue ?? []).filter((waiter) => deadWaiterProjects.has(waiter.project));
|
|
12488
|
+
let releaseReason;
|
|
12489
|
+
if (holder !== undefined && holderMirror !== undefined) {
|
|
12490
|
+
const holderKey = `${holder.project}\x00${holder.at}`;
|
|
12491
|
+
const holderOccupied = occupied.has(holder.project);
|
|
12492
|
+
if (holderMirror.status === "error") {
|
|
12493
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12494
|
+
releaseReason = "stack-dead";
|
|
12495
|
+
} else {
|
|
12496
|
+
try {
|
|
12497
|
+
const mirror = holderMirror.record;
|
|
12498
|
+
if (mirror === null) {
|
|
12499
|
+
if (!holderOccupied)
|
|
12500
|
+
releaseReason = "stack-gone";
|
|
12501
|
+
else
|
|
12502
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12503
|
+
} else {
|
|
12504
|
+
const protectedState = mirror.state === "starting" || mirror.state === "queued";
|
|
12505
|
+
if (!protectedState && mirror.starter !== undefined && (typeof mirror.starter !== "object" || mirror.starter === null || !Number.isSafeInteger(mirror.starter.pid) || mirror.starter.pid <= 0 || typeof mirror.starter.startTime !== "string")) {
|
|
12506
|
+
throw new Error("Shared holder mirror has an invalid starter identity");
|
|
12507
|
+
}
|
|
12508
|
+
const starterLiveness = protectedState || mirror.starter === undefined ? "dead" : probeProcessIdentity(mirror.starter);
|
|
12509
|
+
const protectedStartup = protectedState || starterLiveness === "live";
|
|
12510
|
+
if (protectedStartup) {
|
|
12511
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12512
|
+
} else if (starterLiveness === "unknown") {
|
|
12513
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12514
|
+
} else if (!holderOccupied) {
|
|
12515
|
+
const contractOrigin = resolveSharedContractOrigin(mirror, holder.service);
|
|
12516
|
+
if (contractOrigin === undefined) {
|
|
12517
|
+
const processOccupancy = probeMirrorProcessOccupancy(mirror);
|
|
12518
|
+
if (processOccupancy === "live") {
|
|
12519
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12520
|
+
} else if (processOccupancy === "unknown") {
|
|
12521
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12522
|
+
} else if (this.#recordDeadStrike(record.name, holderKey)) {
|
|
12523
|
+
releaseReason = "stack-dead";
|
|
12524
|
+
}
|
|
12525
|
+
} else {
|
|
12526
|
+
const origin = await this.#probeHolderOrigin(mirror, holder.service);
|
|
12527
|
+
if (origin === "live") {
|
|
12528
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12529
|
+
} else if (origin === "dead") {
|
|
12530
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12531
|
+
releaseReason = "stack-dead";
|
|
12532
|
+
} else {
|
|
12533
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12536
|
+
} else {
|
|
12537
|
+
const origin = await this.#probeHolderOrigin(mirror, holder.service);
|
|
12538
|
+
if (origin === "live") {
|
|
12539
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12540
|
+
} else if (origin === "dead") {
|
|
12541
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12542
|
+
releaseReason = "origin-dead";
|
|
12543
|
+
} else {
|
|
12544
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12545
|
+
}
|
|
12546
|
+
}
|
|
12547
|
+
}
|
|
12548
|
+
} catch {
|
|
12549
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12550
|
+
releaseReason = "stack-dead";
|
|
12551
|
+
}
|
|
12552
|
+
}
|
|
12553
|
+
} else {
|
|
12554
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12555
|
+
}
|
|
12556
|
+
if (releaseReason === undefined && deadWaiters.length === 0)
|
|
12202
12557
|
continue;
|
|
12558
|
+
let released = false;
|
|
12203
12559
|
const updated = await this.#locked(() => updateSharedHostname(record.name, (candidate) => {
|
|
12204
12560
|
const pruned = {
|
|
12205
12561
|
...candidate,
|
|
12206
|
-
queue: (candidate.queue ?? []).filter((waiter) =>
|
|
12562
|
+
queue: (candidate.queue ?? []).filter((waiter) => {
|
|
12563
|
+
if (!deadWaiterProjects.has(waiter.project))
|
|
12564
|
+
return true;
|
|
12565
|
+
const freshMirror = readMirrorStateSafe(waiter.project);
|
|
12566
|
+
return freshMirror.status === "error" || freshMirror.record !== null;
|
|
12567
|
+
})
|
|
12207
12568
|
};
|
|
12208
|
-
const
|
|
12209
|
-
|
|
12569
|
+
const holderMatches = releaseReason !== undefined && pruned.holder?.project === holder?.project && pruned.holder?.at === holder?.at;
|
|
12570
|
+
if (!holderMatches)
|
|
12571
|
+
return pruned;
|
|
12572
|
+
released = true;
|
|
12573
|
+
return this.#grantNext(pruned);
|
|
12210
12574
|
}));
|
|
12211
12575
|
if (updated === null)
|
|
12212
12576
|
continue;
|
|
12213
|
-
changed
|
|
12577
|
+
changed ||= released || deadWaiters.length > 0;
|
|
12578
|
+
if (released && holder !== undefined && releaseReason !== undefined) {
|
|
12579
|
+
releases.push({ name: record.name, project: holder.project, reason: releaseReason });
|
|
12580
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12581
|
+
}
|
|
12214
12582
|
if (updated.holder !== undefined && updated.holder.project !== record.holder?.project) {
|
|
12215
12583
|
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12216
12584
|
}
|
|
12217
12585
|
}
|
|
12218
12586
|
if (changed)
|
|
12219
12587
|
await this.#notify();
|
|
12588
|
+
return releases;
|
|
12589
|
+
}
|
|
12590
|
+
#recordDeadStrike(name, holderKey) {
|
|
12591
|
+
const previous = this.#deadOriginStrikes.get(name);
|
|
12592
|
+
const strikes = previous?.holderKey === holderKey ? previous.strikes + 1 : 1;
|
|
12593
|
+
this.#deadOriginStrikes.set(name, { holderKey, strikes });
|
|
12594
|
+
return strikes >= ORIGIN_DEAD_RELEASE_STRIKES;
|
|
12595
|
+
}
|
|
12596
|
+
#retainStrikeForHolder(name, holderKey) {
|
|
12597
|
+
const previous = this.#deadOriginStrikes.get(name);
|
|
12598
|
+
if (previous?.holderKey !== holderKey) {
|
|
12599
|
+
this.#deadOriginStrikes.set(name, { holderKey, strikes: 0 });
|
|
12600
|
+
}
|
|
12220
12601
|
}
|
|
12221
12602
|
}
|
|
12603
|
+
var ORIGIN_DEAD_RELEASE_STRIKES = 3;
|
|
12222
12604
|
var init_shared_arbiter = __esm(() => {
|
|
12223
12605
|
init_src();
|
|
12606
|
+
init_pidfile();
|
|
12224
12607
|
init_state();
|
|
12608
|
+
init_origin_liveness();
|
|
12225
12609
|
init_shared();
|
|
12226
12610
|
});
|
|
12227
12611
|
|
|
12228
12612
|
// packages/engine/src/doctor.ts
|
|
12229
|
-
import { existsSync as existsSync20, readFileSync as
|
|
12613
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "fs";
|
|
12230
12614
|
import { execFile as execFile9 } from "child_process";
|
|
12231
|
-
import { dirname as
|
|
12615
|
+
import { dirname as dirname7, join as join24 } from "path";
|
|
12232
12616
|
function row(check, level, detail) {
|
|
12233
12617
|
return { check, level, detail };
|
|
12234
12618
|
}
|
|
12235
12619
|
async function bounded(check, fn, timeoutMs = CHECK_TIMEOUT_MS) {
|
|
12236
12620
|
let timer;
|
|
12237
|
-
const timeout = new Promise((
|
|
12238
|
-
timer = setTimeout(() =>
|
|
12621
|
+
const timeout = new Promise((resolve3) => {
|
|
12622
|
+
timer = setTimeout(() => resolve3([row(check, "unknown", "timed out")]), timeoutMs);
|
|
12239
12623
|
});
|
|
12240
12624
|
try {
|
|
12241
12625
|
return await Promise.race([fn(), timeout]);
|
|
@@ -12246,9 +12630,9 @@ async function bounded(check, fn, timeoutMs = CHECK_TIMEOUT_MS) {
|
|
|
12246
12630
|
}
|
|
12247
12631
|
}
|
|
12248
12632
|
function exec(cmd, args, timeoutMs = CHECK_TIMEOUT_MS - 500) {
|
|
12249
|
-
return new Promise((
|
|
12633
|
+
return new Promise((resolve3) => {
|
|
12250
12634
|
execFile9(cmd, args, { timeout: timeoutMs, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
12251
|
-
|
|
12635
|
+
resolve3({
|
|
12252
12636
|
ok: err === null,
|
|
12253
12637
|
stdout: stdout ?? "",
|
|
12254
12638
|
message: err !== null ? (stderr || err.message).trim().split(`
|
|
@@ -12269,21 +12653,21 @@ async function envChecks(ctx) {
|
|
|
12269
12653
|
}
|
|
12270
12654
|
if (ctx !== null) {
|
|
12271
12655
|
const ignored = await exec("git", ["-C", ctx.worktreeRoot, "check-ignore", "-q", ".hestia/"]);
|
|
12272
|
-
rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${
|
|
12273
|
-
const schema = existsSync20(
|
|
12656
|
+
rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${join24(ctx.worktreeRoot, ".gitignore")}`));
|
|
12657
|
+
const schema = existsSync20(join24(ctx.worktreeRoot, ".env.schema"));
|
|
12274
12658
|
if (schema) {
|
|
12275
12659
|
rows.push(detectVarlock(ctx.worktreeRoot) !== null ? row("varlock", "ok", "local varlock found \u2014 worker env composition active") : row("varlock", "warn", ".env.schema present but no local varlock (bun install?)"));
|
|
12276
12660
|
}
|
|
12277
12661
|
const workers = discoverWorkers(ctx.worktreeRoot);
|
|
12278
12662
|
if (workers.length > 0) {
|
|
12279
12663
|
const missing = workers.filter((w) => {
|
|
12280
|
-
let dir =
|
|
12664
|
+
let dir = dirname7(w.configPath);
|
|
12281
12665
|
for (;; ) {
|
|
12282
|
-
if (existsSync20(
|
|
12666
|
+
if (existsSync20(join24(dir, "node_modules", ".bin", "wrangler")))
|
|
12283
12667
|
return false;
|
|
12284
|
-
if (dir === ctx.worktreeRoot ||
|
|
12668
|
+
if (dir === ctx.worktreeRoot || dirname7(dir) === dir)
|
|
12285
12669
|
return true;
|
|
12286
|
-
dir =
|
|
12670
|
+
dir = dirname7(dir);
|
|
12287
12671
|
}
|
|
12288
12672
|
});
|
|
12289
12673
|
rows.push(missing.length === 0 ? row("wrangler", "ok", `${workers.length} config(s), local binary reachable for all`) : row("wrangler", "error", `${missing.length}/${workers.length} config(s) have no local wrangler binary (install deps)`));
|
|
@@ -12338,10 +12722,10 @@ async function stateChecks(ctx) {
|
|
|
12338
12722
|
rows.push(row(`exposure:${exp.service}`, "error", `ingress rule points at port ${exp.originPort} but the service owns ` + `${publishedPort} \u2014 a recycled port serves ANOTHER worktree; ` + `run any hestia command to resync, this should not persist`));
|
|
12339
12723
|
}
|
|
12340
12724
|
}
|
|
12341
|
-
const lockPath2 =
|
|
12725
|
+
const lockPath2 = join24(hestiaDir(worktreeRoot), "lock");
|
|
12342
12726
|
if (existsSync20(lockPath2)) {
|
|
12343
12727
|
try {
|
|
12344
|
-
const holder = JSON.parse(
|
|
12728
|
+
const holder = JSON.parse(readFileSync19(lockPath2, "utf8"));
|
|
12345
12729
|
if (!isLive(holder)) {
|
|
12346
12730
|
rows.push(row("lock", "warn", "stale worktree lock (dead holder) \u2014 auto-broken on next command"));
|
|
12347
12731
|
}
|
|
@@ -12349,7 +12733,7 @@ async function stateChecks(ctx) {
|
|
|
12349
12733
|
rows.push(row("lock", "warn", "unreadable worktree lock file"));
|
|
12350
12734
|
}
|
|
12351
12735
|
}
|
|
12352
|
-
const mirror =
|
|
12736
|
+
const mirror = join24(hestiaHome(), "stacks", record.project, "stack.json");
|
|
12353
12737
|
if (!existsSync20(mirror)) {
|
|
12354
12738
|
rows.push(row("mirror", "warn", "no ~/.hestia mirror \u2014 `down --project` would not work after worktree deletion"));
|
|
12355
12739
|
}
|
|
@@ -12357,16 +12741,16 @@ async function stateChecks(ctx) {
|
|
|
12357
12741
|
}
|
|
12358
12742
|
async function machineChecks() {
|
|
12359
12743
|
const rows = [];
|
|
12360
|
-
const stacksDir =
|
|
12744
|
+
const stacksDir = join24(hestiaHome(), "stacks");
|
|
12361
12745
|
const mirrored = new Set;
|
|
12362
12746
|
if (existsSync20(stacksDir)) {
|
|
12363
12747
|
for (const project of readdirSync8(stacksDir)) {
|
|
12364
|
-
const p =
|
|
12748
|
+
const p = join24(stacksDir, project, "stack.json");
|
|
12365
12749
|
if (!existsSync20(p))
|
|
12366
12750
|
continue;
|
|
12367
12751
|
mirrored.add(project);
|
|
12368
12752
|
try {
|
|
12369
|
-
const rec = parseStackRecord(
|
|
12753
|
+
const rec = parseStackRecord(readFileSync19(p, "utf8"), p);
|
|
12370
12754
|
if (!existsSync20(rec.worktree)) {
|
|
12371
12755
|
rows.push(row(`orphan-mirror:${project}`, "warn", `worktree ${rec.worktree} is gone \u2014 \`hestia down --project ${project}\` to clean up`));
|
|
12372
12756
|
}
|
|
@@ -12467,7 +12851,7 @@ async function daemonChecks() {
|
|
|
12467
12851
|
}
|
|
12468
12852
|
const plist = plistPath();
|
|
12469
12853
|
if (existsSync20(plist)) {
|
|
12470
|
-
const content =
|
|
12854
|
+
const content = readFileSync19(plist, "utf8");
|
|
12471
12855
|
const args = [...content.matchAll(/<string>([^<]+)<\/string>/g)].map((m) => m[1]);
|
|
12472
12856
|
const paths = args.filter((a) => a.startsWith("/") && !a.includes(":"));
|
|
12473
12857
|
const missing = paths.filter((p) => !existsSync20(p));
|
|
@@ -12539,8 +12923,8 @@ var init_doctor = __esm(() => {
|
|
|
12539
12923
|
// packages/engine/src/daemon/fleet-monitor.ts
|
|
12540
12924
|
import { execFile as execFile10 } from "child_process";
|
|
12541
12925
|
import { promisify as promisify8 } from "util";
|
|
12542
|
-
import { existsSync as existsSync21, readFileSync as
|
|
12543
|
-
import { join as
|
|
12926
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, readdirSync as readdirSync9 } from "fs";
|
|
12927
|
+
import { join as join25 } from "path";
|
|
12544
12928
|
|
|
12545
12929
|
class LatestFleetChannel {
|
|
12546
12930
|
#pending;
|
|
@@ -12567,8 +12951,8 @@ class LatestFleetChannel {
|
|
|
12567
12951
|
}
|
|
12568
12952
|
if (this.#closed)
|
|
12569
12953
|
return Promise.resolve({ value: undefined, done: true });
|
|
12570
|
-
return new Promise((
|
|
12571
|
-
this.#waiter =
|
|
12954
|
+
return new Promise((resolve3) => {
|
|
12955
|
+
this.#waiter = resolve3;
|
|
12572
12956
|
});
|
|
12573
12957
|
}
|
|
12574
12958
|
close() {
|
|
@@ -12583,13 +12967,13 @@ class LatestFleetChannel {
|
|
|
12583
12967
|
function readManagedMirrors() {
|
|
12584
12968
|
const records = [];
|
|
12585
12969
|
const warnings = [];
|
|
12586
|
-
const stacksDirectory =
|
|
12970
|
+
const stacksDirectory = join25(hestiaHome(), "stacks");
|
|
12587
12971
|
if (!existsSync21(stacksDirectory))
|
|
12588
12972
|
return { records, warnings };
|
|
12589
12973
|
for (const project of readdirSync9(stacksDirectory).sort()) {
|
|
12590
|
-
const path =
|
|
12974
|
+
const path = join25(stacksDirectory, project, "stack.json");
|
|
12591
12975
|
try {
|
|
12592
|
-
const source =
|
|
12976
|
+
const source = readFileSync20(path);
|
|
12593
12977
|
if (source.byteLength > MAX_MIRROR_BYTES2) {
|
|
12594
12978
|
warnings.push(`Fleet mirror too large for ${project}`);
|
|
12595
12979
|
continue;
|
|
@@ -12971,7 +13355,7 @@ var init_fleet_monitor = __esm(() => {
|
|
|
12971
13355
|
});
|
|
12972
13356
|
|
|
12973
13357
|
// packages/engine/src/init-config.ts
|
|
12974
|
-
import { dirname as
|
|
13358
|
+
import { dirname as dirname8, relative as relative4 } from "path";
|
|
12975
13359
|
import { chmodSync as chmodSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
12976
13360
|
function validateName2(name, what) {
|
|
12977
13361
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(name)) {
|
|
@@ -12987,6 +13371,7 @@ function inferredWorkload(report, name) {
|
|
|
12987
13371
|
composeService: workload.source === "compose" ? workload.name : undefined,
|
|
12988
13372
|
dockerfile: workload.source === "dockerfile" && workload.definitionPath !== undefined ? relative4(report.repository.worktree, workload.definitionPath) : undefined,
|
|
12989
13373
|
wranglerConfig: workload.source === "wrangler" && workload.definitionPath !== undefined ? relative4(report.repository.worktree, workload.definitionPath) : undefined,
|
|
13374
|
+
env: {},
|
|
12990
13375
|
endpoints: {}
|
|
12991
13376
|
};
|
|
12992
13377
|
}
|
|
@@ -13024,6 +13409,7 @@ function applyRequest(report, workloads, request) {
|
|
|
13024
13409
|
command: request.kind === "proc" ? request.command : undefined,
|
|
13025
13410
|
port: request.kind === "proc" ? request.port ?? "auto" : undefined,
|
|
13026
13411
|
wranglerConfig: request.kind === "wrangler" ? request.file ?? "wrangler.toml" : undefined,
|
|
13412
|
+
env: existing?.env ?? {},
|
|
13027
13413
|
endpoints: existing?.endpoints ?? {}
|
|
13028
13414
|
};
|
|
13029
13415
|
}
|
|
@@ -13038,16 +13424,21 @@ async function initializeRepositoryConfig(cwd, request, scope, write) {
|
|
|
13038
13424
|
version: 1,
|
|
13039
13425
|
workloads: Object.fromEntries(Object.entries(layer.config.workloads).map(([name, workload]) => [
|
|
13040
13426
|
name,
|
|
13041
|
-
{
|
|
13427
|
+
{
|
|
13428
|
+
...workload,
|
|
13429
|
+
command: workload.command?.slice(),
|
|
13430
|
+
env: { ...workload.env },
|
|
13431
|
+
endpoints: { ...workload.endpoints }
|
|
13432
|
+
}
|
|
13042
13433
|
]))
|
|
13043
13434
|
};
|
|
13044
13435
|
applyRequest(before, config.workloads, request);
|
|
13045
13436
|
const proposed = renderRepositoryWorkloadConfig(config);
|
|
13046
13437
|
parseRepositoryWorkloadConfig(proposed, path);
|
|
13047
13438
|
if (write) {
|
|
13048
|
-
mkdirSync9(
|
|
13439
|
+
mkdirSync9(dirname8(path), { recursive: true, mode: 448 });
|
|
13049
13440
|
if (scope === "machine")
|
|
13050
|
-
chmodSync6(
|
|
13441
|
+
chmodSync6(dirname8(path), 448);
|
|
13051
13442
|
writeAtomicTextFile(path, proposed, 384);
|
|
13052
13443
|
}
|
|
13053
13444
|
const discovery = write ? await discoverRepository(repo.worktreeRoot) : before;
|
|
@@ -13075,7 +13466,7 @@ var init_init_config = __esm(() => {
|
|
|
13075
13466
|
import { existsSync as existsSync22, rmSync as rmSync10 } from "fs";
|
|
13076
13467
|
import { execFile as execFile11 } from "child_process";
|
|
13077
13468
|
import { promisify as promisify9 } from "util";
|
|
13078
|
-
import { join as
|
|
13469
|
+
import { join as join26, resolve as resolve3 } from "path";
|
|
13079
13470
|
import { createHash as createHash7 } from "crypto";
|
|
13080
13471
|
import { resolve4, resolve6, resolveCname } from "dns/promises";
|
|
13081
13472
|
function composeUnsupported(message) {
|
|
@@ -13185,7 +13576,7 @@ async function assertHestiaStateIgnored(worktreeRoot) {
|
|
|
13185
13576
|
try {
|
|
13186
13577
|
await pexec9("git", ["-C", worktreeRoot, "check-ignore", "-q", ".hestia/"], { timeout: 5000 });
|
|
13187
13578
|
} catch {
|
|
13188
|
-
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${
|
|
13579
|
+
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join26(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join26(worktreeRoot, ".gitignore") });
|
|
13189
13580
|
}
|
|
13190
13581
|
}
|
|
13191
13582
|
async function prepareCompose(worktreeRoot, project, repo, branch, opts, configuredBaseFile) {
|
|
@@ -13202,7 +13593,7 @@ async function prepareCompose(worktreeRoot, project, repo, branch, opts, configu
|
|
|
13202
13593
|
services
|
|
13203
13594
|
});
|
|
13204
13595
|
ensureDir(hestiaDir(worktreeRoot));
|
|
13205
|
-
const overridePath =
|
|
13596
|
+
const overridePath = join26(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
13206
13597
|
writeAtomicTextFile(overridePath, yaml);
|
|
13207
13598
|
return {
|
|
13208
13599
|
ctx: {
|
|
@@ -13235,7 +13626,7 @@ function writeDockerfileComposeModel(worktreeRoot, workloads, conventionalCompos
|
|
|
13235
13626
|
...conventionalComposeFile === undefined ? {} : { include: [{ path: conventionalComposeFile }] },
|
|
13236
13627
|
services
|
|
13237
13628
|
};
|
|
13238
|
-
const path =
|
|
13629
|
+
const path = join26(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
|
|
13239
13630
|
writeAtomicTextFile(path, $stringify(model));
|
|
13240
13631
|
return path;
|
|
13241
13632
|
}
|
|
@@ -13255,9 +13646,9 @@ function freshRecord(project, repoId, repo, branch, worktree) {
|
|
|
13255
13646
|
};
|
|
13256
13647
|
}
|
|
13257
13648
|
function assertCurrentStackIdentity(record, current) {
|
|
13258
|
-
const path =
|
|
13649
|
+
const path = join26(hestiaDir(current.worktreeRoot), "stack.json");
|
|
13259
13650
|
assertMutableStackRecord(record, path);
|
|
13260
|
-
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch &&
|
|
13651
|
+
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch && resolve3(record.worktree) === resolve3(current.worktreeRoot) && record.project === projectName(current.repoId, current.repo, current.branch, current.worktreeRoot);
|
|
13261
13652
|
if (!matches) {
|
|
13262
13653
|
throw new HestiaError("stack-identity-changed", `this checkout no longer matches stack ${record.project}; run hestia down before starting or changing it`, {
|
|
13263
13654
|
recorded: {
|
|
@@ -13434,7 +13825,7 @@ function matchesExpectedStack(record, expected) {
|
|
|
13434
13825
|
return record !== null && (record.repoId === undefined || record.repoId === expected.repoId) && record.worktree === expected.worktree && record.createdAt === expected.createdAt;
|
|
13435
13826
|
}
|
|
13436
13827
|
function projectMutationRoot(project) {
|
|
13437
|
-
return
|
|
13828
|
+
return join26(hestiaHome(), "project-locks", project);
|
|
13438
13829
|
}
|
|
13439
13830
|
async function assertNamedTunnelDns(hostname, tunnelUuid) {
|
|
13440
13831
|
const expected = `${tunnelUuid}.cfargotunnel.com`;
|
|
@@ -13691,12 +14082,25 @@ class ComposeEngine {
|
|
|
13691
14082
|
}
|
|
13692
14083
|
}
|
|
13693
14084
|
}
|
|
14085
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
14086
|
+
if (opts?.workers || configuredWorkers.length > 0) {
|
|
14087
|
+
const configuredFilters = configuredWorkers.map(([name, workload]) => workload.wranglerConfig ?? name);
|
|
14088
|
+
const explicitFilters = Array.isArray(opts?.workers) ? opts.workers : [];
|
|
14089
|
+
await this.#upWorkers(worktreeRoot, record, configuredWorkloads, {
|
|
14090
|
+
...opts,
|
|
14091
|
+
workers: opts?.workers === true ? true : [...new Set([...explicitFilters, ...configuredFilters])]
|
|
14092
|
+
});
|
|
14093
|
+
}
|
|
14094
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13694
14095
|
for (const [name, workload] of configuredProcs) {
|
|
13695
|
-
const command = workload.command;
|
|
13696
14096
|
const configuredSpec = {
|
|
13697
14097
|
name,
|
|
13698
|
-
argv: command,
|
|
14098
|
+
argv: workload.command,
|
|
14099
|
+
cwd: workload.cwd,
|
|
14100
|
+
env: resolveConfiguredEnvironment(worktreeRoot, workload.env, record),
|
|
13699
14101
|
port: workload.port ?? "auto",
|
|
14102
|
+
varlock: workload.varlock,
|
|
14103
|
+
healthPath: workload.healthPath,
|
|
13700
14104
|
backend: "proc"
|
|
13701
14105
|
};
|
|
13702
14106
|
const clash = record.services.find((service) => service.name === name && service.backend === "docker");
|
|
@@ -13717,19 +14121,11 @@ class ComposeEngine {
|
|
|
13717
14121
|
}
|
|
13718
14122
|
const result = await startProc(worktreeRoot, configuredSpec, record.env, (pidfile2) => mirrorPidfile(record.project, pidfile2));
|
|
13719
14123
|
recordProc(record, result.record);
|
|
14124
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13720
14125
|
writeState(worktreeRoot, record);
|
|
13721
14126
|
if (result.error !== undefined)
|
|
13722
14127
|
throw result.error;
|
|
13723
14128
|
}
|
|
13724
|
-
if (opts?.workers || configuredWorkers.length > 0) {
|
|
13725
|
-
const configuredFilters = configuredWorkers.map(([name, workload]) => workload.wranglerConfig ?? name);
|
|
13726
|
-
const explicitFilters = Array.isArray(opts?.workers) ? opts.workers : [];
|
|
13727
|
-
await this.#upWorkers(worktreeRoot, record, {
|
|
13728
|
-
...opts,
|
|
13729
|
-
workers: opts?.workers === true ? true : [...new Set([...explicitFilters, ...configuredFilters])]
|
|
13730
|
-
});
|
|
13731
|
-
}
|
|
13732
|
-
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13733
14129
|
record.state = "up";
|
|
13734
14130
|
delete record.starter;
|
|
13735
14131
|
setTunnelDirty(syncExposures(record));
|
|
@@ -13737,17 +14133,27 @@ class ComposeEngine {
|
|
|
13737
14133
|
return record;
|
|
13738
14134
|
}));
|
|
13739
14135
|
}
|
|
13740
|
-
async#upWorkers(worktreeRoot, record, opts) {
|
|
14136
|
+
async#upWorkers(worktreeRoot, record, configuredWorkloads, opts) {
|
|
13741
14137
|
const plan = await planWorkers(worktreeRoot, {
|
|
13742
14138
|
filter: Array.isArray(opts.workers) ? opts.workers : [],
|
|
13743
14139
|
allowRemote: opts.allowRemote ?? false,
|
|
13744
14140
|
force: opts.force ?? false,
|
|
13745
|
-
varlock: !opts.noVarlock
|
|
14141
|
+
varlock: !opts.noVarlock
|
|
13746
14142
|
});
|
|
13747
14143
|
for (const w of plan.warnings)
|
|
13748
14144
|
process.stderr.write(`warning: ${w}
|
|
13749
14145
|
`);
|
|
13750
14146
|
for (const spec of plan.specs) {
|
|
14147
|
+
const configured = Object.entries(configuredWorkloads).find(([name, workload]) => workload.source === "wrangler" && (name === spec.name || workload.wranglerConfig !== undefined && resolve3(worktreeRoot, workload.wranglerConfig) === spec.configPath))?.[1];
|
|
14148
|
+
if (configured !== undefined && configured.source === "wrangler") {
|
|
14149
|
+
spec.cwd = configured.cwd ?? spec.cwd;
|
|
14150
|
+
spec.varlock = configured.varlock ?? spec.varlock;
|
|
14151
|
+
spec.healthPath = configured.healthPath;
|
|
14152
|
+
spec.env = {
|
|
14153
|
+
...spec.env,
|
|
14154
|
+
...resolveConfiguredEnvironment(worktreeRoot, configured.env, record)
|
|
14155
|
+
};
|
|
14156
|
+
}
|
|
13751
14157
|
const clash = record.services.find((s) => s.name === spec.name && s.backend === "docker");
|
|
13752
14158
|
if (clash !== undefined) {
|
|
13753
14159
|
throw new HestiaError("name-conflict", `worker "${spec.name}" collides with a compose service name`);
|
|
@@ -14041,13 +14447,13 @@ class ComposeEngine {
|
|
|
14041
14447
|
const composeFile = record?.composeFile ?? tryLoadConfig(worktreeRoot)?.composeFile;
|
|
14042
14448
|
if (composeFile !== undefined) {
|
|
14043
14449
|
const project2 = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
14044
|
-
const overrideFile = record?.overrideFile ??
|
|
14450
|
+
const overrideFile = record?.overrideFile ?? join26(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
14045
14451
|
if (existsSync22(overrideFile)) {
|
|
14046
14452
|
await composeDown({ project: project2, baseFile: composeFile, overrideFile, cwd: worktreeRoot }, opts?.destroy ?? false);
|
|
14047
14453
|
} else if (record?.composeFile !== undefined) {
|
|
14048
14454
|
const rest = ["compose", "-p", project2, "down", "--remove-orphans"];
|
|
14049
14455
|
if (opts?.destroy)
|
|
14050
|
-
rest.push("-v");
|
|
14456
|
+
rest.push("-v", "--rmi", "local");
|
|
14051
14457
|
await pexec9("docker", rest, { timeout: 180000 });
|
|
14052
14458
|
}
|
|
14053
14459
|
}
|
|
@@ -14104,7 +14510,7 @@ class ComposeEngine {
|
|
|
14104
14510
|
try {
|
|
14105
14511
|
const rest = ["compose", "-p", project, "down", "--remove-orphans"];
|
|
14106
14512
|
if (opts?.destroy)
|
|
14107
|
-
rest.push("-v");
|
|
14513
|
+
rest.push("-v", "--rmi", "local");
|
|
14108
14514
|
await pexec9("docker", rest, { timeout: 180000 });
|
|
14109
14515
|
} catch (err) {
|
|
14110
14516
|
if (fresh?.composeFile !== undefined) {
|
|
@@ -14115,7 +14521,7 @@ class ComposeEngine {
|
|
|
14115
14521
|
};
|
|
14116
14522
|
if (record !== null && existsSync22(record.worktree)) {
|
|
14117
14523
|
const info = await getRepoInfo(record.worktree);
|
|
14118
|
-
const identityMatches =
|
|
14524
|
+
const identityMatches = resolve3(info.worktreeRoot) === resolve3(record.worktree) && (record.repoId === undefined ? record.repo === info.repo : record.repoId === info.repoId);
|
|
14119
14525
|
const localRecord = readState(record.worktree);
|
|
14120
14526
|
if (identityMatches && localRecord?.project === record.project) {
|
|
14121
14527
|
await this.down(record.worktree, opts);
|
|
@@ -14274,7 +14680,7 @@ class ComposeEngine {
|
|
|
14274
14680
|
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
14275
14681
|
const authority = internalEndpointAuthority(record.project, alias);
|
|
14276
14682
|
const name = `aux-quick-${createHash7("sha256").update(alias).digest("hex").slice(0, 10)}`;
|
|
14277
|
-
const quickCfg =
|
|
14683
|
+
const quickCfg = join26(hestiaDir(worktreeRoot), `quick-${name}.yml`);
|
|
14278
14684
|
writeAtomicTextFile(quickCfg, `ingress:
|
|
14279
14685
|
- service: ${JSON.stringify(`unix:${publicGatewaySocketPath()}`)}
|
|
14280
14686
|
` + ` originRequest:
|
|
@@ -14660,7 +15066,8 @@ var init_src2 = __esm(() => {
|
|
|
14660
15066
|
init_shutdown();
|
|
14661
15067
|
init_ports();
|
|
14662
15068
|
init_local_http_router();
|
|
14663
|
-
|
|
15069
|
+
init_origin_liveness();
|
|
15070
|
+
init_configured_workload_environment();
|
|
14664
15071
|
init_adapter();
|
|
14665
15072
|
init_verify();
|
|
14666
15073
|
init_cloudflared();
|
|
@@ -14718,11 +15125,15 @@ class ReconnectLogDeduper {
|
|
|
14718
15125
|
#capacity;
|
|
14719
15126
|
#history = [];
|
|
14720
15127
|
#reconnectBuffer = null;
|
|
15128
|
+
#reconnectCandidates = null;
|
|
15129
|
+
#completedOverlap = 0;
|
|
14721
15130
|
constructor(capacity = 50) {
|
|
14722
15131
|
this.#capacity = capacity;
|
|
14723
15132
|
}
|
|
14724
15133
|
beginReconnect() {
|
|
14725
15134
|
this.#reconnectBuffer = [];
|
|
15135
|
+
this.#reconnectCandidates = null;
|
|
15136
|
+
this.#completedOverlap = 0;
|
|
14726
15137
|
}
|
|
14727
15138
|
push(line) {
|
|
14728
15139
|
const entry = { signature: logLineSignature(line), line };
|
|
@@ -14732,30 +15143,26 @@ class ReconnectLogDeduper {
|
|
|
14732
15143
|
}
|
|
14733
15144
|
this.#reconnectBuffer.push(entry);
|
|
14734
15145
|
const buffered = this.#reconnectBuffer;
|
|
14735
|
-
const
|
|
14736
|
-
const
|
|
14737
|
-
|
|
14738
|
-
const
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
}
|
|
15146
|
+
const index = buffered.length - 1;
|
|
15147
|
+
const candidates = this.#reconnectCandidates ?? this.#history.map((_candidate, start) => start).filter((start) => this.#history[start].signature === entry.signature);
|
|
15148
|
+
const active = candidates.filter((start) => {
|
|
15149
|
+
const historyIndex = start + index;
|
|
15150
|
+
if (historyIndex >= this.#history.length)
|
|
15151
|
+
return false;
|
|
15152
|
+
if (this.#history[historyIndex].signature !== entry.signature)
|
|
15153
|
+
return false;
|
|
15154
|
+
if (historyIndex === this.#history.length - 1) {
|
|
15155
|
+
this.#completedOverlap = Math.max(this.#completedOverlap, this.#history.length - start);
|
|
14746
15156
|
}
|
|
14747
|
-
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
|
|
14751
|
-
if (buffered.length >= overlapLength)
|
|
14752
|
-
completedOverlapLengths.push(overlapLength);
|
|
14753
|
-
}
|
|
14754
|
-
if (activeOverlapLengths.length > 0)
|
|
15157
|
+
return true;
|
|
15158
|
+
});
|
|
15159
|
+
this.#reconnectCandidates = active;
|
|
15160
|
+
if (active.length > 0)
|
|
14755
15161
|
return [];
|
|
14756
|
-
const
|
|
14757
|
-
const fresh = buffered.slice(suppressed);
|
|
15162
|
+
const fresh = buffered.slice(this.#completedOverlap);
|
|
14758
15163
|
this.#reconnectBuffer = null;
|
|
15164
|
+
this.#reconnectCandidates = null;
|
|
15165
|
+
this.#completedOverlap = 0;
|
|
14759
15166
|
this.#remember(fresh);
|
|
14760
15167
|
return fresh.map((candidate) => candidate.line);
|
|
14761
15168
|
}
|
|
@@ -14788,12 +15195,12 @@ async function reconnectDelay(attempt, signal) {
|
|
|
14788
15195
|
const delay = Math.min(4000, 250 * 2 ** Math.min(attempt, 4));
|
|
14789
15196
|
if (signal.aborted)
|
|
14790
15197
|
return;
|
|
14791
|
-
await new Promise((
|
|
15198
|
+
await new Promise((resolve5) => {
|
|
14792
15199
|
const timer = setTimeout(done, delay);
|
|
14793
15200
|
function done() {
|
|
14794
15201
|
clearTimeout(timer);
|
|
14795
15202
|
signal.removeEventListener("abort", done);
|
|
14796
|
-
|
|
15203
|
+
resolve5();
|
|
14797
15204
|
}
|
|
14798
15205
|
signal.addEventListener("abort", done, { once: true });
|
|
14799
15206
|
});
|
|
@@ -14833,7 +15240,7 @@ class DaemonFleetSource {
|
|
|
14833
15240
|
async* logs(project, service, signal) {
|
|
14834
15241
|
let attempt = 0;
|
|
14835
15242
|
let connectedBefore = false;
|
|
14836
|
-
const deduper = new ReconnectLogDeduper(
|
|
15243
|
+
const deduper = new ReconnectLogDeduper(FLEET_LOG_BACKFILL_LINES);
|
|
14837
15244
|
while (!signal.aborted && !this.#lifetime.signal.aborted) {
|
|
14838
15245
|
const connectedAt = Date.now();
|
|
14839
15246
|
try {
|
|
@@ -14842,7 +15249,7 @@ class DaemonFleetSource {
|
|
|
14842
15249
|
deduper.beginReconnect();
|
|
14843
15250
|
}
|
|
14844
15251
|
connectedBefore = true;
|
|
14845
|
-
for await (const line of streamDaemonServiceLogs(daemon.port, project, service,
|
|
15252
|
+
for await (const line of streamDaemonServiceLogs(daemon.port, project, service, FLEET_LOG_BACKFILL_LINES, signal)) {
|
|
14846
15253
|
if (!isLogLine(line))
|
|
14847
15254
|
throw new Error("log protocol frame is invalid");
|
|
14848
15255
|
for (const freshLine of deduper.push(line))
|
|
@@ -14879,7 +15286,7 @@ class DaemonFleetSource {
|
|
|
14879
15286
|
return Promise.reject(new Error(`stack ${stack.project} has no stable incarnation timestamp`));
|
|
14880
15287
|
}
|
|
14881
15288
|
return engine.downProject(stack.project, {
|
|
14882
|
-
destroy:
|
|
15289
|
+
destroy: true,
|
|
14883
15290
|
expectedStack: {
|
|
14884
15291
|
repoId: stack.repoId,
|
|
14885
15292
|
worktree: stack.worktree,
|
|
@@ -14891,6 +15298,7 @@ class DaemonFleetSource {
|
|
|
14891
15298
|
this.#lifetime.abort();
|
|
14892
15299
|
}
|
|
14893
15300
|
}
|
|
15301
|
+
var FLEET_LOG_BACKFILL_LINES = 1000;
|
|
14894
15302
|
var init_fleet_source = __esm(() => {
|
|
14895
15303
|
init_src2();
|
|
14896
15304
|
});
|
|
@@ -14911,6 +15319,8 @@ function createFleetUiState() {
|
|
|
14911
15319
|
layout: "auto",
|
|
14912
15320
|
filter: "",
|
|
14913
15321
|
follow: true,
|
|
15322
|
+
serviceOffset: 0,
|
|
15323
|
+
serviceOffsetManual: false,
|
|
14914
15324
|
logOffset: 0,
|
|
14915
15325
|
unseenLines: 0,
|
|
14916
15326
|
helpOpen: false,
|
|
@@ -14955,9 +15365,13 @@ function reduceFleetUiState(state, action) {
|
|
|
14955
15365
|
switch (action.type) {
|
|
14956
15366
|
case "reconcile":
|
|
14957
15367
|
const visibleSnapshot = state.filter === "" ? action.snapshot : { ...action.snapshot, stacks: visibleFleetStacks(action.snapshot, state.filter) };
|
|
15368
|
+
const selection = reconcileFleetSelection(state.selection, visibleSnapshot, action.preferredProject);
|
|
15369
|
+
const selectionChanged = selection.project !== state.selection.project || selection.service !== state.selection.service || selection.endpoint !== state.selection.endpoint;
|
|
14958
15370
|
return {
|
|
14959
15371
|
...state,
|
|
14960
|
-
selection
|
|
15372
|
+
selection,
|
|
15373
|
+
serviceOffset: selection.project === state.selection.project ? state.serviceOffset : 0,
|
|
15374
|
+
serviceOffsetManual: selectionChanged ? false : state.serviceOffsetManual
|
|
14961
15375
|
};
|
|
14962
15376
|
case "move-stack": {
|
|
14963
15377
|
const stacks = visibleFleetStacks(action.snapshot, state.filter);
|
|
@@ -14970,6 +15384,8 @@ function reduceFleetUiState(state, action) {
|
|
|
14970
15384
|
service: stack.services[0]?.name,
|
|
14971
15385
|
endpoint: undefined
|
|
14972
15386
|
},
|
|
15387
|
+
serviceOffset: 0,
|
|
15388
|
+
serviceOffsetManual: false,
|
|
14973
15389
|
follow: true,
|
|
14974
15390
|
logOffset: 0,
|
|
14975
15391
|
unseenLines: 0
|
|
@@ -14988,6 +15404,7 @@ function reduceFleetUiState(state, action) {
|
|
|
14988
15404
|
service: service.name,
|
|
14989
15405
|
endpoint: undefined
|
|
14990
15406
|
},
|
|
15407
|
+
serviceOffsetManual: false,
|
|
14991
15408
|
follow: true,
|
|
14992
15409
|
logOffset: 0,
|
|
14993
15410
|
unseenLines: 0
|
|
@@ -15003,6 +15420,8 @@ function reduceFleetUiState(state, action) {
|
|
|
15003
15420
|
service: stack.services[0]?.name,
|
|
15004
15421
|
endpoint: undefined
|
|
15005
15422
|
},
|
|
15423
|
+
serviceOffset: 0,
|
|
15424
|
+
serviceOffsetManual: false,
|
|
15006
15425
|
follow: true,
|
|
15007
15426
|
logOffset: 0,
|
|
15008
15427
|
unseenLines: 0
|
|
@@ -15020,6 +15439,7 @@ function reduceFleetUiState(state, action) {
|
|
|
15020
15439
|
service: action.service,
|
|
15021
15440
|
endpoint: undefined
|
|
15022
15441
|
},
|
|
15442
|
+
serviceOffsetManual: false,
|
|
15023
15443
|
follow: true,
|
|
15024
15444
|
logOffset: 0,
|
|
15025
15445
|
unseenLines: 0
|
|
@@ -15033,7 +15453,8 @@ function reduceFleetUiState(state, action) {
|
|
|
15033
15453
|
return {
|
|
15034
15454
|
...state,
|
|
15035
15455
|
focus: "services",
|
|
15036
|
-
selection: { ...state.selection, service: action.service, endpoint: action.endpoint }
|
|
15456
|
+
selection: { ...state.selection, service: action.service, endpoint: action.endpoint },
|
|
15457
|
+
serviceOffsetManual: false
|
|
15037
15458
|
};
|
|
15038
15459
|
}
|
|
15039
15460
|
case "focus":
|
|
@@ -15047,10 +15468,14 @@ function reduceFleetUiState(state, action) {
|
|
|
15047
15468
|
...action.snapshot,
|
|
15048
15469
|
stacks: visibleFleetStacks(action.snapshot, action.filter)
|
|
15049
15470
|
};
|
|
15471
|
+
const selection2 = reconcileFleetSelection(state.selection, visibleSnapshot2);
|
|
15472
|
+
const selectionChanged2 = selection2.project !== state.selection.project || selection2.service !== state.selection.service || selection2.endpoint !== state.selection.endpoint;
|
|
15050
15473
|
return {
|
|
15051
15474
|
...state,
|
|
15052
15475
|
filter: action.filter,
|
|
15053
|
-
selection:
|
|
15476
|
+
selection: selection2,
|
|
15477
|
+
serviceOffset: selection2.project === state.selection.project ? state.serviceOffset : 0,
|
|
15478
|
+
serviceOffsetManual: selectionChanged2 ? false : state.serviceOffsetManual
|
|
15054
15479
|
};
|
|
15055
15480
|
}
|
|
15056
15481
|
case "follow":
|
|
@@ -15060,6 +15485,12 @@ function reduceFleetUiState(state, action) {
|
|
|
15060
15485
|
logOffset: action.follow ? 0 : state.logOffset,
|
|
15061
15486
|
unseenLines: action.follow ? 0 : state.unseenLines
|
|
15062
15487
|
};
|
|
15488
|
+
case "scroll-services":
|
|
15489
|
+
return {
|
|
15490
|
+
...state,
|
|
15491
|
+
serviceOffset: Math.min(action.maxOffset, Math.max(0, state.serviceOffset + action.delta)),
|
|
15492
|
+
serviceOffsetManual: true
|
|
15493
|
+
};
|
|
15063
15494
|
case "scroll-logs": {
|
|
15064
15495
|
const offset = Math.min(action.maxOffset ?? Number.MAX_SAFE_INTEGER, Math.max(0, state.logOffset - action.delta));
|
|
15065
15496
|
return action.delta > 0 && offset === 0 ? { ...state, follow: true, logOffset: 0, unseenLines: 0 } : { ...state, follow: action.delta > 0 ? state.follow : false, logOffset: offset };
|
|
@@ -15280,6 +15711,9 @@ var init_string_width = __esm(() => {
|
|
|
15280
15711
|
});
|
|
15281
15712
|
|
|
15282
15713
|
// packages/tui/src/fleet-text.ts
|
|
15714
|
+
function fleetTextWidth(text) {
|
|
15715
|
+
return stringWidth(text);
|
|
15716
|
+
}
|
|
15283
15717
|
function fitFleetText(text, width) {
|
|
15284
15718
|
if (width <= 0)
|
|
15285
15719
|
return "";
|
|
@@ -15456,6 +15890,18 @@ function isScrollTopKey(key) {
|
|
|
15456
15890
|
function isDoctorKey(key) {
|
|
15457
15891
|
return isShiftedKey(key, "d");
|
|
15458
15892
|
}
|
|
15893
|
+
function isPageUpKey(key) {
|
|
15894
|
+
return key.name.toLowerCase() === "pageup";
|
|
15895
|
+
}
|
|
15896
|
+
function isPageDownKey(key) {
|
|
15897
|
+
return key.name.toLowerCase() === "pagedown";
|
|
15898
|
+
}
|
|
15899
|
+
function isHalfPageUpKey(key) {
|
|
15900
|
+
return key.ctrl === true && key.name.toLowerCase() === "u";
|
|
15901
|
+
}
|
|
15902
|
+
function isHalfPageDownKey(key) {
|
|
15903
|
+
return key.ctrl === true && key.name.toLowerCase() === "d";
|
|
15904
|
+
}
|
|
15459
15905
|
|
|
15460
15906
|
// packages/tui/src/fleet-layout.ts
|
|
15461
15907
|
function resolveFleetLayout(mode, terminalWidth) {
|
|
@@ -15520,6 +15966,43 @@ var init_fleet_log_rows = __esm(() => {
|
|
|
15520
15966
|
wrapCache = new WeakMap;
|
|
15521
15967
|
});
|
|
15522
15968
|
|
|
15969
|
+
// packages/tui/src/fleet-service-rows.ts
|
|
15970
|
+
function buildFleetServiceRows(stack) {
|
|
15971
|
+
if (stack === undefined)
|
|
15972
|
+
return [];
|
|
15973
|
+
return stack.services.flatMap((service) => {
|
|
15974
|
+
const endpoints = serviceEndpoints(service);
|
|
15975
|
+
return [
|
|
15976
|
+
{ kind: "workload", service },
|
|
15977
|
+
...endpoints.map((endpoint, endpointIndex) => ({
|
|
15978
|
+
kind: "endpoint",
|
|
15979
|
+
service,
|
|
15980
|
+
endpoint,
|
|
15981
|
+
endpointIndex
|
|
15982
|
+
}))
|
|
15983
|
+
];
|
|
15984
|
+
});
|
|
15985
|
+
}
|
|
15986
|
+
function fleetServiceRowCount(stack) {
|
|
15987
|
+
return buildFleetServiceRows(stack).length;
|
|
15988
|
+
}
|
|
15989
|
+
function selectedFleetServiceRowIndex(rows, serviceName, endpointName) {
|
|
15990
|
+
return rows.findIndex((row2) => row2.service.name === serviceName && (endpointName === undefined ? row2.kind === "workload" : row2.kind === "endpoint" && row2.endpoint.name === endpointName));
|
|
15991
|
+
}
|
|
15992
|
+
function ensureFleetServiceRowVisible(offset, selectedIndex, viewportRows, rowCount) {
|
|
15993
|
+
const maxOffset = Math.max(0, rowCount - viewportRows);
|
|
15994
|
+
const clamped = Math.min(maxOffset, Math.max(0, offset));
|
|
15995
|
+
if (selectedIndex < 0)
|
|
15996
|
+
return clamped;
|
|
15997
|
+
if (selectedIndex < clamped)
|
|
15998
|
+
return selectedIndex;
|
|
15999
|
+
if (selectedIndex >= clamped + viewportRows) {
|
|
16000
|
+
return Math.min(maxOffset, selectedIndex - viewportRows + 1);
|
|
16001
|
+
}
|
|
16002
|
+
return clamped;
|
|
16003
|
+
}
|
|
16004
|
+
var init_fleet_service_rows = () => {};
|
|
16005
|
+
|
|
15523
16006
|
// packages/tui/src/fleet-format.ts
|
|
15524
16007
|
function formatUptime(createdAt, now) {
|
|
15525
16008
|
const born = Date.parse(createdAt);
|
|
@@ -15701,6 +16184,15 @@ var init_StackSidebar = __esm(() => {
|
|
|
15701
16184
|
});
|
|
15702
16185
|
});
|
|
15703
16186
|
|
|
16187
|
+
// packages/tui/src/fleet-scroll.ts
|
|
16188
|
+
function fleetMouseScrollDelta(event, rows = 3) {
|
|
16189
|
+
if (event.scroll?.direction === "up")
|
|
16190
|
+
return -rows;
|
|
16191
|
+
if (event.scroll?.direction === "down")
|
|
16192
|
+
return rows;
|
|
16193
|
+
return;
|
|
16194
|
+
}
|
|
16195
|
+
|
|
15704
16196
|
// packages/tui/src/components/ServicePane.tsx
|
|
15705
16197
|
import { memo as memo2 } from "react";
|
|
15706
16198
|
import { jsxDEV as jsxDEV2, Fragment } from "@opentui/react/jsx-dev-runtime";
|
|
@@ -15711,6 +16203,7 @@ function portLabel(service) {
|
|
|
15711
16203
|
}
|
|
15712
16204
|
var ServicePane;
|
|
15713
16205
|
var init_ServicePane = __esm(() => {
|
|
16206
|
+
init_fleet_service_rows();
|
|
15714
16207
|
init_fleet_text();
|
|
15715
16208
|
init_fleet_theme();
|
|
15716
16209
|
init_terminal_text();
|
|
@@ -15719,10 +16212,13 @@ var init_ServicePane = __esm(() => {
|
|
|
15719
16212
|
uptime,
|
|
15720
16213
|
selectedService,
|
|
15721
16214
|
selectedEndpoint,
|
|
16215
|
+
offset = 0,
|
|
16216
|
+
viewportRows = 1,
|
|
15722
16217
|
width = 80,
|
|
15723
16218
|
focused = false,
|
|
15724
16219
|
onSelectService,
|
|
15725
|
-
onSelectEndpoint
|
|
16220
|
+
onSelectEndpoint,
|
|
16221
|
+
onScroll
|
|
15726
16222
|
}) {
|
|
15727
16223
|
const nameWidth = Math.min(24, Math.max(12, Math.floor(width * 0.28)));
|
|
15728
16224
|
const backendWidth = 10;
|
|
@@ -15730,7 +16226,26 @@ var init_ServicePane = __esm(() => {
|
|
|
15730
16226
|
const portWidth = 7;
|
|
15731
16227
|
const title = stack === undefined ? "Workloads" : `Workloads \u2014 ${sanitizeFleetTerminalText(stack.branch)}`;
|
|
15732
16228
|
const warning = stack?.warning === undefined ? undefined : sanitizeFleetTerminalText(stack.warning);
|
|
16229
|
+
const rows = buildFleetServiceRows(stack);
|
|
16230
|
+
const maxOffset = Math.max(0, rows.length - viewportRows);
|
|
16231
|
+
const clampedOffset = Math.min(maxOffset, Math.max(0, offset));
|
|
16232
|
+
const visibleRows = rows.slice(clampedOffset, clampedOffset + viewportRows);
|
|
16233
|
+
const hiddenAbove = clampedOffset;
|
|
16234
|
+
const hiddenBelow = Math.max(0, rows.length - clampedOffset - visibleRows.length);
|
|
16235
|
+
const overflowIndicator = (index) => {
|
|
16236
|
+
const first = index === 0 && hiddenAbove > 0 ? `\u2191 ${hiddenAbove} more` : undefined;
|
|
16237
|
+
const last = index === visibleRows.length - 1 && hiddenBelow > 0 ? `\u2193 ${hiddenBelow} more` : undefined;
|
|
16238
|
+
return [first, last].filter(Boolean).join(" \xB7 ") || undefined;
|
|
16239
|
+
};
|
|
15733
16240
|
return /* @__PURE__ */ jsxDEV2("box", {
|
|
16241
|
+
onMouseScroll: (event) => {
|
|
16242
|
+
const delta = fleetMouseScrollDelta(event);
|
|
16243
|
+
if (delta === undefined)
|
|
16244
|
+
return;
|
|
16245
|
+
event.preventDefault();
|
|
16246
|
+
event.stopPropagation();
|
|
16247
|
+
onScroll?.(delta);
|
|
16248
|
+
},
|
|
15734
16249
|
style: {
|
|
15735
16250
|
height: "100%",
|
|
15736
16251
|
width: "100%",
|
|
@@ -15790,97 +16305,106 @@ var init_ServicePane = __esm(() => {
|
|
|
15790
16305
|
}, undefined, false, undefined, this)
|
|
15791
16306
|
]
|
|
15792
16307
|
}, undefined, true, undefined, this),
|
|
15793
|
-
|
|
16308
|
+
visibleRows.map((row2, visibleIndex) => {
|
|
16309
|
+
const service = row2.service;
|
|
15794
16310
|
const workloadSelected = service.name === selectedService && selectedEndpoint === undefined;
|
|
15795
|
-
const endpoints = serviceEndpoints(service);
|
|
15796
16311
|
const state = sanitizeFleetTerminalText(service.state);
|
|
15797
16312
|
const workloadBg = workloadSelected ? fleetTheme.selectedBg : fleetTheme.background;
|
|
15798
|
-
const
|
|
15799
|
-
|
|
15800
|
-
if (event.button !== 0)
|
|
15801
|
-
return;
|
|
15802
|
-
event.preventDefault();
|
|
15803
|
-
event.stopPropagation();
|
|
15804
|
-
onSelectService?.(service.name);
|
|
15805
|
-
},
|
|
15806
|
-
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: workloadBg },
|
|
15807
|
-
children: [
|
|
15808
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15809
|
-
fg: fleetTheme.stripe,
|
|
15810
|
-
children: workloadSelected ? "\u258E" : " "
|
|
15811
|
-
}, undefined, false, undefined, this),
|
|
15812
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15813
|
-
fg: serviceStateColor(service.state),
|
|
15814
|
-
children: [
|
|
15815
|
-
" ",
|
|
15816
|
-
serviceStateGlyph(service.state),
|
|
15817
|
-
" "
|
|
15818
|
-
]
|
|
15819
|
-
}, undefined, true, undefined, this),
|
|
15820
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15821
|
-
fg: workloadSelected ? fleetTheme.bright : fleetTheme.text,
|
|
15822
|
-
children: padFleetText(sanitizeFleetTerminalText(service.name), nameWidth)
|
|
15823
|
-
}, undefined, false, undefined, this),
|
|
15824
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15825
|
-
fg: backendColor(service.backend),
|
|
15826
|
-
children: padFleetText(service.backend, backendWidth)
|
|
15827
|
-
}, undefined, false, undefined, this),
|
|
15828
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15829
|
-
fg: serviceStateColor(service.state),
|
|
15830
|
-
children: padFleetText(state, stateWidth)
|
|
15831
|
-
}, undefined, false, undefined, this),
|
|
15832
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
15833
|
-
fg: fleetTheme.muted,
|
|
15834
|
-
children: padFleetText(portLabel(service), portWidth)
|
|
15835
|
-
}, undefined, false, undefined, this)
|
|
15836
|
-
]
|
|
15837
|
-
}, `workload:${service.name}`, true, undefined, this);
|
|
15838
|
-
const endpointRows = endpoints.map((endpoint, index) => {
|
|
15839
|
-
const endpointSelected = service.name === selectedService && endpoint.name === selectedEndpoint;
|
|
15840
|
-
const connector = index === endpoints.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
15841
|
-
const kind = (endpoint.kind ?? "http").toUpperCase();
|
|
15842
|
-
const reach = sanitizeFleetTerminalText(endpointReach(endpoint));
|
|
15843
|
-
const aliasWidth = Math.min(18, Math.max(10, Math.floor(width * 0.22)));
|
|
15844
|
-
const pub = endpoint.publicUrl === undefined ? "" : " pub";
|
|
15845
|
-
const endpointBg = endpointSelected ? fleetTheme.selectedBg : fleetTheme.background;
|
|
16313
|
+
const indicator = overflowIndicator(visibleIndex);
|
|
16314
|
+
if (row2.kind === "workload")
|
|
15846
16315
|
return /* @__PURE__ */ jsxDEV2("box", {
|
|
15847
16316
|
onMouseDown: (event) => {
|
|
15848
16317
|
if (event.button !== 0)
|
|
15849
16318
|
return;
|
|
15850
16319
|
event.preventDefault();
|
|
15851
16320
|
event.stopPropagation();
|
|
15852
|
-
|
|
16321
|
+
onSelectService?.(service.name);
|
|
15853
16322
|
},
|
|
15854
|
-
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor:
|
|
16323
|
+
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: workloadBg },
|
|
15855
16324
|
children: [
|
|
15856
16325
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15857
16326
|
fg: fleetTheme.stripe,
|
|
15858
|
-
children:
|
|
16327
|
+
children: workloadSelected ? "\u258E" : " "
|
|
15859
16328
|
}, undefined, false, undefined, this),
|
|
15860
16329
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15861
|
-
fg:
|
|
15862
|
-
children:
|
|
15863
|
-
|
|
16330
|
+
fg: serviceStateColor(service.state),
|
|
16331
|
+
children: [
|
|
16332
|
+
" ",
|
|
16333
|
+
serviceStateGlyph(service.state),
|
|
16334
|
+
" "
|
|
16335
|
+
]
|
|
16336
|
+
}, undefined, true, undefined, this),
|
|
15864
16337
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15865
|
-
fg:
|
|
15866
|
-
children: padFleetText(sanitizeFleetTerminalText(
|
|
16338
|
+
fg: workloadSelected ? fleetTheme.bright : fleetTheme.text,
|
|
16339
|
+
children: padFleetText(sanitizeFleetTerminalText(service.name), nameWidth)
|
|
15867
16340
|
}, undefined, false, undefined, this),
|
|
15868
16341
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15869
|
-
fg:
|
|
15870
|
-
children: padFleetText(
|
|
16342
|
+
fg: backendColor(service.backend),
|
|
16343
|
+
children: padFleetText(service.backend, backendWidth)
|
|
15871
16344
|
}, undefined, false, undefined, this),
|
|
15872
16345
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15873
|
-
fg:
|
|
15874
|
-
children:
|
|
16346
|
+
fg: serviceStateColor(service.state),
|
|
16347
|
+
children: padFleetText(state, stateWidth)
|
|
15875
16348
|
}, undefined, false, undefined, this),
|
|
15876
16349
|
/* @__PURE__ */ jsxDEV2("text", {
|
|
15877
|
-
fg: fleetTheme.
|
|
15878
|
-
children:
|
|
16350
|
+
fg: fleetTheme.muted,
|
|
16351
|
+
children: padFleetText(portLabel(service), portWidth)
|
|
16352
|
+
}, undefined, false, undefined, this),
|
|
16353
|
+
indicator === undefined ? null : /* @__PURE__ */ jsxDEV2("text", {
|
|
16354
|
+
fg: fleetTheme.faint,
|
|
16355
|
+
children: indicator
|
|
15879
16356
|
}, undefined, false, undefined, this)
|
|
15880
16357
|
]
|
|
15881
|
-
}, `
|
|
15882
|
-
|
|
15883
|
-
|
|
16358
|
+
}, `workload:${service.name}`, true, undefined, this);
|
|
16359
|
+
const endpoint = row2.endpoint;
|
|
16360
|
+
const endpointSelected = service.name === selectedService && endpoint.name === selectedEndpoint;
|
|
16361
|
+
const endpointCount = service.endpoints?.length ?? (service.endpoint === undefined ? 0 : 1);
|
|
16362
|
+
const connector = row2.endpointIndex === endpointCount - 1 ? "\u2514\u2500" : "\u251C\u2500";
|
|
16363
|
+
const kind = (endpoint.kind ?? "http").toUpperCase();
|
|
16364
|
+
const reach = sanitizeFleetTerminalText(endpointReach(endpoint));
|
|
16365
|
+
const aliasWidth = Math.min(18, Math.max(10, Math.floor(width * 0.22)));
|
|
16366
|
+
const pub = endpoint.publicUrl === undefined ? "" : " pub";
|
|
16367
|
+
const endpointBg = endpointSelected ? fleetTheme.selectedBg : fleetTheme.background;
|
|
16368
|
+
return /* @__PURE__ */ jsxDEV2("box", {
|
|
16369
|
+
onMouseDown: (event) => {
|
|
16370
|
+
if (event.button !== 0)
|
|
16371
|
+
return;
|
|
16372
|
+
event.preventDefault();
|
|
16373
|
+
event.stopPropagation();
|
|
16374
|
+
onSelectEndpoint?.(service.name, endpoint.name);
|
|
16375
|
+
},
|
|
16376
|
+
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: endpointBg },
|
|
16377
|
+
children: [
|
|
16378
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16379
|
+
fg: fleetTheme.stripe,
|
|
16380
|
+
children: endpointSelected ? "\u258E" : " "
|
|
16381
|
+
}, undefined, false, undefined, this),
|
|
16382
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16383
|
+
fg: fleetTheme.faint,
|
|
16384
|
+
children: ` ${connector} `
|
|
16385
|
+
}, undefined, false, undefined, this),
|
|
16386
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16387
|
+
fg: endpointSelected ? fleetTheme.bright : fleetTheme.accent,
|
|
16388
|
+
children: padFleetText(sanitizeFleetTerminalText(endpoint.name), aliasWidth)
|
|
16389
|
+
}, undefined, false, undefined, this),
|
|
16390
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16391
|
+
fg: fleetTheme.muted,
|
|
16392
|
+
children: padFleetText(kind, 6)
|
|
16393
|
+
}, undefined, false, undefined, this),
|
|
16394
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16395
|
+
fg: fleetTheme.text,
|
|
16396
|
+
children: fitFleetText(reach, Math.max(12, width - aliasWidth - 19 - pub.length))
|
|
16397
|
+
}, undefined, false, undefined, this),
|
|
16398
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
16399
|
+
fg: fleetTheme.publicBadge,
|
|
16400
|
+
children: pub
|
|
16401
|
+
}, undefined, false, undefined, this),
|
|
16402
|
+
indicator === undefined ? null : /* @__PURE__ */ jsxDEV2("text", {
|
|
16403
|
+
fg: fleetTheme.faint,
|
|
16404
|
+
children: indicator
|
|
16405
|
+
}, undefined, false, undefined, this)
|
|
16406
|
+
]
|
|
16407
|
+
}, `endpoint:${service.name}:${endpoint.name}`, true, undefined, this);
|
|
15884
16408
|
})
|
|
15885
16409
|
]
|
|
15886
16410
|
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV2("box", {
|
|
@@ -15914,14 +16438,20 @@ function LogPane({
|
|
|
15914
16438
|
const end = Math.max(0, rows.length - clamped);
|
|
15915
16439
|
const start = Math.max(0, end - viewportRows);
|
|
15916
16440
|
const visible = rows.slice(start, end);
|
|
15917
|
-
const
|
|
16441
|
+
const position = visible.length === 0 ? "0/0" : `${start + 1}\u2013${end}/${rows.length}`;
|
|
16442
|
+
const rawStatus = follow ? "following" : `paused \xB7 ${position}${unseen > 0 ? ` \xB7 ${unseen} new` : ""}`;
|
|
16443
|
+
const headerWidth = Math.max(1, width - 4);
|
|
16444
|
+
const status = fitFleetText(rawStatus, Math.max(1, headerWidth - Math.min(8, headerWidth - 1)));
|
|
16445
|
+
const titleWidth = Math.max(0, headerWidth - fleetTextWidth(status));
|
|
16446
|
+
const title = fitFleetText(`Logs \u2014 ${label}`, titleWidth);
|
|
15918
16447
|
return /* @__PURE__ */ jsxDEV3("box", {
|
|
15919
16448
|
onMouseScroll: (event) => {
|
|
15920
|
-
|
|
16449
|
+
const delta = fleetMouseScrollDelta(event);
|
|
16450
|
+
if (delta === undefined)
|
|
15921
16451
|
return;
|
|
15922
16452
|
event.preventDefault();
|
|
15923
16453
|
event.stopPropagation();
|
|
15924
|
-
onScroll?.(
|
|
16454
|
+
onScroll?.(delta);
|
|
15925
16455
|
},
|
|
15926
16456
|
style: {
|
|
15927
16457
|
height: "100%",
|
|
@@ -15937,11 +16467,11 @@ function LogPane({
|
|
|
15937
16467
|
children: [
|
|
15938
16468
|
/* @__PURE__ */ jsxDEV3("text", {
|
|
15939
16469
|
fg: focused ? fleetTheme.accent : fleetTheme.text,
|
|
15940
|
-
children: padFleetText(title,
|
|
16470
|
+
children: padFleetText(title, titleWidth)
|
|
15941
16471
|
}, undefined, false, undefined, this),
|
|
15942
16472
|
/* @__PURE__ */ jsxDEV3("text", {
|
|
15943
16473
|
fg: follow ? fleetTheme.healthy : fleetTheme.warning,
|
|
15944
|
-
children:
|
|
16474
|
+
children: status
|
|
15945
16475
|
}, undefined, false, undefined, this)
|
|
15946
16476
|
]
|
|
15947
16477
|
}, undefined, true, undefined, this),
|
|
@@ -16058,9 +16588,6 @@ function emptySnapshot(repoId) {
|
|
|
16058
16588
|
function selectedStack(snapshot, project) {
|
|
16059
16589
|
return snapshot.stacks.find((stack) => stack.project === project);
|
|
16060
16590
|
}
|
|
16061
|
-
function fleetServiceRowCount(stack) {
|
|
16062
|
-
return stack?.services.reduce((count, service) => count + 1 + serviceEndpoints(service).length, 0) ?? 0;
|
|
16063
|
-
}
|
|
16064
16591
|
function selectedFleetEndpoint(stack, serviceName, endpointName) {
|
|
16065
16592
|
const service = stack?.services.find((candidate) => candidate.name === serviceName);
|
|
16066
16593
|
const endpoints = service === undefined ? [] : serviceEndpoints(service);
|
|
@@ -16222,6 +16749,11 @@ function FleetApp({
|
|
|
16222
16749
|
const svcRows = fleetServiceRowCount(stack) + warningRows;
|
|
16223
16750
|
const stackedSidebarHeight = stackSidebarHeight(stacks.length);
|
|
16224
16751
|
const svcPaneH = servicePaneHeight(svcRows, effectiveLayout === "split" ? 14 : 12);
|
|
16752
|
+
const serviceViewportRows = Math.max(1, svcPaneH - 4 - warningRows);
|
|
16753
|
+
const serviceRows = useMemo(() => buildFleetServiceRows(stack), [stack]);
|
|
16754
|
+
const maxServiceOffset = Math.max(0, serviceRows.length - serviceViewportRows);
|
|
16755
|
+
const selectedServiceRowIndex = selectedFleetServiceRowIndex(serviceRows, state.selection.service, state.selection.endpoint);
|
|
16756
|
+
const effectiveServiceOffset = ensureFleetServiceRowVisible(state.serviceOffset, state.serviceOffsetManual ? -1 : selectedServiceRowIndex, serviceViewportRows, serviceRows.length);
|
|
16225
16757
|
const logHeight = Math.max(5, terminal.height - headerRows - contextRows - footerRows - svcPaneH - (effectiveLayout === "stack" ? stackedSidebarHeight : 0));
|
|
16226
16758
|
const logWidth = effectiveLayout === "split" ? paneWidths.main : terminal.width;
|
|
16227
16759
|
const logViewportRows = Math.max(1, logHeight - 3);
|
|
@@ -16277,7 +16809,7 @@ function FleetApp({
|
|
|
16277
16809
|
source.down(confirmed).then(() => {
|
|
16278
16810
|
dispatch({ type: "down-pending", pending: false });
|
|
16279
16811
|
dispatch({ type: "confirm-down" });
|
|
16280
|
-
showToast(`${project} is down;
|
|
16812
|
+
showToast(`${project} is down; volumes and project images removed`);
|
|
16281
16813
|
}).catch((error) => {
|
|
16282
16814
|
dispatch({ type: "down-pending", pending: false });
|
|
16283
16815
|
showToast(`down failed: ${error.message}`);
|
|
@@ -16395,6 +16927,21 @@ function FleetApp({
|
|
|
16395
16927
|
if (isScrollTopKey(key)) {
|
|
16396
16928
|
return dispatch({ type: "scroll-logs", delta: -logRows.length, maxOffset: maxLogOffset });
|
|
16397
16929
|
}
|
|
16930
|
+
if (current.focus === "logs") {
|
|
16931
|
+
if (isPageUpKey(key)) {
|
|
16932
|
+
return dispatch({ type: "scroll-logs", delta: -logViewportRows, maxOffset: maxLogOffset });
|
|
16933
|
+
}
|
|
16934
|
+
if (isPageDownKey(key)) {
|
|
16935
|
+
return dispatch({ type: "scroll-logs", delta: logViewportRows, maxOffset: maxLogOffset });
|
|
16936
|
+
}
|
|
16937
|
+
const halfPageRows = Math.max(1, Math.floor(logViewportRows / 2));
|
|
16938
|
+
if (isHalfPageUpKey(key)) {
|
|
16939
|
+
return dispatch({ type: "scroll-logs", delta: -halfPageRows, maxOffset: maxLogOffset });
|
|
16940
|
+
}
|
|
16941
|
+
if (isHalfPageDownKey(key)) {
|
|
16942
|
+
return dispatch({ type: "scroll-logs", delta: halfPageRows, maxOffset: maxLogOffset });
|
|
16943
|
+
}
|
|
16944
|
+
}
|
|
16398
16945
|
if (isDoctorKey(key)) {
|
|
16399
16946
|
if (doctorRunning)
|
|
16400
16947
|
return;
|
|
@@ -16471,10 +17018,17 @@ function FleetApp({
|
|
|
16471
17018
|
uptime,
|
|
16472
17019
|
selectedService: state.selection.service,
|
|
16473
17020
|
selectedEndpoint: state.selection.endpoint,
|
|
17021
|
+
offset: effectiveServiceOffset,
|
|
17022
|
+
viewportRows: serviceViewportRows,
|
|
16474
17023
|
width: logWidth,
|
|
16475
17024
|
focused: state.focus === "services",
|
|
16476
17025
|
onSelectService,
|
|
16477
|
-
onSelectEndpoint
|
|
17026
|
+
onSelectEndpoint,
|
|
17027
|
+
onScroll: (delta) => dispatch({
|
|
17028
|
+
type: "scroll-services",
|
|
17029
|
+
delta: delta + effectiveServiceOffset - state.serviceOffset,
|
|
17030
|
+
maxOffset: maxServiceOffset
|
|
17031
|
+
})
|
|
16478
17032
|
}, undefined, false, undefined, this);
|
|
16479
17033
|
const sidebar = (width) => /* @__PURE__ */ jsxDEV5(StackSidebar, {
|
|
16480
17034
|
stacks,
|
|
@@ -16814,7 +17368,7 @@ function FleetApp({
|
|
|
16814
17368
|
}, undefined, false, undefined, this),
|
|
16815
17369
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
16816
17370
|
fg: fleetTheme.warning,
|
|
16817
|
-
children: "
|
|
17371
|
+
children: "Removes named volumes and project-built images (data loss)."
|
|
16818
17372
|
}, undefined, false, undefined, this),
|
|
16819
17373
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
16820
17374
|
fg: fleetTheme.danger,
|
|
@@ -16832,6 +17386,7 @@ var init_FleetApp = __esm(() => {
|
|
|
16832
17386
|
init_fleet_text();
|
|
16833
17387
|
init_fleet_theme();
|
|
16834
17388
|
init_fleet_log_rows();
|
|
17389
|
+
init_fleet_service_rows();
|
|
16835
17390
|
init_fleet_format();
|
|
16836
17391
|
init_fleet_status();
|
|
16837
17392
|
init_StackSidebar();
|
|
@@ -16843,12 +17398,13 @@ var init_FleetApp = __esm(() => {
|
|
|
16843
17398
|
["j k \u2191\u2193", "move in focused pane \xB7 Tab cycles panes"],
|
|
16844
17399
|
[", . [ ]", "previous / next stack \xB7 service"],
|
|
16845
17400
|
["/", "filter stacks (Esc clears, then exits)"],
|
|
16846
|
-
["f \xB7 g G", "
|
|
17401
|
+
["f \xB7 g G \xB7 PgUp PgDn", "follow \xB7 top / bottom \xB7 page logs"],
|
|
17402
|
+
["^U ^D", "scroll logs by half a page"],
|
|
16847
17403
|
["o", "open selected endpoint in browser"],
|
|
16848
17404
|
["y \xB7 Y", "yank endpoint URL \xB7 stack env block"],
|
|
16849
17405
|
["c l p", "yank direct / local / public URL"],
|
|
16850
17406
|
["s", "shared hostnames (claim, allow, deny, release)"],
|
|
16851
|
-
["D \xB7 d", "doctor report \xB7 down stack (volumes
|
|
17407
|
+
["D \xB7 d", "doctor report \xB7 down stack (removes volumes + images)"],
|
|
16852
17408
|
["1 2 0", "split / stacked / auto layout"],
|
|
16853
17409
|
["q", "quit \xB7 mouse click and wheel work everywhere"]
|
|
16854
17410
|
];
|
|
@@ -16925,7 +17481,7 @@ async function runFleetTui(cwd = process.cwd()) {
|
|
|
16925
17481
|
openConsoleOnError: false
|
|
16926
17482
|
});
|
|
16927
17483
|
const root = createRoot(renderer);
|
|
16928
|
-
await new Promise((
|
|
17484
|
+
await new Promise((resolve5, reject) => {
|
|
16929
17485
|
let shuttingDown = false;
|
|
16930
17486
|
let removeInterrupt = () => {};
|
|
16931
17487
|
let removeSuspend = () => {};
|
|
@@ -16947,7 +17503,7 @@ async function runFleetTui(cwd = process.cwd()) {
|
|
|
16947
17503
|
if (error !== undefined)
|
|
16948
17504
|
reject(error);
|
|
16949
17505
|
else
|
|
16950
|
-
|
|
17506
|
+
resolve5();
|
|
16951
17507
|
};
|
|
16952
17508
|
for (const signal of signals)
|
|
16953
17509
|
process.once(signal, shutdown);
|
|
@@ -17011,9 +17567,9 @@ init_src2();
|
|
|
17011
17567
|
init_src();
|
|
17012
17568
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
17013
17569
|
import { existsSync as existsSync24 } from "fs";
|
|
17014
|
-
import { dirname as
|
|
17570
|
+
import { dirname as dirname9, join as join27 } from "path";
|
|
17015
17571
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
17016
|
-
var CLI_VERSION = "1.
|
|
17572
|
+
var CLI_VERSION = "1.3.0";
|
|
17017
17573
|
var PACKAGE_NAME = "@tridha643/hestia";
|
|
17018
17574
|
function compareVersions(a, b) {
|
|
17019
17575
|
const parse = (value) => {
|
|
@@ -17420,7 +17976,8 @@ usage:
|
|
|
17420
17976
|
health. Exit 1 only on error-level rows; never mutates anything
|
|
17421
17977
|
hestia stop <name> [--json] stop one supervised proc (idempotent)
|
|
17422
17978
|
hestia down [--destroy] [--project <name>] [--json]
|
|
17423
|
-
tear down procs + containers (--destroy also removes volumes
|
|
17979
|
+
tear down procs + containers (--destroy also removes volumes
|
|
17980
|
+
and project-built images);
|
|
17424
17981
|
--project works from the mirror after the worktree is deleted
|
|
17425
17982
|
hestia status [--json] show this worktree's stack
|
|
17426
17983
|
hestia env [--json] print the injected env (export lines)
|
|
@@ -17505,10 +18062,10 @@ async function main() {
|
|
|
17505
18062
|
case "skill": {
|
|
17506
18063
|
if (flags._[1] !== "path")
|
|
17507
18064
|
fail("usage", "usage: hestia skill path", flags.json);
|
|
17508
|
-
const packageRoot =
|
|
18065
|
+
const packageRoot = dirname9(dirname9(fileURLToPath3(import.meta.url)));
|
|
17509
18066
|
const candidates = [
|
|
17510
|
-
|
|
17511
|
-
|
|
18067
|
+
join27(packageRoot, "skills", "hestia", "SKILL.md"),
|
|
18068
|
+
join27(process.cwd(), "skills", "hestia", "SKILL.md")
|
|
17512
18069
|
];
|
|
17513
18070
|
const path = candidates.find(existsSync24);
|
|
17514
18071
|
if (path === undefined)
|
|
@@ -18181,4 +18738,4 @@ export {
|
|
|
18181
18738
|
compareVersions
|
|
18182
18739
|
};
|
|
18183
18740
|
|
|
18184
|
-
//# debugId=
|
|
18741
|
+
//# debugId=EE213D14626327FC64756E2164756E21
|