mainflow 0.7.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { A as createFinding, C as startSession, E as sessionBriefing, F as setFindingStatus, H as TASK_FILE, I as setIssueArchived, L as HubError, M as planIssue, N as reopenIssue, O as appendOps, P as setFindingArchived, R as loadHub, S as setSessionStatus, T as updateAction, V as MAINLINE_FILE, W as WriteError, a as createDecision, b as setSessionArchived, c as dropChecklistItem, d as setChecklistItem, f as setCriterion, g as addAction, h as updateTask, i as addRepo, j as createIssue, k as closeIssue, l as initHub, m as supersedeDecision, n as acceptDecision, o as createMainline, p as setMainlineStatus, q as findHubRoot, r as addHub, s as createTask, t as migrateHub, u as setArchived, y as readSession } from "./dist-BktkzRuK.js";
3
- import { r as launchUi, s as listProgress } from "./launch-BR70uSMc.js";
2
+ import { A as appendOps, B as loadHub, C as setSessionStatus, D as updateAction, F as reopenIssue, I as setFindingArchived, K as WriteError, L as setFindingStatus, M as createFinding, N as createIssue, O as sessionBriefing, P as planIssue, R as setIssueArchived, T as startSession, U as MAINLINE_FILE, W as TASK_FILE, Y as findHubRoot, _ as addAction, a as createDecision, b as readSession, c as dropChecklistItem, d as setChecklistItem, f as setCriterion, g as updateTask, h as supersedeDecision, i as addRepo, j as closeIssue, l as initHub, n as acceptDecision, o as createMainline, p as setMainlineStatus, r as addHub, s as createTask, t as migrateHub, u as setArchived, x as setSessionArchived, z as HubError } from "./dist-_ArTiNbL.js";
3
+ import { c as listProgress, r as launchUi, s as CLI_VERSION } from "./launch-D2WJRvPD.js";
4
4
  import { COMMAND_SPECS, OPTIONS } from "./facts.js";
5
5
  import { runHook } from "./hooks.js";
6
6
  import { readFileSync, realpathSync } from "node:fs";
@@ -249,7 +249,7 @@ function run(argv, cwd) {
249
249
  return fail(`${error.message}\n\n${USAGE}`);
250
250
  }
251
251
  const { values: opts, positionals } = parsed;
252
- if (opts.version) return done(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
252
+ if (opts.version) return done(CLI_VERSION);
253
253
  if (opts.help || positionals.length === 0) {
254
254
  console.log(USAGE);
255
255
  return opts.help ? 0 : 1;
@@ -1042,11 +1042,17 @@ function splitActionRef(ref) {
1042
1042
  action
1043
1043
  };
1044
1044
  }
1045
- /** Every served checklist item must exist, so a session never points at work that is not in a mainline. */
1045
+ /**
1046
+ * Every served checklist item must exist, so a session never points at work that is not in a mainline. Its
1047
+ * mainline must still be open: follow-up work on an ended one either reopens it with a criterion or task for the
1048
+ * follow-up, or starts a new mainline, so the follow-up is tracked instead of hanging off finished work.
1049
+ */
1046
1050
  function requireServes(root, serves) {
1047
1051
  for (const ref of serves) {
1048
1052
  const parsed = parseRef(ref);
1049
1053
  if (!CHECKLIST_REF.test(ref) || parsed?.kind !== "checklistItem") throw new WriteError(`--serves takes checklist references like m001/t002#c003, got "${ref}"`);
1054
+ const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
1055
+ if (CLOSED_STATUS.mainline.includes(mainline.status)) throw new WriteError(`${parsed.mainline} is ${mainline.status}; to continue it, run "mainline set ${parsed.mainline} --status active" and add a criterion or task for the follow-up, or start a new mainline with "new mainline"`);
1050
1056
  if (!readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data.checklist.some((c) => c.id === parsed.item)) throw new WriteError(`checklist item ${ref} not found`);
1051
1057
  }
1052
1058
  }
@@ -1126,6 +1132,31 @@ function setSessionStatus(root, id, status) {
1126
1132
  s.status = value;
1127
1133
  });
1128
1134
  }
