mainflow 0.42.0 → 0.43.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
- import { P as configSchema } from "./dist-zSabglPq.js";
2
- import { $ as linksInto, At as WriteError, C as HANDLED_FILE, D as loadSessions, E as readNotices$1, I as attributeTrace, J as loadConfig, L as traceTranscript, O as readSession, Ot as ConflictError, Q as mainlineEdges, R as transcriptTitle, T as parseWarn, X as sectionText, Y as loadHub, Z as dependencyGraph, _ as setArchivedMany, a as openInApp, g as ARCHIVE_KINDS, k as transcriptPath, kt as DATA_DIR, nt as remoteMembers, q as HubError, st as readYaml, tt as refIndex, u as groupPlan, w as markNoticesHandled } from "./host-BAMZmRy8.js";
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 { A as isSynced, _ as readDaemon, a as remoteMode, b as readRemote, h as callDaemon, j as syncPaths, k as SYNC_DIR, m as DaemonUnavailable, n as hubWriter, r as writtenLines, y as watchTree } from "./operation-2wx2ex6x.js";
5
- import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-DosYhbwc.js";
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, relative, resolve, sep } from "node:path";
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
- /** The 503 body for a config.yaml that cannot be used, with the hub's root left out of paths. */
144
- function configErrorView(root, error) {
145
- const scrub = (text) => text.replaceAll(`${root}${sep}`, "");
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(root, options = {}) {
164
+ function createApp(dirs, options = {}) {
162
165
  const app = new Hono();
163
- const mainlinesRoot = join(root, DATA_DIR, "mainlines");
164
- const writer = hubWriter(root, options.env);
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(root).hub;
191
+ knownName = loadConfig(dirs).hub;
189
192
  return null;
190
193
  } catch (error) {
191
- return configErrorView(root, error);
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(root, DATA_DIR, "config.yaml"), "utf8")));
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.replaceAll(`${root}${sep}`, "");
214
- if (error instanceof HubError) return c.json(configErrorView(root, error), 503);
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(root)?.hostError;
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(root);
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(root))));
253
- const local = () => loadHub(root, { remotes: false });
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(root).sessions.some((s) => s.transcript === path);
260
- const file = transcriptPath(root, path);
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(root, m);
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 (resolve(root, path).startsWith(join(root, mainline.dir) + sep) && !remoteFilesOf(root, mainline).includes(path) && existsSync(resolve(root, path))) return c.json({ fetched: true });
287
- if (!remoteFilesOf(root, mainline).includes(path)) return c.json({ error: `${path} is not a file of ${mainline.id} that is only on the sync server` }, 404);
288
- return c.json(await fetchFromServer(root, path));
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(root, recorded);
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(root);
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(root, s) ? { elsewhere: true } : {}
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(root, c.req.param("id"));
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(root, session.transcript);
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 = join(root, path);
367
- if (!file.startsWith(mainlinesRoot + sep) || !existsSync(file) || !statSync(file).isFile()) return c.json({ error: `file ${path} not found under ${relative(root, mainlinesRoot)}` }, 404);
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(root, options.env),
414
- running: readDaemon(root) !== null,
415
- notices: readNotices(root)
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(root, body.key === void 0 ? void 0 : [body.key]) });
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(root).conflicts, name);
426
- const notice = readNotices(root).find((n) => n.saved === saved);
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(root, file)) return c.json({ error: `${saved} is not a file the sync client saved in the hub` }, 400);
429
- const target = isSynced(notice.path) ? join(root, DATA_DIR, notice.path) : null;
430
- if (!target || !insideHub(root, target)) return c.json({ error: `the notice for ${saved} names ${JSON.stringify(notice.path)}, not a synced file of the hub` }, 400);
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(root, notice.path, target)
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(root, (event) => void stream.writeSSE({
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(root, session) {
455
+ function runsElsewhere(dirs, session) {
453
456
  if (existsSync(session.cwd)) return false;
454
- return !(session.transcript && existsSync(transcriptPath(root, session.transcript)));
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 = (root) => readNotices$1(root);
460
+ const readNotices = (dirs) => readNotices$1(dirs);
458
461
  /**
459
- * Whether `file` really is inside the hub's .mainflow/ once every symbolic link on the way is followed. A file that
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(root, file) {
465
+ function insideHub(dirs, file) {
463
466
  let data;
464
467
  try {
465
- data = realpathSync(join(root, DATA_DIR));
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(root, path, file) {
486
- const daemon = readDaemon(root);
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 paths relative to the hub root. */
502
- function remoteFilesOf(root, mainline) {
503
- return Object.keys(readRemote(root)).map((path) => `${DATA_DIR}/${path}`).filter((path) => path.startsWith(`${mainline.dir}/`) && !existsSync(join(root, path))).sort();
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`, relative to the hub root, through the running sync client. */
506
- async function fetchFromServer(root, path) {
507
- const daemon = readDaemon(root);
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(join(root, path))) return { fetched: true };
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
- root;
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(root) {
587
- this.root = root;
588
- this.offset = sizeOf(syncPaths(root).notices);
589
+ constructor(dirs) {
590
+ this.dirs = dirs;
591
+ this.offset = sizeOf(syncPaths(dirs).notices);
589
592
  this.handledSeen = this.handledSignature();
590
- this.watcher = watchDataDir(join(root, DATA_DIR), (name) => {
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.root).notices;
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.root, DATA_DIR, HANDLED_FILE));
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(root, listener) {
658
- const key = resolve(root);
659
- const hub = watches.get(key) ?? new HubWatch(key);
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
- /** The hubs this interface serves: its own, then every hub registered in its config.yaml, with each one's root when it is here. */
670
- function servedHubs(root) {
671
- const config = readYaml(join(root, DATA_DIR, "config.yaml"), configSchema).data;
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
- if (!existsSync(file)) return {
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
- root: other
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
- root
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(root) {
743
+ function hubListGuard(dirs) {
735
744
  return async (c, next) => {
736
745
  try {
737
- loadConfig(root);
746
+ loadConfig(dirs);
738
747
  } catch (error) {
739
- return c.json(configErrorView(root, error), 503);
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(root) {
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(root));
755
- app.use("/api/hubs/events", hubListGuard(root));
756
- const own = createApp(root);
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(root).find((h) => h.name === name && h.base !== "/");
760
- if (!hub?.available || !hub.root) return null;
761
- let sub = others.get(hub.root);
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.root, { env: {} });
764
- others.set(hub.root, sub);
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(root);
778
+ const hubs = servedHubs(dirs);
770
779
  return c.json({
771
780
  primary: hubs[0].name,
772
- hubs: hubs.map(({ root: _root, ...link }) => link)
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(root).flatMap((hub) => hub.available && hub.root ? [watchHub(hub.root, (event) => void stream.writeSSE({
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(root, port, open = openInApp) {
814
- const app = createUiApp(root);
822
+ function serveUi(dirs, port, open = openInApp) {
823
+ const app = createUiApp(dirs);
815
824
  const dist = webDir();
816
- const hub = loadHub(root, { remotes: false }).config.hub;
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 };