mainflow 0.42.0 → 0.43.0-alpha.2
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 +767 -155
- package/dist/{dist-zSabglPq.js → dist-D5rZ0MQH.js} +71 -23
- package/dist/facts.d.ts +67 -12
- package/dist/facts.js +62 -13
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.d.ts +5 -540
- package/dist/hooks.js +126 -92
- package/dist/{host-BAMZmRy8.js → host-BJvqZy-i.js} +1374 -489
- package/dist/host.d.ts +3 -2
- package/dist/host.js +1 -1
- package/dist/{index-Dk4fCI7q.d.ts → index-BqPQp6TF.d.ts} +2 -8
- package/dist/index-DRpamPge.d.ts +550 -0
- package/dist/launch.d.ts +6 -5
- package/dist/launch.js +323 -1
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation-2wx2ex6x.js → operation-BhBbGauo.js} +395 -337
- package/dist/{ui-D1dg19Jw.js → ui-xxzmhAqF.js} +107 -98
- package/dist/version-Dvv4jwF1.js +6 -0
- package/dist/web/assets/{index-DvYj_6IC.js → index-B4w2iRCQ.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +2 -2
- package/dist/launch-DosYhbwc.js +0 -327
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { $ as
|
|
1
|
+
import { I as configSchema, tt as repoPaths } from "./dist-D5rZ0MQH.js";
|
|
2
|
+
import { $ as loadConfig, $t as WriteError, A as transcriptPath, D as readNotices$1, E as parseWarn, Jt as hubPath, L as attributeTrace, Nt as hubAtRoot, O as loadSessions, Q as HubError, Qt as DATA_DIR, R as traceTranscript, T as markNoticesHandled, Zt as ConflictError, _ as ARCHIVE_KINDS, a as openInApp, et as loadHub, ft as readYaml, it as linksInto, k as readSession, nt as dependencyGraph, ot as refIndex, qt as dataFile, rt as mainlineEdges, st as remoteMembers, tt as sectionText, u as groupPlan, v as setArchivedMany, w as HANDLED_FILE, z as transcriptTitle } from "./host-BJvqZy-i.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { C as readRemote, F as syncPaths, N as SYNC_DIR, P as isSynced, S as watchTree, _ as DaemonUnavailable, b as readDaemon, n as hubWriter, r as writtenLines, s as remoteMode, v as callDaemon } from "./operation-BhBbGauo.js";
|
|
5
|
+
import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
|
|
6
|
+
import { readUiRecord, realRoot } from "./launch.js";
|
|
6
7
|
import { createRequire } from "node:module";
|
|
7
8
|
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
8
|
-
import { basename, dirname, extname, join, normalize,
|
|
9
|
+
import { basename, dirname, extname, join, normalize, resolve, sep } from "node:path";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
11
|
import { spawnSync } from "node:child_process";
|
|
11
12
|
import { parse as parse$1 } from "yaml";
|
|
@@ -140,9 +141,11 @@ const groupView = (hub, g) => ({
|
|
|
140
141
|
summary: groupSummary(g.body),
|
|
141
142
|
order: groupPlan(hub, [], g.id).members.map((m) => m.ref)
|
|
142
143
|
});
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
/** A message with the hub's directories left out of its paths: a file of the hub reads `.mainflow/{path}`. */
|
|
145
|
+
const scrubbed = (dirs, text) => text.replaceAll(`${dirs.data}${sep}`, `${DATA_DIR}/`).replaceAll(`${dirs.root}${sep}`, "");
|
|
146
|
+
/** The 503 body for a config.yaml that cannot be used, with the hub's directories left out of paths. */
|
|
147
|
+
function configErrorView(dirs, error) {
|
|
148
|
+
const scrub = (text) => scrubbed(dirs, text);
|
|
146
149
|
const message = scrub(error.message);
|
|
147
150
|
const problems = error instanceof HubError ? error.problems : [];
|
|
148
151
|
const detail = problems.map((p) => `${p.field ? `${p.field}: ` : ""}${scrub(p.message)}`).join("; ");
|
|
@@ -158,10 +161,10 @@ function configErrorView(root, error) {
|
|
|
158
161
|
* through the hub's writer (src/remote/operation.ts), so in remote mode they are operations the sync server applies;
|
|
159
162
|
* `env` gives the sync server and service token as the environment variables do, `{}` for a hub other than the process's own.
|
|
160
163
|
*/
|
|
161
|
-
function createApp(
|
|
164
|
+
function createApp(dirs, options = {}) {
|
|
162
165
|
const app = new Hono();
|
|
163
|
-
const mainlinesRoot = join(
|
|
164
|
-
const writer = hubWriter(
|
|
166
|
+
const mainlinesRoot = join(dirs.data, "mainlines");
|
|
167
|
+
const writer = hubWriter(dirs, options.env);
|
|
165
168
|
const notes = /* @__PURE__ */ new WeakMap();
|
|
166
169
|
const write = async (c, op, input) => {
|
|
167
170
|
const written = await writer(op, input);
|
|
@@ -185,10 +188,10 @@ function createApp(root, options = {}) {
|
|
|
185
188
|
let knownName;
|
|
186
189
|
const configFailure = () => {
|
|
187
190
|
try {
|
|
188
|
-
knownName = loadConfig(
|
|
191
|
+
knownName = loadConfig(dirs).hub;
|
|
189
192
|
return null;
|
|
190
193
|
} catch (error) {
|
|
191
|
-
return configErrorView(
|
|
194
|
+
return configErrorView(dirs, error);
|
|
192
195
|
}
|
|
193
196
|
};
|
|
194
197
|
/**
|
|
@@ -198,10 +201,10 @@ function createApp(root, options = {}) {
|
|
|
198
201
|
const fallbackName = () => {
|
|
199
202
|
if (knownName) return knownName;
|
|
200
203
|
try {
|
|
201
|
-
const raw = configSchema.safeParse(parse$1(readFileSync(join(
|
|
204
|
+
const raw = configSchema.safeParse(parse$1(readFileSync(join(dirs.data, "config.yaml"), "utf8")));
|
|
202
205
|
if (raw.success) return raw.data.hub;
|
|
203
206
|
} catch {}
|
|
204
|
-
return basename(realRoot(root));
|
|
207
|
+
return basename(realRoot(dirs.root));
|
|
205
208
|
};
|
|
206
209
|
configFailure();
|
|
207
210
|
app.use("/api/*", async (c, next) => {
|
|
@@ -210,8 +213,8 @@ function createApp(root, options = {}) {
|
|
|
210
213
|
return failure ? c.json(failure, 503) : next();
|
|
211
214
|
});
|
|
212
215
|
app.onError((error, c) => {
|
|
213
|
-
const message = error.message
|
|
214
|
-
if (error instanceof HubError) return c.json(configErrorView(
|
|
216
|
+
const message = scrubbed(dirs, error.message);
|
|
217
|
+
if (error instanceof HubError) return c.json(configErrorView(dirs, error), 503);
|
|
215
218
|
if (error instanceof ConflictError) return c.json({ error: message }, 409);
|
|
216
219
|
if (error instanceof WriteError) return c.json({ error: message }, 400);
|
|
217
220
|
throw error;
|
|
@@ -220,7 +223,7 @@ function createApp(root, options = {}) {
|
|
|
220
223
|
const failure = configFailure();
|
|
221
224
|
const who = {
|
|
222
225
|
hub: failure ? fallbackName() : knownName,
|
|
223
|
-
root: realRoot(root),
|
|
226
|
+
root: realRoot(dirs.root),
|
|
224
227
|
version: CLI_VERSION,
|
|
225
228
|
pid: process.pid
|
|
226
229
|
};
|
|
@@ -232,15 +235,15 @@ function createApp(root, options = {}) {
|
|
|
232
235
|
return c.json(who);
|
|
233
236
|
});
|
|
234
237
|
app.get("/api/host", (c) => {
|
|
235
|
-
const hostError = readUiRecord(
|
|
238
|
+
const hostError = readUiRecord(dirs)?.hostError;
|
|
236
239
|
return c.json(hostError ? { hostError } : {});
|
|
237
240
|
});
|
|
238
241
|
app.get("/api/hub", (c) => {
|
|
239
|
-
const hub = loadHub(
|
|
242
|
+
const hub = loadHub(dirs);
|
|
240
243
|
const body = {
|
|
241
244
|
hub: hub.config.hub,
|
|
242
245
|
version: CLI_VERSION,
|
|
243
|
-
repos: hub.config.repos,
|
|
246
|
+
repos: repoPaths(hub.config.repos),
|
|
244
247
|
mainlines: hub.mainlines.map(summarize),
|
|
245
248
|
groups: hub.groups.map((g) => groupView(hub, g)),
|
|
246
249
|
openIssues: hub.issues.filter((i) => i.status !== "closed").length,
|
|
@@ -249,21 +252,21 @@ function createApp(root, options = {}) {
|
|
|
249
252
|
};
|
|
250
253
|
return c.json(body);
|
|
251
254
|
});
|
|
252
|
-
app.get("/api/relations", (c) => c.json(relationsView(loadHub(
|
|
253
|
-
const local = () => loadHub(
|
|
255
|
+
app.get("/api/relations", (c) => c.json(relationsView(loadHub(dirs))));
|
|
256
|
+
const local = () => loadHub(dirs, { remotes: false });
|
|
254
257
|
app.get("/api/issues", (c) => c.json(local().issues));
|
|
255
258
|
app.get("/api/findings", (c) => c.json(local().findings));
|
|
256
259
|
app.get("/api/ops", (c) => c.json(local().ops));
|
|
257
260
|
app.post("/api/reveal", async (c) => {
|
|
258
261
|
const { path } = await c.req.json();
|
|
259
|
-
const known = loadSessions(
|
|
260
|
-
const file = transcriptPath(
|
|
262
|
+
const known = loadSessions(dirs).sessions.some((s) => s.transcript === path);
|
|
263
|
+
const file = transcriptPath(dirs, path);
|
|
261
264
|
if (!known || !existsSync(file)) return c.json({ error: "only a transcript recorded on a session can be revealed" }, 400);
|
|
262
265
|
const opened = spawnSync("open", ["-R", file]);
|
|
263
266
|
return opened.status === 0 ? c.json({ revealed: file }) : c.json({ error: `open -R failed: ${opened.stderr}` }, 500);
|
|
264
267
|
});
|
|
265
268
|
const viewOf = (m) => {
|
|
266
|
-
const remoteFiles = remoteFilesOf(
|
|
269
|
+
const remoteFiles = remoteFilesOf(dirs, m);
|
|
267
270
|
return remoteFiles.length > 0 ? {
|
|
268
271
|
...m,
|
|
269
272
|
remoteFiles
|
|
@@ -283,13 +286,13 @@ function createApp(root, options = {}) {
|
|
|
283
286
|
if (!mainline) return c.json({ error: `mainline ${c.req.param("m")} not found` }, 404);
|
|
284
287
|
const { path } = await c.req.json().catch(() => ({ path: void 0 }));
|
|
285
288
|
if (typeof path !== "string") return c.json({ error: "path is required" }, 400);
|
|
286
|
-
if (
|
|
287
|
-
if (!remoteFilesOf(
|
|
288
|
-
return c.json(await fetchFromServer(
|
|
289
|
+
if (dataFile(dirs, normalize(path)).startsWith(dataFile(dirs, mainline.dir) + sep) && !remoteFilesOf(dirs, mainline).includes(path) && existsSync(dataFile(dirs, normalize(path)))) return c.json({ fetched: true });
|
|
290
|
+
if (!remoteFilesOf(dirs, mainline).includes(path)) return c.json({ error: `${path} is not a file of ${mainline.id} that is only on the sync server` }, 404);
|
|
291
|
+
return c.json(await fetchFromServer(dirs, path));
|
|
289
292
|
});
|
|
290
293
|
const titles = /* @__PURE__ */ new Map();
|
|
291
294
|
const titleOf = (recorded) => {
|
|
292
|
-
const transcript = recorded && transcriptPath(
|
|
295
|
+
const transcript = recorded && transcriptPath(dirs, recorded);
|
|
293
296
|
if (!transcript || !existsSync(transcript)) return void 0;
|
|
294
297
|
const { size, mtimeMs } = statSync(transcript);
|
|
295
298
|
const stamp = `${size}:${mtimeMs}`;
|
|
@@ -303,13 +306,13 @@ function createApp(root, options = {}) {
|
|
|
303
306
|
return title;
|
|
304
307
|
};
|
|
305
308
|
app.get("/api/sessions", (c) => {
|
|
306
|
-
const { sessions, problems } = loadSessions(
|
|
309
|
+
const { sessions, problems } = loadSessions(dirs);
|
|
307
310
|
const views = sessions.map((s) => {
|
|
308
311
|
const title = titleOf(s.transcript);
|
|
309
312
|
return {
|
|
310
313
|
...s,
|
|
311
314
|
...title ? { title } : {},
|
|
312
|
-
...runsElsewhere(
|
|
315
|
+
...runsElsewhere(dirs, s) ? { elsewhere: true } : {}
|
|
313
316
|
};
|
|
314
317
|
});
|
|
315
318
|
return c.json({
|
|
@@ -346,9 +349,9 @@ function createApp(root, options = {}) {
|
|
|
346
349
|
})).value);
|
|
347
350
|
});
|
|
348
351
|
app.get("/api/sessions/:id/trace", (c) => {
|
|
349
|
-
const session = readSession(
|
|
352
|
+
const session = readSession(dirs, c.req.param("id"));
|
|
350
353
|
if (!session) return c.json({ error: `session ${c.req.param("id")} not found` }, 404);
|
|
351
|
-
const transcript = session.transcript && transcriptPath(
|
|
354
|
+
const transcript = session.transcript && transcriptPath(dirs, session.transcript);
|
|
352
355
|
if (!transcript || !existsSync(transcript)) return c.json({
|
|
353
356
|
available: false,
|
|
354
357
|
byAction: {},
|
|
@@ -363,8 +366,8 @@ function createApp(root, options = {}) {
|
|
|
363
366
|
});
|
|
364
367
|
app.get("/api/files/*", (c) => {
|
|
365
368
|
const path = normalize(decodeURIComponent(c.req.path.slice(11)));
|
|
366
|
-
const file =
|
|
367
|
-
if (!file.startsWith(mainlinesRoot + sep) || !existsSync(file) || !statSync(file).isFile()) return c.json({ error: `file ${path} not found under ${
|
|
369
|
+
const file = dataFile(dirs, path);
|
|
370
|
+
if (!file.startsWith(mainlinesRoot + sep) || !existsSync(file) || !statSync(file).isFile()) return c.json({ error: `file ${path} not found under ${hubPath(dirs, mainlinesRoot)}` }, 404);
|
|
368
371
|
return c.body(readFileSync(file), 200, { "content-type": CONTENT_TYPES[extname(file)] ?? "application/octet-stream" });
|
|
369
372
|
});
|
|
370
373
|
app.post("/api/mainlines/:m/archive", async (c) => {
|
|
@@ -410,33 +413,33 @@ function createApp(root, options = {}) {
|
|
|
410
413
|
return c.json((ref.includes("/") ? await write(c, "item.check", input) : await write(c, "criterion.check", input)).value);
|
|
411
414
|
});
|
|
412
415
|
app.get("/api/sync", (c) => c.json({
|
|
413
|
-
remote: remoteMode(
|
|
414
|
-
running: readDaemon(
|
|
415
|
-
notices: readNotices(
|
|
416
|
+
remote: remoteMode(dirs, options.env),
|
|
417
|
+
running: readDaemon(dirs) !== null,
|
|
418
|
+
notices: readNotices(dirs)
|
|
416
419
|
}));
|
|
417
420
|
app.post("/api/sync/handled", async (c) => {
|
|
418
421
|
const body = await c.req.json().catch(() => ({}));
|
|
419
422
|
if (body.key !== void 0 && typeof body.key !== "string") return c.json({ error: "key must be a string" }, 400);
|
|
420
|
-
return c.json({ marked: markNoticesHandled(
|
|
423
|
+
return c.json({ marked: markNoticesHandled(dirs, body.key === void 0 ? void 0 : [body.key]) });
|
|
421
424
|
});
|
|
422
425
|
app.get("/api/sync/conflicts/:name", async (c) => {
|
|
423
426
|
const name = c.req.param("name");
|
|
424
427
|
const saved = `${SYNC_DIR}/conflicts/${name}`;
|
|
425
|
-
const file = join(syncPaths(
|
|
426
|
-
const notice = readNotices(
|
|
428
|
+
const file = join(syncPaths(dirs).conflicts, name);
|
|
429
|
+
const notice = readNotices(dirs).find((n) => n.saved === saved);
|
|
427
430
|
if (name.includes("/") || name.startsWith(".") || !existsSync(file) || !notice) return c.json({ error: `no saved text ${saved} named by a notice` }, 404);
|
|
428
|
-
if (lstatSync(file).isSymbolicLink() || !insideHub(
|
|
429
|
-
const target = isSynced(notice.path) ? join(
|
|
430
|
-
if (!target || !insideHub(
|
|
431
|
+
if (lstatSync(file).isSymbolicLink() || !insideHub(dirs, file)) return c.json({ error: `${saved} is not a file the sync client saved in the hub` }, 400);
|
|
432
|
+
const target = isSynced(notice.path) ? join(dirs.data, notice.path) : null;
|
|
433
|
+
if (!target || !insideHub(dirs, target)) return c.json({ error: `the notice for ${saved} names ${JSON.stringify(notice.path)}, not a synced file of the hub` }, 400);
|
|
431
434
|
return c.json({
|
|
432
435
|
saved,
|
|
433
436
|
path: notice.path,
|
|
434
437
|
mine: readFileSync(file, "utf8"),
|
|
435
|
-
theirs: await serverVersion(
|
|
438
|
+
theirs: await serverVersion(dirs, notice.path, target)
|
|
436
439
|
});
|
|
437
440
|
});
|
|
438
441
|
app.get("/api/events", (c) => streamSSE(c, async (stream) => {
|
|
439
|
-
const stop = watchHub(
|
|
442
|
+
const stop = watchHub(dirs, (event) => void stream.writeSSE({
|
|
440
443
|
event: event.kind,
|
|
441
444
|
data: event.name
|
|
442
445
|
}));
|
|
@@ -449,20 +452,20 @@ function createApp(root, options = {}) {
|
|
|
449
452
|
* Whether a session runs in another environment: its working directory is not on this machine, and neither is a
|
|
450
453
|
* transcript it recorded. A session of this machine whose worktree was removed still has its transcript here.
|
|
451
454
|
*/
|
|
452
|
-
function runsElsewhere(
|
|
455
|
+
function runsElsewhere(dirs, session) {
|
|
453
456
|
if (existsSync(session.cwd)) return false;
|
|
454
|
-
return !(session.transcript && existsSync(transcriptPath(
|
|
457
|
+
return !(session.transcript && existsSync(transcriptPath(dirs, session.transcript)));
|
|
455
458
|
}
|
|
456
459
|
/** The notices of the sync client as the interface lists them (core reads the files and tells which are handled). */
|
|
457
|
-
const readNotices = (
|
|
460
|
+
const readNotices = (dirs) => readNotices$1(dirs);
|
|
458
461
|
/**
|
|
459
|
-
* Whether `file` really is inside the hub's
|
|
462
|
+
* Whether `file` really is inside the hub's data directory once every symbolic link on the way is followed. A file that
|
|
460
463
|
* does not exist yet is judged by the deepest directory above it that does.
|
|
461
464
|
*/
|
|
462
|
-
function insideHub(
|
|
465
|
+
function insideHub(dirs, file) {
|
|
463
466
|
let data;
|
|
464
467
|
try {
|
|
465
|
-
data = realpathSync(
|
|
468
|
+
data = realpathSync(dirs.data);
|
|
466
469
|
} catch {
|
|
467
470
|
return false;
|
|
468
471
|
}
|
|
@@ -482,8 +485,8 @@ function insideHub(root, file) {
|
|
|
482
485
|
* The file at `path` as the hub has it: from the running sync client's copy of the server's version, else this
|
|
483
486
|
* machine's file at `file`, already checked to be inside the hub.
|
|
484
487
|
*/
|
|
485
|
-
async function serverVersion(
|
|
486
|
-
const daemon = readDaemon(
|
|
488
|
+
async function serverVersion(dirs, path, file) {
|
|
489
|
+
const daemon = readDaemon(dirs);
|
|
487
490
|
if (daemon) try {
|
|
488
491
|
const { text } = await callDaemon(daemon, "/file", { path }, 5e3);
|
|
489
492
|
if (text !== void 0) return {
|
|
@@ -498,13 +501,13 @@ async function serverVersion(root, path, file) {
|
|
|
498
501
|
source: "local"
|
|
499
502
|
};
|
|
500
503
|
}
|
|
501
|
-
/** The files of `mainline` that remote.json lists and that are not on disk here, as
|
|
502
|
-
function remoteFilesOf(
|
|
503
|
-
return Object.keys(readRemote(
|
|
504
|
+
/** The files of `mainline` that remote.json lists and that are not on disk here, named as `mainline.dir` is. */
|
|
505
|
+
function remoteFilesOf(dirs, mainline) {
|
|
506
|
+
return Object.keys(readRemote(dirs)).map((path) => `${DATA_DIR}/${path}`).filter((path) => path.startsWith(`${mainline.dir}/`) && !existsSync(dataFile(dirs, path))).sort();
|
|
504
507
|
}
|
|
505
|
-
/** Download `path`,
|
|
506
|
-
async function fetchFromServer(
|
|
507
|
-
const daemon = readDaemon(
|
|
508
|
+
/** Download `path`, a file of the hub named `.mainflow/{path}`, through the running sync client. */
|
|
509
|
+
async function fetchFromServer(dirs, path) {
|
|
510
|
+
const daemon = readDaemon(dirs);
|
|
508
511
|
if (!daemon) return {
|
|
509
512
|
fetched: false,
|
|
510
513
|
error: "mainflow sync is not running for this hub, so the file cannot be downloaded; start it with mainflow sync"
|
|
@@ -523,7 +526,7 @@ async function fetchFromServer(root, path) {
|
|
|
523
526
|
error: error.message
|
|
524
527
|
};
|
|
525
528
|
}
|
|
526
|
-
if (existsSync(
|
|
529
|
+
if (existsSync(dataFile(dirs, path))) return { fetched: true };
|
|
527
530
|
if (result.error) return {
|
|
528
531
|
fetched: false,
|
|
529
532
|
error: result.error
|
|
@@ -576,18 +579,18 @@ function watchDataDir(dataDir, onName, platform = process.platform) {
|
|
|
576
579
|
}
|
|
577
580
|
/** One file watcher per hub, shared by every listener of that hub. */
|
|
578
581
|
var HubWatch = class {
|
|
579
|
-
|
|
582
|
+
dirs;
|
|
580
583
|
listeners = /* @__PURE__ */ new Set();
|
|
581
584
|
watcher;
|
|
582
585
|
/** How far notices.jsonl has been read */
|
|
583
586
|
offset;
|
|
584
587
|
/** handled.json as last told of, so the several events of one write tell the interface once */
|
|
585
588
|
handledSeen;
|
|
586
|
-
constructor(
|
|
587
|
-
this.
|
|
588
|
-
this.offset = sizeOf(syncPaths(
|
|
589
|
+
constructor(dirs) {
|
|
590
|
+
this.dirs = dirs;
|
|
591
|
+
this.offset = sizeOf(syncPaths(dirs).notices);
|
|
589
592
|
this.handledSeen = this.handledSignature();
|
|
590
|
-
this.watcher = watchDataDir(
|
|
593
|
+
this.watcher = watchDataDir(dirs.data, (name) => {
|
|
591
594
|
const kind = eventOf(name);
|
|
592
595
|
if (kind === null || kind === "notices" && !this.newWarnings() || kind === "handled" && !this.handledChanged()) return;
|
|
593
596
|
const event = {
|
|
@@ -602,7 +605,7 @@ var HubWatch = class {
|
|
|
602
605
|
}
|
|
603
606
|
/** Whether the lines added to notices.jsonl since the last read hold a warn-level notice. */
|
|
604
607
|
newWarnings() {
|
|
605
|
-
const file = syncPaths(this.
|
|
608
|
+
const file = syncPaths(this.dirs).notices;
|
|
606
609
|
const size = sizeOf(file);
|
|
607
610
|
if (size < this.offset) this.offset = 0;
|
|
608
611
|
if (size === this.offset) return false;
|
|
@@ -627,7 +630,7 @@ var HubWatch = class {
|
|
|
627
630
|
/** handled.json's time and size, empty while there is none. */
|
|
628
631
|
handledSignature() {
|
|
629
632
|
try {
|
|
630
|
-
const info = statSync(join(this.
|
|
633
|
+
const info = statSync(join(this.dirs.data, HANDLED_FILE));
|
|
631
634
|
return `${info.mtimeMs}:${info.size}`;
|
|
632
635
|
} catch {
|
|
633
636
|
return "";
|
|
@@ -654,9 +657,9 @@ function sizeOf(file) {
|
|
|
654
657
|
* Call `listener` for every change the interface reloads on, and once for each batch of new warn-level notices.
|
|
655
658
|
* Every listener of a hub shares one file watcher, which closes with the last of them. Returns the unsubscribe.
|
|
656
659
|
*/
|
|
657
|
-
function watchHub(
|
|
658
|
-
const key = resolve(
|
|
659
|
-
const hub = watches.get(key) ?? new HubWatch(
|
|
660
|
+
function watchHub(dirs, listener) {
|
|
661
|
+
const key = resolve(dirs.data);
|
|
662
|
+
const hub = watches.get(key) ?? new HubWatch(dirs);
|
|
660
663
|
watches.set(key, hub);
|
|
661
664
|
hub.listeners.add(listener);
|
|
662
665
|
return () => {
|
|
@@ -666,20 +669,23 @@ function watchHub(root, listener) {
|
|
|
666
669
|
watches.delete(key);
|
|
667
670
|
};
|
|
668
671
|
}
|
|
669
|
-
/**
|
|
670
|
-
|
|
671
|
-
|
|
672
|
+
/**
|
|
673
|
+
* The hubs this interface serves: its own, then every hub registered in its config.yaml, with each one's directories
|
|
674
|
+
* when this machine registered it from the path config.yaml gives.
|
|
675
|
+
*/
|
|
676
|
+
function servedHubs(dirs) {
|
|
677
|
+
const config = readYaml(join(dirs.data, "config.yaml"), configSchema).data;
|
|
672
678
|
const others = Object.entries(config.hubs ?? {}).map(([name, path]) => {
|
|
673
679
|
const base = `/h/${name}/`;
|
|
674
|
-
const other = resolve(root, path);
|
|
675
|
-
const file = join(other, DATA_DIR, "config.yaml");
|
|
676
680
|
if (name === config.hub) return {
|
|
677
681
|
name,
|
|
678
682
|
base,
|
|
679
683
|
available: false,
|
|
680
684
|
reason: "this hub"
|
|
681
685
|
};
|
|
682
|
-
|
|
686
|
+
const other = hubAtRoot(resolve(dirs.root, path));
|
|
687
|
+
const file = other ? join(other.data, "config.yaml") : null;
|
|
688
|
+
if (!other || !file || !existsSync(file)) return {
|
|
683
689
|
name,
|
|
684
690
|
base,
|
|
685
691
|
available: false,
|
|
@@ -711,14 +717,17 @@ function servedHubs(root) {
|
|
|
711
717
|
name,
|
|
712
718
|
base,
|
|
713
719
|
available: true,
|
|
714
|
-
|
|
720
|
+
dirs: {
|
|
721
|
+
root: other.root,
|
|
722
|
+
data: other.data
|
|
723
|
+
}
|
|
715
724
|
};
|
|
716
725
|
});
|
|
717
726
|
return [{
|
|
718
727
|
name: config.hub,
|
|
719
728
|
base: "/",
|
|
720
729
|
available: true,
|
|
721
|
-
|
|
730
|
+
dirs
|
|
722
731
|
}, ...others];
|
|
723
732
|
}
|
|
724
733
|
let lastTurn = Promise.resolve();
|
|
@@ -731,12 +740,12 @@ const pace = async (c, next) => {
|
|
|
731
740
|
}
|
|
732
741
|
await next();
|
|
733
742
|
};
|
|
734
|
-
function hubListGuard(
|
|
743
|
+
function hubListGuard(dirs) {
|
|
735
744
|
return async (c, next) => {
|
|
736
745
|
try {
|
|
737
|
-
loadConfig(
|
|
746
|
+
loadConfig(dirs);
|
|
738
747
|
} catch (error) {
|
|
739
|
-
return c.json(configErrorView(
|
|
748
|
+
return c.json(configErrorView(dirs, error), 503);
|
|
740
749
|
}
|
|
741
750
|
return next();
|
|
742
751
|
};
|
|
@@ -747,33 +756,33 @@ function hubListGuard(root) {
|
|
|
747
756
|
* hub's changes and notices tagged with its name, so the switcher shows where something happened. Another hub is
|
|
748
757
|
* written through its own sync settings only, never through this process's environment variables.
|
|
749
758
|
*/
|
|
750
|
-
function createUiApp(
|
|
759
|
+
function createUiApp(dirs) {
|
|
751
760
|
const app = new Hono();
|
|
752
761
|
app.use("/api/*", pace);
|
|
753
762
|
app.use("/h/:name/api/*", pace);
|
|
754
|
-
app.use("/api/hubs", hubListGuard(
|
|
755
|
-
app.use("/api/hubs/events", hubListGuard(
|
|
756
|
-
const own = createApp(
|
|
763
|
+
app.use("/api/hubs", hubListGuard(dirs));
|
|
764
|
+
app.use("/api/hubs/events", hubListGuard(dirs));
|
|
765
|
+
const own = createApp(dirs);
|
|
757
766
|
const others = /* @__PURE__ */ new Map();
|
|
758
767
|
const appOf = (name) => {
|
|
759
|
-
const hub = servedHubs(
|
|
760
|
-
if (!hub?.available || !hub.
|
|
761
|
-
let sub = others.get(hub.
|
|
768
|
+
const hub = servedHubs(dirs).find((h) => h.name === name && h.base !== "/");
|
|
769
|
+
if (!hub?.available || !hub.dirs) return null;
|
|
770
|
+
let sub = others.get(hub.dirs.data);
|
|
762
771
|
if (!sub) {
|
|
763
|
-
sub = createApp(hub.
|
|
764
|
-
others.set(hub.
|
|
772
|
+
sub = createApp(hub.dirs, { env: {} });
|
|
773
|
+
others.set(hub.dirs.data, sub);
|
|
765
774
|
}
|
|
766
775
|
return sub;
|
|
767
776
|
};
|
|
768
777
|
app.get("/api/hubs", (c) => {
|
|
769
|
-
const hubs = servedHubs(
|
|
778
|
+
const hubs = servedHubs(dirs);
|
|
770
779
|
return c.json({
|
|
771
780
|
primary: hubs[0].name,
|
|
772
|
-
hubs: hubs.map(({
|
|
781
|
+
hubs: hubs.map(({ dirs: _dirs, ...link }) => link)
|
|
773
782
|
});
|
|
774
783
|
});
|
|
775
784
|
app.get("/api/hubs/events", (c) => streamSSE(c, async (stream) => {
|
|
776
|
-
const stops = servedHubs(
|
|
785
|
+
const stops = servedHubs(dirs).flatMap((hub) => hub.available && hub.dirs ? [watchHub(hub.dirs, (event) => void stream.writeSSE({
|
|
777
786
|
event: event.kind,
|
|
778
787
|
data: JSON.stringify({
|
|
779
788
|
hub: hub.name,
|
|
@@ -810,10 +819,10 @@ function webDir() {
|
|
|
810
819
|
* Resolves once the port is listening. A port taken between the free-port probe and here rejects with a
|
|
811
820
|
* WriteError instead of Node's unhandled 'error' event and stack trace.
|
|
812
821
|
*/
|
|
813
|
-
function serveUi(
|
|
814
|
-
const app = createUiApp(
|
|
822
|
+
function serveUi(dirs, port, open = openInApp) {
|
|
823
|
+
const app = createUiApp(dirs);
|
|
815
824
|
const dist = webDir();
|
|
816
|
-
const hub = loadHub(
|
|
825
|
+
const hub = loadHub(dirs, { remotes: false }).config.hub;
|
|
817
826
|
app.get("/open/*", (c) => {
|
|
818
827
|
const url = new URL(c.req.url);
|
|
819
828
|
const target = `${url.pathname.slice(5) || "/"}${url.search}`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
//#region src/version.ts
|
|
3
|
+
/** This CLI's own version, from the package manifest one level above both src/ and dist/. */
|
|
4
|
+
const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { CLI_VERSION as t };
|