1135
+ /**
1136
+ * Settle a session when its turn ends. A session that tracked no action, and whose subagents tracked none, leaves
1137
+ * no record, since nothing in it points at the hub's work. An active session whose actions are all done or dropped
1138
+ * and whose subagents have all stopped ends. The next prompt or action brings it back.
1139
+ */
1140
+ function settleSession(root, id) {
1141
+ return withLock(root, () => {
1142
+ const session = readSession(root, id);
1143
+ if (!session) return "kept";
1144
+ const { sessions } = loadSessions(root);
1145
+ const subagents = (parent) => sessions.filter((s) => s.delegatedFrom === parent).flatMap((s) => [s, ...subagents(s.id)]);
1146
+ const family = [session, ...subagents(id)];
1147
+ if (family.every((s) => s.actions.length === 0)) {
1148
+ for (const s of family) rmSync(sessionFile(root, s.id), { force: true });
1149
+ return "removed";
1150
+ }
1151
+ const finished = session.actions.length > 0 && session.actions.every((a) => a.status === "done" || a.status === "dropped");
1152
+ if (session.status !== "active" || !finished || family.slice(1).some((s) => s.status === "active")) return "kept";
1153
+ writeAtomic(sessionFile(root, id), toYaml(validate(sessionSchema, {
1154
+ ...session,
1155
+ status: "ended"
1156
+ }, sessionFile(root, id))));
1157
+ return "ended";
1158
+ });
1159
+ }
1129
1160
  /** Archive or unarchive a session; the status stays as it is, so an ended session is visible until archived. */
1130
1161
  function setSessionArchived(root, id, archived) {
1131
1162
  return mutateYaml(root, requireSession(root, id), sessionSchema, (s) => {
@@ -1153,6 +1184,7 @@ function addAction(root, sessionId, input) {
1153
1184
  status: "todo",
1154
1185
  serves: input.serves
1155
1186
  }, input.before);
1187
+ if (s.status === "ended") s.status = "active";
1156
1188
  });
1157
1189
  return `${sessionId}#${created}`;
1158
1190
  }
@@ -1292,6 +1324,15 @@ function setMainlineStatus(root, id, status, expectedVersion) {
1292
1324
  stampClosed(m, closes("mainline", next), today());
1293
1325
  }, expectedVersion);
1294
1326
  }
1327
+ /** Rename a mainline. References use its ID, so they keep resolving and show the new title. */
1328
+ function setMainlineTitle(root, id, title, expectedVersion) {
1329
+ requireRef(id, "mainline");
1330
+ const next = title.replace(/\s+/g, " ").trim();
1331
+ if (!next) throw new WriteError(`${id} needs a title, e.g. "mainflow 改發布到 npm 公開套件"`);
1332
+ return mutateFrontMatter(root, mainlineFile(root, id), mainlineSchema, (m) => {
1333
+ m.title = next;
1334
+ }, expectedVersion);
1335
+ }
1295
1336
  function setArchived(root, id, archived, expectedVersion) {
1296
1337
  requireRef(id, "mainline");
1297
1338
  return mutateFrontMatter(root, mainlineFile(root, id), mainlineSchema, (m) => {
@@ -1612,4 +1653,4 @@ function migrateHub(root, options = {}) {
1612
1653
  });
1613
1654
  }
1614
1655
  //#endregion
1615
- export { createFinding as A, refIndex as B, startSession as C, turnReminder as D, sessionBriefing as E, setFindingStatus as F, DATA_DIR as G, TASK_FILE as H, setIssueArchived as I, worktreesOf as J, currentBranch as K, HubError as L, planIssue as M, reopenIssue as N, appendOps as O, setFindingArchived as P, loadHub as R, setSessionStatus as S, updateAction as T, ConflictError as U, MAINLINE_FILE as V, WriteError as W, ensureSession as _, createDecision as a, setSessionArchived as b, dropChecklistItem as c, setChecklistItem as d, setCriterion as f, addAction as g, updateTask as h, addRepo as i, createIssue as j, closeIssue as k, initHub as l, supersedeDecision as m, acceptDecision as n, createMainline as o, setMainlineStatus as p, findHubRoot as q, addHub as r, createTask as s, migrateHub as t, setArchived as u, loadSessions as v, transcriptPath as w, setSessionInfo as x, readSession as y, linksInto as z };
1656
+ export { appendOps as A, loadHub as B, setSessionStatus as C, updateAction as D, transcriptPath as E, reopenIssue as F, ConflictError as G, refIndex as H, setFindingArchived as I, currentBranch as J, WriteError as K, setFindingStatus as L, createFinding as M, createIssue as N, sessionBriefing as O, planIssue as P, setIssueArchived as R, setSessionInfo as S, startSession as T, MAINLINE_FILE as U, linksInto as V, TASK_FILE as W, worktreesOf as X, findHubRoot as Y, addAction as _, createDecision as a, readSession as b, dropChecklistItem as c, setChecklistItem as d, setCriterion as f, updateTask as g, supersedeDecision as h, addRepo as i, closeIssue as j, turnReminder as k, initHub as l, setMainlineTitle as m, acceptDecision as n, createMainline as o, setMainlineStatus as p, DATA_DIR as q, addHub as r, createTask as s, migrateHub as t, setArchived as u, ensureSession as v, settleSession as w, setSessionArchived as x, loadSessions as y, HubError as z };
package/dist/hooks.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as turnReminder, E as sessionBriefing, G as DATA_DIR, J as worktreesOf, K as currentBranch, L as HubError, R as loadHub, S as setSessionStatus, _ as ensureSession, q as findHubRoot, x as setSessionInfo, y as readSession } from "./dist-BktkzRuK.js";
1
+ import { B as loadHub, C as setSessionStatus, J as currentBranch, O as sessionBriefing, S as setSessionInfo, X as worktreesOf, Y as findHubRoot, b as readSession, k as turnReminder, q as DATA_DIR, v as ensureSession, w as settleSession, z as HubError } from "./dist-_ArTiNbL.js";
2
2
  import { appendFileSync, existsSync, readFileSync } from "node:fs";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  //#region src/hooks.ts
