mainflow 0.34.0 → 0.35.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 +81 -9
- package/dist/{dist-IdbnCDqM.js → dist-BdvWGTsb.js} +47 -4
- package/dist/facts.d.ts +67 -13
- package/dist/facts.js +63 -14
- package/dist/grant-commands.d.ts +1 -1
- package/dist/grant-commands.js +1 -1
- package/dist/hooks.d.ts +63 -9
- package/dist/hooks.js +3 -3
- package/dist/{host-79T-_NMu.js → host-bnHYIwuY.js} +476 -199
- package/dist/host.d.ts +9 -0
- package/dist/host.js +2 -2
- package/dist/{index-ihgXABQc.d.ts → index-DgYB5u1f.d.ts} +52 -1
- package/dist/launch-D80CYPcm.js +327 -0
- package/dist/launch.d.ts +50 -4
- package/dist/launch.js +2 -2
- package/dist/native/mainflow-swap-darwin-arm64 +0 -0
- package/dist/native/mainflow-swap-darwin-x64 +0 -0
- package/dist/{operation--fJnUqvA.js → operation-DVcJgKM9.js} +2 -2
- package/dist/{ui-BcXPk-0P.js → ui-C4h4iLDu.js} +132 -12
- package/dist/web/assets/index-BDnlbKcr.js +82 -0
- package/dist/web/assets/index-Z6UfQ6pW.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/launch-DQVyIbpf.js +0 -205
- package/dist/web/assets/index-CC7l6bNN.css +0 -1
- package/dist/web/assets/index-CDNCPMjD.js +0 -82
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { F as configSchema, f as GROUP_SECTIONS } from "./dist-BdvWGTsb.js";
|
|
2
|
+
import { C as readSession, G as sectionText, H as HubError, J as linksInto, K as dependencyGraph, M as traceTranscript, N as transcriptTitle, S as loadSessions, U as loadConfig, W as loadHub, Y as refIndex, Z as readYaml, _t as DATA_DIR, a as openInApp, g as setArchivedMany, gt as ConflictError, h as ARCHIVE_KINDS, j as attributeTrace, q as mainlineEdges, vt as WriteError, w as transcriptPath } from "./host-bnHYIwuY.js";
|
|
3
3
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
4
|
-
import { A as
|
|
5
|
-
import {
|
|
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-DVcJgKM9.js";
|
|
5
|
+
import { c as realRoot, f as CLI_VERSION, s as readUiRecord } from "./launch-D80CYPcm.js";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync
|
|
7
|
+
import { closeSync, existsSync, lstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
8
8
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { parse as parse$1 } from "yaml";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
13
|
import { serve } from "@hono/node-server";
|
|
13
14
|
import { Hono } from "hono";
|
|
@@ -32,7 +33,11 @@ function prefixedRefs(hub) {
|
|
|
32
33
|
refs.push(...t.checklist.map((c) => c.supersededBy));
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
for (const d of [
|
|
36
|
+
for (const d of [
|
|
37
|
+
...hub.issues,
|
|
38
|
+
...hub.findings,
|
|
39
|
+
...hub.groups
|
|
40
|
+
]) doc(d);
|
|
36
41
|
return [...new Set(refs.filter((r) => !!r && /^[a-z0-9-]+:/.test(r)))];
|
|
37
42
|
}
|
|
38
43
|
function relationsView(hub) {
|
|
@@ -104,6 +109,8 @@ function summarize(m) {
|
|
|
104
109
|
priority: m.priority ?? null,
|
|
105
110
|
labels: m.labels ?? [],
|
|
106
111
|
due: m.due ?? null,
|
|
112
|
+
group: m.group ?? null,
|
|
113
|
+
role: m.role ?? "impl",
|
|
107
114
|
updated: [
|
|
108
115
|
m,
|
|
109
116
|
...m.tasks,
|
|
@@ -118,6 +125,27 @@ function summarize(m) {
|
|
|
118
125
|
decisions: m.decisions.length
|
|
119
126
|
};
|
|
120
127
|
}
|
|
128
|
+
/** The first paragraph of a group's 說明 on one line, for the heading of its group in a list. */
|
|
129
|
+
function groupSummary(body) {
|
|
130
|
+
return ((sectionText(body, GROUP_SECTIONS[0]) ?? "").split(/\n\s*\n/)[0] ?? "").replace(/\s+/g, " ").trim();
|
|
131
|
+
}
|
|
132
|
+
const groupView = (g) => ({
|
|
133
|
+
...g,
|
|
134
|
+
summary: groupSummary(g.body)
|
|
135
|
+
});
|
|
136
|
+
/** The 503 body for a config.yaml that cannot be used, with the hub's root left out of paths. */
|
|
137
|
+
function configErrorView(root, error) {
|
|
138
|
+
const scrub = (text) => text.replaceAll(`${root}${sep}`, "");
|
|
139
|
+
const message = scrub(error.message);
|
|
140
|
+
const problems = error instanceof HubError ? error.problems : [];
|
|
141
|
+
const detail = problems.map((p) => `${p.field ? `${p.field}: ` : ""}${scrub(p.message)}`).join("; ");
|
|
142
|
+
return {
|
|
143
|
+
error: `config.yaml cannot be used: ${message}${detail ? ` (${detail})` : ""}`,
|
|
144
|
+
kind: "hub-config",
|
|
145
|
+
message,
|
|
146
|
+
problems
|
|
147
|
+
};
|
|
148
|
+
}
|
|
121
149
|
/**
|
|
122
150
|
* HTTP API over one hub. The CLI `ui` command and the web dev server both mount it, through createUiApp. Writes go
|
|
123
151
|
* through the hub's writer (src/remote/operation.ts), so in remote mode they are operations the sync server applies;
|
|
@@ -147,12 +175,59 @@ function createApp(root, options = {}) {
|
|
|
147
175
|
}
|
|
148
176
|
return written;
|
|
149
177
|
};
|
|
178
|
+
let knownName;
|
|
179
|
+
const configFailure = () => {
|
|
180
|
+
try {
|
|
181
|
+
knownName = loadConfig(root).hub;
|
|
182
|
+
return null;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return configErrorView(root, error);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* The name /api/ping gives while config.yaml cannot be used: the last good read, else what a complete file of
|
|
189
|
+
* another format version says, else the directory's. A half-written file holds only part of its name, so it is not read.
|
|
190
|
+
*/
|
|
191
|
+
const fallbackName = () => {
|
|
192
|
+
if (knownName) return knownName;
|
|
193
|
+
try {
|
|
194
|
+
const raw = configSchema.safeParse(parse$1(readFileSync(join(root, DATA_DIR, "config.yaml"), "utf8")));
|
|
195
|
+
if (raw.success) return raw.data.hub;
|
|
196
|
+
} catch {}
|
|
197
|
+
return basename(realRoot(root));
|
|
198
|
+
};
|
|
199
|
+
configFailure();
|
|
200
|
+
app.use("/api/*", async (c, next) => {
|
|
201
|
+
if (/\/api\/(ping|events)$/.test(c.req.path)) return next();
|
|
202
|
+
const failure = configFailure();
|
|
203
|
+
return failure ? c.json(failure, 503) : next();
|
|
204
|
+
});
|
|
150
205
|
app.onError((error, c) => {
|
|
151
206
|
const message = error.message.replaceAll(`${root}${sep}`, "");
|
|
207
|
+
if (error instanceof HubError) return c.json(configErrorView(root, error), 503);
|
|
152
208
|
if (error instanceof ConflictError) return c.json({ error: message }, 409);
|
|
153
209
|
if (error instanceof WriteError) return c.json({ error: message }, 400);
|
|
154
210
|
throw error;
|
|
155
211
|
});
|
|
212
|
+
app.get("/api/ping", (c) => {
|
|
213
|
+
const failure = configFailure();
|
|
214
|
+
const who = {
|
|
215
|
+
hub: failure ? fallbackName() : knownName,
|
|
216
|
+
root: realRoot(root),
|
|
217
|
+
version: CLI_VERSION,
|
|
218
|
+
pid: process.pid
|
|
219
|
+
};
|
|
220
|
+
if (failure) return c.json({
|
|
221
|
+
...failure,
|
|
222
|
+
...who,
|
|
223
|
+
status: "config-error"
|
|
224
|
+
}, 503);
|
|
225
|
+
return c.json(who);
|
|
226
|
+
});
|
|
227
|
+
app.get("/api/host", (c) => {
|
|
228
|
+
const hostError = readUiRecord(root)?.hostError;
|
|
229
|
+
return c.json(hostError ? { hostError } : {});
|
|
230
|
+
});
|
|
156
231
|
app.get("/api/hub", (c) => {
|
|
157
232
|
const hub = loadHub(root);
|
|
158
233
|
const body = {
|
|
@@ -160,6 +235,7 @@ function createApp(root, options = {}) {
|
|
|
160
235
|
version: CLI_VERSION,
|
|
161
236
|
repos: hub.config.repos,
|
|
162
237
|
mainlines: hub.mainlines.map(summarize),
|
|
238
|
+
groups: hub.groups.map(groupView),
|
|
163
239
|
openIssues: hub.issues.filter((i) => i.status !== "closed").length,
|
|
164
240
|
validFindings: hub.findings.filter((f) => f.status === "valid").length,
|
|
165
241
|
problems: hub.problems
|
|
@@ -484,7 +560,8 @@ async function fetchFromServer(root, path) {
|
|
|
484
560
|
* client's notices (`notices`), or nothing. The sync client's own state under local/sync/ (known.json, queue.json,
|
|
485
561
|
* the conflicts it saves, its tmp/) is not the hub's content, so writing it reloads nothing; notices.jsonl only
|
|
486
562
|
* matters when a warn-level line arrives. remote.json lists the files only on the server, which the data view shows,
|
|
487
|
-
* so a change to it reloads. Lock and temporary files are left out as well.
|
|
563
|
+
* so a change to it reloads. Lock and temporary files are left out as well. A session file changes with every hook of a
|
|
564
|
+
* running session, so it is its own kind (`session`) and the interface reloads the sessions only, not the whole hub.
|
|
488
565
|
*/
|
|
489
566
|
function eventOf(name) {
|
|
490
567
|
const path = name.split(sep).join("/");
|
|
@@ -492,8 +569,28 @@ function eventOf(name) {
|
|
|
492
569
|
if (path === `local/sync/remote.json`) return "change";
|
|
493
570
|
if (path === "local/sync" || path.startsWith(`local/sync/`)) return null;
|
|
494
571
|
if (path.endsWith("write.lock") || path.endsWith(".tmp")) return null;
|
|
572
|
+
if (path === SESSIONS_DIR || path.startsWith(`${SESSIONS_DIR}/`)) return "session";
|
|
495
573
|
return "change";
|
|
496
574
|
}
|
|
575
|
+
const SESSIONS_DIR = "local/sessions";
|
|
576
|
+
/**
|
|
577
|
+
* Watch the hub's .mainflow/ for `onName`, a path relative to it. macOS and Windows watch the tree natively. Linux has no
|
|
578
|
+
* native tree watch, and Node's emulation of it rereads a directory on every event in it, so hundreds of session files
|
|
579
|
+
* written at once keep the process busy for seconds and every request waits; there the watcher keeps one watch per directory.
|
|
580
|
+
*/
|
|
581
|
+
function watchDataDir(dataDir, onName, platform = process.platform) {
|
|
582
|
+
const mode = platform === "linux" ? "directories" : "recursive";
|
|
583
|
+
const watcher = watchTree(dataDir, {
|
|
584
|
+
mode,
|
|
585
|
+
include: () => true,
|
|
586
|
+
onChange: onName,
|
|
587
|
+
onError: () => void 0
|
|
588
|
+
});
|
|
589
|
+
return {
|
|
590
|
+
mode,
|
|
591
|
+
close: () => watcher.close()
|
|
592
|
+
};
|
|
593
|
+
}
|
|
497
594
|
/** One file watcher per hub, shared by every listener of that hub. */
|
|
498
595
|
var HubWatch = class {
|
|
499
596
|
root;
|
|
@@ -504,16 +601,15 @@ var HubWatch = class {
|
|
|
504
601
|
constructor(root) {
|
|
505
602
|
this.root = root;
|
|
506
603
|
this.offset = sizeOf(syncPaths(root).notices);
|
|
507
|
-
this.watcher =
|
|
508
|
-
const kind =
|
|
604
|
+
this.watcher = watchDataDir(join(root, DATA_DIR), (name) => {
|
|
605
|
+
const kind = eventOf(name);
|
|
509
606
|
if (kind === null || kind === "notices" && !this.newWarnings()) return;
|
|
510
607
|
const event = {
|
|
511
|
-
kind: kind === "
|
|
512
|
-
name
|
|
608
|
+
kind: kind === "notices" ? "notice" : kind,
|
|
609
|
+
name
|
|
513
610
|
};
|
|
514
611
|
for (const notify of this.listeners) notify(event);
|
|
515
612
|
});
|
|
516
|
-
this.watcher.on("error", () => void 0);
|
|
517
613
|
}
|
|
518
614
|
close() {
|
|
519
615
|
this.watcher.close();
|
|
@@ -623,6 +719,26 @@ function servedHubs(root) {
|
|
|
623
719
|
root
|
|
624
720
|
}, ...others];
|
|
625
721
|
}
|
|
722
|
+
let lastTurn = Promise.resolve();
|
|
723
|
+
/** Hono middleware: a request waits for its turn, one per event loop iteration, so I/O such as a ping is served between. */
|
|
724
|
+
const pace = async (c, next) => {
|
|
725
|
+
if (!/\/api\/(ping|events|hubs\/events)$/.test(c.req.path)) {
|
|
726
|
+
const turn = lastTurn.then(() => new Promise((done) => setImmediate(done)));
|
|
727
|
+
lastTurn = turn;
|
|
728
|
+
await turn;
|
|
729
|
+
}
|
|
730
|
+
await next();
|
|
731
|
+
};
|
|
732
|
+
function hubListGuard(root) {
|
|
733
|
+
return async (c, next) => {
|
|
734
|
+
try {
|
|
735
|
+
loadConfig(root);
|
|
736
|
+
} catch (error) {
|
|
737
|
+
return c.json(configErrorView(root, error), 503);
|
|
738
|
+
}
|
|
739
|
+
return next();
|
|
740
|
+
};
|
|
741
|
+
}
|
|
626
742
|
/**
|
|
627
743
|
* The interface's API: this hub at /api, and every registered hub that is on this machine at /h/{name}/api, so one
|
|
628
744
|
* interface follows several hubs and switches between them. /api/hubs lists them, and /api/hubs/events sends every
|
|
@@ -631,6 +747,10 @@ function servedHubs(root) {
|
|
|
631
747
|
*/
|
|
632
748
|
function createUiApp(root) {
|
|
633
749
|
const app = new Hono();
|
|
750
|
+
app.use("/api/*", pace);
|
|
751
|
+
app.use("/h/:name/api/*", pace);
|
|
752
|
+
app.use("/api/hubs", hubListGuard(root));
|
|
753
|
+
app.use("/api/hubs/events", hubListGuard(root));
|
|
634
754
|
const own = createApp(root);
|
|
635
755
|
const others = /* @__PURE__ */ new Map();
|
|
636
756
|
const appOf = (name) => {
|