mainflow 0.28.0 → 0.29.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.
@@ -1,46 +0,0 @@
1
- import { z } from "zod";
2
- //#region ../schema/dist/index.d.ts
3
- declare const FORMAT_VERSION = 2;
4
- declare const ID_PATTERNS: {
5
- readonly mainline: RegExp;
6
- readonly task: RegExp;
7
- readonly checklistItem: RegExp;
8
- readonly criterion: RegExp;
9
- readonly decision: RegExp;
10
- readonly action: RegExp;
11
- readonly issue: RegExp;
12
- readonly finding: RegExp;
13
- readonly grant: RegExp;
14
- };
15
- declare const CHECKLIST_REF: RegExp;
16
- /** What an action may serve: a checklist item, or a mainline's criterion while the mainline has no task for it yet. */
17
- declare const SERVES_REF: RegExp;
18
- declare const SLUG: RegExp;
19
- /** One source the survey checks: the line's label, what it holds, and how it may be left unchecked. */
20
- interface SurveyLine {
21
- label: string;
22
- holds: string;
23
- /** The word that marks the source as not checked, followed by the reason, e.g. 不適用:{理由}. Absent when the source is always checked. */
24
- skip?: string;
25
- /** Text a checked line must contain, e.g. the environment a spike ran in. */
26
- requires?: string;
27
- }
28
- /**
29
- * A condition a program can check, written `{kind}:{argument}`. `commit` holds once a commit message on the
30
- * branch contains the text, `action-done` once the action is done, `file` once the path exists under the hub
31
- * root. Git branch names cannot contain a colon, so the text after the branch may.
32
- */
33
- type Until = {
34
- kind: 'commit';
35
- branch: string;
36
- text: string;
37
- } | {
38
- kind: 'action-done';
39
- session: string;
40
- action: string;
41
- } | {
42
- kind: 'file';
43
- path: string;
44
- };
45
- //#endregion
46
- export { SLUG as a, SERVES_REF as i, FORMAT_VERSION as n, SurveyLine as o, ID_PATTERNS as r, Until as s, CHECKLIST_REF as t };
@@ -1,501 +0,0 @@
1
- import { B as setSessionArchived, C as setCriterion, E as setMainlineTitle, G as transcriptPath, L as loadSessions, R as readSession, S as setChecklistItem, T as setMainlineStatus, _t as WriteError, a as openInApp, at as setFindingArchived, d as ARCHIVE_KINDS, dt as mainlineEdges, f as setArchivedMany, ft as linksInto, gt as ConflictError, k as updateTask, lt as loadHub, pt as refIndex, st as setIssueArchived, ut as dependencyGraph, x as setArchived, yt as DATA_DIR } from "./host-CeN3dvXg.js";
2
- import { t as listProgress } from "./progress-1BfPg2zN.js";
3
- import { l as CLI_VERSION } from "./launch-BKKoCyTj.js";
4
- import { createRequire } from "node:module";
5
- import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
6
- import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
7
- import { fileURLToPath } from "node:url";
8
- import { spawnSync } from "node:child_process";
9
- import { serve } from "@hono/node-server";
10
- import { homedir } from "node:os";
11
- import { Hono } from "hono";
12
- import { streamSSE } from "hono/streaming";
13
- //#region ../core/dist/trace.js
14
- const HOOK_BLOCK = /^(\w+:\w+) hook error:\s*(.*)$/s;
15
- const SCRIPT_IN_COMMAND = /(\S+\.(?:mjs|cjs|js|sh|py|ts))\b/;
16
- /**
17
- * Rules live under a rules/ directory. Everything else the instructions record loads at session start is
18
- * an instruction, including files CLAUDE.md pulls in with @imports. A nested memory outside rules/ is an
19
- * instruction only when it is a CLAUDE.md or AGENTS.md of a subdirectory.
20
- */
21
- const isRule = (path) => /\/rules\//.test(path);
22
- const startupKind = (path) => isRule(path) ? "rule" : "instruction";
23
- const nestedKind = (path) => isRule(path) ? "rule" : /(^|\/)(CLAUDE|AGENTS)(\.local)?\.md$/.test(path) ? "instruction" : "file";
24
- /** Where an installed skill or agent definition sits: the project's .claude first, then the user's. */
25
- function installed(kind, name, cwd, home) {
26
- const leaf = kind === "skills" ? join(name, "SKILL.md") : `${name}.md`;
27
- return [...cwd ? [join(cwd, ".claude", kind, leaf)] : [], join(home, ".claude", kind, leaf)].find((p) => existsSync(p));
28
- }
29
- function traceTranscript(file, options) {
30
- const entries = [];
31
- let model;
32
- if (!existsSync(file)) return { entries };
33
- for (const line of readFileSync(file, "utf8").split("\n")) {
34
- if (!line.trim()) continue;
35
- let record;
36
- try {
37
- record = JSON.parse(line);
38
- } catch {
39
- continue;
40
- }
41
- const at = record.timestamp;
42
- if (!at) continue;
43
- const a = record.attachment;
44
- if (a?.type === "model" && a.identity?.modelId) model = a.identity.modelId;
45
- if (a?.type === "nested_memory" && a.path) entries.push({
46
- at,
47
- kind: nestedKind(a.path),
48
- name: basename(a.path),
49
- path: a.path
50
- });
51
- if (a?.type === "instructions") {
52
- for (const f of a.files ?? []) if (f.path) entries.push({
53
- at,
54
- kind: startupKind(f.path),
55
- name: basename(f.path),
56
- path: f.path
57
- });
58
- }
59
- if ((a?.type === "hook_success" || a?.type === "hook_additional_context") && a.hookName) {
60
- const script = a.command?.match(SCRIPT_IN_COMMAND)?.[1]?.replace("$HOME", options.home);
61
- entries.push({
62
- at,
63
- kind: "hook",
64
- name: a.hookName,
65
- ...script ? { path: script } : {},
66
- ...a.exitCode !== void 0 ? { detail: `exit ${a.exitCode}` } : {}
67
- });
68
- }
69
- const content = Array.isArray(record.message?.content) ? record.message.content : [];
70
- if (record.type === "assistant" && record.message?.model && record.message.model !== "<synthetic>") model = record.message.model;
71
- for (const block of content) {
72
- if (block.type === "tool_use") {
73
- const input = block.input ?? {};
74
- if (block.name === "Skill" && typeof input.skill === "string") {
75
- const path = installed("skills", input.skill, record.cwd, options.home);
76
- entries.push({
77
- at,
78
- kind: "skill",
79
- name: input.skill,
80
- ...path ? { path } : {}
81
- });
82
- }
83
- if ((block.name === "Agent" || block.name === "Task") && typeof input.subagent_type === "string") {
84
- const path = installed("agents", input.subagent_type, record.cwd, options.home);
85
- entries.push({
86
- at,
87
- kind: "agent",
88
- name: input.subagent_type,
89
- ...path ? { path } : {}
90
- });
91
- }
92
- if (block.name === "Read" && typeof input.file_path === "string") entries.push({
93
- at,
94
- kind: "file",
95
- name: basename(input.file_path),
96
- path: input.file_path
97
- });
98
- }
99
- if (block.type === "tool_result" && block.is_error) {
100
- const blocked = (typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "")).match(HOOK_BLOCK);
101
- if (blocked) entries.push({
102
- at,
103
- kind: "hook",
104
- name: blocked[1],
105
- detail: `blocked: ${blocked[2].slice(0, 120)}`
106
- });
107
- }
108
- }
109
- }
110
- return {
111
- ...model ? { model } : {},
112
- entries
113
- };
114
- }
115
- /** Titles are appended again as the conversation goes on, so the end of the file almost always holds the latest. */
116
- const TITLE_TAIL_BYTES = 262144;
117
- function readTail(file, bytes) {
118
- const size = statSync(file).size;
119
- if (size <= bytes) return {
120
- text: readFileSync(file, "utf8"),
121
- whole: true
122
- };
123
- const fd = openSync(file, "r");
124
- try {
125
- const buffer = Buffer.alloc(bytes);
126
- readSync(fd, buffer, 0, bytes, size - bytes);
127
- return {
128
- text: buffer.subarray(buffer.indexOf(10) + 1).toString("utf8"),
129
- whole: false
130
- };
131
- } finally {
132
- closeSync(fd);
133
- }
134
- }
135
- function latestTitle(text) {
136
- let custom;
137
- let agent;
138
- let ai;
139
- for (const line of text.split("\n")) {
140
- if (!line.includes("\"custom-title\"") && !line.includes("\"agent-name\"") && !line.includes("\"ai-title\"")) continue;
141
- let record;
142
- try {
143
- record = JSON.parse(line);
144
- } catch {
145
- continue;
146
- }
147
- if (record.type === "custom-title" && record.customTitle?.trim()) custom = record.customTitle.trim();
148
- if (record.type === "agent-name" && record.agentName?.trim()) agent = record.agentName.trim();
149
- if (record.type === "ai-title" && record.aiTitle?.trim()) ai = record.aiTitle.trim();
150
- }
151
- return custom ?? agent ?? ai;
152
- }
153
- /**
154
- * The name the conversation goes by: the latest title the user gave it, else the latest name the platform
155
- * gave the session, else the latest title the platform generated from the conversation. Only the end of the file is read unless it holds no title.
156
- */
157
- function transcriptTitle(file) {
158
- if (!existsSync(file)) return void 0;
159
- const tail = readTail(file, TITLE_TAIL_BYTES);
160
- return latestTitle(tail.text) ?? (tail.whole ? void 0 : latestTitle(readFileSync(file, "utf8")));
161
- }
162
- /** Action stamps are local time to the minute; the window closes at the end of the finishing minute. */
163
- const localMinute = (stamp) => {
164
- const [date = "", time = ""] = stamp.split("T");
165
- const [y, m, d] = date.split("-").map(Number);
166
- const [hh, mm] = time.split(":").map(Number);
167
- return new Date(y, m - 1, d, hh, mm).getTime();
168
- };
169
- function summarize$1(entries) {
170
- const uses = /* @__PURE__ */ new Map();
171
- for (const e of entries) {
172
- const key = `${e.kind}\u0000${e.path ?? e.name}`;
173
- const use = uses.get(key);
174
- if (use) use.count += 1;
175
- else uses.set(key, {
176
- kind: e.kind,
177
- name: e.name,
178
- ...e.path ? { path: e.path } : {},
179
- count: 1,
180
- first: e.at,
181
- ...e.detail ? { detail: e.detail } : {}
182
- });
183
- }
184
- return [...uses.values()];
185
- }
186
- /**
187
- * Group trace entries by the action that was doing when they happened. Actions without a start stamp
188
- * get nothing; entries outside every window are returned as unattributed.
189
- */
190
- function attributeTrace(trace, actions, now = Date.now()) {
191
- const windows = actions.filter((a) => a.startedAt).map((a) => ({
192
- id: a.id,
193
- from: localMinute(a.startedAt),
194
- to: a.doneAt ? localMinute(a.doneAt) + 6e4 : now
195
- }));
196
- const grouped = {};
197
- const rest = [];
198
- for (const entry of trace.entries) {
199
- const t = Date.parse(entry.at);
200
- const owner = windows.filter((w) => t >= w.from && t < w.to).sort((x, y) => y.from - x.from)[0];
201
- if (owner) (grouped[owner.id] ??= []).push(entry);
202
- else rest.push(entry);
203
- }
204
- return {
205
- byAction: Object.fromEntries(Object.entries(grouped).map(([id, list]) => [id, summarize$1(list)])),
206
- unattributed: summarize$1(rest)
207
- };
208
- }
209
- //#endregion
210
- //#region src/server.ts
211
- /** Every reference with a hub prefix in the documents of `hub`: links, origins and replaced items. */
212
- function prefixedRefs(hub) {
213
- const refs = [];
214
- const doc = (d) => refs.push(d.origin, ...(d.links ?? []).map((l) => l.to));
215
- for (const m of hub.mainlines) {
216
- doc(m);
217
- refs.push(...m.criteria.map((c) => c.supersededBy));
218
- for (const d of m.decisions) doc(d);
219
- for (const t of m.tasks) {
220
- doc(t);
221
- refs.push(...t.checklist.map((c) => c.supersededBy));
222
- }
223
- }
224
- for (const d of [...hub.issues, ...hub.findings]) doc(d);
225
- return [...new Set(refs.filter((r) => !!r && /^[a-z0-9-]+:/.test(r)))];
226
- }
227
- function relationsView(hub) {
228
- const own = refIndex(hub);
229
- const indexes = new Map(hub.remotes.flatMap((r) => r.hub ? [[r.name, refIndex(r.hub)]] : []));
230
- const targets = {};
231
- const graph = dependencyGraph(hub);
232
- const depRefs = graph.edges.flatMap((e) => [e.from, e.to]);
233
- for (const ref of /* @__PURE__ */ new Set([...prefixedRefs(hub), ...depRefs.filter((r) => r.includes(":"))])) {
234
- const colon = ref.indexOf(":");
235
- const name = ref.slice(0, colon);
236
- const found = name === hub.config.hub ? own.get(ref.slice(colon + 1)) : indexes.get(name)?.get(ref.slice(colon + 1));
237
- if (found) targets[ref] = found;
238
- }
239
- const backlinks = {};
240
- const add = (to, link) => (backlinks[to] ??= []).push(link);
241
- for (const { to, link } of linksInto(hub, void 0)) add(to, {
242
- ...link,
243
- ...pick(own.get(link.from))
244
- });
245
- for (const remote of hub.remotes) {
246
- if (!remote.hub) continue;
247
- const index = indexes.get(remote.name);
248
- for (const { to, link } of linksInto(remote.hub, hub.config.hub)) add(to, {
249
- ...link,
250
- from: `${remote.name}:${link.from}`,
251
- ...pick(index?.get(link.from))
252
- });
253
- }
254
- return {
255
- hubs: hub.remotes.map((r) => ({
256
- name: r.name,
257
- path: r.path,
258
- available: !!r.hub,
259
- ...r.unavailable ? { reason: r.unavailable } : {}
260
- })),
261
- targets,
262
- backlinks,
263
- deps: graph.edges,
264
- mainlineDeps: mainlineEdges(graph),
265
- cycles: graph.cycles
266
- };
267
- }
268
- const pick = (t) => t ? {
269
- title: t.title,
270
- status: t.status
271
- } : {};
272
- const CONTENT_TYPES = {
273
- ".html": "text/html; charset=utf-8",
274
- ".md": "text/markdown; charset=utf-8",
275
- ".js": "text/javascript; charset=utf-8",
276
- ".mjs": "text/javascript; charset=utf-8",
277
- ".vue": "text/plain; charset=utf-8",
278
- ".css": "text/css; charset=utf-8",
279
- ".json": "application/json; charset=utf-8",
280
- ".svg": "image/svg+xml",
281
- ".png": "image/png",
282
- ".jpg": "image/jpeg",
283
- ".txt": "text/plain; charset=utf-8"
284
- };
285
- function summarize(m) {
286
- return {
287
- id: m.id,
288
- title: m.title,
289
- status: m.status,
290
- created: m.created,
291
- archivedAt: m.archivedAt ?? null,
292
- category: m.category ?? null,
293
- priority: m.priority ?? null,
294
- labels: m.labels ?? [],
295
- due: m.due ?? null,
296
- updated: [
297
- m,
298
- ...m.tasks,
299
- ...m.decisions
300
- ].map((d) => d.updated).reduce((a, b) => b > a ? b : a),
301
- tasks: {
302
- total: m.tasks.length,
303
- done: m.tasks.filter((t) => t.status === "done").length
304
- },
305
- items: listProgress(m.tasks.flatMap((t) => t.checklist)),
306
- criteria: listProgress(m.criteria),
307
- decisions: m.decisions.length
308
- };
309
- }
310
- /** HTTP API over one hub. The CLI `ui` command and the web dev server both mount it. */
311
- function createApp(root) {
312
- const app = new Hono();
313
- const mainlinesRoot = join(root, DATA_DIR, "mainlines");
314
- app.onError((error, c) => {
315
- const message = error.message.replaceAll(`${root}${sep}`, "");
316
- if (error instanceof ConflictError) return c.json({ error: message }, 409);
317
- if (error instanceof WriteError) return c.json({ error: message }, 400);
318
- throw error;
319
- });
320
- app.get("/api/hub", (c) => {
321
- const hub = loadHub(root);
322
- const body = {
323
- hub: hub.config.hub,
324
- version: CLI_VERSION,
325
- repos: hub.config.repos,
326
- mainlines: hub.mainlines.map(summarize),
327
- openIssues: hub.issues.filter((i) => i.status !== "closed").length,
328
- validFindings: hub.findings.filter((f) => f.status === "valid").length,
329
- problems: hub.problems
330
- };
331
- return c.json(body);
332
- });
333
- app.get("/api/relations", (c) => c.json(relationsView(loadHub(root))));
334
- const local = () => loadHub(root, { remotes: false });
335
- app.get("/api/issues", (c) => c.json(local().issues));
336
- app.get("/api/findings", (c) => c.json(local().findings));
337
- app.get("/api/ops", (c) => c.json(local().ops));
338
- app.post("/api/reveal", async (c) => {
339
- const { path } = await c.req.json();
340
- const known = loadSessions(root).sessions.some((s) => s.transcript === path);
341
- const file = transcriptPath(root, path);
342
- if (!known || !existsSync(file)) return c.json({ error: "only a transcript recorded on a session can be revealed" }, 400);
343
- const opened = spawnSync("open", ["-R", file]);
344
- return opened.status === 0 ? c.json({ revealed: file }) : c.json({ error: `open -R failed: ${opened.stderr}` }, 500);
345
- });
346
- app.get("/api/mainlines", (c) => {
347
- const ids = c.req.query("ids")?.split(",").filter(Boolean);
348
- const all = local().mainlines;
349
- return c.json(ids ? all.filter((m) => ids.includes(m.id)) : all.filter((m) => !m.archivedAt));
350
- });
351
- app.get("/api/mainlines/:m", (c) => {
352
- const mainline = local().mainlines.find((m) => m.id === c.req.param("m"));
353
- return mainline ? c.json(mainline) : c.json({ error: `mainline ${c.req.param("m")} not found` }, 404);
354
- });
355
- const titles = /* @__PURE__ */ new Map();
356
- const titleOf = (recorded) => {
357
- const transcript = recorded && transcriptPath(root, recorded);
358
- if (!transcript || !existsSync(transcript)) return void 0;
359
- const { size, mtimeMs } = statSync(transcript);
360
- const stamp = `${size}:${mtimeMs}`;
361
- const cached = titles.get(transcript);
362
- if (cached?.stamp === stamp) return cached.title;
363
- const title = transcriptTitle(transcript);
364
- titles.set(transcript, {
365
- stamp,
366
- title
367
- });
368
- return title;
369
- };
370
- app.get("/api/sessions", (c) => {
371
- const { sessions, problems } = loadSessions(root);
372
- const views = sessions.map((s) => {
373
- const title = titleOf(s.transcript);
374
- return title ? {
375
- ...s,
376
- title
377
- } : s;
378
- });
379
- return c.json({
380
- sessions: views,
381
- problems
382
- });
383
- });
384
- app.post("/api/archive", async (c) => {
385
- const { kind, ids, archived } = await c.req.json();
386
- if (!ARCHIVE_KINDS.includes(kind) || !Array.isArray(ids)) return c.json({ error: `kind must be one of ${ARCHIVE_KINDS.join(", ")} and ids a list` }, 400);
387
- return c.json(setArchivedMany(root, kind, ids, archived));
388
- });
389
- app.post("/api/sessions/:id/archive", async (c) => {
390
- const { archived } = await c.req.json();
391
- return c.json(setSessionArchived(root, c.req.param("id"), archived));
392
- });
393
- app.post("/api/issues/:id/archive", async (c) => {
394
- const { archived, version } = await c.req.json();
395
- return c.json(setIssueArchived(root, c.req.param("id"), archived, version));
396
- });
397
- app.post("/api/findings/:id/archive", async (c) => {
398
- const { archived, version } = await c.req.json();
399
- return c.json(setFindingArchived(root, c.req.param("id"), archived, version));
400
- });
401
- app.get("/api/sessions/:id/trace", (c) => {
402
- const session = readSession(root, c.req.param("id"));
403
- if (!session) return c.json({ error: `session ${c.req.param("id")} not found` }, 404);
404
- const transcript = session.transcript && transcriptPath(root, session.transcript);
405
- if (!transcript || !existsSync(transcript)) return c.json({
406
- available: false,
407
- byAction: {},
408
- unattributed: []
409
- });
410
- const trace = traceTranscript(transcript, { home: homedir() });
411
- return c.json({
412
- available: true,
413
- model: trace.model ?? session.model,
414
- ...attributeTrace(trace, session.actions)
415
- });
416
- });
417
- app.get("/api/files/*", (c) => {
418
- const path = normalize(decodeURIComponent(c.req.path.slice(11)));
419
- const file = join(root, path);
420
- if (!file.startsWith(mainlinesRoot + sep) || !existsSync(file) || !statSync(file).isFile()) return c.json({ error: `file ${path} not found under ${relative(root, mainlinesRoot)}` }, 404);
421
- return c.body(readFileSync(file), 200, { "content-type": CONTENT_TYPES[extname(file)] ?? "application/octet-stream" });
422
- });
423
- app.post("/api/mainlines/:m/archive", async (c) => {
424
- const { archived, version } = await c.req.json();
425
- return c.json(setArchived(root, c.req.param("m"), archived, version));
426
- });
427
- app.post("/api/mainlines/:m/title", async (c) => {
428
- const { title, version } = await c.req.json();
429
- return c.json(setMainlineTitle(root, c.req.param("m"), title, version));
430
- });
431
- app.post("/api/mainlines/:m/status", async (c) => {
432
- const { status, version } = await c.req.json();
433
- return c.json(setMainlineStatus(root, c.req.param("m"), status, version));
434
- });
435
- app.post("/api/tasks/:m/:t/status", async (c) => {
436
- const { status, version } = await c.req.json();
437
- return c.json(updateTask(root, `${c.req.param("m")}/${c.req.param("t")}`, { status }, version));
438
- });
439
- app.post("/api/check", async (c) => {
440
- const { ref, done, evidence, version } = await c.req.json();
441
- const result = ref.includes("/") ? setChecklistItem(root, ref, done, evidence, version) : setCriterion(root, ref, done, evidence, version);
442
- return c.json(result);
443
- });
444
- app.get("/api/events", (c) => streamSSE(c, async (stream) => {
445
- const watcher = watch(join(root, DATA_DIR), { recursive: true }, (_event, name) => {
446
- if (name && !String(name).endsWith("write.lock") && !String(name).endsWith(".tmp")) stream.writeSSE({
447
- event: "change",
448
- data: String(name)
449
- });
450
- });
451
- stream.onAbort(() => watcher.close());
452
- while (!stream.aborted) await stream.sleep(3e4);
453
- }));
454
- return app;
455
- }
456
- //#endregion
457
- //#region src/ui.ts
458
- /**
459
- * Where the built web client lives: `web/` beside the bundled CLI in the published package, or the
460
- * workspace's web package when running from source before a bundle exists.
461
- */
462
- function webDir() {
463
- const bundled = join(dirname(fileURLToPath(import.meta.url)), "web");
464
- if (existsSync(join(bundled, "index.html"))) return bundled;
465
- return join(dirname(createRequire(import.meta.url).resolve("@carlos-space/mainflow-web/package.json")), "dist");
466
- }
467
- /**
468
- * Serve the API plus the built web client; unknown paths fall back to index.html for client-side routing.
469
- * Resolves once the port is listening. A port taken between the free-port probe and here rejects with a
470
- * WriteError instead of Node's unhandled 'error' event and stack trace.
471
- */
472
- function serveUi(root, port, open = openInApp) {
473
- const app = createApp(root);
474
- const dist = webDir();
475
- const hub = loadHub(root, { remotes: false }).config.hub;
476
- app.get("/open/*", (c) => {
477
- const url = new URL(c.req.url);
478
- const target = `${url.pathname.slice(5) || "/"}${url.search}`;
479
- const opened = open(hub, target);
480
- return c.html(openPage(target, opened));
481
- });
482
- app.get("*", (c) => {
483
- let file = join(dist, normalize(decodeURIComponent(c.req.path)));
484
- if (!file.startsWith(dist + sep) || !existsSync(file) || !statSync(file).isFile()) file = join(dist, "index.html");
485
- return c.body(readFileSync(file), 200, { "content-type": CONTENT_TYPES[extname(file)] ?? "application/octet-stream" });
486
- });
487
- return new Promise((done, fail) => {
488
- const server = serve({
489
- fetch: app.fetch,
490
- port
491
- }, () => done(server));
492
- server.once("error", (error) => fail(error.code === "EADDRINUSE" ? new WriteError(`port ${port} is already in use; run mainflow ui again to pick another`) : error));
493
- });
494
- }
495
- /** The page an /open link leaves in the browser: blank in the interface's colours, or a jump to the address. */
496
- function openPage(target, opened) {
497
- const escaped = target.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
498
- return `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="light dark">${opened ? "" : `<meta http-equiv="refresh" content="0;url=${escaped}">`}<title>mainflow</title><style>html{background:#fff}@media (prefers-color-scheme: dark){html{background:#111113}}</style>`;
499
- }
500
- //#endregion
501
- export { serveUi };