@@ -26,22 +26,36 @@ const claudeCodeSessionId = (raw) => `claude-code-${shortId(raw)}`;
26
26
  * One handler per Claude Code event; each returns what to print for the model, or null. SessionStart and
27
27
  * UserPromptSubmit take plain text; PostToolUse takes the hook JSON, since its plain stdout never reaches the model.
28
28
  */
29
+ /**
30
+ * Record the main session: create it when it is missing, since a turn that tracked nothing left no record, and
31
+ * bring an ended or archived one back, since the viewer is working in it again. Its environment facts are
32
+ * written when it is created and whenever `refresh` asks, such as on SessionStart after a resume.
33
+ */
34
+ function recordSession(root, payload, id, refresh) {
35
+ const cwd = payload.cwd ?? root;
36
+ const { session, created } = ensureSession(root, {
37
+ id,
38
+ agent: "claude-code",
39
+ cwd
40
+ });
41
+ if (!created && !refresh) return session;
42
+ return setSessionInfo(root, id, {
43
+ transcript: payload.transcript_path,
44
+ worktree: worktreesOf(cwd)?.current,
45
+ branch: currentBranch(cwd) ?? void 0,
46
+ effort: payload.effort?.level
47
+ });
48
+ }
49
+ /** Settle the main session by the rules settleSession states; prints nothing. */
50
+ const settleOnEnd = (root, { payload }) => {
51
+ if (payload.session_id) settleSession(root, claudeCodeSessionId(payload.session_id));
52
+ return null;
53
+ };
29
54
  const CLAUDE_CODE_EVENTS = {
30
55
  "session-start": (root, { payload, envFile, cli }) => {
31
56
  if (!payload.session_id) return null;
32
57
  const id = claudeCodeSessionId(payload.session_id);
33
- const cwd = payload.cwd ?? root;
34
- ensureSession(root, {
35
- id,
36
- agent: "claude-code",
37
- cwd
38
- });
39
- const session = setSessionInfo(root, id, {
40
- transcript: payload.transcript_path,
41
- worktree: worktreesOf(cwd)?.current,
42
- branch: currentBranch(cwd) ?? void 0,
43
- effort: payload.effort?.level
44
- });
58
+ const session = recordSession(root, payload, id, true);
45
59
  if (envFile) appendFileSync(envFile, `export MAINFLOW_SESSION=${id}\n`);
46
60
  return sessionBriefing(root, session, {
47
61
  cli,
@@ -49,9 +63,11 @@ const CLAUDE_CODE_EVENTS = {
49
63
  });
50
64
  },
51
65
  "user-prompt-submit": (root, { payload, cli }) => {
52
- const session = payload.session_id ? readSession(root, claudeCodeSessionId(payload.session_id)) : null;
53
- return session ? turnReminder(root, session, { cli }) : null;
66
+ if (!payload.session_id) return null;
67
+ return turnReminder(root, recordSession(root, payload, claudeCodeSessionId(payload.session_id), false), { cli });
54
68
  },
69
+ stop: settleOnEnd,
70
+ "session-end": settleOnEnd,
55
71
  "subagent-start": (root, { payload }) => {
56
72
  if (!payload.agent_id) return null;
57
73
  const parent = payload.session_id ? claudeCodeSessionId(payload.session_id) : null;
package/dist/host.js CHANGED
@@ -1,4 +1,4 @@
1
- import { G as DATA_DIR, W as WriteError } from "./dist-BktkzRuK.js";
1
+ import { K as WriteError, q as DATA_DIR } from "./dist-_ArTiNbL.js";
2
2
  import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
@@ -1,4 +1,4 @@
1
- import { B as refIndex, G as DATA_DIR, I as setIssueArchived, P as setFindingArchived, R as loadHub, U as ConflictError, W as WriteError, b as setSessionArchived, d as setChecklistItem, f as setCriterion, h as updateTask, p as setMainlineStatus, u as setArchived, v as loadSessions, w as transcriptPath, y as readSession, z as linksInto } from "./dist-BktkzRuK.js";
1
+ import { B as loadHub, E as transcriptPath, G as ConflictError, H as refIndex, I as setFindingArchived, K as WriteError, R as setIssueArchived, V as linksInto, b as readSession, d as setChecklistItem, f as setCriterion, g as updateTask, m as setMainlineTitle, p as setMainlineStatus, q as DATA_DIR, u as setArchived, x as setSessionArchived, y as loadSessions } from "./dist-_ArTiNbL.js";
2
2
  import { DEFAULT_UI_PORT, syncHost } from "./host.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, watch, writeFileSync } from "node:fs";
@@ -214,6 +214,10 @@ function listProgress(items) {
214
214
  };
215
215
  }
216
216
  //#endregion
217
+ //#region src/version.ts
218
+ /** This CLI's own version, from the package manifest one level above both src/ and dist/. */
219
+ const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
220
+ //#endregion
217
221
  //#region src/server.ts
218
222
  /** Every reference with a hub prefix in the documents of `hub`: links, origins and replaced items. */
219
223
  function prefixedRefs(hub) {
@@ -323,6 +327,7 @@ function createApp(root) {
323
327
  const hub = loadHub(root);
324
328
  const body = {
325
329
  hub: hub.config.hub,
330
+ version: CLI_VERSION,
326
331
  repos: hub.config.repos,
327
332
  mainlines: hub.mainlines.map(summarize),
328
333
  openIssues: hub.issues.filter((i) => i.status !== "closed").length,
@@ -415,6 +420,10 @@ function createApp(root) {
415
420
  const { archived, version } = await c.req.json();
416
421
  return c.json(setArchived(root, c.req.param("m"), archived, version));
417
422
  });
423
+ app.post("/api/mainlines/:m/title", async (c) => {
424
+ const { title, version } = await c.req.json();
425
+ return c.json(setMainlineTitle(root, c.req.param("m"), title, version));
426
+ });
418
427
  app.post("/api/mainlines/:m/status", async (c) => {
419
428
  const { status, version } = await c.req.json();
420
429
  return c.json(setMainlineStatus(root, c.req.param("m"), status, version));
@@ -476,19 +485,24 @@ function serveUi(root, port) {
476
485
  //#region src/launch.ts
477
486
  /** How many ports above DEFAULT_UI_PORT are tried before giving up. */
478
487
  const PORT_SPAN = 100;
479
- /** Where the last port used is remembered: machine-specific, so under the unversioned local/ folder. */
480
- const lastPortFile = (root) => join(root, DATA_DIR, "local", "ui.json");
481
- function readLastPort(root) {
488
+ /** Where the last server started is remembered: machine-specific, so under the unversioned local/ folder. */
489
+ const uiRecordFile = (root) => join(root, DATA_DIR, "local", "ui.json");
490
+ function readUiRecord(root) {
482
491
  try {
483
- const port = JSON.parse(readFileSync(lastPortFile(root), "utf8")).port;
484
- return Number.isInteger(port) ? port : void 0;
492
+ const record = JSON.parse(readFileSync(uiRecordFile(root), "utf8"));
493
+ if (!Number.isInteger(record.port)) return void 0;
494
+ return {
495
+ port: record.port,
496
+ ...Number.isInteger(record.pid) && { pid: record.pid },
497
+ ...typeof record.version === "string" && { version: record.version }
498
+ };
485
499
  } catch {
486
500
  return;
487
501
  }
488
502
  }
489
- function writeLastPort(root, port) {
503
+ function writeUiRecord(root, record) {
490
504
  mkdirSync(join(root, DATA_DIR, "local"), { recursive: true });
491
- writeFileSync(lastPortFile(root), `${JSON.stringify({ port })}\n`);
505
+ writeFileSync(uiRecordFile(root), `${JSON.stringify(record)}\n`);
492
506
  }
493
507
  /**
494
508
  * Ports to try, in order. An explicit --port is the only candidate: the user asked for that port. Otherwise
@@ -503,13 +517,18 @@ function portCandidates(options) {
503
517
  ...range
504
518
  ].filter((p) => p !== void 0))];
505
519
  }
520
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
506
521
  const networkProbe = {
507
- async hubAt(port) {
522
+ async serverAt(port) {
508
523
  try {
509
524
  const res = await fetch(`http://localhost:${port}/api/hub`, { signal: AbortSignal.timeout(500) });
510
525
  if (!res.ok) return null;
511
526
  const body = await res.json();
512
- return typeof body.hub === "string" ? body.hub : null;
527
+ if (typeof body.hub !== "string") return null;
528
+ return {
529
+ hub: body.hub,
530
+ ...typeof body.version === "string" && { version: body.version }
531
+ };
513
532
  } catch {
514
533
  return null;
515
534
  }
@@ -517,7 +536,29 @@ const networkProbe = {
517
536
  isFree: (port) => new Promise((done) => {
518
537
  const probe = createServer().once("error", () => done(false));
519
538
  probe.listen(port, () => probe.close(() => done(true)));
520
- })
539
+ }),
540
+ listenerOf(port) {
541
+ const out = spawnSync("lsof", [
542
+ "-nP",
543
+ `-iTCP:${port}`,
544
+ "-sTCP:LISTEN",
545
+ "-t"
546
+ ], { encoding: "utf8" });
547
+ const pid = Number(out.stdout?.split("\n")[0]);
548
+ return out.status === 0 && Number.isInteger(pid) && pid > 0 ? pid : null;
549
+ },
550
+ async stop(pid, port) {
551
+ try {
552
+ process.kill(pid, "SIGTERM");
553
+ } catch {
554
+ return false;
555
+ }
556
+ for (let i = 0; i < 25; i++) {
557
+ await sleep(200);
558
+ if (await networkProbe.isFree(port)) return true;
559
+ }
560
+ return false;
561
+ }
521
562
  };
522
563
  /**
523
564
  * A candidate already serving this hub wins wherever it sits in the order, so a lost last-port record or a
@@ -525,15 +566,15 @@ const networkProbe = {
525
566
  * probe timeout. Otherwise the first free candidate; ports held by another hub or program are skipped.
526
567
  */
527
568
  async function choosePort(hub, candidates, probe = networkProbe) {
528
- const owners = await Promise.all(candidates.map((port) => probe.hubAt(port)));
529
- const running = candidates.find((_, i) => owners[i] === hub);
530
- if (running !== void 0) return {
531
- port: running,
532
- running: true
569
+ const servers = await Promise.all(candidates.map((port) => probe.serverAt(port)));
570
+ const at = candidates.findIndex((_, i) => servers[i]?.hub === hub);
571
+ if (at >= 0) return {
572
+ port: candidates[at],
573
+ running: servers[at].version ?? ""
533
574
  };
534
- for (const [i, port] of candidates.entries()) if (owners[i] === null && await probe.isFree(port)) return {
575
+ for (const [i, port] of candidates.entries()) if (servers[i] === null && await probe.isFree(port)) return {
535
576
  port,
536
- running: false
577
+ running: void 0
537
578
  };
538
579
  const tried = candidates.length === 1 ? `port ${candidates[0]} is` : `ports ${candidates[0]} to ${candidates.at(-1)} are`;
539
580
  throw new WriteError(`${tried} taken by other programs; free one, or pass --port <port>`);
@@ -560,20 +601,35 @@ async function launchUi(root, hub, configured, options) {
560
601
  explicit = Number(options.port);
561
602
  if (!Number.isInteger(explicit) || explicit < 1024 || explicit > 65535) throw new WriteError("--port must be an integer from 1024 to 65535, e.g. --port 4320");
562
603
  }
604
+ const probe = options.probe ?? networkProbe;
605
+ const record = readUiRecord(root);
563
606
  const choice = await choosePort(hub, portCandidates({
564
607
  explicit,
565
608
  configured,
566
- last: readLastPort(root)
567
- }), options.probe);
609
+ last: record?.port
610
+ }), probe);
611
+ let reuse = choice.running !== void 0;
612
+ if (reuse && choice.running !== CLI_VERSION) {
613
+ const from = choice.running || "unknown";
614
+ const pid = record?.port === choice.port && record.pid !== void 0 ? record.pid : probe.listenerOf(choice.port);
615
+ if (pid !== null && await probe.stop(pid, choice.port)) {
616
+ log(`! restarted: mainflow ${from} → ${CLI_VERSION}`);
617
+ reuse = false;
618
+ } else log(`! localhost:${choice.port} still runs mainflow ${from}, not ${CLI_VERSION}; stop it with \`kill $(lsof -ti tcp:${choice.port})\` and run mainflow ui again`);
619
+ }
568
620
  let server;
569
- if (!choice.running) {
621
+ if (!reuse) {
570
622
  server = await serveUi(root, choice.port);
571
- writeLastPort(root, choice.port);
623
+ writeUiRecord(root, {
624
+ port: choice.port,
625
+ pid: process.pid,
626
+ version: CLI_VERSION
627
+ });
572
628
  }
573
629
  const host = await syncHost(root, hub, choice.port, options.host);
574
630
  if (host.url === null) log(`! host name not available: ${host.reason}`);
575
631
  const url = host.url ?? `http://localhost:${choice.port}`;
576
- log(`${choice.running ? "already running" : "mainflow ui"}: ${url} (localhost:${choice.port})`);
632
+ log(`${reuse ? "already running" : "mainflow ui"}: ${url} (localhost:${choice.port})`);
577
633
  if (options.open) openUrl(url);
578
634
  return {
579
635
  url,
@@ -581,4 +637,4 @@ async function launchUi(root, hub, configured, options) {
581
637
  };
582
638
  }
583
639
  //#endregion
584
- export { portCandidates as a, networkProbe as i, choosePort as n, readLastPort as o, launchUi as r, listProgress as s, PORT_SPAN as t };
640
+ export { portCandidates as a, listProgress as c, networkProbe as i, choosePort as n, readUiRecord as o, launchUi as r, CLI_VERSION as s, PORT_SPAN as t };
package/dist/launch.d.ts CHANGED
@@ -3,7 +3,13 @@ import { ServerType } from "@hono/node-server";
3
3
  //#region src/launch.d.ts
4
4
  /** How many ports above DEFAULT_UI_PORT are tried before giving up. */
5
5
  export declare const PORT_SPAN = 100;
6
- export declare function readLastPort(root: string): number | undefined;
6
+ /** The server this hub last started: its port, and the process and version when it recorded them. */
7
+ export interface UiRecord {
8
+ port: number;
9
+ pid?: number;
10
+ version?: string;
11
+ }
12
+ export declare function readUiRecord(root: string): UiRecord | undefined;
7
13
  /**
8
14
  * Ports to try, in order. An explicit --port is the only candidate: the user asked for that port. Otherwise
9
15
  * the hub's configured port, then the last one used, then the default range.
@@ -13,15 +19,25 @@ export declare function portCandidates(options: {
13
19
  configured?: number;
14
20
  last?: number;
15
21
  }): number[];
22
+ /** What a mainflow interface on a port reports; servers older than 0.8.1 report no version. */
23
+ export interface ServerInfo {
24
+ hub: string;
25
+ version?: string;
26
+ }
16
27
  export interface PortProbe {
17
- /** The hub name a mainflow interface on this port reports, or null when nothing mainflow answers. */
18
- hubAt: (port: number) => Promise<string | null>;
28
+ /** The mainflow interface on this port, or null when nothing mainflow answers. */
29
+ serverAt: (port: number) => Promise<ServerInfo | null>;
19
30
  isFree: (port: number) => Promise<boolean>;
31
+ /** The process listening on this port, or null when it cannot be found. */
32
+ listenerOf: (port: number) => number | null;
33
+ /** Stop a process and wait until the port is free; false when it did not free up. */
34
+ stop: (pid: number, port: number) => Promise<boolean>;
20
35
  }
21
36
  export declare const networkProbe: PortProbe;
37
+ /** A port to serve on; `running` is the version already serving this hub there, '' when it reports none. */
22
38
  export type PortChoice = {
23
39
  port: number;
24
- running: boolean;
40
+ running?: string;
25
41
  };
26
42
  /**
27
43
  * A candidate already serving this hub wins wherever it sits in the order, so a lost last-port record or a
package/dist/launch.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as portCandidates, i as networkProbe, n as choosePort, o as readLastPort, r as launchUi, t as PORT_SPAN } from "./launch-BR70uSMc.js";
2
- export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readLastPort };
1
+ import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, t as PORT_SPAN } from "./launch-D2WJRvPD.js";
2
+ export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord };