@tridha643/hestia 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2381 -604
- package/dist/cli.js.map +33 -24
- package/dist/daemon.js +983 -109
- package/dist/daemon.js.map +17 -14
- package/package.json +1 -1
- package/skills/hestia/SKILL.md +72 -10
package/dist/cli.js
CHANGED
|
@@ -7158,6 +7158,19 @@ import {
|
|
|
7158
7158
|
} from "fs";
|
|
7159
7159
|
import { dirname as dirname2 } from "path";
|
|
7160
7160
|
import { randomUUID } from "crypto";
|
|
7161
|
+
function fsyncDir(path) {
|
|
7162
|
+
let fd;
|
|
7163
|
+
try {
|
|
7164
|
+
fd = openSync(dirname2(path), "r");
|
|
7165
|
+
} catch {
|
|
7166
|
+
return;
|
|
7167
|
+
}
|
|
7168
|
+
try {
|
|
7169
|
+
fsyncSync(fd);
|
|
7170
|
+
} catch {} finally {
|
|
7171
|
+
closeSync(fd);
|
|
7172
|
+
}
|
|
7173
|
+
}
|
|
7161
7174
|
function writeAtomicJsonFile(path, value, options = {}) {
|
|
7162
7175
|
mkdirSync(dirname2(path), { recursive: true });
|
|
7163
7176
|
const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
@@ -7179,6 +7192,7 @@ function writeAtomicJsonFile(path, value, options = {}) {
|
|
|
7179
7192
|
rmSync(temporaryPath, { force: true });
|
|
7180
7193
|
throw error;
|
|
7181
7194
|
}
|
|
7195
|
+
fsyncDir(path);
|
|
7182
7196
|
}
|
|
7183
7197
|
function writeAtomicTextFile(path, source, mode = 384) {
|
|
7184
7198
|
mkdirSync(dirname2(path), { recursive: true });
|
|
@@ -7200,6 +7214,7 @@ function writeAtomicTextFile(path, source, mode = 384) {
|
|
|
7200
7214
|
rmSync(temporaryPath, { force: true });
|
|
7201
7215
|
throw error;
|
|
7202
7216
|
}
|
|
7217
|
+
fsyncDir(path);
|
|
7203
7218
|
}
|
|
7204
7219
|
var init_atomic_json_file = () => {};
|
|
7205
7220
|
|
|
@@ -9252,6 +9267,39 @@ async function releaseSlot(port, project) {
|
|
|
9252
9267
|
});
|
|
9253
9268
|
} catch {}
|
|
9254
9269
|
}
|
|
9270
|
+
async function sharedVerb(port, verb, body) {
|
|
9271
|
+
const timeoutMs = (body.waitMs ?? 0) + 5000;
|
|
9272
|
+
let response;
|
|
9273
|
+
try {
|
|
9274
|
+
response = await fetch(`http://127.0.0.1:${port}/hestia/shared/${verb}`, {
|
|
9275
|
+
method: "POST",
|
|
9276
|
+
headers: { "content-type": "application/json", ...daemonAuthHeaders(port) },
|
|
9277
|
+
body: JSON.stringify(body),
|
|
9278
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
9279
|
+
});
|
|
9280
|
+
} catch (error) {
|
|
9281
|
+
throw new HestiaError("daemon-unreachable", `could not reach hestiad on 127.0.0.1:${port}: ${error.message}`);
|
|
9282
|
+
}
|
|
9283
|
+
const payload = await response.json().catch(() => ({}));
|
|
9284
|
+
if (!response.ok) {
|
|
9285
|
+
throw new HestiaError(payload.code ?? "daemon-unreachable", payload.error ?? `daemon rejected shared ${verb}: HTTP ${response.status}`);
|
|
9286
|
+
}
|
|
9287
|
+
return {
|
|
9288
|
+
granted: payload.granted === true,
|
|
9289
|
+
holder: payload.holder,
|
|
9290
|
+
queued: payload.queued ?? []
|
|
9291
|
+
};
|
|
9292
|
+
}
|
|
9293
|
+
async function releaseSharedForProject(port, project, service) {
|
|
9294
|
+
try {
|
|
9295
|
+
await fetch(`http://127.0.0.1:${port}/hestia/shared/release-project`, {
|
|
9296
|
+
method: "POST",
|
|
9297
|
+
headers: { "content-type": "application/json", ...daemonAuthHeaders(port) },
|
|
9298
|
+
body: JSON.stringify({ project, service }),
|
|
9299
|
+
signal: AbortSignal.timeout(2000)
|
|
9300
|
+
});
|
|
9301
|
+
} catch {}
|
|
9302
|
+
}
|
|
9255
9303
|
var DAEMON_PIDFILE_NAME = "hestiad", MAX_NDJSON_FRAME_BYTES;
|
|
9256
9304
|
var init_client = __esm(() => {
|
|
9257
9305
|
init_src();
|
|
@@ -9575,6 +9623,7 @@ class Admission {
|
|
|
9575
9623
|
}
|
|
9576
9624
|
var HESTIAD_PROTOCOL_VERSION = 5, MAX_JSON_BODY_BYTES;
|
|
9577
9625
|
var init_routes = __esm(() => {
|
|
9626
|
+
init_src();
|
|
9578
9627
|
init_state();
|
|
9579
9628
|
init_slots();
|
|
9580
9629
|
MAX_JSON_BODY_BYTES = 16 * 1024;
|
|
@@ -10155,25 +10204,182 @@ var init_portless_adapter = __esm(() => {
|
|
|
10155
10204
|
HESTIA_ROUTER_RUNTIME_STATE_DIR = join15(HESTIA_ROUTER_INSTALL_DIR, "state");
|
|
10156
10205
|
});
|
|
10157
10206
|
|
|
10207
|
+
// packages/engine/src/tunnel/shared.ts
|
|
10208
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync8 } from "fs";
|
|
10209
|
+
import { join as join16 } from "path";
|
|
10210
|
+
function sharedRoot() {
|
|
10211
|
+
return join16(hestiaHome(), "shared");
|
|
10212
|
+
}
|
|
10213
|
+
function sharedPath(name) {
|
|
10214
|
+
return join16(sharedRoot(), `${name}.json`);
|
|
10215
|
+
}
|
|
10216
|
+
function assertSharedName(name) {
|
|
10217
|
+
if (!NAME_RE.test(name)) {
|
|
10218
|
+
throw new HestiaError("usage", `shared hostname handle ${JSON.stringify(name)} must be a DNS label: ` + `lowercase alphanumerics and hyphens, at most 63 characters`);
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
function assertSharedHostnameFqdn(hostname) {
|
|
10222
|
+
const labels = hostname.split(".");
|
|
10223
|
+
const valid = hostname.length <= 253 && hostname === hostname.toLowerCase() && labels.length >= 2 && labels.every((label) => LABEL_RE.test(label));
|
|
10224
|
+
if (!valid) {
|
|
10225
|
+
throw new HestiaError("usage", `shared hostname ${JSON.stringify(hostname)} must be a fully-qualified ` + `lowercase domain (e.g. slack.acme.com)`);
|
|
10226
|
+
}
|
|
10227
|
+
}
|
|
10228
|
+
function normalizeSharedPath(input) {
|
|
10229
|
+
if (input === undefined)
|
|
10230
|
+
return;
|
|
10231
|
+
const trimmed = input.trim();
|
|
10232
|
+
const segments = trimmed.split("/").filter((segment) => segment !== "");
|
|
10233
|
+
for (const segment of segments) {
|
|
10234
|
+
if (segment === "." || segment === ".." || !PATH_SEGMENT_RE.test(segment)) {
|
|
10235
|
+
throw new HestiaError("usage", `shared hostname path ${JSON.stringify(input)} must be a URL path prefix ` + `like /webhooks/slack (no query, fragment, or ".." segments)`);
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
if (segments.length === 0)
|
|
10239
|
+
return;
|
|
10240
|
+
return `/${segments.join("/")}`;
|
|
10241
|
+
}
|
|
10242
|
+
function sharedPathMatches(prefix, requestPath) {
|
|
10243
|
+
if (prefix === undefined)
|
|
10244
|
+
return true;
|
|
10245
|
+
return requestPath === prefix || requestPath.startsWith(`${prefix}/`);
|
|
10246
|
+
}
|
|
10247
|
+
function withSharedLock(fn) {
|
|
10248
|
+
return withLock(sharedRoot(), fn);
|
|
10249
|
+
}
|
|
10250
|
+
function parseShared(source) {
|
|
10251
|
+
let value;
|
|
10252
|
+
try {
|
|
10253
|
+
value = JSON.parse(source);
|
|
10254
|
+
} catch {
|
|
10255
|
+
return null;
|
|
10256
|
+
}
|
|
10257
|
+
if (value.schemaVersion !== STATE_SCHEMA_VERSION || typeof value.name !== "string" || !NAME_RE.test(value.name) || typeof value.hostname !== "string" || value.hostname.length === 0 || typeof value.tunnelUuid !== "string" || value.tunnelUuid.length === 0 || typeof value.zone !== "string" || value.zone.length === 0 || typeof value.service !== "string" || value.service.length === 0 || typeof value.createdAt !== "string")
|
|
10258
|
+
return null;
|
|
10259
|
+
if (value.path !== undefined && (typeof value.path !== "string" || !value.path.startsWith("/"))) {
|
|
10260
|
+
return null;
|
|
10261
|
+
}
|
|
10262
|
+
if (value.holder !== undefined) {
|
|
10263
|
+
const holder = value.holder;
|
|
10264
|
+
if (typeof holder.project !== "string" || typeof holder.worktree !== "string" || typeof holder.service !== "string" || typeof holder.at !== "string")
|
|
10265
|
+
return null;
|
|
10266
|
+
}
|
|
10267
|
+
if (value.queue !== undefined) {
|
|
10268
|
+
if (!Array.isArray(value.queue))
|
|
10269
|
+
return null;
|
|
10270
|
+
for (const entry of value.queue) {
|
|
10271
|
+
if (typeof entry !== "object" || entry === null || typeof entry.project !== "string" || typeof entry.worktree !== "string" || typeof entry.at !== "string")
|
|
10272
|
+
return null;
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
return value;
|
|
10276
|
+
}
|
|
10277
|
+
function readSharedHostname(name) {
|
|
10278
|
+
const p = sharedPath(name);
|
|
10279
|
+
if (!existsSync13(p))
|
|
10280
|
+
return null;
|
|
10281
|
+
try {
|
|
10282
|
+
return parseShared(readFileSync13(p, "utf8"));
|
|
10283
|
+
} catch {
|
|
10284
|
+
return null;
|
|
10285
|
+
}
|
|
10286
|
+
}
|
|
10287
|
+
function listSharedHostnames() {
|
|
10288
|
+
const root = sharedRoot();
|
|
10289
|
+
if (!existsSync13(root))
|
|
10290
|
+
return [];
|
|
10291
|
+
const records = [];
|
|
10292
|
+
for (const entry of readdirSync4(root).sort()) {
|
|
10293
|
+
if (!entry.endsWith(".json"))
|
|
10294
|
+
continue;
|
|
10295
|
+
const record = readSharedHostname(entry.slice(0, -".json".length));
|
|
10296
|
+
if (record !== null && `${record.name}.json` === entry)
|
|
10297
|
+
records.push(record);
|
|
10298
|
+
}
|
|
10299
|
+
return records;
|
|
10300
|
+
}
|
|
10301
|
+
async function declareSharedHostname(record) {
|
|
10302
|
+
assertSharedName(record.name);
|
|
10303
|
+
assertSharedHostnameFqdn(record.hostname);
|
|
10304
|
+
const path = normalizeSharedPath(record.path);
|
|
10305
|
+
return withSharedLock(async () => {
|
|
10306
|
+
const collision = listSharedHostnames().find((candidate) => candidate.name !== record.name && candidate.hostname === record.hostname && (candidate.path ?? undefined) === (path ?? undefined));
|
|
10307
|
+
if (collision !== undefined) {
|
|
10308
|
+
throw new HestiaError("shared-conflict", `${record.hostname}${path ?? ""} is already declared as shared hostname ` + `"${collision.name}" \u2014 pick a distinct path or handle`);
|
|
10309
|
+
}
|
|
10310
|
+
const existing = readSharedHostname(record.name);
|
|
10311
|
+
if (existing !== null) {
|
|
10312
|
+
if (existing.hostname !== record.hostname || existing.tunnelUuid !== record.tunnelUuid || (existing.path ?? undefined) !== (path ?? undefined)) {
|
|
10313
|
+
throw new HestiaError("shared-conflict", `shared hostname "${record.name}" already declared as ${existing.hostname}` + `${existing.path ?? ""} on tunnel ${existing.tunnelUuid} \u2014 release and remove it before re-pointing`);
|
|
10314
|
+
}
|
|
10315
|
+
const merged = { ...existing, service: record.service, zone: record.zone };
|
|
10316
|
+
writeAtomicJsonFile(sharedPath(record.name), merged);
|
|
10317
|
+
return merged;
|
|
10318
|
+
}
|
|
10319
|
+
const fresh = {
|
|
10320
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
10321
|
+
...record,
|
|
10322
|
+
createdAt: new Date().toISOString()
|
|
10323
|
+
};
|
|
10324
|
+
if (path === undefined)
|
|
10325
|
+
delete fresh.path;
|
|
10326
|
+
else
|
|
10327
|
+
fresh.path = path;
|
|
10328
|
+
writeAtomicJsonFile(sharedPath(record.name), fresh);
|
|
10329
|
+
return fresh;
|
|
10330
|
+
});
|
|
10331
|
+
}
|
|
10332
|
+
async function updateSharedHostname(name, mutate) {
|
|
10333
|
+
return withSharedLock(async () => {
|
|
10334
|
+
const existing = readSharedHostname(name);
|
|
10335
|
+
if (existing === null)
|
|
10336
|
+
return null;
|
|
10337
|
+
const next = mutate(structuredClone(existing));
|
|
10338
|
+
if (next.queue !== undefined && next.queue.length === 0)
|
|
10339
|
+
delete next.queue;
|
|
10340
|
+
if (next.holder === undefined)
|
|
10341
|
+
delete next.holder;
|
|
10342
|
+
writeAtomicJsonFile(sharedPath(name), next);
|
|
10343
|
+
return next;
|
|
10344
|
+
});
|
|
10345
|
+
}
|
|
10346
|
+
var NAME_RE, LABEL_RE, PATH_SEGMENT_RE;
|
|
10347
|
+
var init_shared = __esm(() => {
|
|
10348
|
+
init_src();
|
|
10349
|
+
init_state();
|
|
10350
|
+
init_lock();
|
|
10351
|
+
init_atomic_json_file();
|
|
10352
|
+
NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
10353
|
+
LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10354
|
+
PATH_SEGMENT_RE = /^[A-Za-z0-9._~!$&'()*+,;=:@%-]+$/;
|
|
10355
|
+
});
|
|
10356
|
+
|
|
10158
10357
|
// packages/engine/src/router/local-http-router.ts
|
|
10159
10358
|
import { execFile as execFile7 } from "child_process";
|
|
10160
10359
|
import { Agent, createServer as createServer2, request as httpRequest } from "http";
|
|
10161
10360
|
import { createConnection as createConnection2, createServer as createNetServer } from "net";
|
|
10162
10361
|
import { promisify as promisify6 } from "util";
|
|
10163
|
-
import { existsSync as
|
|
10164
|
-
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as
|
|
10362
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
10363
|
+
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as rmSync9 } from "fs";
|
|
10165
10364
|
import { tmpdir as tmpdir2 } from "os";
|
|
10166
|
-
import { join as
|
|
10365
|
+
import { join as join17 } from "path";
|
|
10167
10366
|
import { createHash as createHash5 } from "crypto";
|
|
10367
|
+
function pathnameOf(target) {
|
|
10368
|
+
if (target === undefined || target.length === 0)
|
|
10369
|
+
return "/";
|
|
10370
|
+
const cut = target.search(/[?#]/);
|
|
10371
|
+
const path = cut < 0 ? target : target.slice(0, cut);
|
|
10372
|
+
return path.startsWith("/") ? path : "/";
|
|
10373
|
+
}
|
|
10168
10374
|
function readRouterStackRecords() {
|
|
10169
|
-
const stacksDir =
|
|
10170
|
-
if (!
|
|
10375
|
+
const stacksDir = join17(hestiaHome(), "stacks");
|
|
10376
|
+
if (!existsSync14(stacksDir))
|
|
10171
10377
|
return [];
|
|
10172
10378
|
const records = [];
|
|
10173
|
-
for (const project of
|
|
10174
|
-
const path =
|
|
10379
|
+
for (const project of readdirSync5(stacksDir).sort()) {
|
|
10380
|
+
const path = join17(stacksDir, project, "stack.json");
|
|
10175
10381
|
try {
|
|
10176
|
-
const source =
|
|
10382
|
+
const source = readFileSync14(path);
|
|
10177
10383
|
if (source.byteLength > MAX_MIRROR_BYTES)
|
|
10178
10384
|
continue;
|
|
10179
10385
|
const record = parseStackRecord(source.toString("utf8"), path);
|
|
@@ -10325,11 +10531,11 @@ function internalEndpointAuthority(project, service) {
|
|
|
10325
10531
|
function publicGatewaySocketPath() {
|
|
10326
10532
|
const uid = process.getuid?.() ?? 501;
|
|
10327
10533
|
const homeHash = createHash5("sha256").update(hestiaHome()).digest("hex").slice(0, 12);
|
|
10328
|
-
return
|
|
10534
|
+
return join17(tmpdir2(), `hestia-${uid}`, homeHash, "origin.sock");
|
|
10329
10535
|
}
|
|
10330
10536
|
function prepareGatewaySocket(path) {
|
|
10331
10537
|
const uid = process.getuid?.();
|
|
10332
|
-
const directories = [
|
|
10538
|
+
const directories = [join17(tmpdir2(), `hestia-${uid ?? 501}`), join17(tmpdir2(), `hestia-${uid ?? 501}`, createHash5("sha256").update(hestiaHome()).digest("hex").slice(0, 12))];
|
|
10333
10539
|
for (const directory of directories) {
|
|
10334
10540
|
mkdirSync7(directory, { recursive: true, mode: 448 });
|
|
10335
10541
|
const stat2 = lstatSync(directory);
|
|
@@ -10338,13 +10544,13 @@ function prepareGatewaySocket(path) {
|
|
|
10338
10544
|
}
|
|
10339
10545
|
chmodSync5(directory, 448);
|
|
10340
10546
|
}
|
|
10341
|
-
if (!
|
|
10547
|
+
if (!existsSync14(path))
|
|
10342
10548
|
return;
|
|
10343
10549
|
const stat = lstatSync(path);
|
|
10344
10550
|
if (stat.isSymbolicLink() || !stat.isSocket() || uid !== undefined && stat.uid !== uid) {
|
|
10345
10551
|
throw new Error(`refusing unsafe existing Hestia gateway socket ${path}`);
|
|
10346
10552
|
}
|
|
10347
|
-
|
|
10553
|
+
rmSync9(path);
|
|
10348
10554
|
}
|
|
10349
10555
|
function sendRouterResponse(response, status, message) {
|
|
10350
10556
|
response.writeHead(status, { "content-type": "text/plain; charset=utf-8", "x-hestia-router": "1" });
|
|
@@ -10357,6 +10563,7 @@ class HestiaLocalHttpRouter {
|
|
|
10357
10563
|
#frontServer = createNetServer((socket) => this.#acceptFrontSocket(socket));
|
|
10358
10564
|
#unixServer = createNetServer((socket) => this.#acceptFrontSocket(socket));
|
|
10359
10565
|
#targets = new Map;
|
|
10566
|
+
#sharedRoutes = new Map;
|
|
10360
10567
|
#timer;
|
|
10361
10568
|
async start() {
|
|
10362
10569
|
await new Promise((resolve2, reject) => {
|
|
@@ -10460,12 +10667,52 @@ class HestiaLocalHttpRouter {
|
|
|
10460
10667
|
}
|
|
10461
10668
|
}
|
|
10462
10669
|
}
|
|
10670
|
+
const sharedRoutes = new Map;
|
|
10671
|
+
for (const shared of listSharedHostnames()) {
|
|
10672
|
+
const hostname = shared.hostname.toLowerCase();
|
|
10673
|
+
targets.delete(hostname);
|
|
10674
|
+
const holder = shared.holder;
|
|
10675
|
+
let target;
|
|
10676
|
+
if (holder !== undefined) {
|
|
10677
|
+
const stack = records.find((record) => record.project === holder.project);
|
|
10678
|
+
const endpoint = stack?.endpoints.find((candidate2) => (candidate2.alias ?? candidate2.name) === holder.service);
|
|
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;
|
|
10682
|
+
const candidate = {
|
|
10683
|
+
hostname,
|
|
10684
|
+
project: holder.project,
|
|
10685
|
+
service: service === undefined || service.backend === "tunnel" || publishedPort === undefined ? undefined : { ...service, publishedPort }
|
|
10686
|
+
};
|
|
10687
|
+
const previous = this.#sharedRoutes.get(hostname)?.find((entry) => entry.name === shared.name)?.target;
|
|
10688
|
+
target = previous !== undefined && targetIdentity(previous) === targetIdentity(candidate) ? previous : candidate;
|
|
10689
|
+
}
|
|
10690
|
+
const list = sharedRoutes.get(hostname) ?? [];
|
|
10691
|
+
list.push({ path: shared.path, name: shared.name, claimed: holder !== undefined, target });
|
|
10692
|
+
sharedRoutes.set(hostname, list);
|
|
10693
|
+
}
|
|
10694
|
+
for (const list of sharedRoutes.values()) {
|
|
10695
|
+
list.sort((left, right) => (right.path?.length ?? -1) - (left.path?.length ?? -1));
|
|
10696
|
+
}
|
|
10463
10697
|
reconcilePortlessAliases([...targets.keys()].filter((hostname) => hostname.endsWith(".localhost")), this.port);
|
|
10464
|
-
|
|
10465
|
-
|
|
10698
|
+
const retained = new Set(targets.values());
|
|
10699
|
+
for (const list of sharedRoutes.values()) {
|
|
10700
|
+
for (const entry of list)
|
|
10701
|
+
if (entry.target !== undefined)
|
|
10702
|
+
retained.add(entry.target);
|
|
10703
|
+
}
|
|
10704
|
+
const priorTargets = [...this.#targets.values()];
|
|
10705
|
+
for (const list of this.#sharedRoutes.values()) {
|
|
10706
|
+
for (const entry of list)
|
|
10707
|
+
if (entry.target !== undefined)
|
|
10708
|
+
priorTargets.push(entry.target);
|
|
10709
|
+
}
|
|
10710
|
+
for (const target of priorTargets) {
|
|
10711
|
+
if (!retained.has(target))
|
|
10466
10712
|
target.agent?.destroy();
|
|
10467
10713
|
}
|
|
10468
10714
|
this.#targets = targets;
|
|
10715
|
+
this.#sharedRoutes = sharedRoutes;
|
|
10469
10716
|
}
|
|
10470
10717
|
stop() {
|
|
10471
10718
|
if (this.#timer)
|
|
@@ -10479,7 +10726,7 @@ class HestiaLocalHttpRouter {
|
|
|
10479
10726
|
target.agent?.destroy();
|
|
10480
10727
|
this.#frontServer.close();
|
|
10481
10728
|
this.#unixServer.close();
|
|
10482
|
-
|
|
10729
|
+
rmSync9(this.socketPath, { force: true });
|
|
10483
10730
|
this.#server.close();
|
|
10484
10731
|
}
|
|
10485
10732
|
#acceptFrontSocket(socket) {
|
|
@@ -10524,16 +10771,40 @@ Connection: close\r
|
|
|
10524
10771
|
`));
|
|
10525
10772
|
socket.once("error", () => internal.destroy());
|
|
10526
10773
|
}
|
|
10774
|
+
#matchTarget(hostname, requestPath) {
|
|
10775
|
+
const routes = this.#sharedRoutes.get(hostname);
|
|
10776
|
+
if (routes !== undefined) {
|
|
10777
|
+
const entry = routes.find((candidate) => sharedPathMatches(candidate.path, requestPath));
|
|
10778
|
+
if (entry === undefined)
|
|
10779
|
+
return { status: 404, message: "Hestia route not found" };
|
|
10780
|
+
if (!entry.claimed) {
|
|
10781
|
+
return {
|
|
10782
|
+
status: 503,
|
|
10783
|
+
message: `Shared hostname unclaimed \u2014 run \`hestia claim ${entry.name}\` in the worktree that should receive this traffic`
|
|
10784
|
+
};
|
|
10785
|
+
}
|
|
10786
|
+
if (entry.target?.service?.publishedPort === undefined) {
|
|
10787
|
+
return { status: 503, message: "Hestia route origin unavailable" };
|
|
10788
|
+
}
|
|
10789
|
+
return { target: entry.target };
|
|
10790
|
+
}
|
|
10791
|
+
const target = this.#targets.get(hostname);
|
|
10792
|
+
if (target === undefined)
|
|
10793
|
+
return { status: 404, message: "Hestia route not found" };
|
|
10794
|
+
if (target.service?.publishedPort === undefined) {
|
|
10795
|
+
return { status: 503, message: "Hestia route origin unavailable" };
|
|
10796
|
+
}
|
|
10797
|
+
return { target };
|
|
10798
|
+
}
|
|
10527
10799
|
async#proxyHttp(request, response) {
|
|
10528
10800
|
const hostname = parseRouteAuthority(request.headers.host);
|
|
10529
10801
|
if (hostname === null)
|
|
10530
10802
|
return sendRouterResponse(response, 400, "Malformed Host authority");
|
|
10531
|
-
const
|
|
10532
|
-
if (
|
|
10533
|
-
return sendRouterResponse(response,
|
|
10534
|
-
const
|
|
10535
|
-
|
|
10536
|
-
return sendRouterResponse(response, 503, "Hestia route origin unavailable");
|
|
10803
|
+
const match = this.#matchTarget(hostname, pathnameOf(request.url));
|
|
10804
|
+
if ("status" in match)
|
|
10805
|
+
return sendRouterResponse(response, match.status, match.message);
|
|
10806
|
+
const target = match.target;
|
|
10807
|
+
const port = target.service.publishedPort;
|
|
10537
10808
|
if (await verifyLocalRouterTarget(target) !== port) {
|
|
10538
10809
|
return sendRouterResponse(response, 503, "Hestia route origin unavailable");
|
|
10539
10810
|
}
|
|
@@ -10574,16 +10845,19 @@ Connection: close\r
|
|
|
10574
10845
|
`);
|
|
10575
10846
|
return;
|
|
10576
10847
|
}
|
|
10577
|
-
const
|
|
10578
|
-
|
|
10579
|
-
|
|
10580
|
-
|
|
10848
|
+
const rawHeaderLines = header.slice(0, -4).split(`\r
|
|
10849
|
+
`);
|
|
10850
|
+
const requestPath = pathnameOf(rawHeaderLines[0]?.split(" ")[1]);
|
|
10851
|
+
const match = this.#matchTarget(host, requestPath);
|
|
10852
|
+
if ("status" in match) {
|
|
10853
|
+
const reason = match.status === 404 ? "404 Not Found" : "503 Service Unavailable";
|
|
10854
|
+
socket.end(`HTTP/1.1 ${reason}\r
|
|
10581
10855
|
\r
|
|
10582
10856
|
`);
|
|
10583
10857
|
return;
|
|
10584
10858
|
}
|
|
10585
|
-
const
|
|
10586
|
-
|
|
10859
|
+
const target = match.target;
|
|
10860
|
+
const port = target.service.publishedPort;
|
|
10587
10861
|
const connectionValues = rawHeaderLines.slice(1).filter((line) => /^connection:/i.test(line)).map((line) => line.slice(line.indexOf(":") + 1));
|
|
10588
10862
|
const connectionNames = connectionHeaderTokens(connectionValues);
|
|
10589
10863
|
const retainedHeaders = rawHeaderLines.slice(1).filter((line) => {
|
|
@@ -10639,6 +10913,7 @@ var init_local_http_router = __esm(() => {
|
|
|
10639
10913
|
init_state();
|
|
10640
10914
|
init_router_config();
|
|
10641
10915
|
init_portless_adapter();
|
|
10916
|
+
init_shared();
|
|
10642
10917
|
pexec6 = promisify6(execFile7);
|
|
10643
10918
|
MAX_MIRROR_BYTES = 2 * 1024 * 1024;
|
|
10644
10919
|
MAX_REQUEST_HEADER_BYTES = 64 * 1024;
|
|
@@ -10657,15 +10932,15 @@ var init_local_http_router = __esm(() => {
|
|
|
10657
10932
|
});
|
|
10658
10933
|
|
|
10659
10934
|
// packages/engine/src/wrangler/adapter.ts
|
|
10660
|
-
import { existsSync as
|
|
10935
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15, statSync as statSync2 } from "fs";
|
|
10661
10936
|
import { homedir as homedir3 } from "os";
|
|
10662
|
-
import { dirname as dirname5, join as
|
|
10937
|
+
import { dirname as dirname5, join as join18, relative as relative3 } from "path";
|
|
10663
10938
|
import { connect } from "net";
|
|
10664
10939
|
function privateRegistryDir(worktreeRoot) {
|
|
10665
|
-
return
|
|
10940
|
+
return join18(worktreeRoot, ".hestia", "wrangler-registry");
|
|
10666
10941
|
}
|
|
10667
10942
|
function globalRegistryDir() {
|
|
10668
|
-
return process.env.WRANGLER_REGISTRY_PATH ??
|
|
10943
|
+
return process.env.WRANGLER_REGISTRY_PATH ?? join18(homedir3(), ".wrangler", "config", "registry");
|
|
10669
10944
|
}
|
|
10670
10945
|
function tcpConnectable(address, timeoutMs = 500) {
|
|
10671
10946
|
const [host, portStr] = address.split(":");
|
|
@@ -10688,19 +10963,19 @@ function tcpConnectable(address, timeoutMs = 500) {
|
|
|
10688
10963
|
}
|
|
10689
10964
|
async function preflightForeignSessions(workers) {
|
|
10690
10965
|
const dir = globalRegistryDir();
|
|
10691
|
-
if (!
|
|
10966
|
+
if (!existsSync15(dir))
|
|
10692
10967
|
return;
|
|
10693
10968
|
for (const w of workers) {
|
|
10694
10969
|
if (w.name === null)
|
|
10695
10970
|
continue;
|
|
10696
|
-
const entryPath =
|
|
10697
|
-
if (!
|
|
10971
|
+
const entryPath = join18(dir, w.name);
|
|
10972
|
+
if (!existsSync15(entryPath))
|
|
10698
10973
|
continue;
|
|
10699
10974
|
const freshMs = Date.now() - statSync2(entryPath).mtimeMs;
|
|
10700
10975
|
let live = freshMs < HEARTBEAT_FRESH_MS;
|
|
10701
10976
|
if (!live) {
|
|
10702
10977
|
try {
|
|
10703
|
-
const entry = JSON.parse(
|
|
10978
|
+
const entry = JSON.parse(readFileSync15(entryPath, "utf8"));
|
|
10704
10979
|
live = entry.debugPortAddress ? await tcpConnectable(entry.debugPortAddress) : false;
|
|
10705
10980
|
} catch {
|
|
10706
10981
|
live = false;
|
|
@@ -10723,8 +10998,8 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
10723
10998
|
const wranglerBinFor = (configPath) => {
|
|
10724
10999
|
let dir = dirname5(configPath);
|
|
10725
11000
|
for (;; ) {
|
|
10726
|
-
const candidate =
|
|
10727
|
-
if (
|
|
11001
|
+
const candidate = join18(dir, "node_modules", ".bin", "wrangler");
|
|
11002
|
+
if (existsSync15(candidate))
|
|
10728
11003
|
return candidate;
|
|
10729
11004
|
if (dir === worktreeRoot)
|
|
10730
11005
|
return null;
|
|
@@ -10795,11 +11070,11 @@ var init_adapter = __esm(() => {
|
|
|
10795
11070
|
});
|
|
10796
11071
|
|
|
10797
11072
|
// packages/engine/src/wrangler/verify.ts
|
|
10798
|
-
import { existsSync as
|
|
11073
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6 } from "fs";
|
|
10799
11074
|
function namesIn(dir) {
|
|
10800
|
-
if (!
|
|
11075
|
+
if (!existsSync16(dir))
|
|
10801
11076
|
return new Set;
|
|
10802
|
-
return new Set(
|
|
11077
|
+
return new Set(readdirSync6(dir));
|
|
10803
11078
|
}
|
|
10804
11079
|
function snapshotGlobalRegistry() {
|
|
10805
11080
|
return namesIn(globalRegistryDir());
|
|
@@ -10828,14 +11103,14 @@ var init_verify = __esm(() => {
|
|
|
10828
11103
|
// packages/engine/src/tunnel/cloudflared.ts
|
|
10829
11104
|
import { execFile as execFile8 } from "child_process";
|
|
10830
11105
|
import { promisify as promisify7 } from "util";
|
|
10831
|
-
import { existsSync as
|
|
11106
|
+
import { existsSync as existsSync17 } from "fs";
|
|
10832
11107
|
import { homedir as homedir4 } from "os";
|
|
10833
|
-
import { join as
|
|
11108
|
+
import { join as join19 } from "path";
|
|
10834
11109
|
function cloudflaredHome() {
|
|
10835
|
-
return process.env.HESTIA_CLOUDFLARED_HOME ??
|
|
11110
|
+
return process.env.HESTIA_CLOUDFLARED_HOME ?? join19(homedir4(), ".cloudflared");
|
|
10836
11111
|
}
|
|
10837
11112
|
function credFilePath(uuid) {
|
|
10838
|
-
return process.env.TUNNEL_CRED_FILE ??
|
|
11113
|
+
return process.env.TUNNEL_CRED_FILE ?? join19(cloudflaredHome(), `${uuid}.json`);
|
|
10839
11114
|
}
|
|
10840
11115
|
async function run(args, timeoutMs = 30000) {
|
|
10841
11116
|
try {
|
|
@@ -10869,7 +11144,7 @@ async function adoptTunnel(name) {
|
|
|
10869
11144
|
throw new HestiaError("tunnel-not-found", `no tunnel named "${name}" on this account \u2014 create it once with ` + `\`cloudflared tunnel create ${name}\``);
|
|
10870
11145
|
}
|
|
10871
11146
|
const credFile = credFilePath(entry.id);
|
|
10872
|
-
if (!
|
|
11147
|
+
if (!existsSync17(credFile)) {
|
|
10873
11148
|
throw new HestiaError("tunnel-auth-missing", `credentials for tunnel "${name}" not found at ${credFile} \u2014 ` + `run \`cloudflared tunnel create\` on this machine or copy the JSON`);
|
|
10874
11149
|
}
|
|
10875
11150
|
return { uuid: entry.id, credFile, connections: entry.connections.length };
|
|
@@ -10882,8 +11157,8 @@ var init_cloudflared = __esm(() => {
|
|
|
10882
11157
|
|
|
10883
11158
|
// packages/engine/src/tunnel/ingress.ts
|
|
10884
11159
|
import { createHash as createHash6 } from "crypto";
|
|
10885
|
-
import { existsSync as
|
|
10886
|
-
import { join as
|
|
11160
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
11161
|
+
import { join as join20 } from "path";
|
|
10887
11162
|
function hostnameFor(tunnelName, branch, service, zone) {
|
|
10888
11163
|
const label = `${slug(tunnelName)}-${slug(branch)}-${slug(service)}`;
|
|
10889
11164
|
if (label.length <= LABEL_MAX)
|
|
@@ -10901,12 +11176,12 @@ function inferZone(baseRules) {
|
|
|
10901
11176
|
return zones.size === 1 ? [...zones][0] : undefined;
|
|
10902
11177
|
}
|
|
10903
11178
|
function importBaseRules(uuid, name) {
|
|
10904
|
-
const p =
|
|
10905
|
-
if (!
|
|
11179
|
+
const p = join20(cloudflaredHome(), "config.yml");
|
|
11180
|
+
if (!existsSync18(p))
|
|
10906
11181
|
return [];
|
|
10907
11182
|
let parsed;
|
|
10908
11183
|
try {
|
|
10909
|
-
parsed = $parse(
|
|
11184
|
+
parsed = $parse(readFileSync16(p, "utf8"));
|
|
10910
11185
|
} catch {
|
|
10911
11186
|
return [];
|
|
10912
11187
|
}
|
|
@@ -10918,7 +11193,12 @@ function importBaseRules(uuid, name) {
|
|
|
10918
11193
|
return parsed.ingress.filter((r) => r.hostname !== undefined || r.path !== undefined);
|
|
10919
11194
|
}
|
|
10920
11195
|
function generateMergedConfig(opts) {
|
|
10921
|
-
assertDisjoint(opts.baseRules, opts.dynamicRules);
|
|
11196
|
+
assertDisjoint(opts.baseRules, opts.dynamicRules, opts.sharedRules ?? []);
|
|
11197
|
+
const sharedHostnames = [...new Set((opts.sharedRules ?? []).map((s) => s.hostname))];
|
|
11198
|
+
const shared = sharedHostnames.map((hostname) => ({
|
|
11199
|
+
hostname,
|
|
11200
|
+
service: `unix:${publicGatewaySocketPath()}`
|
|
11201
|
+
}));
|
|
10922
11202
|
const dynamic = opts.dynamicRules.map((d) => ({
|
|
10923
11203
|
hostname: d.hostname,
|
|
10924
11204
|
service: `unix:${publicGatewaySocketPath()}`,
|
|
@@ -10929,17 +11209,30 @@ function generateMergedConfig(opts) {
|
|
|
10929
11209
|
"credentials-file": opts.credFile,
|
|
10930
11210
|
ingress: [
|
|
10931
11211
|
...opts.baseRules,
|
|
11212
|
+
...shared,
|
|
10932
11213
|
...dynamic,
|
|
10933
11214
|
{ service: "http_status:404" }
|
|
10934
11215
|
]
|
|
10935
11216
|
});
|
|
10936
11217
|
}
|
|
10937
|
-
function assertDisjoint(base, dynamic) {
|
|
11218
|
+
function assertDisjoint(base, dynamic, shared) {
|
|
10938
11219
|
const seen = new Map;
|
|
10939
11220
|
for (const r of base) {
|
|
10940
11221
|
if (r.hostname !== undefined)
|
|
10941
11222
|
seen.set(r.hostname, "the user's static rules");
|
|
10942
11223
|
}
|
|
11224
|
+
const sharedByHost = new Map;
|
|
11225
|
+
for (const s of shared) {
|
|
11226
|
+
if (!sharedByHost.has(s.hostname))
|
|
11227
|
+
sharedByHost.set(s.hostname, s.name);
|
|
11228
|
+
}
|
|
11229
|
+
for (const [hostname, name] of sharedByHost) {
|
|
11230
|
+
const holder = seen.get(hostname);
|
|
11231
|
+
if (holder !== undefined) {
|
|
11232
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} ("${name}") is already claimed by ${holder}`);
|
|
11233
|
+
}
|
|
11234
|
+
seen.set(hostname, `shared hostname "${name}"`);
|
|
11235
|
+
}
|
|
10943
11236
|
for (const d of dynamic) {
|
|
10944
11237
|
const holder = seen.get(d.hostname);
|
|
10945
11238
|
if (holder !== undefined) {
|
|
@@ -10956,6 +11249,87 @@ var init_ingress = __esm(() => {
|
|
|
10956
11249
|
init_local_http_router();
|
|
10957
11250
|
});
|
|
10958
11251
|
|
|
11252
|
+
// packages/engine/src/tunnel/orphans.ts
|
|
11253
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
11254
|
+
import { join as join21 } from "path";
|
|
11255
|
+
function hestiaTunnelMarker(uuid) {
|
|
11256
|
+
return join21(hestiaHome(), "tunnel", uuid);
|
|
11257
|
+
}
|
|
11258
|
+
function parseLocalHestiaConnectors(psOutput, marker) {
|
|
11259
|
+
const rows = [];
|
|
11260
|
+
for (const line of psOutput.split(`
|
|
11261
|
+
`)) {
|
|
11262
|
+
const trimmed = line.trim();
|
|
11263
|
+
if (trimmed === "")
|
|
11264
|
+
continue;
|
|
11265
|
+
const m = trimmed.match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
11266
|
+
if (m === null)
|
|
11267
|
+
continue;
|
|
11268
|
+
const command = m[3];
|
|
11269
|
+
if (!command.includes(marker))
|
|
11270
|
+
continue;
|
|
11271
|
+
if (!/(^|[\/\s])cloudflared(\s|$)/.test(command))
|
|
11272
|
+
continue;
|
|
11273
|
+
rows.push({ pid: Number(m[1]), pgid: Number(m[2]), command });
|
|
11274
|
+
}
|
|
11275
|
+
return rows;
|
|
11276
|
+
}
|
|
11277
|
+
function listLocalHestiaConnectors(uuid) {
|
|
11278
|
+
let out;
|
|
11279
|
+
try {
|
|
11280
|
+
out = execFileSync4("ps", ["-Ao", "pid=,pgid=,command="], {
|
|
11281
|
+
encoding: "utf8",
|
|
11282
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
11283
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" }
|
|
11284
|
+
});
|
|
11285
|
+
} catch {
|
|
11286
|
+
return [];
|
|
11287
|
+
}
|
|
11288
|
+
return parseLocalHestiaConnectors(out, hestiaTunnelMarker(uuid));
|
|
11289
|
+
}
|
|
11290
|
+
async function reapOrphanConnectors(uuid, keep) {
|
|
11291
|
+
const procs = listLocalHestiaConnectors(uuid).filter((p) => {
|
|
11292
|
+
if (keep === undefined)
|
|
11293
|
+
return true;
|
|
11294
|
+
return p.pgid !== keep.pgid && p.pid !== keep.pid;
|
|
11295
|
+
});
|
|
11296
|
+
if (procs.length === 0)
|
|
11297
|
+
return { reapedGroups: 0, pids: [] };
|
|
11298
|
+
const groups = new Set(procs.map((p) => p.pgid));
|
|
11299
|
+
const pids = procs.map((p) => p.pid);
|
|
11300
|
+
for (const pgid of groups) {
|
|
11301
|
+
try {
|
|
11302
|
+
process.kill(-pgid, "SIGTERM");
|
|
11303
|
+
} catch {}
|
|
11304
|
+
}
|
|
11305
|
+
const deadline = Date.now() + REAP_GRACE_MS;
|
|
11306
|
+
while (Date.now() < deadline) {
|
|
11307
|
+
const still = listLocalHestiaConnectors(uuid).filter((p) => {
|
|
11308
|
+
if (keep === undefined)
|
|
11309
|
+
return true;
|
|
11310
|
+
return p.pgid !== keep.pgid && p.pid !== keep.pid;
|
|
11311
|
+
});
|
|
11312
|
+
if (still.length === 0)
|
|
11313
|
+
return { reapedGroups: groups.size, pids };
|
|
11314
|
+
await new Promise((r) => setTimeout(r, REAP_POLL_MS));
|
|
11315
|
+
}
|
|
11316
|
+
for (const p of listLocalHestiaConnectors(uuid)) {
|
|
11317
|
+
if (keep !== undefined && (p.pgid === keep.pgid || p.pid === keep.pid))
|
|
11318
|
+
continue;
|
|
11319
|
+
try {
|
|
11320
|
+
process.kill(-p.pgid, "SIGKILL");
|
|
11321
|
+
} catch {}
|
|
11322
|
+
try {
|
|
11323
|
+
process.kill(p.pid, "SIGKILL");
|
|
11324
|
+
} catch {}
|
|
11325
|
+
}
|
|
11326
|
+
return { reapedGroups: groups.size, pids };
|
|
11327
|
+
}
|
|
11328
|
+
var REAP_GRACE_MS = 5000, REAP_POLL_MS = 100;
|
|
11329
|
+
var init_orphans = __esm(() => {
|
|
11330
|
+
init_state();
|
|
11331
|
+
});
|
|
11332
|
+
|
|
10959
11333
|
// packages/engine/src/tunnel/verify.ts
|
|
10960
11334
|
async function get2(port, path) {
|
|
10961
11335
|
try {
|
|
@@ -11000,31 +11374,31 @@ var POLL_MS4 = 300;
|
|
|
11000
11374
|
|
|
11001
11375
|
// packages/engine/src/tunnel/registry.ts
|
|
11002
11376
|
import {
|
|
11003
|
-
existsSync as
|
|
11377
|
+
existsSync as existsSync19,
|
|
11004
11378
|
mkdirSync as mkdirSync8,
|
|
11005
|
-
readFileSync as
|
|
11006
|
-
readdirSync as
|
|
11379
|
+
readFileSync as readFileSync17,
|
|
11380
|
+
readdirSync as readdirSync7
|
|
11007
11381
|
} from "fs";
|
|
11008
|
-
import { join as
|
|
11382
|
+
import { join as join22 } from "path";
|
|
11009
11383
|
function tunnelDir(uuid) {
|
|
11010
|
-
return
|
|
11384
|
+
return join22(hestiaHome(), "tunnel", uuid);
|
|
11011
11385
|
}
|
|
11012
11386
|
function configPath(uuid) {
|
|
11013
|
-
return
|
|
11387
|
+
return join22(tunnelDir(uuid), "config.yml");
|
|
11014
11388
|
}
|
|
11015
11389
|
function adoptedMarker(uuid) {
|
|
11016
|
-
return
|
|
11390
|
+
return join22(tunnelDir(uuid), "adopted.json");
|
|
11017
11391
|
}
|
|
11018
11392
|
function isAdopted(uuid) {
|
|
11019
|
-
return
|
|
11393
|
+
return existsSync19(adoptedMarker(uuid));
|
|
11020
11394
|
}
|
|
11021
11395
|
function readAdopted(uuid) {
|
|
11022
11396
|
const p = adoptedMarker(uuid);
|
|
11023
|
-
if (!
|
|
11397
|
+
if (!existsSync19(p))
|
|
11024
11398
|
return null;
|
|
11025
11399
|
let marker;
|
|
11026
11400
|
try {
|
|
11027
|
-
marker = JSON.parse(
|
|
11401
|
+
marker = JSON.parse(readFileSync17(p, "utf8"));
|
|
11028
11402
|
} catch {
|
|
11029
11403
|
marker = {};
|
|
11030
11404
|
}
|
|
@@ -11032,14 +11406,14 @@ function readAdopted(uuid) {
|
|
|
11032
11406
|
return { uuid, name: marker.name, credFile: marker.credFile, reconstructed: false };
|
|
11033
11407
|
}
|
|
11034
11408
|
let name;
|
|
11035
|
-
const stacksDir =
|
|
11036
|
-
if (
|
|
11037
|
-
for (const project of
|
|
11038
|
-
const sp =
|
|
11039
|
-
if (!
|
|
11409
|
+
const stacksDir = join22(hestiaHome(), "stacks");
|
|
11410
|
+
if (existsSync19(stacksDir)) {
|
|
11411
|
+
for (const project of readdirSync7(stacksDir)) {
|
|
11412
|
+
const sp = join22(stacksDir, project, "stack.json");
|
|
11413
|
+
if (!existsSync19(sp))
|
|
11040
11414
|
continue;
|
|
11041
11415
|
try {
|
|
11042
|
-
const record = parseStackRecord(
|
|
11416
|
+
const record = parseStackRecord(readFileSync17(sp, "utf8"), sp);
|
|
11043
11417
|
if (record.tunnel?.uuid === uuid) {
|
|
11044
11418
|
name = record.tunnel.name;
|
|
11045
11419
|
break;
|
|
@@ -11055,24 +11429,24 @@ function readAdopted(uuid) {
|
|
|
11055
11429
|
};
|
|
11056
11430
|
}
|
|
11057
11431
|
function listAdopted() {
|
|
11058
|
-
const root =
|
|
11059
|
-
if (!
|
|
11432
|
+
const root = join22(hestiaHome(), "tunnel");
|
|
11433
|
+
if (!existsSync19(root))
|
|
11060
11434
|
return [];
|
|
11061
|
-
return
|
|
11435
|
+
return readdirSync7(root).filter((uuid) => isAdopted(uuid));
|
|
11062
11436
|
}
|
|
11063
11437
|
function collectDynamicRules(uuid) {
|
|
11064
|
-
const stacksDir =
|
|
11438
|
+
const stacksDir = join22(hestiaHome(), "stacks");
|
|
11065
11439
|
const rules = [];
|
|
11066
11440
|
const dropped = [];
|
|
11067
|
-
if (!
|
|
11441
|
+
if (!existsSync19(stacksDir))
|
|
11068
11442
|
return { rules, dropped };
|
|
11069
|
-
for (const project of
|
|
11070
|
-
const p =
|
|
11071
|
-
if (!
|
|
11443
|
+
for (const project of readdirSync7(stacksDir)) {
|
|
11444
|
+
const p = join22(stacksDir, project, "stack.json");
|
|
11445
|
+
if (!existsSync19(p))
|
|
11072
11446
|
continue;
|
|
11073
11447
|
let record;
|
|
11074
11448
|
try {
|
|
11075
|
-
record = parseStackRecord(
|
|
11449
|
+
record = parseStackRecord(readFileSync17(p, "utf8"), p);
|
|
11076
11450
|
} catch {
|
|
11077
11451
|
continue;
|
|
11078
11452
|
}
|
|
@@ -11101,20 +11475,26 @@ async function reconcileTunnel(ref, opts) {
|
|
|
11101
11475
|
for (const d of dropped) {
|
|
11102
11476
|
warnings.push(`exposure ${d.hostname} dropped \u2014 service "${d.service}" of ` + `${d.project} is not running (hostname now 404s)`);
|
|
11103
11477
|
}
|
|
11478
|
+
const sharedRules = listSharedHostnames().filter((record) => record.tunnelUuid === ref.uuid).map((record) => ({ name: record.name, hostname: record.hostname }));
|
|
11104
11479
|
const cfgPath = configPath(ref.uuid);
|
|
11105
11480
|
const nextConfig = generateMergedConfig({
|
|
11106
11481
|
uuid: ref.uuid,
|
|
11107
11482
|
credFile: ref.credFile,
|
|
11108
11483
|
baseRules,
|
|
11109
|
-
dynamicRules: rules
|
|
11484
|
+
dynamicRules: rules,
|
|
11485
|
+
sharedRules
|
|
11110
11486
|
});
|
|
11111
11487
|
const pf = readPidfile(dir, CONNECTOR);
|
|
11112
11488
|
const live = pf !== null && isLive(pf);
|
|
11113
|
-
const
|
|
11489
|
+
const { reapedGroups } = await reapOrphanConnectors(ref.uuid, live ? pf : undefined);
|
|
11490
|
+
if (reapedGroups > 0) {
|
|
11491
|
+
warnings.push(`reaped ${reapedGroups} orphan hestia connector process group(s) for this tunnel`);
|
|
11492
|
+
}
|
|
11493
|
+
const currentConfig = existsSync19(cfgPath) ? readFileSync17(cfgPath, "utf8") : null;
|
|
11114
11494
|
if (live && currentConfig === nextConfig) {
|
|
11115
11495
|
return { restarted: false, metricsPort: pf.port, ready: true, warnings };
|
|
11116
11496
|
}
|
|
11117
|
-
if (baseRules.length === 0 && rules.length === 0) {
|
|
11497
|
+
if (baseRules.length === 0 && rules.length === 0 && sharedRules.length === 0) {
|
|
11118
11498
|
if (pf !== null) {
|
|
11119
11499
|
await stopProcTree(pf);
|
|
11120
11500
|
removePidfile(dir, CONNECTOR);
|
|
@@ -11185,6 +11565,8 @@ var init_registry = __esm(() => {
|
|
|
11185
11565
|
init_pidfile();
|
|
11186
11566
|
init_shutdown();
|
|
11187
11567
|
init_ingress();
|
|
11568
|
+
init_shared();
|
|
11569
|
+
init_orphans();
|
|
11188
11570
|
init_atomic_json_file();
|
|
11189
11571
|
});
|
|
11190
11572
|
|
|
@@ -11623,10 +12005,230 @@ var init_stream = __esm(() => {
|
|
|
11623
12005
|
init_tail();
|
|
11624
12006
|
});
|
|
11625
12007
|
|
|
12008
|
+
// packages/engine/src/daemon/shared-arbiter.ts
|
|
12009
|
+
class SharedArbiter {
|
|
12010
|
+
onChange;
|
|
12011
|
+
#mutex = Promise.resolve();
|
|
12012
|
+
#polls = new Map;
|
|
12013
|
+
constructor(onChange) {
|
|
12014
|
+
this.onChange = onChange;
|
|
12015
|
+
}
|
|
12016
|
+
#locked(fn) {
|
|
12017
|
+
const next = this.#mutex.then(fn, fn);
|
|
12018
|
+
this.#mutex = next.catch(() => {});
|
|
12019
|
+
return next;
|
|
12020
|
+
}
|
|
12021
|
+
async#notify() {
|
|
12022
|
+
try {
|
|
12023
|
+
await this.onChange?.();
|
|
12024
|
+
} catch {}
|
|
12025
|
+
}
|
|
12026
|
+
#result(record, granted) {
|
|
12027
|
+
return { granted, holder: record.holder, queued: [...record.queue ?? []] };
|
|
12028
|
+
}
|
|
12029
|
+
#wake(name, project, result) {
|
|
12030
|
+
const key = `${name}\x00${project}`;
|
|
12031
|
+
for (const poll of this.#polls.get(key) ?? []) {
|
|
12032
|
+
clearTimeout(poll.timer);
|
|
12033
|
+
poll.resolve(result);
|
|
12034
|
+
}
|
|
12035
|
+
this.#polls.delete(key);
|
|
12036
|
+
}
|
|
12037
|
+
#grantNext(record) {
|
|
12038
|
+
const queue = [...record.queue ?? []];
|
|
12039
|
+
for (;; ) {
|
|
12040
|
+
const head = queue.shift();
|
|
12041
|
+
if (head === undefined) {
|
|
12042
|
+
const next = { ...record, queue: [] };
|
|
12043
|
+
delete next.holder;
|
|
12044
|
+
return next;
|
|
12045
|
+
}
|
|
12046
|
+
if (readMirrorState(head.project) === null)
|
|
12047
|
+
continue;
|
|
12048
|
+
return {
|
|
12049
|
+
...record,
|
|
12050
|
+
holder: {
|
|
12051
|
+
project: head.project,
|
|
12052
|
+
worktree: head.worktree,
|
|
12053
|
+
service: record.service,
|
|
12054
|
+
at: new Date().toISOString()
|
|
12055
|
+
},
|
|
12056
|
+
queue
|
|
12057
|
+
};
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
12060
|
+
async request(name, requester, waitMs) {
|
|
12061
|
+
const outcome = await this.#locked(async () => {
|
|
12062
|
+
let granted = false;
|
|
12063
|
+
const updated = await updateSharedHostname(name, (record) => {
|
|
12064
|
+
if (record.holder?.project === requester.project) {
|
|
12065
|
+
granted = true;
|
|
12066
|
+
return {
|
|
12067
|
+
...record,
|
|
12068
|
+
holder: { ...record.holder, worktree: requester.worktree },
|
|
12069
|
+
queue: (record.queue ?? []).filter((waiter) => waiter.project !== requester.project)
|
|
12070
|
+
};
|
|
12071
|
+
}
|
|
12072
|
+
if (record.holder === undefined) {
|
|
12073
|
+
granted = true;
|
|
12074
|
+
return {
|
|
12075
|
+
...record,
|
|
12076
|
+
holder: {
|
|
12077
|
+
project: requester.project,
|
|
12078
|
+
worktree: requester.worktree,
|
|
12079
|
+
service: record.service,
|
|
12080
|
+
at: new Date().toISOString()
|
|
12081
|
+
},
|
|
12082
|
+
queue: (record.queue ?? []).filter((waiter) => waiter.project !== requester.project)
|
|
12083
|
+
};
|
|
12084
|
+
}
|
|
12085
|
+
const queue = [...record.queue ?? []];
|
|
12086
|
+
const existing = queue.find((waiter) => waiter.project === requester.project);
|
|
12087
|
+
if (existing !== undefined)
|
|
12088
|
+
existing.worktree = requester.worktree;
|
|
12089
|
+
else
|
|
12090
|
+
queue.push({ project: requester.project, worktree: requester.worktree, at: new Date().toISOString() });
|
|
12091
|
+
return { ...record, queue };
|
|
12092
|
+
});
|
|
12093
|
+
if (updated === null) {
|
|
12094
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12095
|
+
}
|
|
12096
|
+
return { granted, record: updated };
|
|
12097
|
+
});
|
|
12098
|
+
if (outcome.granted) {
|
|
12099
|
+
await this.#notify();
|
|
12100
|
+
return this.#result(outcome.record, true);
|
|
12101
|
+
}
|
|
12102
|
+
if (waitMs <= 0)
|
|
12103
|
+
return this.#result(outcome.record, false);
|
|
12104
|
+
return new Promise((resolve2) => {
|
|
12105
|
+
const key = `${name}\x00${requester.project}`;
|
|
12106
|
+
const polls = this.#polls.get(key) ?? [];
|
|
12107
|
+
const poll = {
|
|
12108
|
+
resolve: resolve2,
|
|
12109
|
+
timer: setTimeout(() => {
|
|
12110
|
+
const remaining = (this.#polls.get(key) ?? []).filter((candidate) => candidate !== poll);
|
|
12111
|
+
if (remaining.length === 0)
|
|
12112
|
+
this.#polls.delete(key);
|
|
12113
|
+
else
|
|
12114
|
+
this.#polls.set(key, remaining);
|
|
12115
|
+
const record = readSharedHostname(name);
|
|
12116
|
+
resolve2(record === null ? { granted: false, queued: [] } : this.#result(record, record.holder?.project === requester.project));
|
|
12117
|
+
}, waitMs)
|
|
12118
|
+
};
|
|
12119
|
+
polls.push(poll);
|
|
12120
|
+
this.#polls.set(key, polls);
|
|
12121
|
+
});
|
|
12122
|
+
}
|
|
12123
|
+
async cancel(name, project) {
|
|
12124
|
+
const record = await this.#locked(() => updateSharedHostname(name, (current) => ({
|
|
12125
|
+
...current,
|
|
12126
|
+
queue: (current.queue ?? []).filter((waiter) => waiter.project !== project)
|
|
12127
|
+
})));
|
|
12128
|
+
if (record === null)
|
|
12129
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12130
|
+
this.#wake(name, project, this.#result(record, false));
|
|
12131
|
+
return this.#result(record, false);
|
|
12132
|
+
}
|
|
12133
|
+
#assertHolder(record, project, verb) {
|
|
12134
|
+
if (record.holder?.project !== project) {
|
|
12135
|
+
throw new HestiaError("shared-not-holder", record.holder === undefined ? `cannot ${verb} "${record.name}" \u2014 it is unclaimed` : `cannot ${verb} "${record.name}" \u2014 it is held by ${record.holder.project}, not ${project}`);
|
|
12136
|
+
}
|
|
12137
|
+
}
|
|
12138
|
+
async allow(name, callerProject) {
|
|
12139
|
+
const record = await this.#transfer(name, callerProject, "allow");
|
|
12140
|
+
return this.#result(record, false);
|
|
12141
|
+
}
|
|
12142
|
+
async deny(name, callerProject) {
|
|
12143
|
+
const record = await this.#locked(async () => {
|
|
12144
|
+
const current = readSharedHostname(name);
|
|
12145
|
+
if (current === null)
|
|
12146
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12147
|
+
this.#assertHolder(current, callerProject, "deny");
|
|
12148
|
+
const updated = await updateSharedHostname(name, (candidate) => {
|
|
12149
|
+
const queue = [...candidate.queue ?? []];
|
|
12150
|
+
if (queue.length > 0)
|
|
12151
|
+
queue[0] = { ...queue[0], denied: true };
|
|
12152
|
+
return { ...candidate, queue };
|
|
12153
|
+
});
|
|
12154
|
+
return updated;
|
|
12155
|
+
});
|
|
12156
|
+
return this.#result(record, false);
|
|
12157
|
+
}
|
|
12158
|
+
async release(name, callerProject) {
|
|
12159
|
+
const record = await this.#transfer(name, callerProject, "release");
|
|
12160
|
+
return this.#result(record, false);
|
|
12161
|
+
}
|
|
12162
|
+
async#transfer(name, callerProject, verb) {
|
|
12163
|
+
const record = await this.#locked(async () => {
|
|
12164
|
+
const current = readSharedHostname(name);
|
|
12165
|
+
if (current === null)
|
|
12166
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12167
|
+
this.#assertHolder(current, callerProject, verb);
|
|
12168
|
+
return await updateSharedHostname(name, (candidate) => this.#grantNext(candidate));
|
|
12169
|
+
});
|
|
12170
|
+
if (record.holder !== undefined) {
|
|
12171
|
+
this.#wake(record.name, record.holder.project, this.#result(record, true));
|
|
12172
|
+
}
|
|
12173
|
+
await this.#notify();
|
|
12174
|
+
return record;
|
|
12175
|
+
}
|
|
12176
|
+
async releaseProject(project, service) {
|
|
12177
|
+
const held = listSharedHostnames().filter((record) => record.holder?.project === project && (service === undefined || record.service === service));
|
|
12178
|
+
let changed = false;
|
|
12179
|
+
for (const record of held) {
|
|
12180
|
+
const updated = await this.#locked(async () => {
|
|
12181
|
+
const current = readSharedHostname(record.name);
|
|
12182
|
+
if (current?.holder?.project !== project)
|
|
12183
|
+
return null;
|
|
12184
|
+
return await updateSharedHostname(record.name, (candidate) => this.#grantNext(candidate));
|
|
12185
|
+
});
|
|
12186
|
+
if (updated === null || updated === undefined)
|
|
12187
|
+
continue;
|
|
12188
|
+
changed = true;
|
|
12189
|
+
if (updated.holder !== undefined) {
|
|
12190
|
+
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12191
|
+
}
|
|
12192
|
+
}
|
|
12193
|
+
if (changed)
|
|
12194
|
+
await this.#notify();
|
|
12195
|
+
}
|
|
12196
|
+
async sweep(occupied) {
|
|
12197
|
+
let changed = false;
|
|
12198
|
+
for (const record of listSharedHostnames()) {
|
|
12199
|
+
const holderDead = record.holder !== undefined && !occupied.has(record.holder.project) && readMirrorState(record.holder.project) === null;
|
|
12200
|
+
const deadWaiters = (record.queue ?? []).filter((waiter) => readMirrorState(waiter.project) === null);
|
|
12201
|
+
if (!holderDead && deadWaiters.length === 0)
|
|
12202
|
+
continue;
|
|
12203
|
+
const updated = await this.#locked(() => updateSharedHostname(record.name, (candidate) => {
|
|
12204
|
+
const pruned = {
|
|
12205
|
+
...candidate,
|
|
12206
|
+
queue: (candidate.queue ?? []).filter((waiter) => readMirrorState(waiter.project) !== null)
|
|
12207
|
+
};
|
|
12208
|
+
const currentHolderDead = pruned.holder !== undefined && !occupied.has(pruned.holder.project) && readMirrorState(pruned.holder.project) === null;
|
|
12209
|
+
return currentHolderDead ? this.#grantNext(pruned) : pruned;
|
|
12210
|
+
}));
|
|
12211
|
+
if (updated === null)
|
|
12212
|
+
continue;
|
|
12213
|
+
changed = true;
|
|
12214
|
+
if (updated.holder !== undefined && updated.holder.project !== record.holder?.project) {
|
|
12215
|
+
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12216
|
+
}
|
|
12217
|
+
}
|
|
12218
|
+
if (changed)
|
|
12219
|
+
await this.#notify();
|
|
12220
|
+
}
|
|
12221
|
+
}
|
|
12222
|
+
var init_shared_arbiter = __esm(() => {
|
|
12223
|
+
init_src();
|
|
12224
|
+
init_state();
|
|
12225
|
+
init_shared();
|
|
12226
|
+
});
|
|
12227
|
+
|
|
11626
12228
|
// packages/engine/src/doctor.ts
|
|
11627
|
-
import { existsSync as
|
|
12229
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "fs";
|
|
11628
12230
|
import { execFile as execFile9 } from "child_process";
|
|
11629
|
-
import { dirname as dirname6, join as
|
|
12231
|
+
import { dirname as dirname6, join as join23 } from "path";
|
|
11630
12232
|
function row(check, level, detail) {
|
|
11631
12233
|
return { check, level, detail };
|
|
11632
12234
|
}
|
|
@@ -11667,8 +12269,8 @@ async function envChecks(ctx) {
|
|
|
11667
12269
|
}
|
|
11668
12270
|
if (ctx !== null) {
|
|
11669
12271
|
const ignored = await exec("git", ["-C", ctx.worktreeRoot, "check-ignore", "-q", ".hestia/"]);
|
|
11670
|
-
rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${
|
|
11671
|
-
const schema =
|
|
12272
|
+
rows.push(ignored.ok ? row("state-ignore", "ok", ".hestia/ is ignored") : row("state-ignore", "error", `add the exact line ".hestia/" to ${join23(ctx.worktreeRoot, ".gitignore")}`));
|
|
12273
|
+
const schema = existsSync20(join23(ctx.worktreeRoot, ".env.schema"));
|
|
11672
12274
|
if (schema) {
|
|
11673
12275
|
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?)"));
|
|
11674
12276
|
}
|
|
@@ -11677,7 +12279,7 @@ async function envChecks(ctx) {
|
|
|
11677
12279
|
const missing = workers.filter((w) => {
|
|
11678
12280
|
let dir = dirname6(w.configPath);
|
|
11679
12281
|
for (;; ) {
|
|
11680
|
-
if (
|
|
12282
|
+
if (existsSync20(join23(dir, "node_modules", ".bin", "wrangler")))
|
|
11681
12283
|
return false;
|
|
11682
12284
|
if (dir === ctx.worktreeRoot || dirname6(dir) === dir)
|
|
11683
12285
|
return true;
|
|
@@ -11736,10 +12338,10 @@ async function stateChecks(ctx) {
|
|
|
11736
12338
|
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`));
|
|
11737
12339
|
}
|
|
11738
12340
|
}
|
|
11739
|
-
const lockPath2 =
|
|
11740
|
-
if (
|
|
12341
|
+
const lockPath2 = join23(hestiaDir(worktreeRoot), "lock");
|
|
12342
|
+
if (existsSync20(lockPath2)) {
|
|
11741
12343
|
try {
|
|
11742
|
-
const holder = JSON.parse(
|
|
12344
|
+
const holder = JSON.parse(readFileSync18(lockPath2, "utf8"));
|
|
11743
12345
|
if (!isLive(holder)) {
|
|
11744
12346
|
rows.push(row("lock", "warn", "stale worktree lock (dead holder) \u2014 auto-broken on next command"));
|
|
11745
12347
|
}
|
|
@@ -11747,25 +12349,25 @@ async function stateChecks(ctx) {
|
|
|
11747
12349
|
rows.push(row("lock", "warn", "unreadable worktree lock file"));
|
|
11748
12350
|
}
|
|
11749
12351
|
}
|
|
11750
|
-
const mirror =
|
|
11751
|
-
if (!
|
|
12352
|
+
const mirror = join23(hestiaHome(), "stacks", record.project, "stack.json");
|
|
12353
|
+
if (!existsSync20(mirror)) {
|
|
11752
12354
|
rows.push(row("mirror", "warn", "no ~/.hestia mirror \u2014 `down --project` would not work after worktree deletion"));
|
|
11753
12355
|
}
|
|
11754
12356
|
return rows;
|
|
11755
12357
|
}
|
|
11756
12358
|
async function machineChecks() {
|
|
11757
12359
|
const rows = [];
|
|
11758
|
-
const stacksDir =
|
|
12360
|
+
const stacksDir = join23(hestiaHome(), "stacks");
|
|
11759
12361
|
const mirrored = new Set;
|
|
11760
|
-
if (
|
|
11761
|
-
for (const project of
|
|
11762
|
-
const p =
|
|
11763
|
-
if (!
|
|
12362
|
+
if (existsSync20(stacksDir)) {
|
|
12363
|
+
for (const project of readdirSync8(stacksDir)) {
|
|
12364
|
+
const p = join23(stacksDir, project, "stack.json");
|
|
12365
|
+
if (!existsSync20(p))
|
|
11764
12366
|
continue;
|
|
11765
12367
|
mirrored.add(project);
|
|
11766
12368
|
try {
|
|
11767
|
-
const rec = parseStackRecord(
|
|
11768
|
-
if (!
|
|
12369
|
+
const rec = parseStackRecord(readFileSync18(p, "utf8"), p);
|
|
12370
|
+
if (!existsSync20(rec.worktree)) {
|
|
11769
12371
|
rows.push(row(`orphan-mirror:${project}`, "warn", `worktree ${rec.worktree} is gone \u2014 \`hestia down --project ${project}\` to clean up`));
|
|
11770
12372
|
}
|
|
11771
12373
|
} catch {
|
|
@@ -11812,17 +12414,44 @@ async function tunnelChecks() {
|
|
|
11812
12414
|
} catch {
|
|
11813
12415
|
connectors = null;
|
|
11814
12416
|
}
|
|
12417
|
+
const local = listLocalHestiaConnectors(uuid);
|
|
12418
|
+
const localOrphans = live && pf !== null ? local.filter((p) => p.pgid !== pf.pgid && p.pid !== pf.pid) : local;
|
|
12419
|
+
if (localOrphans.length > 0) {
|
|
12420
|
+
rows.push(row(`${label}:local-orphans`, "error", `${localOrphans.length} untracked hestia connector(s) still running ` + `(pids ${localOrphans.map((p) => p.pid).join(", ")}) \u2014 the daemon sweep ` + `or next hestia command reaps them; do not start another cloudflared`));
|
|
12421
|
+
}
|
|
11815
12422
|
if (connectors === null) {
|
|
11816
12423
|
rows.push(row(`${label}:connectors`, "unknown", "cloudflare unreachable \u2014 cannot count connectors"));
|
|
11817
12424
|
} else {
|
|
11818
12425
|
const expected = live ? 1 : 0;
|
|
11819
12426
|
if (connectors > expected) {
|
|
11820
|
-
|
|
12427
|
+
const detail = localOrphans.length > 0 ? `${connectors} connector(s) registered but hestia runs ${expected} \u2014 ` + `${localOrphans.length} are local hestia-owned orphans (auto-reaped on next sweep); ` + `any remainder is a foreign replica` : `${connectors} connector(s) registered but hestia runs ${expected} \u2014 a foreign ` + `replica cross-wires worktrees; stop the other cloudflared (only processes whose ` + `argv lacks ~/.hestia/tunnel/<uuid>/ are truly foreign)`;
|
|
12428
|
+
rows.push(row(`${label}:connectors`, "error", detail));
|
|
11821
12429
|
}
|
|
11822
12430
|
}
|
|
11823
12431
|
}
|
|
11824
12432
|
return rows;
|
|
11825
12433
|
}
|
|
12434
|
+
async function sharedHostnameChecks() {
|
|
12435
|
+
const rows = [];
|
|
12436
|
+
for (const shared of listSharedHostnames()) {
|
|
12437
|
+
const label = `shared:${shared.name}`;
|
|
12438
|
+
const url = `https://${shared.hostname}${shared.path ?? ""}`;
|
|
12439
|
+
const pending = (shared.queue ?? []).length;
|
|
12440
|
+
const queueSuffix = pending > 0 ? ` (${pending} queued)` : "";
|
|
12441
|
+
if (shared.holder === undefined) {
|
|
12442
|
+
rows.push(row(label, "warn", `${url} unclaimed \u2014 requests answer 503 until \`hestia claim ${shared.name}\`${queueSuffix}`));
|
|
12443
|
+
continue;
|
|
12444
|
+
}
|
|
12445
|
+
const mirror = readMirrorState(shared.holder.project);
|
|
12446
|
+
if (mirror === null) {
|
|
12447
|
+
rows.push(row(label, "warn", `held by ${shared.holder.project} but its stack mirror is gone \u2014 the daemon sweep auto-releases it${queueSuffix}`));
|
|
12448
|
+
continue;
|
|
12449
|
+
}
|
|
12450
|
+
const endpoint = mirror.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === shared.service);
|
|
12451
|
+
rows.push(endpoint === undefined ? row(label, "warn", `held by ${shared.holder.project} but endpoint "${shared.service}" is not running \u2014 requests answer 503${queueSuffix}`) : row(label, "ok", `${url} \u2192 ${shared.holder.project}${queueSuffix}`));
|
|
12452
|
+
}
|
|
12453
|
+
return rows;
|
|
12454
|
+
}
|
|
11826
12455
|
async function daemonChecks() {
|
|
11827
12456
|
const rows = [];
|
|
11828
12457
|
const j = readDaemonJson();
|
|
@@ -11837,11 +12466,11 @@ async function daemonChecks() {
|
|
|
11837
12466
|
rows.push(row("daemon-config", "warn", w));
|
|
11838
12467
|
}
|
|
11839
12468
|
const plist = plistPath();
|
|
11840
|
-
if (
|
|
11841
|
-
const content =
|
|
12469
|
+
if (existsSync20(plist)) {
|
|
12470
|
+
const content = readFileSync18(plist, "utf8");
|
|
11842
12471
|
const args = [...content.matchAll(/<string>([^<]+)<\/string>/g)].map((m) => m[1]);
|
|
11843
12472
|
const paths = args.filter((a) => a.startsWith("/") && !a.includes(":"));
|
|
11844
|
-
const missing = paths.filter((p) => !
|
|
12473
|
+
const missing = paths.filter((p) => !existsSync20(p));
|
|
11845
12474
|
rows.push(missing.length === 0 ? row("launchd", "ok", `installed (${plist})`) : row("launchd", "error", `plist references missing paths: ${missing.join(", ")} \u2014 re-run \`hestia daemon install\``));
|
|
11846
12475
|
}
|
|
11847
12476
|
return rows;
|
|
@@ -11880,6 +12509,7 @@ async function doctor(cwd) {
|
|
|
11880
12509
|
ctx !== null ? bounded("state", () => stateChecks(ctx)) : Promise.resolve([]),
|
|
11881
12510
|
bounded("machine", () => machineChecks()),
|
|
11882
12511
|
bounded("tunnel", () => tunnelChecks()),
|
|
12512
|
+
bounded("shared", () => sharedHostnameChecks()),
|
|
11883
12513
|
bounded("daemon", () => daemonChecks()),
|
|
11884
12514
|
bounded("local-router", () => localRouterChecks())
|
|
11885
12515
|
]);
|
|
@@ -11891,11 +12521,13 @@ var init_doctor = __esm(() => {
|
|
|
11891
12521
|
init_git();
|
|
11892
12522
|
init_config();
|
|
11893
12523
|
init_state();
|
|
12524
|
+
init_shared();
|
|
11894
12525
|
init_pidfile();
|
|
11895
12526
|
init_resolver();
|
|
11896
12527
|
init_discover();
|
|
11897
12528
|
init_registry();
|
|
11898
12529
|
init_cloudflared();
|
|
12530
|
+
init_orphans();
|
|
11899
12531
|
init_client();
|
|
11900
12532
|
init_routes();
|
|
11901
12533
|
init_launchd();
|
|
@@ -11907,8 +12539,8 @@ var init_doctor = __esm(() => {
|
|
|
11907
12539
|
// packages/engine/src/daemon/fleet-monitor.ts
|
|
11908
12540
|
import { execFile as execFile10 } from "child_process";
|
|
11909
12541
|
import { promisify as promisify8 } from "util";
|
|
11910
|
-
import { existsSync as
|
|
11911
|
-
import { join as
|
|
12542
|
+
import { existsSync as existsSync21, readFileSync as readFileSync19, readdirSync as readdirSync9 } from "fs";
|
|
12543
|
+
import { join as join24 } from "path";
|
|
11912
12544
|
|
|
11913
12545
|
class LatestFleetChannel {
|
|
11914
12546
|
#pending;
|
|
@@ -11951,13 +12583,13 @@ class LatestFleetChannel {
|
|
|
11951
12583
|
function readManagedMirrors() {
|
|
11952
12584
|
const records = [];
|
|
11953
12585
|
const warnings = [];
|
|
11954
|
-
const stacksDirectory =
|
|
11955
|
-
if (!
|
|
12586
|
+
const stacksDirectory = join24(hestiaHome(), "stacks");
|
|
12587
|
+
if (!existsSync21(stacksDirectory))
|
|
11956
12588
|
return { records, warnings };
|
|
11957
|
-
for (const project of
|
|
11958
|
-
const path =
|
|
12589
|
+
for (const project of readdirSync9(stacksDirectory).sort()) {
|
|
12590
|
+
const path = join24(stacksDirectory, project, "stack.json");
|
|
11959
12591
|
try {
|
|
11960
|
-
const source =
|
|
12592
|
+
const source = readFileSync19(path);
|
|
11961
12593
|
if (source.byteLength > MAX_MIRROR_BYTES2) {
|
|
11962
12594
|
warnings.push(`Fleet mirror too large for ${project}`);
|
|
11963
12595
|
continue;
|
|
@@ -11977,7 +12609,7 @@ function readManagedMirrors() {
|
|
|
11977
12609
|
async function attributeLegacyRepoId(record) {
|
|
11978
12610
|
if (record.repoId !== undefined)
|
|
11979
12611
|
return record.repoId;
|
|
11980
|
-
if (!
|
|
12612
|
+
if (!existsSync21(record.worktree))
|
|
11981
12613
|
return null;
|
|
11982
12614
|
const info = await getRepoInfo(record.worktree);
|
|
11983
12615
|
if (projectName(info.repoId, info.repo, info.branch, info.worktreeRoot) !== record.project)
|
|
@@ -12083,6 +12715,25 @@ function reservationIdentity(reservation, records) {
|
|
|
12083
12715
|
worktree: record.worktree
|
|
12084
12716
|
};
|
|
12085
12717
|
}
|
|
12718
|
+
function collectFleetShared(repoProjects) {
|
|
12719
|
+
return listSharedHostnames().map((record) => ({
|
|
12720
|
+
name: record.name,
|
|
12721
|
+
hostname: record.hostname,
|
|
12722
|
+
...record.path === undefined ? {} : { path: record.path },
|
|
12723
|
+
url: `https://${record.hostname}${record.path ?? ""}`,
|
|
12724
|
+
holder: record.holder === undefined ? undefined : {
|
|
12725
|
+
project: record.holder.project,
|
|
12726
|
+
worktree: record.holder.worktree,
|
|
12727
|
+
mine: repoProjects.has(record.holder.project)
|
|
12728
|
+
},
|
|
12729
|
+
queue: (record.queue ?? []).map((waiter) => ({
|
|
12730
|
+
project: waiter.project,
|
|
12731
|
+
worktree: waiter.worktree,
|
|
12732
|
+
...waiter.denied === true ? { denied: true } : {},
|
|
12733
|
+
mine: repoProjects.has(waiter.project)
|
|
12734
|
+
}))
|
|
12735
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
12736
|
+
}
|
|
12086
12737
|
async function collectFleetSnapshot(repoId, admission) {
|
|
12087
12738
|
const mirrorResult = readManagedMirrors();
|
|
12088
12739
|
const machineConfig = readHestiaMachineConfig();
|
|
@@ -12178,6 +12829,7 @@ async function collectFleetSnapshot(repoId, admission) {
|
|
|
12178
12829
|
const allRecordedProjects = new Set(attributed.map((record) => record.project));
|
|
12179
12830
|
const unbackedReservations = reservations.filter((reservation) => !allRecordedProjects.has(reservation.project));
|
|
12180
12831
|
const { maxStacks, warnings: capWarnings } = resolveMaxStacks();
|
|
12832
|
+
const repoProjects = new Set(attributed.filter((record) => record.repoId === repoId).map((record) => record.project));
|
|
12181
12833
|
return {
|
|
12182
12834
|
repoId,
|
|
12183
12835
|
observedAt: new Date().toISOString(),
|
|
@@ -12188,6 +12840,7 @@ async function collectFleetSnapshot(repoId, admission) {
|
|
|
12188
12840
|
queued: queued.length
|
|
12189
12841
|
},
|
|
12190
12842
|
stacks: stackViews,
|
|
12843
|
+
shared: collectFleetShared(repoProjects),
|
|
12191
12844
|
warnings: [...capWarnings, ...warnings].sort()
|
|
12192
12845
|
};
|
|
12193
12846
|
}
|
|
@@ -12196,6 +12849,7 @@ function semanticFleetSnapshot(snapshot) {
|
|
|
12196
12849
|
repoId: snapshot.repoId,
|
|
12197
12850
|
capacity: snapshot.capacity,
|
|
12198
12851
|
stacks: snapshot.stacks,
|
|
12852
|
+
shared: snapshot.shared,
|
|
12199
12853
|
warnings: snapshot.warnings
|
|
12200
12854
|
});
|
|
12201
12855
|
}
|
|
@@ -12307,6 +12961,7 @@ var init_fleet_monitor = __esm(() => {
|
|
|
12307
12961
|
init_ports();
|
|
12308
12962
|
init_pidfile();
|
|
12309
12963
|
init_state();
|
|
12964
|
+
init_shared();
|
|
12310
12965
|
init_router_config();
|
|
12311
12966
|
init_local_http_router();
|
|
12312
12967
|
init_portless_adapter();
|
|
@@ -12417,10 +13072,10 @@ var init_init_config = __esm(() => {
|
|
|
12417
13072
|
});
|
|
12418
13073
|
|
|
12419
13074
|
// packages/engine/src/index.ts
|
|
12420
|
-
import { existsSync as
|
|
13075
|
+
import { existsSync as existsSync22, rmSync as rmSync10 } from "fs";
|
|
12421
13076
|
import { execFile as execFile11 } from "child_process";
|
|
12422
13077
|
import { promisify as promisify9 } from "util";
|
|
12423
|
-
import { join as
|
|
13078
|
+
import { join as join25, resolve as resolve2 } from "path";
|
|
12424
13079
|
import { createHash as createHash7 } from "crypto";
|
|
12425
13080
|
import { resolve4, resolve6, resolveCname } from "dns/promises";
|
|
12426
13081
|
function composeUnsupported(message) {
|
|
@@ -12530,7 +13185,7 @@ async function assertHestiaStateIgnored(worktreeRoot) {
|
|
|
12530
13185
|
try {
|
|
12531
13186
|
await pexec9("git", ["-C", worktreeRoot, "check-ignore", "-q", ".hestia/"], { timeout: 5000 });
|
|
12532
13187
|
} catch {
|
|
12533
|
-
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${
|
|
13188
|
+
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join25(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join25(worktreeRoot, ".gitignore") });
|
|
12534
13189
|
}
|
|
12535
13190
|
}
|
|
12536
13191
|
async function prepareCompose(worktreeRoot, project, repo, branch, opts, configuredBaseFile) {
|
|
@@ -12547,7 +13202,7 @@ async function prepareCompose(worktreeRoot, project, repo, branch, opts, configu
|
|
|
12547
13202
|
services
|
|
12548
13203
|
});
|
|
12549
13204
|
ensureDir(hestiaDir(worktreeRoot));
|
|
12550
|
-
const overridePath =
|
|
13205
|
+
const overridePath = join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
12551
13206
|
writeAtomicTextFile(overridePath, yaml);
|
|
12552
13207
|
return {
|
|
12553
13208
|
ctx: {
|
|
@@ -12580,7 +13235,7 @@ function writeDockerfileComposeModel(worktreeRoot, workloads, conventionalCompos
|
|
|
12580
13235
|
...conventionalComposeFile === undefined ? {} : { include: [{ path: conventionalComposeFile }] },
|
|
12581
13236
|
services
|
|
12582
13237
|
};
|
|
12583
|
-
const path =
|
|
13238
|
+
const path = join25(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
|
|
12584
13239
|
writeAtomicTextFile(path, $stringify(model));
|
|
12585
13240
|
return path;
|
|
12586
13241
|
}
|
|
@@ -12600,7 +13255,7 @@ function freshRecord(project, repoId, repo, branch, worktree) {
|
|
|
12600
13255
|
};
|
|
12601
13256
|
}
|
|
12602
13257
|
function assertCurrentStackIdentity(record, current) {
|
|
12603
|
-
const path =
|
|
13258
|
+
const path = join25(hestiaDir(current.worktreeRoot), "stack.json");
|
|
12604
13259
|
assertMutableStackRecord(record, path);
|
|
12605
13260
|
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch && resolve2(record.worktree) === resolve2(current.worktreeRoot) && record.project === projectName(current.repoId, current.repo, current.branch, current.worktreeRoot);
|
|
12606
13261
|
if (!matches) {
|
|
@@ -12735,6 +13390,27 @@ function applyLocalRouteProjection(record) {
|
|
|
12735
13390
|
record.env[directUrlKey(endpoint.name)] = endpoint.url;
|
|
12736
13391
|
record.env[localUrlKey(endpoint.name)] = endpoint.localUrl;
|
|
12737
13392
|
}
|
|
13393
|
+
syncSharedProjection(record);
|
|
13394
|
+
}
|
|
13395
|
+
function syncSharedProjection(record) {
|
|
13396
|
+
for (const shared of listSharedHostnames()) {
|
|
13397
|
+
const url = `https://${shared.hostname}${shared.path ?? ""}`;
|
|
13398
|
+
if (shared.holder?.project === record.project) {
|
|
13399
|
+
const endpoint = record.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === shared.service);
|
|
13400
|
+
if (endpoint !== undefined) {
|
|
13401
|
+
endpoint.publicUrl = url;
|
|
13402
|
+
record.env[urlKey(shared.service)] = url;
|
|
13403
|
+
}
|
|
13404
|
+
continue;
|
|
13405
|
+
}
|
|
13406
|
+
for (const endpoint of record.endpoints) {
|
|
13407
|
+
if (endpoint.publicUrl === url)
|
|
13408
|
+
delete endpoint.publicUrl;
|
|
13409
|
+
}
|
|
13410
|
+
if (record.env[urlKey(shared.service)] === url) {
|
|
13411
|
+
delete record.env[urlKey(shared.service)];
|
|
13412
|
+
}
|
|
13413
|
+
}
|
|
12738
13414
|
}
|
|
12739
13415
|
function syncExposures(record) {
|
|
12740
13416
|
const t = record.tunnel;
|
|
@@ -12758,7 +13434,7 @@ function matchesExpectedStack(record, expected) {
|
|
|
12758
13434
|
return record !== null && (record.repoId === undefined || record.repoId === expected.repoId) && record.worktree === expected.worktree && record.createdAt === expected.createdAt;
|
|
12759
13435
|
}
|
|
12760
13436
|
function projectMutationRoot(project) {
|
|
12761
|
-
return
|
|
13437
|
+
return join25(hestiaHome(), "project-locks", project);
|
|
12762
13438
|
}
|
|
12763
13439
|
async function assertNamedTunnelDns(hostname, tunnelUuid) {
|
|
12764
13440
|
const expected = `${tunnelUuid}.cfargotunnel.com`;
|
|
@@ -13164,6 +13840,7 @@ class ComposeEngine {
|
|
|
13164
13840
|
const { repo, repoId, branch, worktreeRoot } = currentIdentity;
|
|
13165
13841
|
let tunnel;
|
|
13166
13842
|
let tunnelDirty = false;
|
|
13843
|
+
const stoppedAliases = [];
|
|
13167
13844
|
const project = readState(worktreeRoot)?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13168
13845
|
await withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
13169
13846
|
const record = readState(worktreeRoot);
|
|
@@ -13172,6 +13849,11 @@ class ComposeEngine {
|
|
|
13172
13849
|
if (record.services.some((service) => service.name === name && service.backend === "docker")) {
|
|
13173
13850
|
throw new HestiaError("backend-not-stoppable", `Docker workload ${name} cannot be stopped individually; use hestia down`);
|
|
13174
13851
|
}
|
|
13852
|
+
for (const endpoint of record.endpoints) {
|
|
13853
|
+
if ((endpoint.workload ?? endpoint.name) === name) {
|
|
13854
|
+
stoppedAliases.push(endpoint.alias ?? endpoint.name);
|
|
13855
|
+
}
|
|
13856
|
+
}
|
|
13175
13857
|
}
|
|
13176
13858
|
const pf = readPidfile(worktreeRoot, name);
|
|
13177
13859
|
if (pf !== null) {
|
|
@@ -13217,6 +13899,9 @@ class ComposeEngine {
|
|
|
13217
13899
|
}
|
|
13218
13900
|
}
|
|
13219
13901
|
}));
|
|
13902
|
+
for (const alias of new Set(stoppedAliases)) {
|
|
13903
|
+
await this.#releaseSharedHoldings(project, alias);
|
|
13904
|
+
}
|
|
13220
13905
|
if (tunnelDirty)
|
|
13221
13906
|
await this.#reconcileAdopted(tunnel);
|
|
13222
13907
|
await this.#refreshLocalRoutes();
|
|
@@ -13352,12 +14037,12 @@ class ComposeEngine {
|
|
|
13352
14037
|
await stopProcTree(pf);
|
|
13353
14038
|
removePidfile(worktreeRoot, pf.name);
|
|
13354
14039
|
}
|
|
13355
|
-
|
|
14040
|
+
rmSync10(privateRegistryDir(worktreeRoot), { recursive: true, force: true });
|
|
13356
14041
|
const composeFile = record?.composeFile ?? tryLoadConfig(worktreeRoot)?.composeFile;
|
|
13357
14042
|
if (composeFile !== undefined) {
|
|
13358
14043
|
const project2 = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13359
|
-
const overrideFile = record?.overrideFile ??
|
|
13360
|
-
if (
|
|
14044
|
+
const overrideFile = record?.overrideFile ?? join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
14045
|
+
if (existsSync22(overrideFile)) {
|
|
13361
14046
|
await composeDown({ project: project2, baseFile: composeFile, overrideFile, cwd: worktreeRoot }, opts?.destroy ?? false);
|
|
13362
14047
|
} else if (record?.composeFile !== undefined) {
|
|
13363
14048
|
const rest = ["compose", "-p", project2, "down", "--remove-orphans"];
|
|
@@ -13369,6 +14054,7 @@ class ComposeEngine {
|
|
|
13369
14054
|
const project = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13370
14055
|
clearState(worktreeRoot, project);
|
|
13371
14056
|
await this.#releaseAdmission(project);
|
|
14057
|
+
await this.#releaseSharedHoldings(project);
|
|
13372
14058
|
}));
|
|
13373
14059
|
await this.#reconcileAdopted(tunnel);
|
|
13374
14060
|
await this.#refreshLocalRoutes();
|
|
@@ -13378,6 +14064,11 @@ class ComposeEngine {
|
|
|
13378
14064
|
if (j !== null)
|
|
13379
14065
|
await releaseSlot(j.port, project);
|
|
13380
14066
|
}
|
|
14067
|
+
async#releaseSharedHoldings(project, service) {
|
|
14068
|
+
const j = readDaemonJson();
|
|
14069
|
+
if (j !== null)
|
|
14070
|
+
await releaseSharedForProject(j.port, project, service);
|
|
14071
|
+
}
|
|
13381
14072
|
async downProject(project, opts) {
|
|
13382
14073
|
if (!/^[a-z0-9][a-z0-9-]{0,99}$/.test(project)) {
|
|
13383
14074
|
throw new HestiaError("usage", `invalid project name ${JSON.stringify(project)}`);
|
|
@@ -13420,9 +14111,9 @@ class ComposeEngine {
|
|
|
13420
14111
|
throw new HestiaError("compose-failed", `docker compose -p ${project} down failed: ${err.message}`);
|
|
13421
14112
|
}
|
|
13422
14113
|
}
|
|
13423
|
-
|
|
14114
|
+
rmSync10(mirrorDir(project), { recursive: true, force: true });
|
|
13424
14115
|
};
|
|
13425
|
-
if (record !== null &&
|
|
14116
|
+
if (record !== null && existsSync22(record.worktree)) {
|
|
13426
14117
|
const info = await getRepoInfo(record.worktree);
|
|
13427
14118
|
const identityMatches = resolve2(info.worktreeRoot) === resolve2(record.worktree) && (record.repoId === undefined ? record.repo === info.repo : record.repoId === info.repoId);
|
|
13428
14119
|
const localRecord = readState(record.worktree);
|
|
@@ -13443,6 +14134,7 @@ class ComposeEngine {
|
|
|
13443
14134
|
return;
|
|
13444
14135
|
}
|
|
13445
14136
|
await this.#releaseAdmission(project);
|
|
14137
|
+
await this.#releaseSharedHoldings(project);
|
|
13446
14138
|
await this.#reconcileAdopted(record.tunnel);
|
|
13447
14139
|
await this.#refreshLocalRoutes();
|
|
13448
14140
|
return;
|
|
@@ -13451,6 +14143,7 @@ class ComposeEngine {
|
|
|
13451
14143
|
const projectLockRoot = projectMutationRoot(project);
|
|
13452
14144
|
await withLock(projectLockRoot, teardownFromMirror);
|
|
13453
14145
|
await this.#releaseAdmission(project);
|
|
14146
|
+
await this.#releaseSharedHoldings(project);
|
|
13454
14147
|
await this.#reconcileAdopted(record?.tunnel);
|
|
13455
14148
|
await this.#refreshLocalRoutes();
|
|
13456
14149
|
}
|
|
@@ -13469,11 +14162,100 @@ class ComposeEngine {
|
|
|
13469
14162
|
}
|
|
13470
14163
|
await ensureDaemon();
|
|
13471
14164
|
await this.#refreshLocalRoutes(true);
|
|
14165
|
+
if (opts?.shared !== undefined) {
|
|
14166
|
+
if (tunnelName === undefined) {
|
|
14167
|
+
throw new HestiaError("shared-requires-named-tunnel", "shared hostnames need an adopted named tunnel (stable DNS is the point) \u2014 pass --tunnel <name>");
|
|
14168
|
+
}
|
|
14169
|
+
return this.#exposeShared(worktreeRoot, services, tunnelName, opts);
|
|
14170
|
+
}
|
|
13472
14171
|
if (tunnelName === undefined) {
|
|
13473
14172
|
return this.#exposeQuick(worktreeRoot, services, opts);
|
|
13474
14173
|
}
|
|
13475
14174
|
return this.#exposeNamed(worktreeRoot, services, tunnelName, opts);
|
|
13476
14175
|
}
|
|
14176
|
+
async#exposeShared(worktreeRoot, services, tunnelName, opts) {
|
|
14177
|
+
if (services.length !== 1) {
|
|
14178
|
+
throw new HestiaError("usage", "--shared declares exactly one service per shared hostname");
|
|
14179
|
+
}
|
|
14180
|
+
const name = opts.shared;
|
|
14181
|
+
assertSharedName(name);
|
|
14182
|
+
const project = readState(worktreeRoot)?.project;
|
|
14183
|
+
if (project === undefined) {
|
|
14184
|
+
throw new HestiaError("service-not-found", "no stack in this worktree \u2014 `hestia up`/`run` something first");
|
|
14185
|
+
}
|
|
14186
|
+
const adopted = await adoptTunnel(tunnelName);
|
|
14187
|
+
if (adopted.connections > 0 && !isAdopted(adopted.uuid) && !opts?.force) {
|
|
14188
|
+
throw new HestiaError("tunnel-busy", `tunnel "${tunnelName}" already has ${adopted.connections} live edge ` + `connection(s) from a foreign connector \u2014 stop the other cloudflared first (hestia's ` + `connector serves your static hostnames too), or pass --force to ` + `accept nondeterministic routing`);
|
|
14189
|
+
}
|
|
14190
|
+
const preflightRecord = readState(worktreeRoot);
|
|
14191
|
+
if (preflightRecord === null) {
|
|
14192
|
+
throw new HestiaError("service-not-found", "stack disappeared while preparing the shared hostname");
|
|
14193
|
+
}
|
|
14194
|
+
const baseRules = importBaseRules(adopted.uuid, tunnelName);
|
|
14195
|
+
const zoneHint = opts?.zone ?? preflightRecord.tunnel?.zone ?? inferZone(baseRules);
|
|
14196
|
+
let hostname;
|
|
14197
|
+
let zone;
|
|
14198
|
+
if (opts?.hostname !== undefined) {
|
|
14199
|
+
hostname = opts.hostname.toLowerCase();
|
|
14200
|
+
assertSharedHostnameFqdn(hostname);
|
|
14201
|
+
zone = zoneOf(hostname) ?? zoneHint ?? hostname;
|
|
14202
|
+
} else {
|
|
14203
|
+
if (zoneHint === undefined) {
|
|
14204
|
+
throw new HestiaError("usage", "cannot infer a zone from the tunnel's existing rules \u2014 pass --zone or an explicit --hostname");
|
|
14205
|
+
}
|
|
14206
|
+
zone = zoneHint;
|
|
14207
|
+
hostname = `${name}.${zone}`;
|
|
14208
|
+
}
|
|
14209
|
+
const path = normalizeSharedPath(opts?.path);
|
|
14210
|
+
const selection = resolveEndpointSelection(preflightRecord, services[0]);
|
|
14211
|
+
if (selection.endpoint.kind !== undefined && selection.endpoint.kind !== "http") {
|
|
14212
|
+
throw new HestiaError("usage", `public tunnels require an HTTP endpoint; ${services[0]} is ${selection.endpoint.kind}`);
|
|
14213
|
+
}
|
|
14214
|
+
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
14215
|
+
if (baseRules.some((rule) => rule.hostname === hostname)) {
|
|
14216
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} is already claimed by the user's static rules`);
|
|
14217
|
+
}
|
|
14218
|
+
const dynamicConflict = collectDynamicRules(adopted.uuid).rules.find((rule) => rule.hostname === hostname);
|
|
14219
|
+
if (dynamicConflict !== undefined) {
|
|
14220
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} is already claimed by project ${dynamicConflict.project}`);
|
|
14221
|
+
}
|
|
14222
|
+
await assertNamedTunnelDns(hostname, adopted.uuid);
|
|
14223
|
+
await declareSharedHostname({
|
|
14224
|
+
name,
|
|
14225
|
+
hostname,
|
|
14226
|
+
path,
|
|
14227
|
+
tunnelUuid: adopted.uuid,
|
|
14228
|
+
zone,
|
|
14229
|
+
service: alias
|
|
14230
|
+
});
|
|
14231
|
+
const handle = await ensureDaemon();
|
|
14232
|
+
const claim = await sharedVerb(handle.port, "claim", {
|
|
14233
|
+
name,
|
|
14234
|
+
project,
|
|
14235
|
+
worktree: worktreeRoot,
|
|
14236
|
+
waitMs: 0
|
|
14237
|
+
});
|
|
14238
|
+
if (!claim.granted) {
|
|
14239
|
+
throw new HestiaError("shared-held", `shared hostname "${name}" is held by ${claim.holder?.project ?? "another stack"} ` + `(${claim.queued.length} queued) \u2014 \`hestia claim ${name} --wait\` to queue for it`);
|
|
14240
|
+
}
|
|
14241
|
+
const outcome = await reconcileTunnel({ name: tunnelName, uuid: adopted.uuid, credFile: adopted.credFile }, { force: opts?.force, readyTimeoutMs: opts?.readyTimeoutMs });
|
|
14242
|
+
for (const w of outcome.warnings)
|
|
14243
|
+
process.stderr.write(`warning: ${w}
|
|
14244
|
+
`);
|
|
14245
|
+
const final = await withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
14246
|
+
const record = readState(worktreeRoot);
|
|
14247
|
+
if (record === null) {
|
|
14248
|
+
throw new HestiaError("service-not-found", "stack disappeared while exposing (concurrent down?)");
|
|
14249
|
+
}
|
|
14250
|
+
applyLocalRouteProjection(record);
|
|
14251
|
+
writeState(worktreeRoot, record);
|
|
14252
|
+
return record;
|
|
14253
|
+
}));
|
|
14254
|
+
if (outcome.error !== undefined)
|
|
14255
|
+
throw outcome.error;
|
|
14256
|
+
await this.#refreshLocalRoutes(true);
|
|
14257
|
+
return final;
|
|
14258
|
+
}
|
|
13477
14259
|
async#exposeQuick(worktreeRoot, services, opts) {
|
|
13478
14260
|
const project = readState(worktreeRoot)?.project;
|
|
13479
14261
|
if (project === undefined) {
|
|
@@ -13492,7 +14274,7 @@ class ComposeEngine {
|
|
|
13492
14274
|
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
13493
14275
|
const authority = internalEndpointAuthority(record.project, alias);
|
|
13494
14276
|
const name = `aux-quick-${createHash7("sha256").update(alias).digest("hex").slice(0, 10)}`;
|
|
13495
|
-
const quickCfg =
|
|
14277
|
+
const quickCfg = join25(hestiaDir(worktreeRoot), `quick-${name}.yml`);
|
|
13496
14278
|
writeAtomicTextFile(quickCfg, `ingress:
|
|
13497
14279
|
- service: ${JSON.stringify(`unix:${publicGatewaySocketPath()}`)}
|
|
13498
14280
|
` + ` originRequest:
|
|
@@ -13684,6 +14466,80 @@ class ComposeEngine {
|
|
|
13684
14466
|
await this.#refreshLocalRoutes(true);
|
|
13685
14467
|
return final;
|
|
13686
14468
|
}
|
|
14469
|
+
async#sharedContext(cwd, name) {
|
|
14470
|
+
assertSharedName(name);
|
|
14471
|
+
const { worktreeRoot } = await getRepoInfo(cwd);
|
|
14472
|
+
const record = readState(worktreeRoot);
|
|
14473
|
+
if (record === null) {
|
|
14474
|
+
throw new HestiaError("service-not-found", "no stack in this worktree \u2014 `hestia up`/`run` something first");
|
|
14475
|
+
}
|
|
14476
|
+
const shared = readSharedHostname(name);
|
|
14477
|
+
if (shared === null) {
|
|
14478
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared \u2014 \`hestia expose <svc> --shared ${name}\` creates one`);
|
|
14479
|
+
}
|
|
14480
|
+
return { worktreeRoot, record, shared };
|
|
14481
|
+
}
|
|
14482
|
+
async#applySharedProjection(worktreeRoot, project) {
|
|
14483
|
+
return withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
14484
|
+
const record = readState(worktreeRoot);
|
|
14485
|
+
if (record === null) {
|
|
14486
|
+
throw new HestiaError("service-not-found", "stack disappeared during the shared-hostname transition");
|
|
14487
|
+
}
|
|
14488
|
+
applyLocalRouteProjection(record);
|
|
14489
|
+
writeState(worktreeRoot, record);
|
|
14490
|
+
return record;
|
|
14491
|
+
}));
|
|
14492
|
+
}
|
|
14493
|
+
async claimShared(cwd, name, opts) {
|
|
14494
|
+
const { worktreeRoot, record, shared } = await this.#sharedContext(cwd, name);
|
|
14495
|
+
const endpoint = record.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === shared.service);
|
|
14496
|
+
if (endpoint === undefined) {
|
|
14497
|
+
throw new HestiaError("service-not-found", `shared hostname "${name}" routes to endpoint alias "${shared.service}", which this ` + `stack does not expose \u2014 \`hestia up\`/\`run\` it first`);
|
|
14498
|
+
}
|
|
14499
|
+
if (endpoint.kind !== undefined && endpoint.kind !== "http") {
|
|
14500
|
+
throw new HestiaError("usage", `shared hostnames require an HTTP endpoint; ${shared.service} is ${endpoint.kind}`);
|
|
14501
|
+
}
|
|
14502
|
+
const handle = await ensureDaemon();
|
|
14503
|
+
const result = await sharedVerb(handle.port, "claim", {
|
|
14504
|
+
name,
|
|
14505
|
+
project: record.project,
|
|
14506
|
+
worktree: worktreeRoot,
|
|
14507
|
+
waitMs: opts?.waitMs ?? 0
|
|
14508
|
+
});
|
|
14509
|
+
if (!result.granted) {
|
|
14510
|
+
const position = result.queued.findIndex((waiter) => waiter.project === record.project) + 1;
|
|
14511
|
+
throw new HestiaError("shared-held", `shared hostname "${name}" is held by ${result.holder?.project ?? "another stack"}` + (position > 0 ? ` \u2014 queued durably at position ${position}/${result.queued.length}; the holder can ` + `\`hestia share allow ${name}\`, or re-run \`hestia claim ${name} --wait\` to keep waiting` : ""), { holder: result.holder, queued: result.queued });
|
|
14512
|
+
}
|
|
14513
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14514
|
+
await this.#refreshLocalRoutes();
|
|
14515
|
+
return { record: updated, result };
|
|
14516
|
+
}
|
|
14517
|
+
async releaseShared(cwd, name) {
|
|
14518
|
+
const { worktreeRoot, record } = await this.#sharedContext(cwd, name);
|
|
14519
|
+
const handle = await ensureDaemon();
|
|
14520
|
+
await sharedVerb(handle.port, "release", { name, project: record.project });
|
|
14521
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14522
|
+
await this.#refreshLocalRoutes();
|
|
14523
|
+
return updated;
|
|
14524
|
+
}
|
|
14525
|
+
async cancelSharedClaim(cwd, name) {
|
|
14526
|
+
const { record } = await this.#sharedContext(cwd, name);
|
|
14527
|
+
const handle = await ensureDaemon();
|
|
14528
|
+
await sharedVerb(handle.port, "cancel", { name, project: record.project });
|
|
14529
|
+
}
|
|
14530
|
+
async allowShared(cwd, name) {
|
|
14531
|
+
const { worktreeRoot, record } = await this.#sharedContext(cwd, name);
|
|
14532
|
+
const handle = await ensureDaemon();
|
|
14533
|
+
await sharedVerb(handle.port, "allow", { name, project: record.project });
|
|
14534
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14535
|
+
await this.#refreshLocalRoutes();
|
|
14536
|
+
return updated;
|
|
14537
|
+
}
|
|
14538
|
+
async denyShared(cwd, name) {
|
|
14539
|
+
const { record } = await this.#sharedContext(cwd, name);
|
|
14540
|
+
const handle = await ensureDaemon();
|
|
14541
|
+
await sharedVerb(handle.port, "deny", { name, project: record.project });
|
|
14542
|
+
}
|
|
13687
14543
|
async status(cwd) {
|
|
13688
14544
|
const { worktreeRoot } = await getRepoInfo(cwd);
|
|
13689
14545
|
const record = readState(worktreeRoot);
|
|
@@ -13810,11 +14666,14 @@ var init_src2 = __esm(() => {
|
|
|
13810
14666
|
init_cloudflared();
|
|
13811
14667
|
init_ingress();
|
|
13812
14668
|
init_registry();
|
|
14669
|
+
init_shared();
|
|
13813
14670
|
init_ensure();
|
|
13814
14671
|
init_client();
|
|
13815
14672
|
init_stream();
|
|
13816
14673
|
init_router_config();
|
|
13817
14674
|
init_cli();
|
|
14675
|
+
init_shared();
|
|
14676
|
+
init_shared_arbiter();
|
|
13818
14677
|
init_lock();
|
|
13819
14678
|
init_supervisor();
|
|
13820
14679
|
init_tail();
|
|
@@ -14003,6 +14862,18 @@ class DaemonFleetSource {
|
|
|
14003
14862
|
diagnose(worktree) {
|
|
14004
14863
|
return doctor(worktree);
|
|
14005
14864
|
}
|
|
14865
|
+
async claimShared(worktree, name) {
|
|
14866
|
+
await engine.claimShared(worktree, name);
|
|
14867
|
+
}
|
|
14868
|
+
async allowShared(worktree, name) {
|
|
14869
|
+
await engine.allowShared(worktree, name);
|
|
14870
|
+
}
|
|
14871
|
+
async denyShared(worktree, name) {
|
|
14872
|
+
await engine.denyShared(worktree, name);
|
|
14873
|
+
}
|
|
14874
|
+
async releaseShared(worktree, name) {
|
|
14875
|
+
await engine.releaseShared(worktree, name);
|
|
14876
|
+
}
|
|
14006
14877
|
down(stack) {
|
|
14007
14878
|
if (stack.createdAt === undefined) {
|
|
14008
14879
|
return Promise.reject(new Error(`stack ${stack.project} has no stable incarnation timestamp`));
|
|
@@ -14024,6 +14895,14 @@ var init_fleet_source = __esm(() => {
|
|
|
14024
14895
|
init_src2();
|
|
14025
14896
|
});
|
|
14026
14897
|
|
|
14898
|
+
// packages/tui/src/fleet-endpoints.ts
|
|
14899
|
+
function serviceEndpoints(service) {
|
|
14900
|
+
return service.endpoints ?? (service.endpoint === undefined ? [] : [service.endpoint]);
|
|
14901
|
+
}
|
|
14902
|
+
function endpointReach(endpoint) {
|
|
14903
|
+
return endpoint.localUrl ?? endpoint.publicUrl ?? endpoint.url ?? `${endpoint.host}:${endpoint.port}`;
|
|
14904
|
+
}
|
|
14905
|
+
|
|
14027
14906
|
// packages/tui/src/fleet-controller.ts
|
|
14028
14907
|
function createFleetUiState() {
|
|
14029
14908
|
return {
|
|
@@ -14036,6 +14915,8 @@ function createFleetUiState() {
|
|
|
14036
14915
|
unseenLines: 0,
|
|
14037
14916
|
helpOpen: false,
|
|
14038
14917
|
doctorOpen: false,
|
|
14918
|
+
sharedOpen: false,
|
|
14919
|
+
sharedSelection: 0,
|
|
14039
14920
|
downPending: false
|
|
14040
14921
|
};
|
|
14041
14922
|
}
|
|
@@ -14059,7 +14940,7 @@ function reconcileFleetSelection(previous, snapshot, preferredProject) {
|
|
|
14059
14940
|
const previousIndex = snapshot.stacks.findIndex((stack2) => stack2.project === previous.project);
|
|
14060
14941
|
const stack = snapshot.stacks.find((candidate) => candidate.project === previous.project) ?? snapshot.stacks.find((candidate) => candidate.project === preferredProject) ?? nearestItem(snapshot.stacks, previousIndex < 0 ? 0 : previousIndex) ?? snapshot.stacks[0];
|
|
14061
14942
|
const service = stack.services.find((candidate) => candidate.name === previous.service) ?? stack.services[0];
|
|
14062
|
-
const endpoints = service
|
|
14943
|
+
const endpoints = service === undefined ? [] : serviceEndpoints(service);
|
|
14063
14944
|
const endpoint = endpoints.find((candidate) => candidate.name === previous.endpoint);
|
|
14064
14945
|
return { project: stack.project, service: service?.name, endpoint: endpoint?.name };
|
|
14065
14946
|
}
|
|
@@ -14147,7 +15028,7 @@ function reduceFleetUiState(state, action) {
|
|
|
14147
15028
|
case "select-endpoint": {
|
|
14148
15029
|
const stack = action.snapshot.stacks.find((candidate) => candidate.project === state.selection.project);
|
|
14149
15030
|
const service = stack?.services.find((candidate) => candidate.name === action.service);
|
|
14150
|
-
if (!service
|
|
15031
|
+
if (service === undefined || !serviceEndpoints(service).some((endpoint) => endpoint.name === action.endpoint))
|
|
14151
15032
|
return state;
|
|
14152
15033
|
return {
|
|
14153
15034
|
...state,
|
|
@@ -14179,12 +15060,10 @@ function reduceFleetUiState(state, action) {
|
|
|
14179
15060
|
logOffset: action.follow ? 0 : state.logOffset,
|
|
14180
15061
|
unseenLines: action.follow ? 0 : state.unseenLines
|
|
14181
15062
|
};
|
|
14182
|
-
case "scroll-logs":
|
|
14183
|
-
|
|
14184
|
-
|
|
14185
|
-
|
|
14186
|
-
logOffset: Math.max(0, state.logOffset - action.delta)
|
|
14187
|
-
};
|
|
15063
|
+
case "scroll-logs": {
|
|
15064
|
+
const offset = Math.min(action.maxOffset ?? Number.MAX_SAFE_INTEGER, Math.max(0, state.logOffset - action.delta));
|
|
15065
|
+
return action.delta > 0 && offset === 0 ? { ...state, follow: true, logOffset: 0, unseenLines: 0 } : { ...state, follow: action.delta > 0 ? state.follow : false, logOffset: offset };
|
|
15066
|
+
}
|
|
14188
15067
|
case "new-lines":
|
|
14189
15068
|
return state.follow ? state : {
|
|
14190
15069
|
...state,
|
|
@@ -14195,6 +15074,14 @@ function reduceFleetUiState(state, action) {
|
|
|
14195
15074
|
return { ...state, helpOpen: action.open };
|
|
14196
15075
|
case "doctor":
|
|
14197
15076
|
return { ...state, doctorOpen: action.open };
|
|
15077
|
+
case "shared":
|
|
15078
|
+
return { ...state, sharedOpen: action.open, sharedSelection: action.open ? 0 : state.sharedSelection };
|
|
15079
|
+
case "move-shared": {
|
|
15080
|
+
if (action.count <= 0)
|
|
15081
|
+
return { ...state, sharedSelection: 0 };
|
|
15082
|
+
const next = Math.max(0, Math.min(action.count - 1, state.sharedSelection + action.delta));
|
|
15083
|
+
return { ...state, sharedSelection: next };
|
|
15084
|
+
}
|
|
14198
15085
|
case "confirm-down":
|
|
14199
15086
|
return {
|
|
14200
15087
|
...state,
|
|
@@ -14204,11 +15091,24 @@ function reduceFleetUiState(state, action) {
|
|
|
14204
15091
|
return { ...state, downPending: action.pending };
|
|
14205
15092
|
}
|
|
14206
15093
|
}
|
|
15094
|
+
var init_fleet_controller = () => {};
|
|
14207
15095
|
|
|
14208
15096
|
// packages/tui/src/terminal-text.ts
|
|
15097
|
+
function collapseCarriageReturns(text) {
|
|
15098
|
+
if (!text.includes("\r"))
|
|
15099
|
+
return text;
|
|
15100
|
+
return text.split(`
|
|
15101
|
+
`).map((line) => {
|
|
15102
|
+
const parts = line.replace(/\r+$/, "").split("\r");
|
|
15103
|
+
return parts[parts.length - 1] ?? "";
|
|
15104
|
+
}).join(`
|
|
15105
|
+
`);
|
|
15106
|
+
}
|
|
14209
15107
|
function sanitizeFleetTerminalText(text, preserveNewlines = false) {
|
|
15108
|
+
const withoutEscapes = text.replace(ESCAPE_CONTROL_SEQUENCE, "").replace(C1_CONTROL_SEQUENCE, "");
|
|
15109
|
+
const collapsed = collapseCarriageReturns(withoutEscapes);
|
|
14210
15110
|
const controls = preserveNewlines ? /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g : /[\x00-\x1f\x7f-\x9f]/g;
|
|
14211
|
-
return
|
|
15111
|
+
return collapsed.replace(controls, "");
|
|
14212
15112
|
}
|
|
14213
15113
|
var ESCAPE_CONTROL_SEQUENCE, C1_CONTROL_SEQUENCE;
|
|
14214
15114
|
var init_terminal_text = __esm(() => {
|
|
@@ -14216,24 +15116,6 @@ var init_terminal_text = __esm(() => {
|
|
|
14216
15116
|
C1_CONTROL_SEQUENCE = /(?:[\x90\x98\x9d\x9e\x9f][\s\S]*?(?:\x07|\x9c)|\x9b[0-?]*[ -/]*[@-~])/g;
|
|
14217
15117
|
});
|
|
14218
15118
|
|
|
14219
|
-
// packages/tui/src/fleet-theme.ts
|
|
14220
|
-
var fleetTheme;
|
|
14221
|
-
var init_fleet_theme = __esm(() => {
|
|
14222
|
-
fleetTheme = {
|
|
14223
|
-
background: "#0d1117",
|
|
14224
|
-
panel: "#161b22",
|
|
14225
|
-
panelAlt: "#21262d",
|
|
14226
|
-
border: "#30363d",
|
|
14227
|
-
text: "#c9d1d9",
|
|
14228
|
-
muted: "#8b949e",
|
|
14229
|
-
accent: "#58a6ff",
|
|
14230
|
-
healthy: "#3fb950",
|
|
14231
|
-
warning: "#d29922",
|
|
14232
|
-
danger: "#f85149",
|
|
14233
|
-
selected: "#1f6feb"
|
|
14234
|
-
};
|
|
14235
|
-
});
|
|
14236
|
-
|
|
14237
15119
|
// node_modules/.bun/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
14238
15120
|
function ansiRegex({ onlyFirst = false } = {}) {
|
|
14239
15121
|
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
|
@@ -14416,229 +15298,623 @@ function padFleetText(text, width) {
|
|
|
14416
15298
|
const fitted = fitFleetText(text, width);
|
|
14417
15299
|
return fitted + " ".repeat(Math.max(0, width - stringWidth(fitted)));
|
|
14418
15300
|
}
|
|
15301
|
+
function wrapFleetText(text, width, maxLines = 3) {
|
|
15302
|
+
if (width <= 0)
|
|
15303
|
+
return [];
|
|
15304
|
+
if (text === "")
|
|
15305
|
+
return [""];
|
|
15306
|
+
if (stringWidth(text) <= width)
|
|
15307
|
+
return [text];
|
|
15308
|
+
const characters = Array.from(text);
|
|
15309
|
+
const lines = [];
|
|
15310
|
+
let index = 0;
|
|
15311
|
+
while (index < characters.length && lines.length < maxLines) {
|
|
15312
|
+
const last = lines.length === maxLines - 1;
|
|
15313
|
+
if (last) {
|
|
15314
|
+
lines.push(fitFleetText(characters.slice(index).join(""), width));
|
|
15315
|
+
break;
|
|
15316
|
+
}
|
|
15317
|
+
let line = "";
|
|
15318
|
+
let taken = 0;
|
|
15319
|
+
while (index + taken < characters.length) {
|
|
15320
|
+
const next = characters[index + taken];
|
|
15321
|
+
if (stringWidth(line + next) > width)
|
|
15322
|
+
break;
|
|
15323
|
+
line += next;
|
|
15324
|
+
taken += 1;
|
|
15325
|
+
}
|
|
15326
|
+
if (taken === 0) {
|
|
15327
|
+
lines.push(characters[index]);
|
|
15328
|
+
index += 1;
|
|
15329
|
+
continue;
|
|
15330
|
+
}
|
|
15331
|
+
lines.push(line);
|
|
15332
|
+
index += taken;
|
|
15333
|
+
}
|
|
15334
|
+
return lines;
|
|
15335
|
+
}
|
|
14419
15336
|
var init_fleet_text = __esm(() => {
|
|
14420
15337
|
init_string_width();
|
|
14421
15338
|
});
|
|
14422
15339
|
|
|
14423
|
-
// packages/tui/src/
|
|
14424
|
-
|
|
14425
|
-
|
|
15340
|
+
// packages/tui/src/fleet-theme.ts
|
|
15341
|
+
function blendHex(fg, bg, amount) {
|
|
15342
|
+
const parse = (hex2) => [
|
|
15343
|
+
Number.parseInt(hex2.slice(1, 3), 16),
|
|
15344
|
+
Number.parseInt(hex2.slice(3, 5), 16),
|
|
15345
|
+
Number.parseInt(hex2.slice(5, 7), 16)
|
|
15346
|
+
];
|
|
15347
|
+
const [fr, fg2, fb] = parse(fg);
|
|
15348
|
+
const [br, bg2, bb] = parse(bg);
|
|
15349
|
+
const channel = (f, b) => Math.round(b + (f - b) * amount);
|
|
15350
|
+
const hex = (value) => value.toString(16).padStart(2, "0");
|
|
15351
|
+
return `#${hex(channel(fr, br))}${hex(channel(fg2, bg2))}${hex(channel(fb, bb))}`;
|
|
15352
|
+
}
|
|
15353
|
+
function backendColor(backend) {
|
|
15354
|
+
if (backend === "docker")
|
|
15355
|
+
return fleetTheme.backendDocker;
|
|
15356
|
+
if (backend === "tunnel")
|
|
15357
|
+
return fleetTheme.backendTunnel;
|
|
15358
|
+
if (backend === "wrangler" || backend === "proc")
|
|
15359
|
+
return fleetTheme.backendProc;
|
|
15360
|
+
return fleetTheme.muted;
|
|
15361
|
+
}
|
|
15362
|
+
function serviceStateColor(state) {
|
|
15363
|
+
if (state === "healthy")
|
|
15364
|
+
return fleetTheme.healthy;
|
|
15365
|
+
if (state === "unknown" || state === "unhealthy" || state === "starting")
|
|
15366
|
+
return fleetTheme.warning;
|
|
15367
|
+
return fleetTheme.danger;
|
|
15368
|
+
}
|
|
15369
|
+
function serviceStateGlyph(state) {
|
|
15370
|
+
if (state === "healthy")
|
|
15371
|
+
return "\u25CF";
|
|
15372
|
+
if (state === "unhealthy")
|
|
15373
|
+
return "\u25C6";
|
|
15374
|
+
if (state === "starting")
|
|
15375
|
+
return "\u25CC";
|
|
15376
|
+
return "\u25CB";
|
|
15377
|
+
}
|
|
15378
|
+
function stackPhaseColor(phase) {
|
|
14426
15379
|
if (phase === "up")
|
|
14427
15380
|
return fleetTheme.healthy;
|
|
14428
|
-
if (phase === "degraded" || phase === "unknown")
|
|
15381
|
+
if (phase === "degraded" || phase === "unknown" || phase === "starting")
|
|
14429
15382
|
return fleetTheme.warning;
|
|
15383
|
+
if (phase === "queued" || phase === "reserved")
|
|
15384
|
+
return fleetTheme.queued;
|
|
14430
15385
|
if (phase === "stopped")
|
|
14431
|
-
return fleetTheme.
|
|
15386
|
+
return fleetTheme.faint;
|
|
14432
15387
|
return fleetTheme.accent;
|
|
14433
15388
|
}
|
|
14434
|
-
function
|
|
14435
|
-
|
|
14436
|
-
|
|
14437
|
-
|
|
14438
|
-
|
|
14439
|
-
|
|
14440
|
-
|
|
14441
|
-
|
|
14442
|
-
|
|
14443
|
-
|
|
14444
|
-
|
|
14445
|
-
|
|
14446
|
-
fg: fleetTheme.text,
|
|
14447
|
-
children: "Fleet"
|
|
14448
|
-
}, undefined, false, undefined, this)
|
|
14449
|
-
}, undefined, false, undefined, this),
|
|
14450
|
-
stacks.length === 0 ? /* @__PURE__ */ jsxDEV("box", {
|
|
14451
|
-
style: { paddingLeft: 1, paddingTop: 1 },
|
|
14452
|
-
children: /* @__PURE__ */ jsxDEV("text", {
|
|
14453
|
-
fg: fleetTheme.muted,
|
|
14454
|
-
children: "No managed stacks"
|
|
14455
|
-
}, undefined, false, undefined, this)
|
|
14456
|
-
}, undefined, false, undefined, this) : stacks.map((stack) => {
|
|
14457
|
-
const selected = stack.project === selectedProject;
|
|
14458
|
-
const branch = sanitizeFleetTerminalText(stack.branch);
|
|
14459
|
-
return /* @__PURE__ */ jsxDEV("box", {
|
|
14460
|
-
onMouseDown: (event) => {
|
|
14461
|
-
if (event.button !== 0)
|
|
14462
|
-
return;
|
|
14463
|
-
event.preventDefault();
|
|
14464
|
-
event.stopPropagation();
|
|
14465
|
-
onSelectProject?.(stack.project);
|
|
14466
|
-
},
|
|
14467
|
-
style: {
|
|
14468
|
-
height: 1,
|
|
14469
|
-
paddingLeft: 1,
|
|
14470
|
-
paddingRight: 1,
|
|
14471
|
-
flexDirection: "row",
|
|
14472
|
-
backgroundColor: selected ? fleetTheme.selected : fleetTheme.background
|
|
14473
|
-
},
|
|
14474
|
-
children: [
|
|
14475
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
14476
|
-
fg: selected ? "#ffffff" : phaseColor(stack.phase),
|
|
14477
|
-
children: selected ? "\u25B8 " : " "
|
|
14478
|
-
}, undefined, false, undefined, this),
|
|
14479
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
14480
|
-
fg: selected ? "#ffffff" : fleetTheme.text,
|
|
14481
|
-
children: padFleetText(branch, Math.max(4, width - 13))
|
|
14482
|
-
}, undefined, false, undefined, this),
|
|
14483
|
-
/* @__PURE__ */ jsxDEV("text", {
|
|
14484
|
-
fg: selected ? "#ffffff" : phaseColor(stack.phase),
|
|
14485
|
-
children: padFleetText(stack.phase, 9)
|
|
14486
|
-
}, undefined, false, undefined, this)
|
|
14487
|
-
]
|
|
14488
|
-
}, stack.project, true, undefined, this);
|
|
14489
|
-
})
|
|
14490
|
-
]
|
|
14491
|
-
}, undefined, true, undefined, this);
|
|
15389
|
+
function stackPhaseGlyph(phase, spinnerFrame = 0) {
|
|
15390
|
+
if (phase === "up")
|
|
15391
|
+
return "\u25CF";
|
|
15392
|
+
if (phase === "degraded")
|
|
15393
|
+
return "\u25C6";
|
|
15394
|
+
if (phase === "starting")
|
|
15395
|
+
return SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
15396
|
+
if (phase === "queued" || phase === "reserved")
|
|
15397
|
+
return "\u25CC";
|
|
15398
|
+
if (phase === "stopped")
|
|
15399
|
+
return "\u25CB";
|
|
15400
|
+
return "\u25CB";
|
|
14492
15401
|
}
|
|
14493
|
-
var
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
15402
|
+
var background = "#0d1117", text = "#c9d1d9", accent = "#58a6ff", fleetTheme, SPINNER_FRAMES;
|
|
15403
|
+
var init_fleet_theme = __esm(() => {
|
|
15404
|
+
fleetTheme = {
|
|
15405
|
+
background,
|
|
15406
|
+
panel: blendHex(text, background, 0.04),
|
|
15407
|
+
panelAlt: blendHex(text, background, 0.1),
|
|
15408
|
+
border: blendHex(text, background, 0.18),
|
|
15409
|
+
text,
|
|
15410
|
+
bright: "#e6edf3",
|
|
15411
|
+
muted: "#8b949e",
|
|
15412
|
+
faint: "#6e7681",
|
|
15413
|
+
accent,
|
|
15414
|
+
selectedBg: blendHex(accent, background, 0.16),
|
|
15415
|
+
stripe: accent,
|
|
15416
|
+
keycapBg: blendHex(text, background, 0.22),
|
|
15417
|
+
healthy: "#3fb950",
|
|
15418
|
+
warning: "#d29922",
|
|
15419
|
+
danger: "#f85149",
|
|
15420
|
+
queued: "#bc8cff",
|
|
15421
|
+
backendDocker: "#58a6ff",
|
|
15422
|
+
backendProc: "#bc8cff",
|
|
15423
|
+
backendWrangler: "#bc8cff",
|
|
15424
|
+
backendTunnel: "#ffa657",
|
|
15425
|
+
publicBadge: "#ffa657"
|
|
15426
|
+
};
|
|
15427
|
+
SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
14497
15428
|
});
|
|
14498
15429
|
|
|
14499
|
-
// packages/tui/src/
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
return fleetTheme.healthy;
|
|
14504
|
-
if (service.state === "unknown" || service.state === "unhealthy")
|
|
14505
|
-
return fleetTheme.warning;
|
|
14506
|
-
return fleetTheme.danger;
|
|
15430
|
+
// packages/tui/src/fleet-keys.ts
|
|
15431
|
+
function isEscapeKey(key) {
|
|
15432
|
+
const name = key.name.toLowerCase();
|
|
15433
|
+
return name === "escape" || name === "esc" || key.sequence === "\x1B";
|
|
14507
15434
|
}
|
|
14508
|
-
function
|
|
14509
|
-
return
|
|
15435
|
+
function isEnterKey(key) {
|
|
15436
|
+
return key.name === "return" || key.name === "enter";
|
|
14510
15437
|
}
|
|
14511
|
-
function
|
|
14512
|
-
|
|
14513
|
-
|
|
14514
|
-
|
|
14515
|
-
|
|
14516
|
-
|
|
14517
|
-
|
|
14518
|
-
return
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
14531
|
-
|
|
14532
|
-
|
|
14533
|
-
|
|
14534
|
-
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
|
|
14542
|
-
|
|
14543
|
-
|
|
14544
|
-
|
|
14545
|
-
|
|
15438
|
+
function isPlainKey(key, value) {
|
|
15439
|
+
return !key.ctrl && !key.meta && !key.option && !key.super && !key.hyper && (key.name === value || key.sequence === value);
|
|
15440
|
+
}
|
|
15441
|
+
function isUpKey(key) {
|
|
15442
|
+
return key.name === "up" || isPlainKey(key, "k");
|
|
15443
|
+
}
|
|
15444
|
+
function isDownKey(key) {
|
|
15445
|
+
return key.name === "down" || isPlainKey(key, "j");
|
|
15446
|
+
}
|
|
15447
|
+
function isShiftedKey(key, lower) {
|
|
15448
|
+
return key.sequence === lower.toUpperCase() || isPlainKey(key, lower) && key.shift === true;
|
|
15449
|
+
}
|
|
15450
|
+
function isFollowBottomKey(key) {
|
|
15451
|
+
return isShiftedKey(key, "g");
|
|
15452
|
+
}
|
|
15453
|
+
function isScrollTopKey(key) {
|
|
15454
|
+
return !isShiftedKey(key, "g") && (key.sequence === "g" || isPlainKey(key, "g"));
|
|
15455
|
+
}
|
|
15456
|
+
function isDoctorKey(key) {
|
|
15457
|
+
return isShiftedKey(key, "d");
|
|
15458
|
+
}
|
|
15459
|
+
|
|
15460
|
+
// packages/tui/src/fleet-layout.ts
|
|
15461
|
+
function resolveFleetLayout(mode, terminalWidth) {
|
|
15462
|
+
if (mode === "auto")
|
|
15463
|
+
return terminalWidth >= SPLIT_MIN_WIDTH ? "split" : "stack";
|
|
15464
|
+
return mode;
|
|
15465
|
+
}
|
|
15466
|
+
function fleetPaneWidths(layout, terminalWidth) {
|
|
15467
|
+
if (layout === "stack")
|
|
15468
|
+
return { sidebar: terminalWidth, main: terminalWidth };
|
|
15469
|
+
const sidebar = Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.floor(terminalWidth * 0.26)));
|
|
15470
|
+
return { sidebar, main: Math.max(20, terminalWidth - sidebar) };
|
|
15471
|
+
}
|
|
15472
|
+
function servicePaneHeight(rows, maxHeight) {
|
|
15473
|
+
const content = Math.max(2, rows + 1);
|
|
15474
|
+
return Math.min(maxHeight, content + 3);
|
|
15475
|
+
}
|
|
15476
|
+
function stackSidebarHeight(stackCount) {
|
|
15477
|
+
return Math.min(9, Math.max(4, stackCount + 3));
|
|
15478
|
+
}
|
|
15479
|
+
var SPLIT_MIN_WIDTH = 110, SIDEBAR_MIN_WIDTH = 28, SIDEBAR_MAX_WIDTH = 44;
|
|
15480
|
+
|
|
15481
|
+
// packages/tui/src/fleet-log-rows.ts
|
|
15482
|
+
function lineId(line) {
|
|
15483
|
+
let id = lineIds.get(line);
|
|
15484
|
+
if (id === undefined) {
|
|
15485
|
+
id = nextLineId++;
|
|
15486
|
+
lineIds.set(line, id);
|
|
15487
|
+
}
|
|
15488
|
+
return id;
|
|
15489
|
+
}
|
|
15490
|
+
function wrappedBody(line, bodyWidth) {
|
|
15491
|
+
const cached = wrapCache.get(line);
|
|
15492
|
+
if (cached !== undefined && cached.width === bodyWidth)
|
|
15493
|
+
return cached.wrapped;
|
|
15494
|
+
const wrapped = wrapFleetText(line.text, bodyWidth, 3);
|
|
15495
|
+
wrapCache.set(line, { width: bodyWidth, wrapped });
|
|
15496
|
+
return wrapped;
|
|
15497
|
+
}
|
|
15498
|
+
function buildFleetLogRows(lines, contentWidth) {
|
|
15499
|
+
const rows = [];
|
|
15500
|
+
for (const line of lines) {
|
|
15501
|
+
const meta = line.meta === true;
|
|
15502
|
+
const bodyWidth = meta ? Math.max(1, contentWidth - META_TAG.length) : contentWidth;
|
|
15503
|
+
const wrapped = wrappedBody(line, bodyWidth);
|
|
15504
|
+
const id = lineId(line);
|
|
15505
|
+
for (let part = 0;part < wrapped.length; part += 1) {
|
|
15506
|
+
rows.push({
|
|
15507
|
+
key: `${id}:${part}`,
|
|
15508
|
+
text: wrapped[part],
|
|
15509
|
+
meta,
|
|
15510
|
+
tag: !meta ? undefined : part === 0 ? META_TAG : " ".repeat(META_TAG.length)
|
|
15511
|
+
});
|
|
15512
|
+
}
|
|
15513
|
+
}
|
|
15514
|
+
return rows;
|
|
15515
|
+
}
|
|
15516
|
+
var META_TAG = "hestia \u2502 ", nextLineId = 0, lineIds, wrapCache;
|
|
15517
|
+
var init_fleet_log_rows = __esm(() => {
|
|
15518
|
+
init_fleet_text();
|
|
15519
|
+
lineIds = new WeakMap;
|
|
15520
|
+
wrapCache = new WeakMap;
|
|
15521
|
+
});
|
|
15522
|
+
|
|
15523
|
+
// packages/tui/src/fleet-format.ts
|
|
15524
|
+
function formatUptime(createdAt, now) {
|
|
15525
|
+
const born = Date.parse(createdAt);
|
|
15526
|
+
if (!Number.isFinite(born))
|
|
15527
|
+
return;
|
|
15528
|
+
const seconds = Math.max(0, Math.floor((now - born) / 1000));
|
|
15529
|
+
if (seconds < 60)
|
|
15530
|
+
return `${seconds}s`;
|
|
15531
|
+
const minutes = Math.floor(seconds / 60);
|
|
15532
|
+
if (minutes < 60)
|
|
15533
|
+
return `${minutes}m`;
|
|
15534
|
+
const hours = Math.floor(minutes / 60);
|
|
15535
|
+
if (hours < 24)
|
|
15536
|
+
return `${hours}h${minutes % 60 === 0 ? "" : `${minutes % 60}m`}`;
|
|
15537
|
+
const days = Math.floor(hours / 24);
|
|
15538
|
+
return `${days}d${hours % 24 === 0 ? "" : `${hours % 24}h`}`;
|
|
15539
|
+
}
|
|
15540
|
+
function buildEnvBlock(stack) {
|
|
15541
|
+
const lines = [];
|
|
15542
|
+
for (const service of stack.services) {
|
|
15543
|
+
if (service.backend === "tunnel")
|
|
15544
|
+
continue;
|
|
15545
|
+
if (service.publishedPort !== undefined) {
|
|
15546
|
+
lines.push(`HESTIA_${envKey(service.name)}_PORT=${service.publishedPort}`);
|
|
15547
|
+
if (service.backend === "docker") {
|
|
15548
|
+
lines.push(`HESTIA_${envKey(service.name)}_MAIN_TCP_PORT=${service.publishedPort}`);
|
|
15549
|
+
}
|
|
15550
|
+
}
|
|
15551
|
+
for (const endpoint of serviceEndpoints(service)) {
|
|
15552
|
+
const alias = envKey(endpoint.name);
|
|
15553
|
+
lines.push(`HESTIA_${alias}_PORT=${endpoint.port}`);
|
|
15554
|
+
if (endpoint.publicUrl !== undefined)
|
|
15555
|
+
lines.push(`HESTIA_${alias}_URL=${endpoint.publicUrl}`);
|
|
15556
|
+
if (endpoint.localUrl !== undefined)
|
|
15557
|
+
lines.push(`HESTIA_${alias}_LOCAL_URL=${endpoint.localUrl}`);
|
|
15558
|
+
if (endpoint.url !== undefined)
|
|
15559
|
+
lines.push(`HESTIA_${alias}_DIRECT_URL=${endpoint.url}`);
|
|
15560
|
+
}
|
|
15561
|
+
}
|
|
15562
|
+
return [...new Set(lines)].join(`
|
|
15563
|
+
`);
|
|
15564
|
+
}
|
|
15565
|
+
var init_fleet_format = __esm(() => {
|
|
15566
|
+
init_src2();
|
|
15567
|
+
});
|
|
15568
|
+
|
|
15569
|
+
// packages/tui/src/fleet-status.ts
|
|
15570
|
+
function fleetCapacitySummary(capacity, connected) {
|
|
15571
|
+
if (!connected)
|
|
15572
|
+
return "daemon unreachable";
|
|
15573
|
+
const parts = [`${capacity.live}/${capacity.maxStacks}`];
|
|
15574
|
+
if (capacity.reserved > 0)
|
|
15575
|
+
parts.push(`${capacity.reserved} reserved`);
|
|
15576
|
+
if (capacity.queued > 0)
|
|
15577
|
+
parts.push(`${capacity.queued} queued`);
|
|
15578
|
+
parts.push("daemon ok");
|
|
15579
|
+
return parts.join(" \xB7 ");
|
|
15580
|
+
}
|
|
15581
|
+
function resolveStatusNotice(connectionNotice, toast) {
|
|
15582
|
+
return toast ?? connectionNotice;
|
|
15583
|
+
}
|
|
15584
|
+
var FLEET_KEY_HINTS;
|
|
15585
|
+
var init_fleet_status = __esm(() => {
|
|
15586
|
+
FLEET_KEY_HINTS = [
|
|
15587
|
+
{ keys: ", .", label: "stack" },
|
|
15588
|
+
{ keys: "[ ]", label: "svc" },
|
|
15589
|
+
{ keys: "o", label: "open" },
|
|
15590
|
+
{ keys: "y", label: "yank" },
|
|
15591
|
+
{ keys: "?", label: "help" }
|
|
15592
|
+
];
|
|
15593
|
+
});
|
|
15594
|
+
|
|
15595
|
+
// packages/tui/src/components/StackSidebar.tsx
|
|
15596
|
+
import { memo } from "react";
|
|
15597
|
+
import { jsxDEV } from "@opentui/react/jsx-dev-runtime";
|
|
15598
|
+
var StackSidebar;
|
|
15599
|
+
var init_StackSidebar = __esm(() => {
|
|
15600
|
+
init_string_width();
|
|
15601
|
+
init_fleet_text();
|
|
15602
|
+
init_fleet_theme();
|
|
15603
|
+
init_terminal_text();
|
|
15604
|
+
StackSidebar = memo(function StackSidebar2({
|
|
15605
|
+
stacks,
|
|
15606
|
+
capacity,
|
|
15607
|
+
selectedProject,
|
|
15608
|
+
width,
|
|
15609
|
+
focused = false,
|
|
15610
|
+
spinnerFrame = 0,
|
|
15611
|
+
onSelectProject
|
|
15612
|
+
}) {
|
|
15613
|
+
const branchWidth = Math.max(6, width - 17);
|
|
15614
|
+
const slots = capacity.maxStacks > 0 ? ` \xB7 ${capacity.live}/${capacity.maxStacks} slots` : "";
|
|
15615
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
15616
|
+
style: {
|
|
15617
|
+
width,
|
|
15618
|
+
height: "100%",
|
|
15619
|
+
flexDirection: "column",
|
|
15620
|
+
border: true,
|
|
15621
|
+
borderColor: focused ? fleetTheme.accent : fleetTheme.border,
|
|
15622
|
+
backgroundColor: fleetTheme.background
|
|
15623
|
+
},
|
|
15624
|
+
children: [
|
|
15625
|
+
/* @__PURE__ */ jsxDEV("box", {
|
|
15626
|
+
style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
|
|
14546
15627
|
children: [
|
|
14547
|
-
/* @__PURE__ */
|
|
14548
|
-
fg:
|
|
14549
|
-
children:
|
|
14550
|
-
}, undefined, false, undefined, this),
|
|
14551
|
-
/* @__PURE__ */ jsxDEV2("text", {
|
|
14552
|
-
fg: workloadSelected ? "#ffffff" : fleetTheme.text,
|
|
14553
|
-
children: padFleetText(sanitizeFleetTerminalText(service.name), 20)
|
|
15628
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15629
|
+
fg: focused ? fleetTheme.accent : fleetTheme.text,
|
|
15630
|
+
children: "Stacks"
|
|
14554
15631
|
}, undefined, false, undefined, this),
|
|
14555
|
-
/* @__PURE__ */
|
|
14556
|
-
fg:
|
|
14557
|
-
children:
|
|
15632
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15633
|
+
fg: fleetTheme.faint,
|
|
15634
|
+
children: slots
|
|
15635
|
+
}, undefined, false, undefined, this)
|
|
15636
|
+
]
|
|
15637
|
+
}, undefined, true, undefined, this),
|
|
15638
|
+
stacks.length === 0 ? /* @__PURE__ */ jsxDEV("box", {
|
|
15639
|
+
style: { paddingLeft: 1, paddingTop: 1, flexDirection: "column" },
|
|
15640
|
+
children: [
|
|
15641
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15642
|
+
fg: fleetTheme.muted,
|
|
15643
|
+
children: "No managed stacks."
|
|
14558
15644
|
}, undefined, false, undefined, this),
|
|
14559
|
-
/* @__PURE__ */
|
|
14560
|
-
fg:
|
|
14561
|
-
children:
|
|
15645
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15646
|
+
fg: fleetTheme.faint,
|
|
15647
|
+
children: "`hestia up` starts one here."
|
|
14562
15648
|
}, undefined, false, undefined, this)
|
|
14563
15649
|
]
|
|
14564
|
-
},
|
|
14565
|
-
|
|
14566
|
-
const
|
|
14567
|
-
const
|
|
14568
|
-
const
|
|
14569
|
-
const
|
|
14570
|
-
|
|
15650
|
+
}, undefined, true, undefined, this) : stacks.map((stack) => {
|
|
15651
|
+
const selected = stack.project === selectedProject;
|
|
15652
|
+
const branch = sanitizeFleetTerminalText(stack.branch);
|
|
15653
|
+
const phase = sanitizeFleetTerminalText(stack.phase);
|
|
15654
|
+
const glyph = stackPhaseGlyph(stack.phase, spinnerFrame);
|
|
15655
|
+
const meta = stack.services.length > 0 ? ` \xB7${stack.services.length}` : "";
|
|
15656
|
+
const warn = stack.warning === undefined ? "" : " \u26A0";
|
|
15657
|
+
const available = Math.max(4, branchWidth - stringWidth(meta) - stringWidth(warn));
|
|
15658
|
+
const rowBg = selected ? fleetTheme.selectedBg : fleetTheme.background;
|
|
15659
|
+
return /* @__PURE__ */ jsxDEV("box", {
|
|
14571
15660
|
onMouseDown: (event) => {
|
|
14572
15661
|
if (event.button !== 0)
|
|
14573
15662
|
return;
|
|
14574
15663
|
event.preventDefault();
|
|
14575
15664
|
event.stopPropagation();
|
|
14576
|
-
|
|
14577
|
-
},
|
|
14578
|
-
style: {
|
|
14579
|
-
height: 1,
|
|
14580
|
-
paddingLeft: 2,
|
|
14581
|
-
paddingRight: 1,
|
|
14582
|
-
flexDirection: "row",
|
|
14583
|
-
backgroundColor: endpointSelected ? fleetTheme.selected : fleetTheme.background
|
|
15665
|
+
onSelectProject?.(stack.project);
|
|
14584
15666
|
},
|
|
15667
|
+
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: rowBg },
|
|
14585
15668
|
children: [
|
|
14586
|
-
/* @__PURE__ */
|
|
14587
|
-
fg:
|
|
14588
|
-
children:
|
|
15669
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15670
|
+
fg: fleetTheme.stripe,
|
|
15671
|
+
children: selected ? "\u258E" : " "
|
|
14589
15672
|
}, undefined, false, undefined, this),
|
|
14590
|
-
/* @__PURE__ */
|
|
14591
|
-
fg:
|
|
14592
|
-
children:
|
|
15673
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15674
|
+
fg: stackPhaseColor(stack.phase),
|
|
15675
|
+
children: [
|
|
15676
|
+
glyph,
|
|
15677
|
+
" "
|
|
15678
|
+
]
|
|
15679
|
+
}, undefined, true, undefined, this),
|
|
15680
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15681
|
+
fg: selected ? fleetTheme.bright : fleetTheme.text,
|
|
15682
|
+
children: padFleetText(fitFleetText(branch, available), available)
|
|
14593
15683
|
}, undefined, false, undefined, this),
|
|
14594
|
-
/* @__PURE__ */
|
|
14595
|
-
fg:
|
|
14596
|
-
children:
|
|
15684
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15685
|
+
fg: fleetTheme.warning,
|
|
15686
|
+
children: warn
|
|
14597
15687
|
}, undefined, false, undefined, this),
|
|
14598
|
-
/* @__PURE__ */
|
|
14599
|
-
fg:
|
|
14600
|
-
children:
|
|
15688
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15689
|
+
fg: fleetTheme.faint,
|
|
15690
|
+
children: meta
|
|
14601
15691
|
}, undefined, false, undefined, this),
|
|
14602
|
-
/* @__PURE__ */
|
|
14603
|
-
fg:
|
|
14604
|
-
children:
|
|
15692
|
+
/* @__PURE__ */ jsxDEV("text", {
|
|
15693
|
+
fg: stackPhaseColor(stack.phase),
|
|
15694
|
+
children: ` ${padFleetText(phase, 8)}`
|
|
14605
15695
|
}, undefined, false, undefined, this)
|
|
14606
15696
|
]
|
|
14607
|
-
},
|
|
14608
|
-
})
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
15697
|
+
}, stack.project, true, undefined, this);
|
|
15698
|
+
})
|
|
15699
|
+
]
|
|
15700
|
+
}, undefined, true, undefined, this);
|
|
15701
|
+
});
|
|
15702
|
+
});
|
|
15703
|
+
|
|
15704
|
+
// packages/tui/src/components/ServicePane.tsx
|
|
15705
|
+
import { memo as memo2 } from "react";
|
|
15706
|
+
import { jsxDEV as jsxDEV2, Fragment } from "@opentui/react/jsx-dev-runtime";
|
|
15707
|
+
function portLabel(service) {
|
|
15708
|
+
if (service.publishedPort === undefined)
|
|
15709
|
+
return "\u2014";
|
|
15710
|
+
return `:${service.publishedPort}`;
|
|
15711
|
+
}
|
|
15712
|
+
var ServicePane;
|
|
14620
15713
|
var init_ServicePane = __esm(() => {
|
|
14621
15714
|
init_fleet_text();
|
|
14622
15715
|
init_fleet_theme();
|
|
14623
15716
|
init_terminal_text();
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
15717
|
+
ServicePane = memo2(function ServicePane2({
|
|
15718
|
+
stack,
|
|
15719
|
+
uptime,
|
|
15720
|
+
selectedService,
|
|
15721
|
+
selectedEndpoint,
|
|
15722
|
+
width = 80,
|
|
15723
|
+
focused = false,
|
|
15724
|
+
onSelectService,
|
|
15725
|
+
onSelectEndpoint
|
|
15726
|
+
}) {
|
|
15727
|
+
const nameWidth = Math.min(24, Math.max(12, Math.floor(width * 0.28)));
|
|
15728
|
+
const backendWidth = 10;
|
|
15729
|
+
const stateWidth = 10;
|
|
15730
|
+
const portWidth = 7;
|
|
15731
|
+
const title = stack === undefined ? "Workloads" : `Workloads \u2014 ${sanitizeFleetTerminalText(stack.branch)}`;
|
|
15732
|
+
const warning = stack?.warning === undefined ? undefined : sanitizeFleetTerminalText(stack.warning);
|
|
15733
|
+
return /* @__PURE__ */ jsxDEV2("box", {
|
|
15734
|
+
style: {
|
|
15735
|
+
height: "100%",
|
|
15736
|
+
width: "100%",
|
|
15737
|
+
flexDirection: "column",
|
|
15738
|
+
border: true,
|
|
15739
|
+
borderColor: focused ? fleetTheme.accent : fleetTheme.border,
|
|
15740
|
+
backgroundColor: fleetTheme.background
|
|
15741
|
+
},
|
|
15742
|
+
children: [
|
|
15743
|
+
/* @__PURE__ */ jsxDEV2("box", {
|
|
15744
|
+
style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
|
|
15745
|
+
children: [
|
|
15746
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15747
|
+
fg: focused ? fleetTheme.accent : fleetTheme.text,
|
|
15748
|
+
children: fitFleetText(title, Math.max(8, width - 14))
|
|
15749
|
+
}, undefined, false, undefined, this),
|
|
15750
|
+
uptime === undefined ? null : /* @__PURE__ */ jsxDEV2("text", {
|
|
15751
|
+
fg: fleetTheme.faint,
|
|
15752
|
+
children: [
|
|
15753
|
+
" \xB7 up ",
|
|
15754
|
+
uptime
|
|
15755
|
+
]
|
|
15756
|
+
}, undefined, true, undefined, this)
|
|
15757
|
+
]
|
|
15758
|
+
}, undefined, true, undefined, this),
|
|
15759
|
+
warning === undefined ? null : /* @__PURE__ */ jsxDEV2("box", {
|
|
15760
|
+
style: { height: 1, paddingLeft: 1, paddingRight: 1 },
|
|
15761
|
+
children: /* @__PURE__ */ jsxDEV2("text", {
|
|
15762
|
+
fg: fleetTheme.warning,
|
|
15763
|
+
children: fitFleetText(`\u26A0 ${warning}`, Math.max(8, width - 4))
|
|
15764
|
+
}, undefined, false, undefined, this)
|
|
15765
|
+
}, undefined, false, undefined, this),
|
|
15766
|
+
stack?.services.length ? /* @__PURE__ */ jsxDEV2(Fragment, {
|
|
15767
|
+
children: [
|
|
15768
|
+
/* @__PURE__ */ jsxDEV2("box", {
|
|
15769
|
+
style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row" },
|
|
15770
|
+
children: [
|
|
15771
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15772
|
+
fg: fleetTheme.faint,
|
|
15773
|
+
children: padFleetText("", 4)
|
|
15774
|
+
}, undefined, false, undefined, this),
|
|
15775
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15776
|
+
fg: fleetTheme.faint,
|
|
15777
|
+
children: padFleetText("NAME", nameWidth)
|
|
15778
|
+
}, undefined, false, undefined, this),
|
|
15779
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15780
|
+
fg: fleetTheme.faint,
|
|
15781
|
+
children: padFleetText("BACKEND", backendWidth)
|
|
15782
|
+
}, undefined, false, undefined, this),
|
|
15783
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15784
|
+
fg: fleetTheme.faint,
|
|
15785
|
+
children: padFleetText("STATE", stateWidth)
|
|
15786
|
+
}, undefined, false, undefined, this),
|
|
15787
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15788
|
+
fg: fleetTheme.faint,
|
|
15789
|
+
children: padFleetText("PORT", portWidth)
|
|
15790
|
+
}, undefined, false, undefined, this)
|
|
15791
|
+
]
|
|
15792
|
+
}, undefined, true, undefined, this),
|
|
15793
|
+
stack.services.flatMap((service) => {
|
|
15794
|
+
const workloadSelected = service.name === selectedService && selectedEndpoint === undefined;
|
|
15795
|
+
const endpoints = serviceEndpoints(service);
|
|
15796
|
+
const state = sanitizeFleetTerminalText(service.state);
|
|
15797
|
+
const workloadBg = workloadSelected ? fleetTheme.selectedBg : fleetTheme.background;
|
|
15798
|
+
const workloadRow = /* @__PURE__ */ jsxDEV2("box", {
|
|
15799
|
+
onMouseDown: (event) => {
|
|
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;
|
|
15846
|
+
return /* @__PURE__ */ jsxDEV2("box", {
|
|
15847
|
+
onMouseDown: (event) => {
|
|
15848
|
+
if (event.button !== 0)
|
|
15849
|
+
return;
|
|
15850
|
+
event.preventDefault();
|
|
15851
|
+
event.stopPropagation();
|
|
15852
|
+
onSelectEndpoint?.(service.name, endpoint.name);
|
|
15853
|
+
},
|
|
15854
|
+
style: { height: 1, paddingRight: 1, flexDirection: "row", backgroundColor: endpointBg },
|
|
15855
|
+
children: [
|
|
15856
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15857
|
+
fg: fleetTheme.stripe,
|
|
15858
|
+
children: endpointSelected ? "\u258E" : " "
|
|
15859
|
+
}, undefined, false, undefined, this),
|
|
15860
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15861
|
+
fg: fleetTheme.faint,
|
|
15862
|
+
children: ` ${connector} `
|
|
15863
|
+
}, undefined, false, undefined, this),
|
|
15864
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15865
|
+
fg: endpointSelected ? fleetTheme.bright : fleetTheme.accent,
|
|
15866
|
+
children: padFleetText(sanitizeFleetTerminalText(endpoint.name), aliasWidth)
|
|
15867
|
+
}, undefined, false, undefined, this),
|
|
15868
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15869
|
+
fg: fleetTheme.muted,
|
|
15870
|
+
children: padFleetText(kind, 6)
|
|
15871
|
+
}, undefined, false, undefined, this),
|
|
15872
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15873
|
+
fg: fleetTheme.text,
|
|
15874
|
+
children: fitFleetText(reach, Math.max(12, width - aliasWidth - 19 - pub.length))
|
|
15875
|
+
}, undefined, false, undefined, this),
|
|
15876
|
+
/* @__PURE__ */ jsxDEV2("text", {
|
|
15877
|
+
fg: fleetTheme.publicBadge,
|
|
15878
|
+
children: pub
|
|
15879
|
+
}, undefined, false, undefined, this)
|
|
15880
|
+
]
|
|
15881
|
+
}, `endpoint:${service.name}:${endpoint.name}`, true, undefined, this);
|
|
15882
|
+
});
|
|
15883
|
+
return [workloadRow, ...endpointRows];
|
|
15884
|
+
})
|
|
15885
|
+
]
|
|
15886
|
+
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV2("box", {
|
|
15887
|
+
style: { paddingLeft: 1, paddingTop: 1 },
|
|
15888
|
+
children: /* @__PURE__ */ jsxDEV2("text", {
|
|
15889
|
+
fg: fleetTheme.muted,
|
|
15890
|
+
children: stack?.phase === "queued" || stack?.phase === "reserved" ? "Waiting for a machine slot \u2014 services appear once startup begins." : "No workloads recorded"
|
|
15891
|
+
}, undefined, false, undefined, this)
|
|
15892
|
+
}, undefined, false, undefined, this)
|
|
15893
|
+
]
|
|
15894
|
+
}, undefined, true, undefined, this);
|
|
15895
|
+
});
|
|
15896
|
+
});
|
|
15897
|
+
|
|
15898
|
+
// packages/tui/src/components/LogPane.tsx
|
|
15899
|
+
import { jsxDEV as jsxDEV3 } from "@opentui/react/jsx-dev-runtime";
|
|
14628
15900
|
function LogPane({
|
|
14629
|
-
|
|
15901
|
+
rows,
|
|
14630
15902
|
height,
|
|
14631
15903
|
width,
|
|
14632
15904
|
offset,
|
|
14633
15905
|
follow,
|
|
14634
15906
|
unseen,
|
|
14635
15907
|
label,
|
|
15908
|
+
focused = false,
|
|
14636
15909
|
onScroll
|
|
14637
15910
|
}) {
|
|
14638
15911
|
const viewportRows = Math.max(1, height - 3);
|
|
14639
|
-
const
|
|
14640
|
-
const
|
|
14641
|
-
const
|
|
15912
|
+
const maxOffset = Math.max(0, rows.length - viewportRows);
|
|
15913
|
+
const clamped = Math.min(offset, maxOffset);
|
|
15914
|
+
const end = Math.max(0, rows.length - clamped);
|
|
15915
|
+
const start = Math.max(0, end - viewportRows);
|
|
15916
|
+
const visible = rows.slice(start, end);
|
|
15917
|
+
const title = fitFleetText(`Logs \u2014 ${label}`, Math.max(8, width - 28));
|
|
14642
15918
|
return /* @__PURE__ */ jsxDEV3("box", {
|
|
14643
15919
|
onMouseScroll: (event) => {
|
|
14644
15920
|
if (event.button !== 4 && event.button !== 5)
|
|
@@ -14647,14 +15923,21 @@ function LogPane({
|
|
|
14647
15923
|
event.stopPropagation();
|
|
14648
15924
|
onScroll?.(event.button === 4 ? -3 : 3);
|
|
14649
15925
|
},
|
|
14650
|
-
style: {
|
|
15926
|
+
style: {
|
|
15927
|
+
height: "100%",
|
|
15928
|
+
width: "100%",
|
|
15929
|
+
flexDirection: "column",
|
|
15930
|
+
border: true,
|
|
15931
|
+
borderColor: focused ? fleetTheme.accent : fleetTheme.border,
|
|
15932
|
+
backgroundColor: fleetTheme.background
|
|
15933
|
+
},
|
|
14651
15934
|
children: [
|
|
14652
15935
|
/* @__PURE__ */ jsxDEV3("box", {
|
|
14653
15936
|
style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
|
|
14654
15937
|
children: [
|
|
14655
15938
|
/* @__PURE__ */ jsxDEV3("text", {
|
|
14656
|
-
fg: fleetTheme.text,
|
|
14657
|
-
children: padFleetText(
|
|
15939
|
+
fg: focused ? fleetTheme.accent : fleetTheme.text,
|
|
15940
|
+
children: padFleetText(title, Math.max(1, width - 24))
|
|
14658
15941
|
}, undefined, false, undefined, this),
|
|
14659
15942
|
/* @__PURE__ */ jsxDEV3("text", {
|
|
14660
15943
|
fg: follow ? fleetTheme.healthy : fleetTheme.warning,
|
|
@@ -14666,15 +15949,21 @@ function LogPane({
|
|
|
14666
15949
|
style: { paddingLeft: 1, paddingTop: 1 },
|
|
14667
15950
|
children: /* @__PURE__ */ jsxDEV3("text", {
|
|
14668
15951
|
fg: fleetTheme.muted,
|
|
14669
|
-
children: "No log output yet \u2014
|
|
14670
|
-
}, undefined, false, undefined, this)
|
|
14671
|
-
}, undefined, false, undefined, this) : visible.map((line, index) => /* @__PURE__ */ jsxDEV3("box", {
|
|
14672
|
-
style: { height: 1, paddingLeft: 1 },
|
|
14673
|
-
children: /* @__PURE__ */ jsxDEV3("text", {
|
|
14674
|
-
fg: line.meta ? fleetTheme.warning : fleetTheme.text,
|
|
14675
|
-
children: fitFleetText(line.meta ? `[hestia] ${line.text}` : line.text, Math.max(1, width - 4))
|
|
15952
|
+
children: "No log output yet \u2014 waiting for this workload."
|
|
14676
15953
|
}, undefined, false, undefined, this)
|
|
14677
|
-
},
|
|
15954
|
+
}, undefined, false, undefined, this) : visible.map((row2) => /* @__PURE__ */ jsxDEV3("box", {
|
|
15955
|
+
style: { height: 1, paddingLeft: 1, flexDirection: "row" },
|
|
15956
|
+
children: [
|
|
15957
|
+
row2.tag === undefined ? null : /* @__PURE__ */ jsxDEV3("text", {
|
|
15958
|
+
fg: fleetTheme.accent,
|
|
15959
|
+
children: row2.tag
|
|
15960
|
+
}, undefined, false, undefined, this),
|
|
15961
|
+
/* @__PURE__ */ jsxDEV3("text", {
|
|
15962
|
+
fg: row2.meta ? fleetTheme.warning : fleetTheme.text,
|
|
15963
|
+
children: row2.text
|
|
15964
|
+
}, undefined, false, undefined, this)
|
|
15965
|
+
]
|
|
15966
|
+
}, row2.key, true, undefined, this))
|
|
14678
15967
|
]
|
|
14679
15968
|
}, undefined, true, undefined, this);
|
|
14680
15969
|
}
|
|
@@ -14684,16 +15973,16 @@ var init_LogPane = __esm(() => {
|
|
|
14684
15973
|
});
|
|
14685
15974
|
|
|
14686
15975
|
// packages/tui/src/components/FleetModal.tsx
|
|
14687
|
-
import { jsxDEV as jsxDEV4, Fragment } from "@opentui/react/jsx-dev-runtime";
|
|
15976
|
+
import { jsxDEV as jsxDEV4, Fragment as Fragment2 } from "@opentui/react/jsx-dev-runtime";
|
|
14688
15977
|
function FleetModal({
|
|
14689
15978
|
title,
|
|
14690
15979
|
terminalWidth,
|
|
14691
15980
|
terminalHeight,
|
|
14692
15981
|
children
|
|
14693
15982
|
}) {
|
|
14694
|
-
const width = Math.min(
|
|
14695
|
-
const height = Math.min(
|
|
14696
|
-
return /* @__PURE__ */ jsxDEV4(
|
|
15983
|
+
const width = Math.min(78, Math.max(36, terminalWidth - 4));
|
|
15984
|
+
const height = Math.min(22, Math.max(8, terminalHeight - 4));
|
|
15985
|
+
return /* @__PURE__ */ jsxDEV4(Fragment2, {
|
|
14697
15986
|
children: [
|
|
14698
15987
|
/* @__PURE__ */ jsxDEV4("box", {
|
|
14699
15988
|
style: { position: "absolute", top: 0, left: 0, width: terminalWidth, height: terminalHeight, zIndex: 50 }
|
|
@@ -14715,10 +16004,22 @@ function FleetModal({
|
|
|
14715
16004
|
paddingTop: 1
|
|
14716
16005
|
},
|
|
14717
16006
|
children: [
|
|
14718
|
-
/* @__PURE__ */ jsxDEV4("
|
|
14719
|
-
|
|
14720
|
-
children:
|
|
14721
|
-
|
|
16007
|
+
/* @__PURE__ */ jsxDEV4("box", {
|
|
16008
|
+
style: { height: 1, flexDirection: "row" },
|
|
16009
|
+
children: [
|
|
16010
|
+
/* @__PURE__ */ jsxDEV4("box", {
|
|
16011
|
+
style: { flexGrow: 1 },
|
|
16012
|
+
children: /* @__PURE__ */ jsxDEV4("text", {
|
|
16013
|
+
fg: fleetTheme.accent,
|
|
16014
|
+
children: title
|
|
16015
|
+
}, undefined, false, undefined, this)
|
|
16016
|
+
}, undefined, false, undefined, this),
|
|
16017
|
+
/* @__PURE__ */ jsxDEV4("text", {
|
|
16018
|
+
fg: fleetTheme.faint,
|
|
16019
|
+
children: "Esc closes"
|
|
16020
|
+
}, undefined, false, undefined, this)
|
|
16021
|
+
]
|
|
16022
|
+
}, undefined, true, undefined, this),
|
|
14722
16023
|
/* @__PURE__ */ jsxDEV4("box", {
|
|
14723
16024
|
style: { height: 1 }
|
|
14724
16025
|
}, undefined, false, undefined, this),
|
|
@@ -14733,10 +16034,10 @@ var init_FleetModal = __esm(() => {
|
|
|
14733
16034
|
});
|
|
14734
16035
|
|
|
14735
16036
|
// packages/tui/src/FleetApp.tsx
|
|
14736
|
-
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
14737
|
-
import { existsSync as
|
|
16037
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
16038
|
+
import { existsSync as existsSync23 } from "fs";
|
|
14738
16039
|
import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
|
|
14739
|
-
import { jsxDEV as jsxDEV5, Fragment as
|
|
16040
|
+
import { jsxDEV as jsxDEV5, Fragment as Fragment3 } from "@opentui/react/jsx-dev-runtime";
|
|
14740
16041
|
function middleTruncateWorktreePath(path, width) {
|
|
14741
16042
|
if (path.length <= width)
|
|
14742
16043
|
return path;
|
|
@@ -14752,34 +16053,26 @@ function middleTruncateWorktreePath(path, width) {
|
|
|
14752
16053
|
return `${path.slice(0, prefixWidth)}\u2026/${basename3}`;
|
|
14753
16054
|
}
|
|
14754
16055
|
function emptySnapshot(repoId) {
|
|
14755
|
-
return { repoId, observedAt: new Date(0).toISOString(), capacity: EMPTY_CAPACITY, stacks: [], warnings: [] };
|
|
14756
|
-
}
|
|
14757
|
-
function isEscape(key) {
|
|
14758
|
-
const name = key.name.toLowerCase();
|
|
14759
|
-
return name === "escape" || name === "esc" || key.sequence === "\x1B";
|
|
14760
|
-
}
|
|
14761
|
-
function isFleetKey(key, value) {
|
|
14762
|
-
return !key.ctrl && !key.meta && !key.option && !key.super && !key.hyper && (key.name === value || key.sequence === value);
|
|
16056
|
+
return { repoId, observedAt: new Date(0).toISOString(), capacity: EMPTY_CAPACITY, stacks: [], shared: [], warnings: [] };
|
|
14763
16057
|
}
|
|
14764
16058
|
function selectedStack(snapshot, project) {
|
|
14765
16059
|
return snapshot.stacks.find((stack) => stack.project === project);
|
|
14766
16060
|
}
|
|
14767
16061
|
function fleetServiceRowCount(stack) {
|
|
14768
|
-
return stack?.services.reduce((count, service) => count + 1 + (service.
|
|
16062
|
+
return stack?.services.reduce((count, service) => count + 1 + serviceEndpoints(service).length, 0) ?? 0;
|
|
14769
16063
|
}
|
|
14770
16064
|
function selectedFleetEndpoint(stack, serviceName, endpointName) {
|
|
14771
16065
|
const service = stack?.services.find((candidate) => candidate.name === serviceName);
|
|
14772
|
-
const endpoints = service
|
|
16066
|
+
const endpoints = service === undefined ? [] : serviceEndpoints(service);
|
|
14773
16067
|
return endpoints.find((endpoint) => endpoint.name === endpointName) ?? endpoints[0];
|
|
14774
16068
|
}
|
|
14775
16069
|
function usableEndpoint(stack, serviceName, endpointName) {
|
|
14776
16070
|
const endpoint = selectedFleetEndpoint(stack, serviceName, endpointName);
|
|
14777
|
-
|
|
14778
|
-
return;
|
|
14779
|
-
return endpoint.localUrl ?? endpoint.publicUrl ?? endpoint.url ?? `${endpoint.host}:${endpoint.port}`;
|
|
16071
|
+
return endpoint === undefined ? undefined : endpointReach(endpoint);
|
|
14780
16072
|
}
|
|
14781
|
-
function
|
|
14782
|
-
|
|
16073
|
+
function downTargetChanged(confirmed, snapshot) {
|
|
16074
|
+
const current = snapshot.stacks.find((candidate) => candidate.project === confirmed.project);
|
|
16075
|
+
return current?.repoId !== confirmed.repoId || current.worktree !== confirmed.worktree || current.createdAt !== confirmed.createdAt;
|
|
14783
16076
|
}
|
|
14784
16077
|
function fleetLogSelectionKey(stack, serviceName) {
|
|
14785
16078
|
if (stack === undefined || serviceName === undefined)
|
|
@@ -14805,6 +16098,20 @@ function doctorOmissionSummary(rows, visibleRows = 10) {
|
|
|
14805
16098
|
const warnings = omitted.filter((row2) => row2.level === "warn").length;
|
|
14806
16099
|
return `\u2026 ${omitted.length} more (${errors2} errors, ${warnings} warnings)`;
|
|
14807
16100
|
}
|
|
16101
|
+
function budgetDoctorReport(rows, width, lineBudget) {
|
|
16102
|
+
const entries = [];
|
|
16103
|
+
let used = 0;
|
|
16104
|
+
for (const row2 of rows) {
|
|
16105
|
+
const detail = sanitizeFleetTerminalText(row2.detail);
|
|
16106
|
+
const detailLines = detail === "" ? [] : wrapFleetText(detail, Math.max(8, width - 6), 2);
|
|
16107
|
+
const cost = 1 + detailLines.length;
|
|
16108
|
+
if (used + cost > lineBudget)
|
|
16109
|
+
break;
|
|
16110
|
+
entries.push({ row: row2, detailLines });
|
|
16111
|
+
used += cost;
|
|
16112
|
+
}
|
|
16113
|
+
return { entries, shown: entries.length };
|
|
16114
|
+
}
|
|
14808
16115
|
function FleetApp({
|
|
14809
16116
|
source,
|
|
14810
16117
|
preferredProject,
|
|
@@ -14817,12 +16124,29 @@ function FleetApp({
|
|
|
14817
16124
|
const [state, dispatch] = useReducer(reduceFleetUiState, undefined, createFleetUiState);
|
|
14818
16125
|
const [logs, setLogs] = useState([]);
|
|
14819
16126
|
const [connected, setConnected] = useState(false);
|
|
14820
|
-
const [
|
|
16127
|
+
const [connectionNotice, setConnectionNotice] = useState("connecting to hestiad\u2026");
|
|
16128
|
+
const [toast, setToast] = useState(undefined);
|
|
14821
16129
|
const [doctorRows, setDoctorRows] = useState([]);
|
|
14822
16130
|
const [doctorRunning, setDoctorRunning] = useState(false);
|
|
16131
|
+
const [sharedBusy, setSharedBusy] = useState(false);
|
|
16132
|
+
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
16133
|
+
const [now, setNow] = useState(() => Date.now());
|
|
14823
16134
|
const lastFrameAt = useRef(Date.now());
|
|
16135
|
+
const toastTimer = useRef(undefined);
|
|
14824
16136
|
const stateRef = useRef(state);
|
|
14825
16137
|
stateRef.current = state;
|
|
16138
|
+
const showToast = (text2, ttl = TOAST_TTL_MS) => {
|
|
16139
|
+
if (toastTimer.current !== undefined)
|
|
16140
|
+
clearTimeout(toastTimer.current);
|
|
16141
|
+
setToast(text2);
|
|
16142
|
+
toastTimer.current = setTimeout(() => {
|
|
16143
|
+
setToast((current) => current === text2 ? undefined : current);
|
|
16144
|
+
}, ttl);
|
|
16145
|
+
};
|
|
16146
|
+
useEffect(() => () => {
|
|
16147
|
+
if (toastTimer.current !== undefined)
|
|
16148
|
+
clearTimeout(toastTimer.current);
|
|
16149
|
+
}, []);
|
|
14826
16150
|
useEffect(() => {
|
|
14827
16151
|
const controller = new AbortController;
|
|
14828
16152
|
(async () => {
|
|
@@ -14830,23 +16154,24 @@ function FleetApp({
|
|
|
14830
16154
|
lastFrameAt.current = Date.now();
|
|
14831
16155
|
if (frame.type === "snapshot") {
|
|
14832
16156
|
setConnected(true);
|
|
14833
|
-
|
|
16157
|
+
setConnectionNotice(undefined);
|
|
14834
16158
|
setSnapshot(frame.snapshot);
|
|
14835
16159
|
dispatch({ type: "reconcile", snapshot: frame.snapshot, preferredProject });
|
|
14836
16160
|
} else if (frame.sequence === -1) {
|
|
14837
16161
|
setConnected(false);
|
|
14838
|
-
|
|
16162
|
+
setConnectionNotice(frame.at);
|
|
14839
16163
|
} else {
|
|
14840
16164
|
setConnected(true);
|
|
16165
|
+
setConnectionNotice(undefined);
|
|
14841
16166
|
}
|
|
14842
16167
|
}
|
|
14843
16168
|
})();
|
|
14844
16169
|
const staleTimer = setInterval(() => {
|
|
14845
16170
|
if (Date.now() - lastFrameAt.current > 20000) {
|
|
14846
16171
|
setConnected(false);
|
|
14847
|
-
|
|
16172
|
+
setConnectionNotice("hestiad heartbeat is stale; reconnecting\u2026");
|
|
14848
16173
|
}
|
|
14849
|
-
},
|
|
16174
|
+
}, 5000);
|
|
14850
16175
|
return () => {
|
|
14851
16176
|
clearInterval(staleTimer);
|
|
14852
16177
|
controller.abort();
|
|
@@ -14854,8 +16179,12 @@ function FleetApp({
|
|
|
14854
16179
|
}, [preferredProject, source]);
|
|
14855
16180
|
const selectedLogStack = selectedStack(snapshot, state.selection.project);
|
|
14856
16181
|
const selectionKey = fleetLogSelectionKey(selectedLogStack, state.selection.service);
|
|
16182
|
+
const appendedLines = useRef(0);
|
|
16183
|
+
const seenAppendedLines = useRef(0);
|
|
14857
16184
|
useEffect(() => {
|
|
14858
16185
|
setLogs([]);
|
|
16186
|
+
appendedLines.current = 0;
|
|
16187
|
+
seenAppendedLines.current = 0;
|
|
14859
16188
|
if (selectionKey === undefined)
|
|
14860
16189
|
return;
|
|
14861
16190
|
const [project, service] = selectionKey.split("\x00");
|
|
@@ -14868,57 +16197,90 @@ function FleetApp({
|
|
|
14868
16197
|
service: sanitizeFleetTerminalText(line.service),
|
|
14869
16198
|
text: sanitizeFleetTerminalText(line.text)
|
|
14870
16199
|
};
|
|
16200
|
+
appendedLines.current += 1;
|
|
14871
16201
|
setLogs((current) => [...current, sanitized].slice(-LOG_RING_CAPACITY));
|
|
14872
|
-
dispatch({ type: "new-lines", count: 1, maxOffset: LOG_RING_CAPACITY - 1 });
|
|
14873
16202
|
}
|
|
14874
16203
|
})();
|
|
14875
16204
|
return () => controller.abort();
|
|
14876
16205
|
}, [selectionKey, source]);
|
|
14877
16206
|
const stacks = useMemo(() => visibleFleetStacks(snapshot, state.filter), [snapshot, state.filter]);
|
|
14878
16207
|
const stack = selectedStack(snapshot, state.selection.project);
|
|
14879
|
-
const context = {
|
|
16208
|
+
const context = useMemo(() => ({
|
|
14880
16209
|
repo: sanitizeFleetTerminalText(stack?.repo ?? invokingRepository.repo),
|
|
14881
16210
|
branch: sanitizeFleetTerminalText(stack?.branch ?? invokingRepository.branch),
|
|
14882
16211
|
worktree: sanitizeFleetTerminalText(stack?.worktree ?? invokingRepository.worktree),
|
|
14883
16212
|
project: sanitizeFleetTerminalText(stack?.project ?? "\u2014")
|
|
14884
|
-
};
|
|
14885
|
-
const endpointView =
|
|
16213
|
+
}), [stack, invokingRepository]);
|
|
16214
|
+
const endpointView = selectedFleetEndpoint(stack, state.selection.service, state.selection.endpoint);
|
|
14886
16215
|
const endpoint = usableEndpoint(stack, state.selection.service, state.selection.endpoint);
|
|
14887
|
-
const effectiveLayout = state.layout
|
|
14888
|
-
const
|
|
16216
|
+
const effectiveLayout = resolveFleetLayout(state.layout, terminal.width);
|
|
16217
|
+
const paneWidths = fleetPaneWidths(effectiveLayout, terminal.width);
|
|
16218
|
+
const headerRows = 1;
|
|
16219
|
+
const contextRows = 2;
|
|
16220
|
+
const footerRows = 1;
|
|
16221
|
+
const warningRows = stack?.warning === undefined ? 0 : 1;
|
|
16222
|
+
const svcRows = fleetServiceRowCount(stack) + warningRows;
|
|
16223
|
+
const stackedSidebarHeight = stackSidebarHeight(stacks.length);
|
|
16224
|
+
const svcPaneH = servicePaneHeight(svcRows, effectiveLayout === "split" ? 14 : 12);
|
|
16225
|
+
const logHeight = Math.max(5, terminal.height - headerRows - contextRows - footerRows - svcPaneH - (effectiveLayout === "stack" ? stackedSidebarHeight : 0));
|
|
16226
|
+
const logWidth = effectiveLayout === "split" ? paneWidths.main : terminal.width;
|
|
16227
|
+
const logViewportRows = Math.max(1, logHeight - 3);
|
|
16228
|
+
const logRows = useMemo(() => buildFleetLogRows(logs, Math.max(1, logWidth - 4)), [logs, logWidth]);
|
|
16229
|
+
const maxLogOffset = Math.max(0, logRows.length - logViewportRows);
|
|
16230
|
+
useEffect(() => {
|
|
16231
|
+
const appended = appendedLines.current - seenAppendedLines.current;
|
|
16232
|
+
seenAppendedLines.current = appendedLines.current;
|
|
16233
|
+
if (appended > 0 && !stateRef.current.follow) {
|
|
16234
|
+
const fresh = buildFleetLogRows(logs.slice(-Math.min(appended, logs.length)), Math.max(1, logWidth - 4)).length;
|
|
16235
|
+
dispatch({ type: "new-lines", count: fresh, maxOffset: maxLogOffset });
|
|
16236
|
+
}
|
|
16237
|
+
}, [logs, logWidth, maxLogOffset]);
|
|
16238
|
+
const anyStarting = snapshot.stacks.some((candidate) => candidate.phase === "starting");
|
|
16239
|
+
useEffect(() => {
|
|
16240
|
+
if (!anyStarting)
|
|
16241
|
+
return;
|
|
16242
|
+
const timer = setInterval(() => setSpinnerFrame((frame) => (frame + 1) % SPINNER_FRAMES.length), 120);
|
|
16243
|
+
return () => clearInterval(timer);
|
|
16244
|
+
}, [anyStarting]);
|
|
16245
|
+
useEffect(() => {
|
|
16246
|
+
const timer = setInterval(() => setNow(Date.now()), 30000);
|
|
16247
|
+
return () => clearInterval(timer);
|
|
16248
|
+
}, []);
|
|
14889
16249
|
useEffect(() => {
|
|
14890
16250
|
const confirmed = state.confirmDown;
|
|
14891
|
-
|
|
14892
|
-
if (confirmed !== undefined && !state.downPending && (currentIncarnation?.repoId !== confirmed.repoId || currentIncarnation.worktree !== confirmed.worktree || currentIncarnation.createdAt !== confirmed.createdAt)) {
|
|
16251
|
+
if (confirmed !== undefined && !state.downPending && downTargetChanged(confirmed, snapshot)) {
|
|
14893
16252
|
dispatch({ type: "confirm-down" });
|
|
14894
|
-
|
|
16253
|
+
showToast(`${confirmed.project} changed; down cancelled`);
|
|
14895
16254
|
}
|
|
14896
16255
|
}, [snapshot, state.confirmDown, state.downPending]);
|
|
16256
|
+
const copyToClipboard = (value, label) => {
|
|
16257
|
+
const copied = renderer.copyToClipboardOSC52(value);
|
|
16258
|
+
showToast(copied ? `copied ${label ?? value}` : `copy unavailable: ${label ?? value}`);
|
|
16259
|
+
};
|
|
14897
16260
|
useKeyboard((key) => {
|
|
14898
16261
|
const current = stateRef.current;
|
|
14899
16262
|
if (current.confirmDown !== undefined) {
|
|
14900
16263
|
key.preventDefault();
|
|
14901
16264
|
key.stopPropagation();
|
|
14902
|
-
if (
|
|
16265
|
+
if (isEscapeKey(key) && !current.downPending) {
|
|
14903
16266
|
dispatch({ type: "confirm-down" });
|
|
14904
|
-
} else if (!current.downPending && (
|
|
16267
|
+
} else if (!current.downPending && (isPlainKey(key, "d") || isEnterKey(key))) {
|
|
14905
16268
|
const confirmed = current.confirmDown;
|
|
14906
|
-
|
|
14907
|
-
if (currentIncarnation?.repoId !== confirmed.repoId || currentIncarnation.worktree !== confirmed.worktree || currentIncarnation.createdAt !== confirmed.createdAt) {
|
|
16269
|
+
if (downTargetChanged(confirmed, snapshot)) {
|
|
14908
16270
|
dispatch({ type: "confirm-down" });
|
|
14909
|
-
|
|
16271
|
+
showToast(`${confirmed.project} changed; down cancelled`);
|
|
14910
16272
|
return;
|
|
14911
16273
|
}
|
|
14912
16274
|
const project = confirmed.project;
|
|
14913
16275
|
dispatch({ type: "down-pending", pending: true });
|
|
14914
|
-
|
|
16276
|
+
showToast(`tearing down ${project}\u2026`);
|
|
14915
16277
|
source.down(confirmed).then(() => {
|
|
14916
16278
|
dispatch({ type: "down-pending", pending: false });
|
|
14917
16279
|
dispatch({ type: "confirm-down" });
|
|
14918
|
-
|
|
16280
|
+
showToast(`${project} is down; named volumes retained`);
|
|
14919
16281
|
}).catch((error) => {
|
|
14920
16282
|
dispatch({ type: "down-pending", pending: false });
|
|
14921
|
-
|
|
16283
|
+
showToast(`down failed: ${error.message}`);
|
|
14922
16284
|
});
|
|
14923
16285
|
}
|
|
14924
16286
|
return;
|
|
@@ -14926,86 +16288,167 @@ function FleetApp({
|
|
|
14926
16288
|
if (current.helpOpen || current.doctorOpen) {
|
|
14927
16289
|
key.preventDefault();
|
|
14928
16290
|
key.stopPropagation();
|
|
14929
|
-
if (
|
|
16291
|
+
if (isEscapeKey(key) || isPlainKey(key, "?")) {
|
|
14930
16292
|
dispatch({ type: current.helpOpen ? "help" : "doctor", open: false });
|
|
14931
16293
|
}
|
|
14932
16294
|
return;
|
|
14933
16295
|
}
|
|
16296
|
+
if (current.sharedOpen) {
|
|
16297
|
+
key.preventDefault();
|
|
16298
|
+
key.stopPropagation();
|
|
16299
|
+
if (isEscapeKey(key) || isPlainKey(key, "s"))
|
|
16300
|
+
return dispatch({ type: "shared", open: false });
|
|
16301
|
+
const list = snapshot.shared;
|
|
16302
|
+
if (list.length === 0)
|
|
16303
|
+
return;
|
|
16304
|
+
if (isDownKey(key))
|
|
16305
|
+
return dispatch({ type: "move-shared", delta: 1, count: list.length });
|
|
16306
|
+
if (isUpKey(key))
|
|
16307
|
+
return dispatch({ type: "move-shared", delta: -1, count: list.length });
|
|
16308
|
+
const selected = list[Math.min(current.sharedSelection, list.length - 1)];
|
|
16309
|
+
if (selected === undefined || sharedBusy)
|
|
16310
|
+
return;
|
|
16311
|
+
const runShared = (label, action) => {
|
|
16312
|
+
setSharedBusy(true);
|
|
16313
|
+
showToast(`${label} ${selected.name}\u2026`);
|
|
16314
|
+
action.then(() => showToast(`${label} ${selected.name}: done`)).catch((error) => showToast(`${label} ${selected.name} failed: ${error.message}`)).finally(() => setSharedBusy(false));
|
|
16315
|
+
};
|
|
16316
|
+
if (isPlainKey(key, "a") || isPlainKey(key, "x") || isPlainKey(key, "r")) {
|
|
16317
|
+
if (selected.holder?.mine !== true) {
|
|
16318
|
+
showToast(`this worktree does not hold ${selected.name}`);
|
|
16319
|
+
return;
|
|
16320
|
+
}
|
|
16321
|
+
const worktree = selected.holder.worktree;
|
|
16322
|
+
if (isPlainKey(key, "a"))
|
|
16323
|
+
runShared("allowing", source.allowShared(worktree, selected.name));
|
|
16324
|
+
else if (isPlainKey(key, "x"))
|
|
16325
|
+
runShared("denying", source.denyShared(worktree, selected.name));
|
|
16326
|
+
else
|
|
16327
|
+
runShared("releasing", source.releaseShared(worktree, selected.name));
|
|
16328
|
+
return;
|
|
16329
|
+
}
|
|
16330
|
+
if (isPlainKey(key, "c")) {
|
|
16331
|
+
const acting = selectedStack(snapshot, current.selection.project);
|
|
16332
|
+
if (acting === undefined) {
|
|
16333
|
+
showToast("select a stack (in the fleet list) to claim as");
|
|
16334
|
+
return;
|
|
16335
|
+
}
|
|
16336
|
+
if (selected.holder?.project === acting.project) {
|
|
16337
|
+
showToast(`${acting.project} already holds ${selected.name}`);
|
|
16338
|
+
return;
|
|
16339
|
+
}
|
|
16340
|
+
runShared(`claiming as ${acting.project},`, source.claimShared(acting.worktree, selected.name));
|
|
16341
|
+
return;
|
|
16342
|
+
}
|
|
16343
|
+
return;
|
|
16344
|
+
}
|
|
14934
16345
|
if (current.focus === "filter") {
|
|
14935
|
-
if (
|
|
16346
|
+
if (isEscapeKey(key)) {
|
|
16347
|
+
key.preventDefault();
|
|
16348
|
+
key.stopPropagation();
|
|
16349
|
+
if (current.filter !== "")
|
|
16350
|
+
dispatch({ type: "filter", filter: "", snapshot });
|
|
16351
|
+
else
|
|
16352
|
+
dispatch({ type: "focus", focus: "stacks" });
|
|
16353
|
+
return;
|
|
16354
|
+
}
|
|
16355
|
+
if (isEnterKey(key)) {
|
|
14936
16356
|
key.preventDefault();
|
|
14937
16357
|
key.stopPropagation();
|
|
14938
16358
|
dispatch({ type: "focus", focus: "stacks" });
|
|
14939
16359
|
}
|
|
14940
16360
|
return;
|
|
14941
16361
|
}
|
|
14942
|
-
if (
|
|
16362
|
+
if (isPlainKey(key, "q"))
|
|
14943
16363
|
return onQuit();
|
|
14944
|
-
if (
|
|
16364
|
+
if (isPlainKey(key, "?"))
|
|
14945
16365
|
return dispatch({ type: "help", open: true });
|
|
14946
|
-
if (
|
|
16366
|
+
if (isPlainKey(key, "s"))
|
|
16367
|
+
return dispatch({ type: "shared", open: true });
|
|
16368
|
+
if (isPlainKey(key, "/"))
|
|
14947
16369
|
return dispatch({ type: "focus", focus: "filter" });
|
|
14948
|
-
if (
|
|
16370
|
+
if (isPlainKey(key, "0"))
|
|
14949
16371
|
return dispatch({ type: "layout", layout: "auto" });
|
|
14950
|
-
if (
|
|
16372
|
+
if (isPlainKey(key, "1"))
|
|
14951
16373
|
return dispatch({ type: "layout", layout: "split" });
|
|
14952
|
-
if (
|
|
16374
|
+
if (isPlainKey(key, "2"))
|
|
14953
16375
|
return dispatch({ type: "layout", layout: "stack" });
|
|
14954
16376
|
if (key.name === "tab") {
|
|
16377
|
+
const focusOrder = ["stacks", "services", "logs"];
|
|
14955
16378
|
const index = focusOrder.indexOf(current.focus);
|
|
14956
16379
|
const delta2 = key.shift ? -1 : 1;
|
|
14957
16380
|
const next = (index + delta2 + focusOrder.length) % focusOrder.length;
|
|
14958
16381
|
return dispatch({ type: "focus", focus: focusOrder[next] });
|
|
14959
16382
|
}
|
|
14960
|
-
if (
|
|
16383
|
+
if (isPlainKey(key, ","))
|
|
16384
|
+
return dispatch({ type: "move-stack", delta: -1, snapshot });
|
|
16385
|
+
if (isPlainKey(key, "."))
|
|
16386
|
+
return dispatch({ type: "move-stack", delta: 1, snapshot });
|
|
16387
|
+
if (isPlainKey(key, "["))
|
|
14961
16388
|
return dispatch({ type: "move-service", delta: -1, snapshot });
|
|
14962
|
-
if (
|
|
16389
|
+
if (isPlainKey(key, "]"))
|
|
14963
16390
|
return dispatch({ type: "move-service", delta: 1, snapshot });
|
|
14964
|
-
if (
|
|
16391
|
+
if (isPlainKey(key, "f"))
|
|
16392
|
+
return dispatch({ type: "follow", follow: !current.follow });
|
|
16393
|
+
if (isFollowBottomKey(key))
|
|
14965
16394
|
return dispatch({ type: "follow", follow: true });
|
|
16395
|
+
if (isScrollTopKey(key)) {
|
|
16396
|
+
return dispatch({ type: "scroll-logs", delta: -logRows.length, maxOffset: maxLogOffset });
|
|
14966
16397
|
}
|
|
14967
|
-
if (
|
|
16398
|
+
if (isDoctorKey(key)) {
|
|
14968
16399
|
if (doctorRunning)
|
|
14969
16400
|
return;
|
|
14970
16401
|
dispatch({ type: "doctor", open: true });
|
|
14971
16402
|
setDoctorRunning(true);
|
|
14972
16403
|
setDoctorRows([]);
|
|
14973
|
-
const worktree = stack !== undefined &&
|
|
16404
|
+
const worktree = stack !== undefined && existsSync23(stack.worktree) ? stack.worktree : process.cwd();
|
|
14974
16405
|
source.diagnose(worktree).then(setDoctorRows).catch((error) => {
|
|
14975
|
-
|
|
16406
|
+
showToast(`doctor failed: ${error.message}`);
|
|
14976
16407
|
}).finally(() => setDoctorRunning(false));
|
|
14977
16408
|
return;
|
|
14978
16409
|
}
|
|
14979
|
-
if (
|
|
16410
|
+
if (isPlainKey(key, "d")) {
|
|
14980
16411
|
if (stack?.phase === "queued" || stack?.phase === "reserved") {
|
|
14981
|
-
|
|
16412
|
+
showToast(`${stack.project} is ${stack.phase}; down is available after startup begins`);
|
|
14982
16413
|
} else if (stack !== undefined) {
|
|
14983
16414
|
dispatch({ type: "confirm-down", stack });
|
|
14984
16415
|
}
|
|
14985
16416
|
return;
|
|
14986
16417
|
}
|
|
14987
|
-
if (
|
|
16418
|
+
if (isPlainKey(key, "o") && endpoint !== undefined) {
|
|
14988
16419
|
openFleetUrl(endpoint);
|
|
14989
|
-
|
|
16420
|
+
showToast(endpoint);
|
|
14990
16421
|
return;
|
|
14991
16422
|
}
|
|
14992
|
-
if (
|
|
14993
|
-
|
|
14994
|
-
|
|
16423
|
+
if (isShiftedKey(key, "y")) {
|
|
16424
|
+
if (stack === undefined)
|
|
16425
|
+
return;
|
|
16426
|
+
const env = buildEnvBlock(stack);
|
|
16427
|
+
if (env === "")
|
|
16428
|
+
showToast(`no env surface recorded for ${stack.project}`);
|
|
16429
|
+
else
|
|
16430
|
+
copyToClipboard(env, `env block (${env.split(`
|
|
16431
|
+
`).length} keys)`);
|
|
14995
16432
|
return;
|
|
14996
16433
|
}
|
|
14997
|
-
|
|
14998
|
-
|
|
14999
|
-
if (surface !== undefined) {
|
|
15000
|
-
const copied = renderer.copyToClipboardOSC52(surface);
|
|
15001
|
-
setNotice(copied ? `copied ${surface}` : `copy unavailable: ${surface}`);
|
|
16434
|
+
if (isPlainKey(key, "y") && endpoint !== undefined) {
|
|
16435
|
+
copyToClipboard(endpoint);
|
|
15002
16436
|
return;
|
|
15003
16437
|
}
|
|
15004
|
-
|
|
15005
|
-
|
|
16438
|
+
const copySurfaces = [
|
|
16439
|
+
["c", endpointView?.url],
|
|
16440
|
+
["l", endpointView?.localUrl],
|
|
16441
|
+
["p", endpointView?.publicUrl]
|
|
16442
|
+
];
|
|
16443
|
+
const copySurface = copySurfaces.find(([name]) => isPlainKey(key, name));
|
|
16444
|
+
if (copySurface !== undefined) {
|
|
16445
|
+
if (copySurface[1] !== undefined)
|
|
16446
|
+
copyToClipboard(copySurface[1]);
|
|
16447
|
+
else
|
|
16448
|
+
showToast("selected URL surface is unavailable");
|
|
15006
16449
|
return;
|
|
15007
16450
|
}
|
|
15008
|
-
const delta =
|
|
16451
|
+
const delta = isDownKey(key) ? 1 : isUpKey(key) ? -1 : 0;
|
|
15009
16452
|
if (delta === 0)
|
|
15010
16453
|
return;
|
|
15011
16454
|
if (current.focus === "stacks")
|
|
@@ -15013,11 +16456,46 @@ function FleetApp({
|
|
|
15013
16456
|
else if (current.focus === "services")
|
|
15014
16457
|
dispatch({ type: "move-service", delta, snapshot });
|
|
15015
16458
|
else
|
|
15016
|
-
dispatch({ type: "scroll-logs", delta });
|
|
16459
|
+
dispatch({ type: "scroll-logs", delta, maxOffset: maxLogOffset });
|
|
15017
16460
|
});
|
|
15018
|
-
const
|
|
15019
|
-
const status = connected ? `${capacity.live}/${capacity.maxStacks} live \xB7 ${capacity.reserved} reserved \xB7 ${capacity.queued} queued` : "disconnected";
|
|
16461
|
+
const uptime = stack?.createdAt !== undefined && (stack.phase === "up" || stack.phase === "degraded" || stack.phase === "starting") ? formatUptime(stack.createdAt, now) : undefined;
|
|
15020
16462
|
const logLabel = state.selection.service ?? "select a service";
|
|
16463
|
+
const statusNotice = resolveStatusNotice(connectionNotice, toast);
|
|
16464
|
+
const doctorWidth = Math.min(72, Math.max(30, terminal.width - 6));
|
|
16465
|
+
const wideFooter = terminal.width >= 100;
|
|
16466
|
+
const onSelectProject = useCallback((project) => dispatch({ type: "select-stack", project, snapshot }), [snapshot]);
|
|
16467
|
+
const onSelectService = useCallback((service) => dispatch({ type: "select-service", service, snapshot }), [snapshot]);
|
|
16468
|
+
const onSelectEndpoint = useCallback((service, endpointName) => dispatch({ type: "select-endpoint", service, endpoint: endpointName, snapshot }), [snapshot]);
|
|
16469
|
+
const servicePane = /* @__PURE__ */ jsxDEV5(ServicePane, {
|
|
16470
|
+
stack,
|
|
16471
|
+
uptime,
|
|
16472
|
+
selectedService: state.selection.service,
|
|
16473
|
+
selectedEndpoint: state.selection.endpoint,
|
|
16474
|
+
width: logWidth,
|
|
16475
|
+
focused: state.focus === "services",
|
|
16476
|
+
onSelectService,
|
|
16477
|
+
onSelectEndpoint
|
|
16478
|
+
}, undefined, false, undefined, this);
|
|
16479
|
+
const sidebar = (width) => /* @__PURE__ */ jsxDEV5(StackSidebar, {
|
|
16480
|
+
stacks,
|
|
16481
|
+
capacity: snapshot.capacity,
|
|
16482
|
+
selectedProject: state.selection.project,
|
|
16483
|
+
width,
|
|
16484
|
+
focused: state.focus === "stacks",
|
|
16485
|
+
spinnerFrame,
|
|
16486
|
+
onSelectProject
|
|
16487
|
+
}, undefined, false, undefined, this);
|
|
16488
|
+
const logPane = /* @__PURE__ */ jsxDEV5(LogPane, {
|
|
16489
|
+
rows: logRows,
|
|
16490
|
+
height: logHeight,
|
|
16491
|
+
width: logWidth,
|
|
16492
|
+
offset: state.logOffset,
|
|
16493
|
+
follow: state.follow,
|
|
16494
|
+
unseen: state.unseenLines,
|
|
16495
|
+
label: logLabel,
|
|
16496
|
+
focused: state.focus === "logs",
|
|
16497
|
+
onScroll: (delta) => dispatch({ type: "scroll-logs", delta, maxOffset: maxLogOffset })
|
|
16498
|
+
}, undefined, false, undefined, this);
|
|
15021
16499
|
return /* @__PURE__ */ jsxDEV5("box", {
|
|
15022
16500
|
style: { width: "100%", height: "100%", flexDirection: "column", backgroundColor: fleetTheme.background },
|
|
15023
16501
|
children: [
|
|
@@ -15026,70 +16504,64 @@ function FleetApp({
|
|
|
15026
16504
|
children: [
|
|
15027
16505
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15028
16506
|
fg: fleetTheme.accent,
|
|
15029
|
-
children:
|
|
15030
|
-
|
|
15031
|
-
context.repo
|
|
15032
|
-
]
|
|
15033
|
-
}, undefined, true, undefined, this),
|
|
15034
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15035
|
-
fg: fleetTheme.muted,
|
|
15036
|
-
children: [
|
|
15037
|
-
" ",
|
|
15038
|
-
status
|
|
15039
|
-
]
|
|
15040
|
-
}, undefined, true, undefined, this)
|
|
15041
|
-
]
|
|
15042
|
-
}, undefined, true, undefined, this),
|
|
15043
|
-
terminal.width >= 90 ? /* @__PURE__ */ jsxDEV5("box", {
|
|
15044
|
-
style: { height: 4, paddingLeft: 1, flexDirection: "column", backgroundColor: fleetTheme.background },
|
|
15045
|
-
children: [
|
|
15046
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15047
|
-
fg: fleetTheme.text,
|
|
15048
|
-
children: [
|
|
15049
|
-
"Repository ",
|
|
15050
|
-
context.repo
|
|
15051
|
-
]
|
|
15052
|
-
}, undefined, true, undefined, this),
|
|
16507
|
+
children: "Hestia Fleet"
|
|
16508
|
+
}, undefined, false, undefined, this),
|
|
15053
16509
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15054
|
-
fg: fleetTheme.
|
|
15055
|
-
children:
|
|
15056
|
-
|
|
15057
|
-
context.branch
|
|
15058
|
-
]
|
|
15059
|
-
}, undefined, true, undefined, this),
|
|
16510
|
+
fg: fleetTheme.faint,
|
|
16511
|
+
children: " \xB7 "
|
|
16512
|
+
}, undefined, false, undefined, this),
|
|
15060
16513
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15061
16514
|
fg: fleetTheme.text,
|
|
15062
|
-
children:
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
}, undefined,
|
|
16515
|
+
children: fitFleetText(context.repo, Math.max(8, terminal.width - 30))
|
|
16516
|
+
}, undefined, false, undefined, this),
|
|
16517
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16518
|
+
style: { flexGrow: 1 }
|
|
16519
|
+
}, undefined, false, undefined, this),
|
|
15067
16520
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15068
|
-
fg: fleetTheme.
|
|
15069
|
-
children:
|
|
15070
|
-
|
|
15071
|
-
context.project
|
|
15072
|
-
]
|
|
15073
|
-
}, undefined, true, undefined, this)
|
|
16521
|
+
fg: connected ? fleetTheme.healthy : fleetTheme.warning,
|
|
16522
|
+
children: connected ? "\u25CF connected" : "\u25CB reconnecting"
|
|
16523
|
+
}, undefined, false, undefined, this)
|
|
15074
16524
|
]
|
|
15075
|
-
}, undefined, true, undefined, this)
|
|
16525
|
+
}, undefined, true, undefined, this),
|
|
16526
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
15076
16527
|
style: { height: 2, paddingLeft: 1, flexDirection: "column", backgroundColor: fleetTheme.background },
|
|
15077
16528
|
children: [
|
|
15078
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15079
|
-
|
|
16529
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16530
|
+
style: { height: 1, flexDirection: "row" },
|
|
15080
16531
|
children: [
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
16532
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16533
|
+
fg: fleetTheme.muted,
|
|
16534
|
+
children: fitFleetText(context.repo, 24)
|
|
16535
|
+
}, undefined, false, undefined, this),
|
|
16536
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16537
|
+
fg: fleetTheme.faint,
|
|
16538
|
+
children: " / "
|
|
16539
|
+
}, undefined, false, undefined, this),
|
|
16540
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16541
|
+
fg: fleetTheme.text,
|
|
16542
|
+
children: context.branch
|
|
16543
|
+
}, undefined, false, undefined, this),
|
|
16544
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16545
|
+
fg: fleetTheme.faint,
|
|
16546
|
+
children: " \xB7 "
|
|
16547
|
+
}, undefined, false, undefined, this),
|
|
16548
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16549
|
+
fg: fleetTheme.muted,
|
|
16550
|
+
children: context.project
|
|
16551
|
+
}, undefined, false, undefined, this)
|
|
15086
16552
|
]
|
|
15087
16553
|
}, undefined, true, undefined, this),
|
|
15088
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15089
|
-
|
|
16554
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16555
|
+
style: { height: 1, flexDirection: "row" },
|
|
15090
16556
|
children: [
|
|
15091
|
-
|
|
15092
|
-
|
|
16557
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16558
|
+
fg: fleetTheme.faint,
|
|
16559
|
+
children: "worktree "
|
|
16560
|
+
}, undefined, false, undefined, this),
|
|
16561
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16562
|
+
fg: fleetTheme.muted,
|
|
16563
|
+
children: middleTruncateWorktreePath(context.worktree, Math.max(16, terminal.width - 11))
|
|
16564
|
+
}, undefined, false, undefined, this)
|
|
15093
16565
|
]
|
|
15094
16566
|
}, undefined, true, undefined, this)
|
|
15095
16567
|
]
|
|
@@ -15097,37 +16569,17 @@ function FleetApp({
|
|
|
15097
16569
|
effectiveLayout === "split" ? /* @__PURE__ */ jsxDEV5("box", {
|
|
15098
16570
|
style: { flexGrow: 1, flexDirection: "row" },
|
|
15099
16571
|
children: [
|
|
15100
|
-
|
|
15101
|
-
stacks,
|
|
15102
|
-
selectedProject: state.selection.project,
|
|
15103
|
-
width: Math.max(28, Math.floor(terminal.width * 0.28)),
|
|
15104
|
-
onSelectProject: (project) => dispatch({ type: "select-stack", project, snapshot })
|
|
15105
|
-
}, undefined, false, undefined, this),
|
|
16572
|
+
sidebar(paneWidths.sidebar),
|
|
15106
16573
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15107
16574
|
style: { flexGrow: 1, flexDirection: "column" },
|
|
15108
16575
|
children: [
|
|
15109
16576
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15110
|
-
style: { height:
|
|
15111
|
-
children:
|
|
15112
|
-
stack,
|
|
15113
|
-
selectedService: state.selection.service,
|
|
15114
|
-
selectedEndpoint: state.selection.endpoint,
|
|
15115
|
-
onSelectService: (service) => dispatch({ type: "select-service", service, snapshot }),
|
|
15116
|
-
onSelectEndpoint: (service, endpoint2) => dispatch({ type: "select-endpoint", service, endpoint: endpoint2, snapshot })
|
|
15117
|
-
}, undefined, false, undefined, this)
|
|
16577
|
+
style: { height: svcPaneH },
|
|
16578
|
+
children: servicePane
|
|
15118
16579
|
}, undefined, false, undefined, this),
|
|
15119
16580
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15120
16581
|
style: { flexGrow: 1 },
|
|
15121
|
-
children:
|
|
15122
|
-
lines: logs,
|
|
15123
|
-
height: Math.max(5, terminal.height - 13),
|
|
15124
|
-
width: Math.max(20, Math.floor(terminal.width * 0.72)),
|
|
15125
|
-
offset: state.logOffset,
|
|
15126
|
-
follow: state.follow,
|
|
15127
|
-
unseen: state.unseenLines,
|
|
15128
|
-
label: logLabel,
|
|
15129
|
-
onScroll: (delta) => dispatch({ type: "scroll-logs", delta })
|
|
15130
|
-
}, undefined, false, undefined, this)
|
|
16582
|
+
children: logPane
|
|
15131
16583
|
}, undefined, false, undefined, this)
|
|
15132
16584
|
]
|
|
15133
16585
|
}, undefined, true, undefined, this)
|
|
@@ -15136,42 +16588,22 @@ function FleetApp({
|
|
|
15136
16588
|
style: { flexGrow: 1, flexDirection: "column" },
|
|
15137
16589
|
children: [
|
|
15138
16590
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15139
|
-
style: { height:
|
|
15140
|
-
children:
|
|
15141
|
-
stacks,
|
|
15142
|
-
selectedProject: state.selection.project,
|
|
15143
|
-
width: terminal.width,
|
|
15144
|
-
onSelectProject: (project) => dispatch({ type: "select-stack", project, snapshot })
|
|
15145
|
-
}, undefined, false, undefined, this)
|
|
16591
|
+
style: { height: stackedSidebarHeight },
|
|
16592
|
+
children: sidebar(terminal.width)
|
|
15146
16593
|
}, undefined, false, undefined, this),
|
|
15147
16594
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15148
|
-
style: { height:
|
|
15149
|
-
children:
|
|
15150
|
-
stack,
|
|
15151
|
-
selectedService: state.selection.service,
|
|
15152
|
-
selectedEndpoint: state.selection.endpoint,
|
|
15153
|
-
onSelectService: (service) => dispatch({ type: "select-service", service, snapshot }),
|
|
15154
|
-
onSelectEndpoint: (service, endpoint2) => dispatch({ type: "select-endpoint", service, endpoint: endpoint2, snapshot })
|
|
15155
|
-
}, undefined, false, undefined, this)
|
|
16595
|
+
style: { height: svcPaneH },
|
|
16596
|
+
children: servicePane
|
|
15156
16597
|
}, undefined, false, undefined, this),
|
|
15157
16598
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15158
16599
|
style: { flexGrow: 1 },
|
|
15159
|
-
children:
|
|
15160
|
-
lines: logs,
|
|
15161
|
-
height: Math.max(5, terminal.height - 17),
|
|
15162
|
-
width: terminal.width,
|
|
15163
|
-
offset: state.logOffset,
|
|
15164
|
-
follow: state.follow,
|
|
15165
|
-
unseen: state.unseenLines,
|
|
15166
|
-
label: logLabel,
|
|
15167
|
-
onScroll: (delta) => dispatch({ type: "scroll-logs", delta })
|
|
15168
|
-
}, undefined, false, undefined, this)
|
|
16600
|
+
children: logPane
|
|
15169
16601
|
}, undefined, false, undefined, this)
|
|
15170
16602
|
]
|
|
15171
16603
|
}, undefined, true, undefined, this),
|
|
15172
16604
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
15173
16605
|
style: { height: 1, paddingLeft: 1, paddingRight: 1, flexDirection: "row", backgroundColor: fleetTheme.panelAlt },
|
|
15174
|
-
children: state.focus === "filter" ? /* @__PURE__ */ jsxDEV5(
|
|
16606
|
+
children: state.focus === "filter" ? /* @__PURE__ */ jsxDEV5(Fragment3, {
|
|
15175
16607
|
children: [
|
|
15176
16608
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15177
16609
|
fg: fleetTheme.accent,
|
|
@@ -15186,73 +16618,137 @@ function FleetApp({
|
|
|
15186
16618
|
onSubmit: () => dispatch({ type: "focus", focus: "stacks" })
|
|
15187
16619
|
}, undefined, false, undefined, this)
|
|
15188
16620
|
]
|
|
15189
|
-
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV5(
|
|
15190
|
-
|
|
15191
|
-
|
|
15192
|
-
|
|
16621
|
+
}, undefined, true, undefined, this) : statusNotice !== undefined ? /* @__PURE__ */ jsxDEV5(Fragment3, {
|
|
16622
|
+
children: [
|
|
16623
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16624
|
+
fg: connectionNotice !== undefined ? fleetTheme.warning : fleetTheme.text,
|
|
16625
|
+
children: fitFleetText(statusNotice, Math.max(8, terminal.width - 20))
|
|
16626
|
+
}, undefined, false, undefined, this),
|
|
16627
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16628
|
+
style: { flexGrow: 1 }
|
|
16629
|
+
}, undefined, false, undefined, this),
|
|
16630
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16631
|
+
fg: connected ? fleetTheme.muted : fleetTheme.warning,
|
|
16632
|
+
children: fleetCapacitySummary(snapshot.capacity, connected)
|
|
16633
|
+
}, undefined, false, undefined, this)
|
|
16634
|
+
]
|
|
16635
|
+
}, undefined, true, undefined, this) : /* @__PURE__ */ jsxDEV5(Fragment3, {
|
|
16636
|
+
children: [
|
|
16637
|
+
wideFooter ? /* @__PURE__ */ jsxDEV5("text", {
|
|
16638
|
+
children: FLEET_KEY_HINTS.map((hint) => /* @__PURE__ */ jsxDEV5("span", {
|
|
16639
|
+
children: [
|
|
16640
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
16641
|
+
fg: fleetTheme.bright,
|
|
16642
|
+
bg: fleetTheme.keycapBg,
|
|
16643
|
+
children: ` ${hint.keys} `
|
|
16644
|
+
}, undefined, false, undefined, this),
|
|
16645
|
+
/* @__PURE__ */ jsxDEV5("span", {
|
|
16646
|
+
fg: fleetTheme.muted,
|
|
16647
|
+
children: ` ${hint.label} `
|
|
16648
|
+
}, undefined, false, undefined, this)
|
|
16649
|
+
]
|
|
16650
|
+
}, hint.keys, true, undefined, this))
|
|
16651
|
+
}, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5("text", {
|
|
16652
|
+
fg: fleetTheme.muted,
|
|
16653
|
+
children: "? help \xB7 q quit"
|
|
16654
|
+
}, undefined, false, undefined, this),
|
|
16655
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16656
|
+
style: { flexGrow: 1 }
|
|
16657
|
+
}, undefined, false, undefined, this),
|
|
16658
|
+
state.filter !== "" ? /* @__PURE__ */ jsxDEV5("text", {
|
|
16659
|
+
fg: fleetTheme.accent,
|
|
16660
|
+
children: `filter=${fitFleetText(state.filter, 16)} `
|
|
16661
|
+
}, undefined, false, undefined, this) : null,
|
|
16662
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16663
|
+
fg: connected ? fleetTheme.muted : fleetTheme.warning,
|
|
16664
|
+
children: fleetCapacitySummary(snapshot.capacity, connected)
|
|
16665
|
+
}, undefined, false, undefined, this)
|
|
16666
|
+
]
|
|
16667
|
+
}, undefined, true, undefined, this)
|
|
15193
16668
|
}, undefined, false, undefined, this),
|
|
15194
16669
|
state.helpOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
|
|
15195
16670
|
title: "Fleet help",
|
|
15196
16671
|
terminalWidth: terminal.width,
|
|
15197
16672
|
terminalHeight: terminal.height,
|
|
15198
|
-
children: [
|
|
15199
|
-
|
|
15200
|
-
|
|
15201
|
-
|
|
15202
|
-
|
|
15203
|
-
|
|
15204
|
-
|
|
15205
|
-
|
|
15206
|
-
|
|
15207
|
-
|
|
15208
|
-
|
|
15209
|
-
|
|
15210
|
-
|
|
15211
|
-
|
|
15212
|
-
fg: fleetTheme.text,
|
|
15213
|
-
children: "[ / ] previous / next service"
|
|
15214
|
-
}, undefined, false, undefined, this),
|
|
15215
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15216
|
-
fg: fleetTheme.text,
|
|
15217
|
-
children: "/ filter \xB7 G follow logs \xB7 0/1/2 layout"
|
|
15218
|
-
}, undefined, false, undefined, this),
|
|
15219
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15220
|
-
fg: fleetTheme.text,
|
|
15221
|
-
children: "o open \xB7 y primary \xB7 c direct \xB7 l local \xB7 p public"
|
|
15222
|
-
}, undefined, false, undefined, this),
|
|
15223
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15224
|
-
fg: fleetTheme.text,
|
|
15225
|
-
children: "D doctor"
|
|
15226
|
-
}, undefined, false, undefined, this),
|
|
15227
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15228
|
-
fg: fleetTheme.danger,
|
|
15229
|
-
children: "d confirmed down (named volumes retained)"
|
|
15230
|
-
}, undefined, false, undefined, this),
|
|
15231
|
-
/* @__PURE__ */ jsxDEV5("text", {
|
|
15232
|
-
fg: fleetTheme.muted,
|
|
15233
|
-
children: "Esc closes this dialog"
|
|
15234
|
-
}, undefined, false, undefined, this)
|
|
15235
|
-
]
|
|
15236
|
-
}, undefined, true, undefined, this) : null,
|
|
16673
|
+
children: HELP_ROWS.map(([keys, action]) => /* @__PURE__ */ jsxDEV5("box", {
|
|
16674
|
+
style: { height: 1, flexDirection: "row" },
|
|
16675
|
+
children: [
|
|
16676
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16677
|
+
fg: fleetTheme.accent,
|
|
16678
|
+
children: padFleetText(keys, 9)
|
|
16679
|
+
}, undefined, false, undefined, this),
|
|
16680
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16681
|
+
fg: fleetTheme.muted,
|
|
16682
|
+
children: action
|
|
16683
|
+
}, undefined, false, undefined, this)
|
|
16684
|
+
]
|
|
16685
|
+
}, keys, true, undefined, this))
|
|
16686
|
+
}, undefined, false, undefined, this) : null,
|
|
15237
16687
|
state.doctorOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
|
|
15238
16688
|
title: "hestia doctor",
|
|
15239
16689
|
terminalWidth: terminal.width,
|
|
15240
16690
|
terminalHeight: terminal.height,
|
|
16691
|
+
children: doctorRunning ? /* @__PURE__ */ jsxDEV5("text", {
|
|
16692
|
+
fg: fleetTheme.muted,
|
|
16693
|
+
children: "Running diagnostics\u2026"
|
|
16694
|
+
}, undefined, false, undefined, this) : (() => {
|
|
16695
|
+
const modalHeight = Math.min(22, Math.max(8, terminal.height - 4));
|
|
16696
|
+
const report = budgetDoctorReport(doctorRows, doctorWidth, Math.max(3, modalHeight - 6));
|
|
16697
|
+
const summary = doctorOmissionSummary(doctorRows, report.shown);
|
|
16698
|
+
return [
|
|
16699
|
+
...report.entries.flatMap(({ row: row2, detailLines }) => {
|
|
16700
|
+
const mark = row2.level === "ok" ? "\u2713" : row2.level === "error" ? "\u2717" : row2.level === "warn" ? "!" : "?";
|
|
16701
|
+
const color = row2.level === "error" ? fleetTheme.danger : row2.level === "warn" ? fleetTheme.warning : row2.level === "ok" ? fleetTheme.healthy : fleetTheme.muted;
|
|
16702
|
+
const head = `${mark} ${sanitizeFleetTerminalText(row2.check)}`;
|
|
16703
|
+
return [
|
|
16704
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16705
|
+
fg: color,
|
|
16706
|
+
children: fitFleetText(head, doctorWidth - 4)
|
|
16707
|
+
}, `${row2.check}:head`, false, undefined, this),
|
|
16708
|
+
...detailLines.map((part, index) => /* @__PURE__ */ jsxDEV5("text", {
|
|
16709
|
+
fg: fleetTheme.muted,
|
|
16710
|
+
children: ` ${part}`
|
|
16711
|
+
}, `${row2.check}:detail:${index}`, false, undefined, this))
|
|
16712
|
+
];
|
|
16713
|
+
}),
|
|
16714
|
+
summary === undefined ? null : /* @__PURE__ */ jsxDEV5("text", {
|
|
16715
|
+
fg: fleetTheme.warning,
|
|
16716
|
+
children: summary
|
|
16717
|
+
}, "doctor:omitted", false, undefined, this)
|
|
16718
|
+
];
|
|
16719
|
+
})()
|
|
16720
|
+
}, undefined, false, undefined, this) : null,
|
|
16721
|
+
state.sharedOpen ? /* @__PURE__ */ jsxDEV5(FleetModal, {
|
|
16722
|
+
title: "Shared hostnames",
|
|
16723
|
+
terminalWidth: terminal.width,
|
|
16724
|
+
terminalHeight: terminal.height,
|
|
15241
16725
|
children: [
|
|
15242
|
-
|
|
16726
|
+
snapshot.shared.length === 0 ? /* @__PURE__ */ jsxDEV5("text", {
|
|
15243
16727
|
fg: fleetTheme.muted,
|
|
15244
|
-
children: "
|
|
15245
|
-
}, undefined, false, undefined, this) :
|
|
15246
|
-
|
|
15247
|
-
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
16728
|
+
children: "No shared hostnames declared. `hestia expose <svc> --shared <name>` creates one."
|
|
16729
|
+
}, undefined, false, undefined, this) : snapshot.shared.map((entry, index) => {
|
|
16730
|
+
const active = index === Math.min(state.sharedSelection, snapshot.shared.length - 1);
|
|
16731
|
+
const holder = entry.holder === undefined ? "unclaimed" : `held by ${entry.holder.project}${entry.holder.mine ? " (yours)" : ""}`;
|
|
16732
|
+
return /* @__PURE__ */ jsxDEV5("box", {
|
|
16733
|
+
style: { flexDirection: "column" },
|
|
16734
|
+
children: [
|
|
16735
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16736
|
+
fg: active ? fleetTheme.accent : fleetTheme.text,
|
|
16737
|
+
children: `${active ? "\u258E" : " "} ${sanitizeFleetTerminalText(entry.name)} ${sanitizeFleetTerminalText(entry.url)} ${holder}`
|
|
16738
|
+
}, undefined, false, undefined, this),
|
|
16739
|
+
entry.queue.map((waiter, position) => /* @__PURE__ */ jsxDEV5("text", {
|
|
16740
|
+
fg: fleetTheme.muted,
|
|
16741
|
+
children: ` ${position + 1}. ${sanitizeFleetTerminalText(waiter.project)}${waiter.mine ? " (yours)" : ""}${waiter.denied ? " \u2014 denied, still queued" : ""}`
|
|
16742
|
+
}, `${entry.name}:${waiter.project}`, false, undefined, this))
|
|
16743
|
+
]
|
|
16744
|
+
}, entry.name, true, undefined, this);
|
|
16745
|
+
}),
|
|
16746
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16747
|
+
style: { height: 1 }
|
|
16748
|
+
}, undefined, false, undefined, this),
|
|
15253
16749
|
/* @__PURE__ */ jsxDEV5("text", {
|
|
15254
16750
|
fg: fleetTheme.muted,
|
|
15255
|
-
children: "
|
|
16751
|
+
children: sharedBusy ? "working\u2026" : "j/k select \xB7 c claim (as selected stack) \xB7 a allow \xB7 x deny \xB7 r release \xB7 Esc/s close"
|
|
15256
16752
|
}, undefined, false, undefined, this)
|
|
15257
16753
|
]
|
|
15258
16754
|
}, undefined, true, undefined, this) : null,
|
|
@@ -15261,32 +16757,56 @@ function FleetApp({
|
|
|
15261
16757
|
terminalWidth: terminal.width,
|
|
15262
16758
|
terminalHeight: terminal.height,
|
|
15263
16759
|
children: [
|
|
15264
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15265
|
-
|
|
16760
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16761
|
+
style: { height: 1, flexDirection: "row" },
|
|
15266
16762
|
children: [
|
|
15267
|
-
|
|
15268
|
-
|
|
16763
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16764
|
+
fg: fleetTheme.faint,
|
|
16765
|
+
children: padFleetText("Branch", 12)
|
|
16766
|
+
}, undefined, false, undefined, this),
|
|
16767
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16768
|
+
fg: fleetTheme.bright,
|
|
16769
|
+
children: sanitizeFleetTerminalText(state.confirmDown.branch)
|
|
16770
|
+
}, undefined, false, undefined, this)
|
|
15269
16771
|
]
|
|
15270
16772
|
}, undefined, true, undefined, this),
|
|
15271
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15272
|
-
|
|
16773
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16774
|
+
style: { height: 1, flexDirection: "row" },
|
|
15273
16775
|
children: [
|
|
15274
|
-
|
|
15275
|
-
|
|
16776
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16777
|
+
fg: fleetTheme.faint,
|
|
16778
|
+
children: padFleetText("Repository", 12)
|
|
16779
|
+
}, undefined, false, undefined, this),
|
|
16780
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16781
|
+
fg: fleetTheme.text,
|
|
16782
|
+
children: sanitizeFleetTerminalText(state.confirmDown.repo)
|
|
16783
|
+
}, undefined, false, undefined, this)
|
|
15276
16784
|
]
|
|
15277
16785
|
}, undefined, true, undefined, this),
|
|
15278
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15279
|
-
|
|
16786
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16787
|
+
style: { height: 1, flexDirection: "row" },
|
|
15280
16788
|
children: [
|
|
15281
|
-
|
|
15282
|
-
|
|
16789
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16790
|
+
fg: fleetTheme.faint,
|
|
16791
|
+
children: padFleetText("Worktree", 12)
|
|
16792
|
+
}, undefined, false, undefined, this),
|
|
16793
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16794
|
+
fg: fleetTheme.text,
|
|
16795
|
+
children: sanitizeFleetTerminalText(state.confirmDown.worktree)
|
|
16796
|
+
}, undefined, false, undefined, this)
|
|
15283
16797
|
]
|
|
15284
16798
|
}, undefined, true, undefined, this),
|
|
15285
|
-
/* @__PURE__ */ jsxDEV5("
|
|
15286
|
-
|
|
16799
|
+
/* @__PURE__ */ jsxDEV5("box", {
|
|
16800
|
+
style: { height: 1, flexDirection: "row" },
|
|
15287
16801
|
children: [
|
|
15288
|
-
|
|
15289
|
-
|
|
16802
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16803
|
+
fg: fleetTheme.faint,
|
|
16804
|
+
children: padFleetText("Project", 12)
|
|
16805
|
+
}, undefined, false, undefined, this),
|
|
16806
|
+
/* @__PURE__ */ jsxDEV5("text", {
|
|
16807
|
+
fg: fleetTheme.text,
|
|
16808
|
+
children: sanitizeFleetTerminalText(state.confirmDown.project)
|
|
16809
|
+
}, undefined, false, undefined, this)
|
|
15290
16810
|
]
|
|
15291
16811
|
}, undefined, true, undefined, this),
|
|
15292
16812
|
/* @__PURE__ */ jsxDEV5("box", {
|
|
@@ -15305,15 +16825,33 @@ function FleetApp({
|
|
|
15305
16825
|
]
|
|
15306
16826
|
}, undefined, true, undefined, this);
|
|
15307
16827
|
}
|
|
15308
|
-
var LOG_RING_CAPACITY = 2000, EMPTY_CAPACITY;
|
|
16828
|
+
var LOG_RING_CAPACITY = 2000, TOAST_TTL_MS = 3500, EMPTY_CAPACITY, HELP_ROWS;
|
|
15309
16829
|
var init_FleetApp = __esm(() => {
|
|
16830
|
+
init_fleet_controller();
|
|
15310
16831
|
init_terminal_text();
|
|
16832
|
+
init_fleet_text();
|
|
15311
16833
|
init_fleet_theme();
|
|
16834
|
+
init_fleet_log_rows();
|
|
16835
|
+
init_fleet_format();
|
|
16836
|
+
init_fleet_status();
|
|
15312
16837
|
init_StackSidebar();
|
|
15313
16838
|
init_ServicePane();
|
|
15314
16839
|
init_LogPane();
|
|
15315
16840
|
init_FleetModal();
|
|
15316
16841
|
EMPTY_CAPACITY = { maxStacks: 0, live: 0, reserved: 0, queued: 0 };
|
|
16842
|
+
HELP_ROWS = [
|
|
16843
|
+
["j k \u2191\u2193", "move in focused pane \xB7 Tab cycles panes"],
|
|
16844
|
+
[", . [ ]", "previous / next stack \xB7 service"],
|
|
16845
|
+
["/", "filter stacks (Esc clears, then exits)"],
|
|
16846
|
+
["f \xB7 g G", "pause/resume follow \xB7 top / bottom of logs"],
|
|
16847
|
+
["o", "open selected endpoint in browser"],
|
|
16848
|
+
["y \xB7 Y", "yank endpoint URL \xB7 stack env block"],
|
|
16849
|
+
["c l p", "yank direct / local / public URL"],
|
|
16850
|
+
["s", "shared hostnames (claim, allow, deny, release)"],
|
|
16851
|
+
["D \xB7 d", "doctor report \xB7 down stack (volumes retained)"],
|
|
16852
|
+
["1 2 0", "split / stacked / auto layout"],
|
|
16853
|
+
["q", "quit \xB7 mouse click and wheel work everywhere"]
|
|
16854
|
+
];
|
|
15317
16855
|
});
|
|
15318
16856
|
|
|
15319
16857
|
// packages/tui/src/job-control.ts
|
|
@@ -15464,16 +17002,51 @@ __export(exports_src, {
|
|
|
15464
17002
|
var init_src3 = __esm(() => {
|
|
15465
17003
|
init_runtime();
|
|
15466
17004
|
init_fleet_source();
|
|
17005
|
+
init_fleet_controller();
|
|
15467
17006
|
init_terminal_text();
|
|
15468
17007
|
});
|
|
15469
17008
|
|
|
15470
17009
|
// packages/cli/src/index.ts
|
|
15471
17010
|
init_src2();
|
|
15472
17011
|
init_src();
|
|
15473
|
-
import { execFileSync as
|
|
15474
|
-
import { existsSync as
|
|
15475
|
-
import { dirname as dirname8, join as
|
|
17012
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
17013
|
+
import { existsSync as existsSync24 } from "fs";
|
|
17014
|
+
import { dirname as dirname8, join as join26 } from "path";
|
|
15476
17015
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
17016
|
+
var CLI_VERSION = "1.2.0";
|
|
17017
|
+
var PACKAGE_NAME = "@tridha643/hestia";
|
|
17018
|
+
function compareVersions(a, b) {
|
|
17019
|
+
const parse = (value) => {
|
|
17020
|
+
const [core, pre] = value.split("-", 2);
|
|
17021
|
+
return {
|
|
17022
|
+
core: (core ?? "").split(".").map((part) => Number.parseInt(part, 10) || 0),
|
|
17023
|
+
pre: pre !== undefined && pre.length > 0
|
|
17024
|
+
};
|
|
17025
|
+
};
|
|
17026
|
+
const left = parse(a);
|
|
17027
|
+
const right = parse(b);
|
|
17028
|
+
const length = Math.max(left.core.length, right.core.length);
|
|
17029
|
+
for (let index = 0;index < length; index += 1) {
|
|
17030
|
+
const diff = (left.core[index] ?? 0) - (right.core[index] ?? 0);
|
|
17031
|
+
if (diff !== 0)
|
|
17032
|
+
return diff;
|
|
17033
|
+
}
|
|
17034
|
+
if (left.pre === right.pre)
|
|
17035
|
+
return 0;
|
|
17036
|
+
return left.pre ? -1 : 1;
|
|
17037
|
+
}
|
|
17038
|
+
async function fetchLatestVersion() {
|
|
17039
|
+
const response = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
17040
|
+
headers: { accept: "application/json" }
|
|
17041
|
+
});
|
|
17042
|
+
if (!response.ok)
|
|
17043
|
+
throw new Error(`npm registry responded ${response.status}`);
|
|
17044
|
+
const body = await response.json();
|
|
17045
|
+
if (typeof body.version !== "string" || body.version.length === 0) {
|
|
17046
|
+
throw new Error("npm registry response has no version");
|
|
17047
|
+
}
|
|
17048
|
+
return body.version;
|
|
17049
|
+
}
|
|
15477
17050
|
function parseFlags(argv) {
|
|
15478
17051
|
const f = {
|
|
15479
17052
|
json: false,
|
|
@@ -15485,6 +17058,8 @@ function parseFlags(argv) {
|
|
|
15485
17058
|
noPort: false,
|
|
15486
17059
|
keepHostHeader: false,
|
|
15487
17060
|
overwriteDns: false,
|
|
17061
|
+
cancel: false,
|
|
17062
|
+
check: false,
|
|
15488
17063
|
noDaemon: false,
|
|
15489
17064
|
print: false,
|
|
15490
17065
|
interactive: false,
|
|
@@ -15566,6 +17141,22 @@ function parseFlags(argv) {
|
|
|
15566
17141
|
f.keepHostHeader = true;
|
|
15567
17142
|
else if (a === "--overwrite-dns")
|
|
15568
17143
|
f.overwriteDns = true;
|
|
17144
|
+
else if (a === "--shared") {
|
|
17145
|
+
f.shared = takeValue();
|
|
17146
|
+
if (!f.shared || f.shared.startsWith("--"))
|
|
17147
|
+
f.errors.push("--shared requires a value");
|
|
17148
|
+
} else if (a === "--hostname") {
|
|
17149
|
+
f.hostname = takeValue();
|
|
17150
|
+
if (!f.hostname || f.hostname.startsWith("--"))
|
|
17151
|
+
f.errors.push("--hostname requires a value");
|
|
17152
|
+
} else if (a === "--path") {
|
|
17153
|
+
f.path = takeValue();
|
|
17154
|
+
if (!f.path || f.path.startsWith("--"))
|
|
17155
|
+
f.errors.push("--path requires a value");
|
|
17156
|
+
} else if (a === "--cancel")
|
|
17157
|
+
f.cancel = true;
|
|
17158
|
+
else if (a === "--check")
|
|
17159
|
+
f.check = true;
|
|
15569
17160
|
else if (a === "--no-daemon")
|
|
15570
17161
|
f.noDaemon = true;
|
|
15571
17162
|
else if (a === "--print")
|
|
@@ -15637,12 +17228,16 @@ function parseFlags(argv) {
|
|
|
15637
17228
|
function validateCommandOptions(argv, command) {
|
|
15638
17229
|
const allowedByCommand = {
|
|
15639
17230
|
version: new Set(["--json"]),
|
|
17231
|
+
upgrade: new Set(["--json", "--check"]),
|
|
15640
17232
|
skill: new Set(["--json"]),
|
|
15641
17233
|
discover: new Set(["--json"]),
|
|
15642
17234
|
init: new Set(["--json", "--print", "--scope", "--write", "--no-port"]),
|
|
15643
17235
|
up: new Set(["--json", "--services", "--workers", "--allow-remote", "--force", "--no-varlock", "--wait", "--no-daemon"]),
|
|
15644
17236
|
run: new Set(["--json", "--name", "--env", "--no-port", "--varlock", "--signal", "--ready-timeout", "--cwd", "--wait", "--no-daemon"]),
|
|
15645
|
-
expose: new Set(["--json", "--tunnel", "--zone", "--keep-host-header", "--overwrite-dns", "--force", "--ready-timeout"]),
|
|
17237
|
+
expose: new Set(["--json", "--tunnel", "--zone", "--keep-host-header", "--overwrite-dns", "--force", "--ready-timeout", "--shared", "--hostname", "--path"]),
|
|
17238
|
+
claim: new Set(["--json", "--wait", "--cancel"]),
|
|
17239
|
+
release: new Set(["--json"]),
|
|
17240
|
+
share: new Set(["--json"]),
|
|
15646
17241
|
route: new Set(["--json"]),
|
|
15647
17242
|
router: new Set(["--json", "--interactive"]),
|
|
15648
17243
|
config: new Set(["--json", "--file"]),
|
|
@@ -15673,7 +17268,10 @@ function validateCommandOptions(argv, command) {
|
|
|
15673
17268
|
"--zone",
|
|
15674
17269
|
"--file",
|
|
15675
17270
|
"--scope",
|
|
15676
|
-
"--tail"
|
|
17271
|
+
"--tail",
|
|
17272
|
+
"--shared",
|
|
17273
|
+
"--hostname",
|
|
17274
|
+
"--path"
|
|
15677
17275
|
]);
|
|
15678
17276
|
for (let index = 0;index < argv.length; index += 1) {
|
|
15679
17277
|
const raw = argv[index];
|
|
@@ -15715,6 +17313,15 @@ function printStackHuman(r) {
|
|
|
15715
17313
|
const pub = r.endpoints.find((e) => e.name === s.name)?.publicUrl;
|
|
15716
17314
|
out.push(` \u25CF ${s.name.padEnd(12)} ${s.backend.padEnd(7)} ${port.padEnd(20)} ${s.state}` + (pub !== undefined ? ` ${pub}` : ""));
|
|
15717
17315
|
}
|
|
17316
|
+
for (const shared of listSharedHostnames()) {
|
|
17317
|
+
const holds = shared.holder?.project === r.project;
|
|
17318
|
+
const queuedHere = (shared.queue ?? []).some((waiter) => waiter.project === r.project);
|
|
17319
|
+
if (!holds && !queuedHere)
|
|
17320
|
+
continue;
|
|
17321
|
+
const pending = (shared.queue ?? []).length;
|
|
17322
|
+
const url = `https://${shared.hostname}${shared.path ?? ""}`;
|
|
17323
|
+
out.push(holds ? ` \u21C4 shared ${shared.name.padEnd(10)} ${url} HELD` + (pending > 0 ? ` (${pending} queued \u2014 \`hestia share requests\`)` : "") : ` \u21C4 shared ${shared.name.padEnd(10)} ${url} queued behind ${shared.holder?.project ?? "?"}`);
|
|
17324
|
+
}
|
|
15718
17325
|
process.stdout.write(out.join(`
|
|
15719
17326
|
`) + `
|
|
15720
17327
|
`);
|
|
@@ -15724,6 +17331,9 @@ var HELP = `hestia \u2014 per-worktree isolated dev stacks
|
|
|
15724
17331
|
usage:
|
|
15725
17332
|
hestia version [--json]
|
|
15726
17333
|
print CLI version, state schema, and daemon protocol
|
|
17334
|
+
hestia upgrade [--check] [--json]
|
|
17335
|
+
upgrade hestia to the latest npm release via \`bun add --global\`;
|
|
17336
|
+
--check only reports whether a newer version is available
|
|
15727
17337
|
hestia skill path [--json]
|
|
15728
17338
|
print the packaged Hestia agent skill path
|
|
15729
17339
|
hestia discover [--json]
|
|
@@ -15765,6 +17375,27 @@ usage:
|
|
|
15765
17375
|
worktrees). URLs land in endpoints[] + HESTIA_<SVC>_URL. These are
|
|
15766
17376
|
fail-closed but unauthenticated public URLs. Named v1 performs no DNS
|
|
15767
17377
|
writes and requires wildcard CNAME *.<zone> \u2192 <uuid>.cfargotunnel.com.
|
|
17378
|
+
hestia expose <endpoint> --shared <name> [--hostname <fqdn>] [--path <prefix>]
|
|
17379
|
+
[--tunnel <t>] [--zone <zone>] [--json]
|
|
17380
|
+
declare + claim a machine-owned SHARED hostname \u2014 a stable public URL
|
|
17381
|
+
for externally-pinned integrations (Slack request URLs, webhook
|
|
17382
|
+
consumers) that cannot chase per-branch hostnames. Exactly one worktree
|
|
17383
|
+
holds it at a time; handoffs are consent-based and switch inside
|
|
17384
|
+
hestiad with zero connector restarts. The target defaults to
|
|
17385
|
+
<name>.<zone>; --hostname points ANY FQDN you control (any zone) at it,
|
|
17386
|
+
and --path /prefix lets several handles share one hostname routed by
|
|
17387
|
+
longest path prefix (e.g. acme.com/slack vs acme.com/stripe). Prefer
|
|
17388
|
+
plain per-branch expose whenever the external side can be configured
|
|
17389
|
+
per branch (dashboards, multi-redirect OAuth)
|
|
17390
|
+
hestia claim <name> [--wait[=secs]] [--cancel] [--json]
|
|
17391
|
+
claim a shared hostname for this worktree. Held names queue DURABLY
|
|
17392
|
+
(position survives daemon restarts and CLI timeouts); --wait blocks
|
|
17393
|
+
until the holder allows or releases, --cancel leaves the queue
|
|
17394
|
+
hestia release <name> [--json]
|
|
17395
|
+
release a held shared hostname; the queue head is granted immediately
|
|
17396
|
+
hestia share list|requests [--json] | share allow|deny <name> [--json]
|
|
17397
|
+
inspect shared hostnames and arbitrate queued claims as the holder:
|
|
17398
|
+
allow hands over now; deny keeps the requester queued until release
|
|
15768
17399
|
hestia open <service> [path] [--json]
|
|
15769
17400
|
resolve a service's public URL (from \`expose\`) and open it in the
|
|
15770
17401
|
browser; the URL is always printed too, so a headless agent can hand
|
|
@@ -15819,7 +17450,7 @@ async function main() {
|
|
|
15819
17450
|
switch (cmd) {
|
|
15820
17451
|
case "version": {
|
|
15821
17452
|
const version = {
|
|
15822
|
-
cliVersion:
|
|
17453
|
+
cliVersion: CLI_VERSION,
|
|
15823
17454
|
stateSchema: STATE_SCHEMA_VERSION,
|
|
15824
17455
|
daemonProtocol: HESTIAD_PROTOCOL_VERSION,
|
|
15825
17456
|
runtime: "bun"
|
|
@@ -15829,6 +17460,45 @@ async function main() {
|
|
|
15829
17460
|
`);
|
|
15830
17461
|
else
|
|
15831
17462
|
process.stdout.write(`hestia ${version.cliVersion} (state v${version.stateSchema}, daemon v${version.daemonProtocol}, Bun)
|
|
17463
|
+
`);
|
|
17464
|
+
break;
|
|
17465
|
+
}
|
|
17466
|
+
case "upgrade": {
|
|
17467
|
+
let latest;
|
|
17468
|
+
try {
|
|
17469
|
+
latest = await fetchLatestVersion();
|
|
17470
|
+
} catch (error) {
|
|
17471
|
+
fail("upgrade-failed", `could not check the latest ${PACKAGE_NAME} version: ${error.message}`, flags.json);
|
|
17472
|
+
}
|
|
17473
|
+
const available = compareVersions(latest, CLI_VERSION) > 0;
|
|
17474
|
+
if (flags.check || !available) {
|
|
17475
|
+
if (flags.json) {
|
|
17476
|
+
process.stdout.write(JSON.stringify({ current: CLI_VERSION, latest, upgradeAvailable: available }) + `
|
|
17477
|
+
`);
|
|
17478
|
+
} else if (available) {
|
|
17479
|
+
process.stdout.write(`hestia ${latest} is available (you have ${CLI_VERSION}) \u2014 run \`hestia upgrade\`
|
|
17480
|
+
`);
|
|
17481
|
+
} else {
|
|
17482
|
+
process.stdout.write(`hestia ${CLI_VERSION} is the latest release
|
|
17483
|
+
`);
|
|
17484
|
+
}
|
|
17485
|
+
break;
|
|
17486
|
+
}
|
|
17487
|
+
const installArgs = ["add", "--global", `${PACKAGE_NAME}@${latest}`];
|
|
17488
|
+
if (!flags.json) {
|
|
17489
|
+
process.stderr.write(`upgrading hestia ${CLI_VERSION} \u2192 ${latest} via \`bun ${installArgs.join(" ")}\`\u2026
|
|
17490
|
+
`);
|
|
17491
|
+
}
|
|
17492
|
+
try {
|
|
17493
|
+
execFileSync5("bun", installArgs, { stdio: flags.json ? "ignore" : "inherit" });
|
|
17494
|
+
} catch (error) {
|
|
17495
|
+
fail("upgrade-failed", `\`bun ${installArgs.join(" ")}\` failed: ${error.message}`, flags.json);
|
|
17496
|
+
}
|
|
17497
|
+
if (flags.json)
|
|
17498
|
+
process.stdout.write(JSON.stringify({ current: CLI_VERSION, latest, upgraded: true }) + `
|
|
17499
|
+
`);
|
|
17500
|
+
else
|
|
17501
|
+
process.stdout.write(`hestia upgraded ${CLI_VERSION} \u2192 ${latest}
|
|
15832
17502
|
`);
|
|
15833
17503
|
break;
|
|
15834
17504
|
}
|
|
@@ -15837,10 +17507,10 @@ async function main() {
|
|
|
15837
17507
|
fail("usage", "usage: hestia skill path", flags.json);
|
|
15838
17508
|
const packageRoot = dirname8(dirname8(fileURLToPath3(import.meta.url)));
|
|
15839
17509
|
const candidates = [
|
|
15840
|
-
|
|
15841
|
-
|
|
17510
|
+
join26(packageRoot, "skills", "hestia", "SKILL.md"),
|
|
17511
|
+
join26(process.cwd(), "skills", "hestia", "SKILL.md")
|
|
15842
17512
|
];
|
|
15843
|
-
const path = candidates.find(
|
|
17513
|
+
const path = candidates.find(existsSync24);
|
|
15844
17514
|
if (path === undefined)
|
|
15845
17515
|
fail("config-missing", "packaged Hestia skill was not found", flags.json);
|
|
15846
17516
|
if (flags.json)
|
|
@@ -15992,11 +17662,17 @@ ${result.proposed}`);
|
|
|
15992
17662
|
if (flags.overwriteDns) {
|
|
15993
17663
|
fail("usage", "--overwrite-dns was removed; named v1 requires user-managed wildcard DNS", flags.json);
|
|
15994
17664
|
}
|
|
17665
|
+
if ((flags.hostname !== undefined || flags.path !== undefined) && flags.shared === undefined) {
|
|
17666
|
+
fail("usage", "--hostname/--path only apply with --shared <name>", flags.json);
|
|
17667
|
+
}
|
|
15995
17668
|
const r = await engine.expose(cwd, services, {
|
|
15996
17669
|
tunnel: flags.tunnel,
|
|
15997
17670
|
zone: flags.zone,
|
|
15998
17671
|
keepHostHeader: flags.keepHostHeader,
|
|
15999
17672
|
overwriteDns: flags.overwriteDns,
|
|
17673
|
+
shared: flags.shared,
|
|
17674
|
+
hostname: flags.hostname,
|
|
17675
|
+
path: flags.path,
|
|
16000
17676
|
force: flags.force,
|
|
16001
17677
|
readyTimeoutMs: flags.readyTimeout
|
|
16002
17678
|
});
|
|
@@ -16007,6 +17683,103 @@ ${result.proposed}`);
|
|
|
16007
17683
|
printStackHuman(r);
|
|
16008
17684
|
break;
|
|
16009
17685
|
}
|
|
17686
|
+
case "claim": {
|
|
17687
|
+
const name = flags._[1];
|
|
17688
|
+
if (!name || flags._.length > 2) {
|
|
17689
|
+
fail("usage", "usage: hestia claim <name> [--wait[=secs]] [--cancel]", flags.json);
|
|
17690
|
+
}
|
|
17691
|
+
if (flags.cancel) {
|
|
17692
|
+
await engine.cancelSharedClaim(cwd, name);
|
|
17693
|
+
if (flags.json)
|
|
17694
|
+
process.stdout.write(JSON.stringify({ ok: true, cancelled: name }) + `
|
|
17695
|
+
`);
|
|
17696
|
+
else
|
|
17697
|
+
process.stdout.write(`left the queue for "${name}"
|
|
17698
|
+
`);
|
|
17699
|
+
break;
|
|
17700
|
+
}
|
|
17701
|
+
const { record, result } = await engine.claimShared(cwd, name, {
|
|
17702
|
+
waitMs: flags.wait !== undefined ? flags.wait * 1000 : 0
|
|
17703
|
+
});
|
|
17704
|
+
const hostname = listSharedHostnames().find((candidate) => candidate.name === name)?.hostname;
|
|
17705
|
+
if (flags.json) {
|
|
17706
|
+
process.stdout.write(JSON.stringify({ granted: true, holder: result.holder, record }, null, 2) + `
|
|
17707
|
+
`);
|
|
17708
|
+
} else {
|
|
17709
|
+
process.stdout.write(`claimed "${name}"${hostname !== undefined ? ` \u2014 https://${hostname}` : ""} now routes here
|
|
17710
|
+
`);
|
|
17711
|
+
}
|
|
17712
|
+
break;
|
|
17713
|
+
}
|
|
17714
|
+
case "release": {
|
|
17715
|
+
const name = flags._[1];
|
|
17716
|
+
if (!name || flags._.length > 2)
|
|
17717
|
+
fail("usage", "usage: hestia release <name>", flags.json);
|
|
17718
|
+
await engine.releaseShared(cwd, name);
|
|
17719
|
+
if (flags.json)
|
|
17720
|
+
process.stdout.write(JSON.stringify({ ok: true, released: name }) + `
|
|
17721
|
+
`);
|
|
17722
|
+
else
|
|
17723
|
+
process.stdout.write(`released "${name}" \u2014 next in queue (if any) now holds it
|
|
17724
|
+
`);
|
|
17725
|
+
break;
|
|
17726
|
+
}
|
|
17727
|
+
case "share": {
|
|
17728
|
+
const action = flags._[1];
|
|
17729
|
+
if (action === "list" || action === "requests") {
|
|
17730
|
+
const records = listSharedHostnames();
|
|
17731
|
+
const view = records.map((record) => ({
|
|
17732
|
+
name: record.name,
|
|
17733
|
+
hostname: record.hostname,
|
|
17734
|
+
path: record.path,
|
|
17735
|
+
url: `https://${record.hostname}${record.path ?? ""}`,
|
|
17736
|
+
service: record.service,
|
|
17737
|
+
holder: record.holder,
|
|
17738
|
+
queued: record.queue ?? []
|
|
17739
|
+
})).filter((entry) => action === "list" || entry.queued.length > 0);
|
|
17740
|
+
if (flags.json)
|
|
17741
|
+
process.stdout.write(JSON.stringify(view, null, 2) + `
|
|
17742
|
+
`);
|
|
17743
|
+
else if (view.length === 0) {
|
|
17744
|
+
process.stdout.write(action === "list" ? `no shared hostnames declared
|
|
17745
|
+
` : `no pending claim requests
|
|
17746
|
+
`);
|
|
17747
|
+
} else {
|
|
17748
|
+
for (const entry of view) {
|
|
17749
|
+
const holder = entry.holder === undefined ? "unclaimed" : `held by ${entry.holder.project}`;
|
|
17750
|
+
process.stdout.write(`${entry.name.padEnd(16)} ${entry.url} ${holder}
|
|
17751
|
+
`);
|
|
17752
|
+
entry.queued.forEach((waiter, index) => {
|
|
17753
|
+
process.stdout.write(` ${String(index + 1).padStart(2)}. ${waiter.project}${waiter.denied ? " (denied \u2014 still queued)" : ""}
|
|
17754
|
+
`);
|
|
17755
|
+
});
|
|
17756
|
+
}
|
|
17757
|
+
}
|
|
17758
|
+
break;
|
|
17759
|
+
}
|
|
17760
|
+
const name = flags._[2];
|
|
17761
|
+
if (action !== "allow" && action !== "deny" || !name) {
|
|
17762
|
+
fail("usage", "usage: hestia share list|requests | share allow|deny <name>", flags.json);
|
|
17763
|
+
}
|
|
17764
|
+
if (action === "allow") {
|
|
17765
|
+
await engine.allowShared(cwd, name);
|
|
17766
|
+
if (flags.json)
|
|
17767
|
+
process.stdout.write(JSON.stringify({ ok: true, allowed: name }) + `
|
|
17768
|
+
`);
|
|
17769
|
+
else
|
|
17770
|
+
process.stdout.write(`allowed \u2014 "${name}" handed to the next in queue
|
|
17771
|
+
`);
|
|
17772
|
+
} else {
|
|
17773
|
+
await engine.denyShared(cwd, name);
|
|
17774
|
+
if (flags.json)
|
|
17775
|
+
process.stdout.write(JSON.stringify({ ok: true, denied: name }) + `
|
|
17776
|
+
`);
|
|
17777
|
+
else
|
|
17778
|
+
process.stdout.write(`denied \u2014 the requester stays queued until you release "${name}"
|
|
17779
|
+
`);
|
|
17780
|
+
}
|
|
17781
|
+
break;
|
|
17782
|
+
}
|
|
16010
17783
|
case "route": {
|
|
16011
17784
|
const action = flags._[1];
|
|
16012
17785
|
const services = flags._.slice(2);
|
|
@@ -16270,8 +18043,8 @@ ${JSON.stringify(result.config, null, 2)}
|
|
|
16270
18043
|
process.stdout.write(JSON.stringify(line) + `
|
|
16271
18044
|
`);
|
|
16272
18045
|
} else {
|
|
16273
|
-
const
|
|
16274
|
-
process.stdout.write(`${line.service.padEnd(nameWidth)} \u2502 ${
|
|
18046
|
+
const text2 = line.meta ? `[hestia] ${line.text}` : line.text;
|
|
18047
|
+
process.stdout.write(`${line.service.padEnd(nameWidth)} \u2502 ${text2}
|
|
16275
18048
|
`);
|
|
16276
18049
|
}
|
|
16277
18050
|
}
|
|
@@ -16336,7 +18109,7 @@ ${JSON.stringify(result.config, null, 2)}
|
|
|
16336
18109
|
} else if (sub === "stop") {
|
|
16337
18110
|
if (launchdManagesThisHome()) {
|
|
16338
18111
|
const uid = process.getuid?.() ?? 501;
|
|
16339
|
-
|
|
18112
|
+
execFileSync5("launchctl", ["bootout", `gui/${uid}/dev.hestia.daemon`]);
|
|
16340
18113
|
process.stderr.write("warning: launchd agent booted out \u2014 `hestia daemon install` re-enables reboot revival\n");
|
|
16341
18114
|
} else {
|
|
16342
18115
|
await stopDaemonProcess();
|
|
@@ -16402,6 +18175,10 @@ ${JSON.stringify(result.config, null, 2)}
|
|
|
16402
18175
|
fail("internal", err.message ?? String(err), flags.json);
|
|
16403
18176
|
}
|
|
16404
18177
|
}
|
|
16405
|
-
main
|
|
18178
|
+
if (import.meta.main)
|
|
18179
|
+
main();
|
|
18180
|
+
export {
|
|
18181
|
+
compareVersions
|
|
18182
|
+
};
|
|
16406
18183
|
|
|
16407
|
-
//# debugId=
|
|
18184
|
+
//# debugId=E655EED0C9FC8A1364756E2164756E21
|