mainflow 0.11.2 → 0.13.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 +50 -3
- package/dist/{dist-D2NK-QQP.js → dist-BQmXEfAf.js} +7 -2
- package/dist/facts.d.ts +29 -2
- package/dist/facts.js +29 -4
- package/dist/hooks.js +36 -12
- package/dist/{host-C3MyDGB_.js → host-daQGe7no.js} +339 -100
- package/dist/host.js +1 -1
- package/dist/{launch-T0sXbTgT.js → launch-JK8l8BYu.js} +2 -2
- package/dist/launch.js +1 -1
- package/dist/{ui-BTLe6I--.js → ui-BoBBdpZH.js} +14 -4
- package/dist/web/assets/index-Ckyguktw.css +1 -0
- package/dist/web/assets/index-LWzVLj2W.js +81 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CZjPKVxY.js +0 -78
- package/dist/web/assets/index-miwi9q2z.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as mainlineSchema, C as TASK_SECTIONS, D as findingSchema, E as decisionSchema, F as refMainline, I as sessionSchema, L as taskSchema, M as parseOpsLine, N as parseRef, O as formatOpsEntry, P as refExample, S as TASK_REQUIRED_SECTIONS, T as configSchema, b as SESSION_STATUS, c as FINDING_STATUS, d as ISSUE_SECTIONS, g as MERGE_STATES, h as MAINLINE_STATUS, i as DECISION_SECTIONS, j as opsEntrySchema, k as issueSchema, m as MAINLINE_SECTIONS, n as CLOSED_STATUS, o as DEFAULT_CATEGORIES, r as DECIDERS, s as FINDING_SECTIONS, t as CHECKLIST_REF, v as REF_EXAMPLES, w as WORK_STATUS, x as SLUG, y as RESERVED_HUB_NAMES } from "./dist-
|
|
1
|
+
import { A as mainlineSchema, C as TASK_SECTIONS, D as findingSchema, E as decisionSchema, F as refMainline, I as sessionSchema, L as taskSchema, M as parseOpsLine, N as parseRef, O as formatOpsEntry, P as refExample, R as waitingForRef, S as TASK_REQUIRED_SECTIONS, T as configSchema, b as SESSION_STATUS, c as FINDING_STATUS, d as ISSUE_SECTIONS, g as MERGE_STATES, h as MAINLINE_STATUS, i as DECISION_SECTIONS, j as opsEntrySchema, k as issueSchema, m as MAINLINE_SECTIONS, n as CLOSED_STATUS, o as DEFAULT_CATEGORIES, r as DECIDERS, s as FINDING_SECTIONS, t as CHECKLIST_REF, v as REF_EXAMPLES, w as WORK_STATUS, x as SLUG, y as RESERVED_HUB_NAMES } from "./dist-BQmXEfAf.js";
|
|
2
2
|
import { appendFileSync, closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -32,10 +32,11 @@ function currentBranch(cwd) {
|
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
34
|
* The hub for `cwd`: the first directory upwards holding `.mainflow/config.yaml`, except that a linked
|
|
35
|
-
* worktree's checkout of a hub resolves to the same hub in the main working tree.
|
|
36
|
-
* checkout of the same repository, so without this every worktree would write its
|
|
37
|
-
* the copies would conflict on merge. A
|
|
38
|
-
*
|
|
35
|
+
* worktree's checkout of a hub at the repository root resolves to the same hub in the main working tree.
|
|
36
|
+
* A worktree is another checkout of the same repository, so without this every worktree would write its
|
|
37
|
+
* own copy of the hub and the copies would conflict on merge. A hub below the root (a sample hub under
|
|
38
|
+
* examples/, a scratch hub under tmp/) stays as found: it is data the worktree's own branch owns, and
|
|
39
|
+
* mapping it would let tests and migrations in the worktree write the main tree's copy.
|
|
39
40
|
*/
|
|
40
41
|
function findHubRoot(cwd) {
|
|
41
42
|
let found = null;
|
|
@@ -48,8 +49,8 @@ function findHubRoot(cwd) {
|
|
|
48
49
|
}
|
|
49
50
|
const trees = worktreesOf(cwd);
|
|
50
51
|
if (!trees || trees.main === trees.current) return found;
|
|
51
|
-
|
|
52
|
-
return isHub(
|
|
52
|
+
if (found && found !== trees.current) return found;
|
|
53
|
+
return isHub(trees.main) ? trees.main : found;
|
|
53
54
|
}
|
|
54
55
|
function subdirs(dir) {
|
|
55
56
|
if (!existsSync(dir)) return [];
|
|
@@ -311,6 +312,113 @@ function linksInto(hub, hubName) {
|
|
|
311
312
|
return out;
|
|
312
313
|
}
|
|
313
314
|
//#endregion
|
|
315
|
+
//#region ../core/dist/deps.js
|
|
316
|
+
const MET = /* @__PURE__ */ new Set([
|
|
317
|
+
"done",
|
|
318
|
+
"closed",
|
|
319
|
+
"accepted",
|
|
320
|
+
"superseded",
|
|
321
|
+
"obsolete"
|
|
322
|
+
]);
|
|
323
|
+
const depState = (target) => !target ? "unknown" : MET.has(target.status) ? "met" : target.status === "dropped" ? "dropped" : "pending";
|
|
324
|
+
function dependencyGraph(hub) {
|
|
325
|
+
const own = refIndex(hub);
|
|
326
|
+
const others = new Map((hub.remotes ?? []).flatMap((r) => r.hub ? [[r.name, refIndex(r.hub)]] : []));
|
|
327
|
+
/** A reference as this hub writes it: this hub's own prefix is dropped, another hub's is kept. */
|
|
328
|
+
const normal = (ref) => parseRef(ref)?.hub === hub.config.hub ? ref.slice(ref.indexOf(":") + 1) : ref;
|
|
329
|
+
const target = (ref) => {
|
|
330
|
+
const hubName = parseRef(ref)?.hub;
|
|
331
|
+
return hubName === void 0 ? own.get(ref) : others.get(hubName)?.get(ref.slice(ref.indexOf(":") + 1));
|
|
332
|
+
};
|
|
333
|
+
const edges = [];
|
|
334
|
+
const edge = (from, to, source, extra) => edges.push({
|
|
335
|
+
from,
|
|
336
|
+
to,
|
|
337
|
+
source,
|
|
338
|
+
state: depState(target(to)),
|
|
339
|
+
...extra
|
|
340
|
+
});
|
|
341
|
+
const links = (from, file, list) => list?.forEach((l, i) => l.rel === "depends-on" && edge(from, normal(l.to), "link", {
|
|
342
|
+
file,
|
|
343
|
+
field: `links.${i}.to`,
|
|
344
|
+
...l.note ? { note: l.note } : {}
|
|
345
|
+
}));
|
|
346
|
+
for (const m of hub.mainlines) {
|
|
347
|
+
links(m.id, m.file, m.links);
|
|
348
|
+
for (const d of m.decisions) links(`${m.id}/${d.id}`, d.file, d.links);
|
|
349
|
+
for (const t of m.tasks) {
|
|
350
|
+
links(`${m.id}/${t.id}`, t.file, t.links);
|
|
351
|
+
t.dependsOn?.forEach((dep, i) => edge(`${m.id}/${t.id}`, `${m.id}/${dep}`, "task", {
|
|
352
|
+
file: t.file,
|
|
353
|
+
field: `dependsOn.${i}`
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const d of [...hub.issues, ...hub.findings]) links(d.id, d.file, d.links);
|
|
358
|
+
for (const remote of hub.remotes ?? []) {
|
|
359
|
+
if (!remote.hub) continue;
|
|
360
|
+
for (const { to, link } of linksInto(remote.hub, hub.config.hub)) if (link.rel === "depends-on") edge(`${remote.name}:${link.from}`, to, "link", link.note ? { note: link.note } : {});
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
edges,
|
|
364
|
+
cycles: findCycles(edges)
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/** Cycles among this hub's own documents, each once, starting from its smallest reference. */
|
|
368
|
+
function findCycles(edges) {
|
|
369
|
+
const next = /* @__PURE__ */ new Map();
|
|
370
|
+
for (const e of edges) if (!e.from.includes(":") && !e.to.includes(":")) next.set(e.from, [...next.get(e.from) ?? [], e.to]);
|
|
371
|
+
const state = /* @__PURE__ */ new Map();
|
|
372
|
+
const stack = [];
|
|
373
|
+
const cycles = /* @__PURE__ */ new Map();
|
|
374
|
+
const visit = (node) => {
|
|
375
|
+
state.set(node, "open");
|
|
376
|
+
stack.push(node);
|
|
377
|
+
for (const to of next.get(node) ?? []) if (state.get(to) === "open") {
|
|
378
|
+
const loop = stack.slice(stack.indexOf(to));
|
|
379
|
+
const start = loop.indexOf([...loop].sort()[0]);
|
|
380
|
+
const ordered = [...loop.slice(start), ...loop.slice(0, start)];
|
|
381
|
+
cycles.set(ordered.join(" "), [...ordered, ordered[0]]);
|
|
382
|
+
} else if (!state.has(to)) visit(to);
|
|
383
|
+
stack.pop();
|
|
384
|
+
state.set(node, "closed");
|
|
385
|
+
};
|
|
386
|
+
for (const node of [...next.keys()].sort()) if (!state.has(node)) visit(node);
|
|
387
|
+
return [...cycles.values()];
|
|
388
|
+
}
|
|
389
|
+
/** The mainline a reference sits in, keeping another hub's prefix; undefined for issues and findings. */
|
|
390
|
+
function mainlineOf(ref) {
|
|
391
|
+
const parsed = parseRef(ref);
|
|
392
|
+
const m = parsed ? refMainline(parsed) : void 0;
|
|
393
|
+
return m && (parsed.hub ? `${parsed.hub}:${m}` : m);
|
|
394
|
+
}
|
|
395
|
+
/** Dependencies lifted to mainlines, for a map of how mainlines wait for one another. */
|
|
396
|
+
function mainlineEdges(graph) {
|
|
397
|
+
const out = /* @__PURE__ */ new Map();
|
|
398
|
+
for (const e of graph.edges) {
|
|
399
|
+
const from = mainlineOf(e.from);
|
|
400
|
+
const to = mainlineOf(e.to);
|
|
401
|
+
if (!from || !to || from === to) continue;
|
|
402
|
+
const key = `${from} ${to}`;
|
|
403
|
+
const entry = out.get(key) ?? {
|
|
404
|
+
from,
|
|
405
|
+
to,
|
|
406
|
+
state: e.state,
|
|
407
|
+
edges: []
|
|
408
|
+
};
|
|
409
|
+
entry.edges.push(e);
|
|
410
|
+
const states = entry.edges.map((x) => x.state);
|
|
411
|
+
entry.state = [
|
|
412
|
+
"pending",
|
|
413
|
+
"unknown",
|
|
414
|
+
"dropped",
|
|
415
|
+
"met"
|
|
416
|
+
].find((s) => states.includes(s));
|
|
417
|
+
out.set(key, entry);
|
|
418
|
+
}
|
|
419
|
+
return [...out.values()];
|
|
420
|
+
}
|
|
421
|
+
//#endregion
|
|
314
422
|
//#region ../core/dist/templates.js
|
|
315
423
|
const body = (headings) => ["", ...headings.flatMap((h) => [`## ${h}`, ""])].join("\n");
|
|
316
424
|
const MAINLINE_BODY = body(MAINLINE_SECTIONS);
|
|
@@ -553,6 +661,21 @@ function loadHub(root, options = {}) {
|
|
|
553
661
|
unreadable,
|
|
554
662
|
problems
|
|
555
663
|
});
|
|
664
|
+
const deps = dependencyGraph({
|
|
665
|
+
config,
|
|
666
|
+
mainlines,
|
|
667
|
+
issues: hubIssues,
|
|
668
|
+
findings,
|
|
669
|
+
remotes
|
|
670
|
+
});
|
|
671
|
+
for (const cycle of deps.cycles) {
|
|
672
|
+
const first = deps.edges.find((e) => e.from === cycle[0] && e.to === cycle[1]);
|
|
673
|
+
problems.push({
|
|
674
|
+
file: first.file,
|
|
675
|
+
field: first.field,
|
|
676
|
+
message: `dependency cycle ${cycle.join(" → ")}; drop one of these dependencies`
|
|
677
|
+
});
|
|
678
|
+
}
|
|
556
679
|
return {
|
|
557
680
|
root,
|
|
558
681
|
config,
|
|
@@ -686,6 +809,12 @@ function checkReferences(input) {
|
|
|
686
809
|
for (const t of m.tasks) {
|
|
687
810
|
add(t.file, t, `${m.id}/${t.id}`, m.id);
|
|
688
811
|
items(t.file, "checklist", t.checklist);
|
|
812
|
+
const waiting = waitingForRef(t.blockedBy?.waitingFor);
|
|
813
|
+
if (waiting) pending.push({
|
|
814
|
+
ref: waiting,
|
|
815
|
+
file: t.file,
|
|
816
|
+
field: "blockedBy.waitingFor"
|
|
817
|
+
});
|
|
689
818
|
}
|
|
690
819
|
}
|
|
691
820
|
for (const i of issues) add(i.file, i, i.id);
|
|
@@ -718,7 +847,7 @@ function checkReferences(input) {
|
|
|
718
847
|
});
|
|
719
848
|
}
|
|
720
849
|
}
|
|
721
|
-
const isOpen = (kind, status) => !CLOSED_STATUS[kind].includes(status);
|
|
850
|
+
const isOpen$2 = (kind, status) => !CLOSED_STATUS[kind].includes(status);
|
|
722
851
|
/**
|
|
723
852
|
* What a reader without the conversation needs to pick the work up: every open task says what it is for
|
|
724
853
|
* and when it is done, and every open criterion or item says what has to be true to check it. Closed
|
|
@@ -743,10 +872,10 @@ function contentGaps(mainline, tasks) {
|
|
|
743
872
|
});
|
|
744
873
|
});
|
|
745
874
|
};
|
|
746
|
-
const mainlineOpen = isOpen("mainline", mainline.status);
|
|
875
|
+
const mainlineOpen = isOpen$2("mainline", mainline.status);
|
|
747
876
|
list(mainline.file, "criteria", mainline.criteria, mainlineOpen);
|
|
748
877
|
for (const task of tasks) {
|
|
749
|
-
const open = mainlineOpen && isOpen("task", task.status);
|
|
878
|
+
const open = mainlineOpen && isOpen$2("task", task.status);
|
|
750
879
|
list(task.file, "checklist", task.checklist, open);
|
|
751
880
|
if (!open) continue;
|
|
752
881
|
for (const section of TASK_REQUIRED_SECTIONS) if (!sectionText(task.body, section)) gaps.push({
|
|
@@ -1010,82 +1139,6 @@ function decorateReply(text, context) {
|
|
|
1010
1139
|
/** Cheap test for text that decorateReply could change, so a display hook can skip loading the hub. */
|
|
1011
1140
|
const mayDecorate = (text) => /m\d{3}|[if]\d{3}|\.mainflow\/mainlines/.test(text) || endsWithMarker(text);
|
|
1012
1141
|
//#endregion
|
|
1013
|
-
//#region ../core/dist/briefing.js
|
|
1014
|
-
function describe(root, ref) {
|
|
1015
|
-
const parsed = parseRef(ref);
|
|
1016
|
-
if (parsed?.kind !== "checklistItem") return null;
|
|
1017
|
-
try {
|
|
1018
|
-
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1019
|
-
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1020
|
-
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1021
|
-
if (!item) return null;
|
|
1022
|
-
return {
|
|
1023
|
-
ref,
|
|
1024
|
-
text: item.text,
|
|
1025
|
-
checked: item.done,
|
|
1026
|
-
where: `${mainline.id} ${mainline.title} › ${task.id} ${task.title}`
|
|
1027
|
-
};
|
|
1028
|
-
} catch {
|
|
1029
|
-
return null;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1033
|
-
function queueWarnings(root, session, cli) {
|
|
1034
|
-
const warnings = [];
|
|
1035
|
-
const doing = session.actions.filter((a) => a.status === "doing");
|
|
1036
|
-
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1037
|
-
for (const action of doing) {
|
|
1038
|
-
const served = action.serves.map((ref) => describe(root, ref));
|
|
1039
|
-
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c> 改正。`);
|
|
1040
|
-
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1041
|
-
}
|
|
1042
|
-
const next = session.actions.find((a) => a.status === "todo");
|
|
1043
|
-
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
1044
|
-
return warnings;
|
|
1045
|
-
}
|
|
1046
|
-
const current = (session) => session.actions.find((a) => a.status === "doing");
|
|
1047
|
-
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1048
|
-
const s = describe(root, ref);
|
|
1049
|
-
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目)`;
|
|
1050
|
-
}).join(";") || "沒有指定服務的 checklist 項目";
|
|
1051
|
-
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1052
|
-
function sessionBriefing(root, session, options) {
|
|
1053
|
-
const { cli } = options;
|
|
1054
|
-
const lines = [`[mainflow] 這場會話的 id 是 ${session.id}${options.envNote ? `(${options.envNote})` : ""}。`];
|
|
1055
|
-
const doing = current(session);
|
|
1056
|
-
const upcoming = session.actions.filter((a) => a.status === "todo").slice(0, 3);
|
|
1057
|
-
if (session.actions.length === 0) {
|
|
1058
|
-
const active = loadHub(root).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
|
|
1059
|
-
lines.push("這場會話還沒有行動。");
|
|
1060
|
-
if (active.length > 0) {
|
|
1061
|
-
lines.push("進行中的主脈絡:");
|
|
1062
|
-
for (const m of active) lines.push(`- ${m.id} ${m.title}`);
|
|
1063
|
-
lines.push(`要推進其中一個時,先用 ${cli} status 與它的任務 checklist 決定要做什麼,再列出行動。`);
|
|
1064
|
-
}
|
|
1065
|
-
} else {
|
|
1066
|
-
lines.push(doing ? `目前行動:${doing.id} ${doing.do}` : "目前沒有進行中的行動。");
|
|
1067
|
-
if (doing) lines.push(`服務:${servedLine(root, doing)}`);
|
|
1068
|
-
if (upcoming.length > 0) lines.push(`接下來:${upcoming.map((a) => `${a.id} ${a.do}`).join("、")}`);
|
|
1069
|
-
}
|
|
1070
|
-
const warnings = queueWarnings(root, session, cli);
|
|
1071
|
-
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
1072
|
-
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1073
|
-
return lines.join("\n");
|
|
1074
|
-
}
|
|
1075
|
-
/** One or two lines for every user turn; null when the session has no actions to anchor to. */
|
|
1076
|
-
function turnReminder(root, session, options) {
|
|
1077
|
-
if (session.actions.length === 0) return null;
|
|
1078
|
-
const doing = current(session);
|
|
1079
|
-
const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
|
|
1080
|
-
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
|
|
1081
|
-
const header = headerLine(session);
|
|
1082
|
-
return [
|
|
1083
|
-
head,
|
|
1084
|
-
...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
|
|
1085
|
-
...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
|
|
1086
|
-
].join("\n");
|
|
1087
|
-
}
|
|
1088
|
-
//#endregion
|
|
1089
1142
|
//#region ../core/dist/sessions.js
|
|
1090
1143
|
const sessionsDir = (root) => join(root, DATA_DIR, "local", "sessions");
|
|
1091
1144
|
const sessionFile = (root, id) => join(sessionsDir(root), `${id}.yaml`);
|
|
@@ -1162,6 +1215,29 @@ function requireServes(root, serves) {
|
|
|
1162
1215
|
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`);
|
|
1163
1216
|
}
|
|
1164
1217
|
}
|
|
1218
|
+
const isOpen$1 = (a) => a.status === "todo" || a.status === "doing";
|
|
1219
|
+
/**
|
|
1220
|
+
* The previous session's unfinished actions, numbered after `existing`, with the previous session marked
|
|
1221
|
+
* handed-off. Runs inside the caller's lock.
|
|
1222
|
+
*/
|
|
1223
|
+
function takeUnfinished(root, from, existing) {
|
|
1224
|
+
const previousFile = requireSession(root, from);
|
|
1225
|
+
const previous = readYaml(previousFile, sessionSchema).data;
|
|
1226
|
+
const ids = [...existing];
|
|
1227
|
+
const carried = previous.actions.filter(isOpen$1).map((a) => {
|
|
1228
|
+
const id = nextId("a", ids);
|
|
1229
|
+
ids.push(id);
|
|
1230
|
+
return {
|
|
1231
|
+
...a,
|
|
1232
|
+
id
|
|
1233
|
+
};
|
|
1234
|
+
});
|
|
1235
|
+
writeAtomic(previousFile, toYaml(validate(sessionSchema, {
|
|
1236
|
+
...previous,
|
|
1237
|
+
status: "handed-off"
|
|
1238
|
+
}, previousFile)));
|
|
1239
|
+
return carried;
|
|
1240
|
+
}
|
|
1165
1241
|
/**
|
|
1166
1242
|
* Start a session. Taking over from another session copies its unfinished actions into this one and marks
|
|
1167
1243
|
* the other session handed-off, so the queue continues where it stopped.
|
|
@@ -1173,19 +1249,7 @@ function startSession(root, input) {
|
|
|
1173
1249
|
const file = sessionFile(root, id);
|
|
1174
1250
|
if (existsSync(file)) throw new WriteError(`session ${id} already exists`);
|
|
1175
1251
|
if (input.delegatedFrom) requireSession(root, input.delegatedFrom);
|
|
1176
|
-
|
|
1177
|
-
if (input.handoffFrom) {
|
|
1178
|
-
const previousFile = requireSession(root, input.handoffFrom);
|
|
1179
|
-
const previous = readYaml(previousFile, sessionSchema).data;
|
|
1180
|
-
carried = previous.actions.filter((a) => a.status === "todo" || a.status === "doing").map((a, i) => ({
|
|
1181
|
-
...a,
|
|
1182
|
-
id: `a${String(i + 1).padStart(3, "0")}`
|
|
1183
|
-
}));
|
|
1184
|
-
writeAtomic(previousFile, toYaml(validate(sessionSchema, {
|
|
1185
|
-
...previous,
|
|
1186
|
-
status: "handed-off"
|
|
1187
|
-
}, previousFile)));
|
|
1188
|
-
}
|
|
1252
|
+
const carried = input.handoffFrom ? takeUnfinished(root, input.handoffFrom, []) : [];
|
|
1189
1253
|
const session = validate(sessionSchema, {
|
|
1190
1254
|
id,
|
|
1191
1255
|
agent: input.agent,
|
|
@@ -1223,6 +1287,32 @@ function ensureSession(root, input) {
|
|
|
1223
1287
|
created: false
|
|
1224
1288
|
};
|
|
1225
1289
|
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Take over another session's unfinished actions into a session that already exists, such as one a host hook
|
|
1292
|
+
* created when the conversation opened. The receiving session must have nothing unfinished of its own, so the
|
|
1293
|
+
* carried queue is not mixed into work already under way; its finished actions stay and the carried ones are
|
|
1294
|
+
* numbered after them.
|
|
1295
|
+
*/
|
|
1296
|
+
function handoffSession(root, id, from) {
|
|
1297
|
+
if (id === from) throw new WriteError(`session ${id} cannot take over from itself`);
|
|
1298
|
+
return withLock(root, () => {
|
|
1299
|
+
const file = requireSession(root, id);
|
|
1300
|
+
const session = readYaml(file, sessionSchema).data;
|
|
1301
|
+
if (session.handoffFrom && session.handoffFrom !== from) throw new WriteError(`session ${id} already took over from ${session.handoffFrom}`);
|
|
1302
|
+
const open = session.actions.filter(isOpen$1);
|
|
1303
|
+
if (open.length > 0) throw new WriteError(`session ${id} has unfinished actions (${open.map((a) => a.id).join(", ")}); finish or drop them before taking over from ${from}`);
|
|
1304
|
+
if (!readYaml(requireSession(root, from), sessionSchema).data.actions.some(isOpen$1)) throw new WriteError(`session ${from} has no unfinished actions to take over`);
|
|
1305
|
+
const carried = takeUnfinished(root, from, session.actions.map((a) => a.id));
|
|
1306
|
+
const next = validate(sessionSchema, {
|
|
1307
|
+
...session,
|
|
1308
|
+
status: "active",
|
|
1309
|
+
handoffFrom: from,
|
|
1310
|
+
actions: [...session.actions, ...carried]
|
|
1311
|
+
}, file);
|
|
1312
|
+
writeAtomic(file, toYaml(next));
|
|
1313
|
+
return next;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1226
1316
|
/** Where a session's transcript is on disk: a platform records an absolute path, a hub may record one relative to its root. */
|
|
1227
1317
|
function transcriptPath(root, transcript) {
|
|
1228
1318
|
return isAbsolute(transcript) ? transcript : join(root, transcript);
|
|
@@ -1327,6 +1417,106 @@ function updateAction(root, ref, patch) {
|
|
|
1327
1417
|
return updated;
|
|
1328
1418
|
}
|
|
1329
1419
|
//#endregion
|
|
1420
|
+
//#region ../core/dist/briefing.js
|
|
1421
|
+
function describe(root, ref) {
|
|
1422
|
+
const parsed = parseRef(ref);
|
|
1423
|
+
if (parsed?.kind !== "checklistItem") return null;
|
|
1424
|
+
try {
|
|
1425
|
+
const mainline = readFrontMatter(mainlineFile(root, parsed.mainline), mainlineSchema).data;
|
|
1426
|
+
const task = readFrontMatter(taskFile(root, parsed.mainline, parsed.task), taskSchema).data;
|
|
1427
|
+
const item = task.checklist.find((c) => c.id === parsed.item);
|
|
1428
|
+
if (!item) return null;
|
|
1429
|
+
return {
|
|
1430
|
+
ref,
|
|
1431
|
+
text: item.text,
|
|
1432
|
+
checked: item.done,
|
|
1433
|
+
where: `${mainline.id} ${mainline.title} › ${task.id} ${task.title}`
|
|
1434
|
+
};
|
|
1435
|
+
} catch {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/** Problems in a session's queue that the executor should fix before going on, each with the command that fixes it. */
|
|
1440
|
+
function queueWarnings(root, session, cli) {
|
|
1441
|
+
const warnings = [];
|
|
1442
|
+
const doing = session.actions.filter((a) => a.status === "doing");
|
|
1443
|
+
if (doing.length > 1) warnings.push(`同時有 ${doing.length} 個進行中的行動(${doing.map((a) => a.id).join("、")})。一次只推進一個,其餘改回 todo。`);
|
|
1444
|
+
for (const action of doing) {
|
|
1445
|
+
const served = action.serves.map((ref) => describe(root, ref));
|
|
1446
|
+
if (served.some((s) => s === null)) warnings.push(`${action.id} 服務的項目有的已不存在,用 ${cli} action set ${action.id} --serves <m/t#c> 改正。`);
|
|
1447
|
+
else if (served.length > 0 && served.every((s) => s.checked)) warnings.push(`${action.id} 服務的項目都已勾選。做完了就執行 ${cli} action set ${action.id} --status done。`);
|
|
1448
|
+
}
|
|
1449
|
+
const next = session.actions.find((a) => a.status === "todo");
|
|
1450
|
+
if (doing.length === 0 && next) warnings.push(`下一個行動是 ${next.id}。開始前先執行 ${cli} action set ${next.id} --status doing。`);
|
|
1451
|
+
return warnings;
|
|
1452
|
+
}
|
|
1453
|
+
const current = (session) => session.actions.find((a) => a.status === "doing");
|
|
1454
|
+
const isOpen = (a) => a.status === "todo" || a.status === "doing";
|
|
1455
|
+
/** Mainlines a session could be pushing forward. */
|
|
1456
|
+
const openMainlines = (root) => loadHub(root).mainlines.filter((m) => !m.archivedAt && (m.status === "active" || m.status === "planning"));
|
|
1457
|
+
/** How recently a session's transcript must have been written for the session to count as possibly still running. */
|
|
1458
|
+
const RUNNING_WINDOW_MS = 9e5;
|
|
1459
|
+
/** Whether the session's transcript was written within the running window; false when no transcript is recorded or readable. */
|
|
1460
|
+
function mayBeRunning(root, session) {
|
|
1461
|
+
if (!session.transcript) return false;
|
|
1462
|
+
try {
|
|
1463
|
+
return Date.now() - statSync(transcriptPath(root, session.transcript)).mtimeMs < RUNNING_WINDOW_MS;
|
|
1464
|
+
} catch {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Lines naming the other top-level sessions that still hold unfinished actions, with the command that takes them
|
|
1470
|
+
* over into this one. Subagent sessions are left out: their work returns to the session that delegated it.
|
|
1471
|
+
*/
|
|
1472
|
+
function handoffLines(root, session, cli) {
|
|
1473
|
+
const others = loadSessions(root).sessions.filter((s) => s.id !== session.id && !s.delegatedFrom && !s.archivedAt && (s.status === "active" || s.status === "paused") && s.actions.some(isOpen));
|
|
1474
|
+
if (others.length === 0) return [];
|
|
1475
|
+
return [`其他會話還有沒做完的行動。要接續的是其中一場已經停下的工作時,先用 ${cli} session handoff <會話> 把它的行動接進這場會話,再開始做:`, ...others.map((s) => `- ${s.id}${mayBeRunning(root, s) ? "(15 分鐘內還有寫入對話紀錄,可能還在執行,不要接手)" : ""}:${s.actions.filter(isOpen).map((a) => `${a.id} ${a.do}(${a.status},服務 ${a.serves.join("、") || "未指定"})`).join(";")}`)];
|
|
1476
|
+
}
|
|
1477
|
+
const servedLine = (root, action) => action.serves.map((ref) => {
|
|
1478
|
+
const s = describe(root, ref);
|
|
1479
|
+
return s ? `${ref} ${s.text}(${s.where})` : `${ref}(找不到這個項目)`;
|
|
1480
|
+
}).join(";") || "沒有指定服務的 checklist 項目";
|
|
1481
|
+
/** Full briefing for the start of a session, after resuming, clearing or compacting. */
|
|
1482
|
+
function sessionBriefing(root, session, options) {
|
|
1483
|
+
const { cli } = options;
|
|
1484
|
+
const lines = [`[mainflow] 這場會話的 id 是 ${session.id}${options.envNote ? `(${options.envNote})` : ""}。`];
|
|
1485
|
+
const doing = current(session);
|
|
1486
|
+
const upcoming = session.actions.filter((a) => a.status === "todo").slice(0, 3);
|
|
1487
|
+
if (session.actions.length === 0) {
|
|
1488
|
+
const active = openMainlines(root);
|
|
1489
|
+
lines.push("這場會話還沒有行動。");
|
|
1490
|
+
if (active.length > 0) {
|
|
1491
|
+
lines.push("進行中的主脈絡:");
|
|
1492
|
+
for (const m of active) lines.push(`- ${m.id} ${m.title}`);
|
|
1493
|
+
lines.push(`要推進其中一個時,先用 ${cli} status 與它的任務 checklist 決定要做什麼,再列出行動。`);
|
|
1494
|
+
}
|
|
1495
|
+
} else {
|
|
1496
|
+
lines.push(doing ? `目前行動:${doing.id} ${doing.do}` : "目前沒有進行中的行動。");
|
|
1497
|
+
if (doing) lines.push(`服務:${servedLine(root, doing)}`);
|
|
1498
|
+
if (upcoming.length > 0) lines.push(`接下來:${upcoming.map((a) => `${a.id} ${a.do}`).join("、")}`);
|
|
1499
|
+
}
|
|
1500
|
+
if (!session.actions.some(isOpen)) lines.push(...handoffLines(root, session, cli));
|
|
1501
|
+
const warnings = queueWarnings(root, session, cli);
|
|
1502
|
+
if (warnings.length > 0) lines.push("需要處理:", ...warnings.map((w) => `- ${w}`));
|
|
1503
|
+
lines.push("工作方式:", `- 開始一件新工作前,先列進佇列:${cli} action add "<做法>" --serves <m/t#c>。`, `- 開始做標 doing,做完標 done:${cli} action set <a編號> --status doing。`, `- 勾選 checklist 要附證據:${cli} item check <m/t#c> --evidence <commit、檔案:行號或指令輸出>。`, `- 內容直接寫進檔案:意圖或需求寫進 mainline.md 的內文,任務的目標與完成定義寫進 task.md,選擇寫成決策(${cli} new decision <m> <slug> --title <標題> 建檔後再寫內文)。`, `- 改完 .mainflow/ 的檔案跑 ${cli} check,把它列出的問題修到沒有。`, "- 用完其他 skill 或子流程後,回到上面的目前行動再繼續。", "- 要開主脈絡、拆任務、記決策、延後或收尾時,用 mainflow skill,它依序帶過並告訴你資訊該寫在哪裡。");
|
|
1504
|
+
return lines.join("\n");
|
|
1505
|
+
}
|
|
1506
|
+
/** One or two lines for every user turn; null when the session has no actions and the hub has no open mainline. */
|
|
1507
|
+
function turnReminder(root, session, options) {
|
|
1508
|
+
if (session.actions.length === 0) return openMainlines(root).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${options.cli} action add 列出行動並標為 doing,或用 ${options.cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
|
|
1509
|
+
const doing = current(session);
|
|
1510
|
+
const open = session.actions.filter((a) => a.status === "todo" || a.status === "doing");
|
|
1511
|
+
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length === 0 ? `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${options.cli} action add 列出下一個行動。` : `[mainflow] ${session.id} 目前沒有進行中的行動。`;
|
|
1512
|
+
const header = headerLine(session);
|
|
1513
|
+
return [
|
|
1514
|
+
head,
|
|
1515
|
+
...header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [],
|
|
1516
|
+
...queueWarnings(root, session, options.cli).map((w) => `- ${w}`)
|
|
1517
|
+
].join("\n");
|
|
1518
|
+
}
|
|
1519
|
+
//#endregion
|
|
1330
1520
|
//#region ../core/dist/write.js
|
|
1331
1521
|
function checkTaskLinks(root, mainline, task) {
|
|
1332
1522
|
if (task.repo && !(task.repo in readConfig(root).repos)) throw new WriteError(`repo "${task.repo}" is not listed in .mainflow/config.yaml; add it under repos first`);
|
|
@@ -1455,6 +1645,17 @@ function setCriterion(root, refInput, done, evidence, expectedVersion) {
|
|
|
1455
1645
|
setListItem(criterion, done, evidence);
|
|
1456
1646
|
}, expectedVersion);
|
|
1457
1647
|
}
|
|
1648
|
+
/** Rewrite a criterion's text or doneWhen, leaving done and evidence as they are. */
|
|
1649
|
+
function setCriterionText(root, refInput, patch, expectedVersion) {
|
|
1650
|
+
const ref = requireRef(refInput, "criterion");
|
|
1651
|
+
if (patch.text === void 0 && patch.doneWhen === void 0) throw new WriteError(`${refInput} needs --text or --done-when, e.g. --text "CLI can create mainlines"`);
|
|
1652
|
+
return mutateFrontMatter(root, mainlineFile(root, ref.mainline), mainlineSchema, (m) => {
|
|
1653
|
+
const criterion = m.criteria.find((c) => c.id === ref.criterion);
|
|
1654
|
+
if (!criterion) throw new WriteError(`criterion ${refInput} not found`);
|
|
1655
|
+
if (patch.text !== void 0) criterion.text = patch.text;
|
|
1656
|
+
if (patch.doneWhen !== void 0) criterion.doneWhen = patch.doneWhen;
|
|
1657
|
+
}, expectedVersion);
|
|
1658
|
+
}
|
|
1458
1659
|
/** Check or uncheck a criterion or checklist item; checking clears a drop. */
|
|
1459
1660
|
function setListItem(item, done, evidence) {
|
|
1460
1661
|
item.done = done;
|
|
@@ -1605,6 +1806,44 @@ function supersedeDecision(root, refInput, by, expectedVersion) {
|
|
|
1605
1806
|
}, expectedVersion);
|
|
1606
1807
|
}
|
|
1607
1808
|
//#endregion
|
|
1809
|
+
//#region ../core/dist/archive.js
|
|
1810
|
+
/** The entities that carry `archivedAt`; tasks and decisions follow their mainline instead. */
|
|
1811
|
+
const ARCHIVE_KINDS = [
|
|
1812
|
+
"mainline",
|
|
1813
|
+
"issue",
|
|
1814
|
+
"finding",
|
|
1815
|
+
"session"
|
|
1816
|
+
];
|
|
1817
|
+
const ARCHIVERS = {
|
|
1818
|
+
mainline: setArchived,
|
|
1819
|
+
issue: setIssueArchived,
|
|
1820
|
+
finding: setFindingArchived,
|
|
1821
|
+
session: setSessionArchived
|
|
1822
|
+
};
|
|
1823
|
+
/**
|
|
1824
|
+
* Archive or unarchive several entities of one kind. Each file is written on its own: one that fails, such
|
|
1825
|
+
* as an ID that no longer exists, is reported and the others still change, because a selection in the
|
|
1826
|
+
* interface can go stale while it is open.
|
|
1827
|
+
*/
|
|
1828
|
+
function setArchivedMany(root, kind, ids, archived) {
|
|
1829
|
+
const archiver = ARCHIVERS[kind];
|
|
1830
|
+
if (!archiver) throw new WriteError(`kind must be one of: ${ARCHIVE_KINDS.join(", ")}`);
|
|
1831
|
+
const result = {
|
|
1832
|
+
done: [],
|
|
1833
|
+
failed: []
|
|
1834
|
+
};
|
|
1835
|
+
for (const id of new Set(ids)) try {
|
|
1836
|
+
archiver(root, id, archived);
|
|
1837
|
+
result.done.push(id);
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
result.failed.push({
|
|
1840
|
+
id,
|
|
1841
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
return result;
|
|
1845
|
+
}
|
|
1846
|
+
//#endregion
|
|
1608
1847
|
//#region ../core/dist/migrate.js
|
|
1609
1848
|
const drop = (o, ...keys) => keys.forEach((k) => delete o[k]);
|
|
1610
1849
|
const dropNull = (o, ...keys) => keys.forEach((k) => o[k] === null && delete o[k]);
|
|
@@ -1888,4 +2127,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
1888
2127
|
return true;
|
|
1889
2128
|
}
|
|
1890
2129
|
//#endregion
|
|
1891
|
-
export {
|
|
2130
|
+
export { setFindingStatus as $, addAction as A, transcriptPath as B, setCriterionText as C, updateTask as D, supersedeDecision as E, setSessionArchived as F, mayDecorate as G, decorateReply as H, setSessionInfo as I, createFinding as J, appendOps as K, setSessionStatus as L, handoffSession as M, loadSessions as N, sessionBriefing as O, readSession as P, setFindingArchived as Q, settleSession as R, setCriterion as S, setMainlineTitle as T, endsWithMarker as U, updateAction as V, headerLine as W, planIssue as X, createIssue as Y, reopenIssue as Z, createTask as _, openInApp as a, linksInto as at, setArchived as b, syncHost as c, TASK_FILE as ct, setArchivedMany as d, DATA_DIR as dt, setIssueArchived as et, acceptDecision as f, currentBranch as ft, createMainline as g, createDecision as h, hostName as i, mainlineEdges as it, ensureSession as j, turnReminder as k, migrateHub as l, ConflictError as lt, addRepo as m, worktreesOf as mt, DEFAULT_UI_PORT as n, loadHub as nt, removeHostLink as o, refIndex as ot, addHub as p, findHubRoot as pt, closeIssue as q, caddySites as r, dependencyGraph as rt, siteBlock as s, MAINLINE_FILE as st, APP_BUNDLE_ID as t, HubError as tt, ARCHIVE_KINDS as u, WriteError as ut, dropChecklistItem as v, setMainlineStatus as w, setChecklistItem as x, initHub as y, startSession as z };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, c as syncHost, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, r as caddySites, s as siteBlock, t as APP_BUNDLE_ID } from "./host-daQGe7no.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeHostLink, siteBlock, syncHost };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as syncHost, dt as DATA_DIR, i as hostName, n as DEFAULT_UI_PORT, o as removeHostLink, ut as WriteError } from "./host-daQGe7no.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -176,7 +176,7 @@ async function launchUi(root, hub, configured, options) {
|
|
|
176
176
|
}
|
|
177
177
|
let server;
|
|
178
178
|
if (!reuse) {
|
|
179
|
-
const { serveUi } = await import("./ui-
|
|
179
|
+
const { serveUi } = await import("./ui-BoBBdpZH.js");
|
|
180
180
|
server = await serveUi(root, choice.port);
|
|
181
181
|
writeUiRecord(root, {
|
|
182
182
|
port: choice.port,
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-
|
|
1
|
+
import { a as portCandidates, c as watchHubGone, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, s as startUiInBackground, t as PORT_SPAN } from "./launch-JK8l8BYu.js";
|
|
2
2
|
export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord, startUiInBackground, watchHubGone };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as transcriptPath, D as updateTask, F as setSessionArchived, N as loadSessions, P as readSession, Q as setFindingArchived, S as setCriterion, T as setMainlineTitle, a as openInApp, at as linksInto, b as setArchived, d as setArchivedMany, dt as DATA_DIR, et as setIssueArchived, it as mainlineEdges, lt as ConflictError, nt as loadHub, ot as refIndex, rt as dependencyGraph, u as ARCHIVE_KINDS, ut as WriteError, w as setMainlineStatus, x as setChecklistItem } from "./host-daQGe7no.js";
|
|
2
2
|
import { t as listProgress } from "./progress-1BfPg2zN.js";
|
|
3
|
-
import { l as CLI_VERSION } from "./launch-
|
|
3
|
+
import { l as CLI_VERSION } from "./launch-JK8l8BYu.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync, watch } from "node:fs";
|
|
6
6
|
import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
|
|
@@ -226,7 +226,9 @@ function relationsView(hub) {
|
|
|
226
226
|
const own = refIndex(hub);
|
|
227
227
|
const indexes = new Map(hub.remotes.flatMap((r) => r.hub ? [[r.name, refIndex(r.hub)]] : []));
|
|
228
228
|
const targets = {};
|
|
229
|
-
|
|
229
|
+
const graph = dependencyGraph(hub);
|
|
230
|
+
const depRefs = graph.edges.flatMap((e) => [e.from, e.to]);
|
|
231
|
+
for (const ref of /* @__PURE__ */ new Set([...prefixedRefs(hub), ...depRefs.filter((r) => r.includes(":"))])) {
|
|
230
232
|
const colon = ref.indexOf(":");
|
|
231
233
|
const name = ref.slice(0, colon);
|
|
232
234
|
const found = name === hub.config.hub ? own.get(ref.slice(colon + 1)) : indexes.get(name)?.get(ref.slice(colon + 1));
|
|
@@ -255,7 +257,10 @@ function relationsView(hub) {
|
|
|
255
257
|
...r.unavailable ? { reason: r.unavailable } : {}
|
|
256
258
|
})),
|
|
257
259
|
targets,
|
|
258
|
-
backlinks
|
|
260
|
+
backlinks,
|
|
261
|
+
deps: graph.edges,
|
|
262
|
+
mainlineDeps: mainlineEdges(graph),
|
|
263
|
+
cycles: graph.cycles
|
|
259
264
|
};
|
|
260
265
|
}
|
|
261
266
|
const pick = (t) => t ? {
|
|
@@ -369,6 +374,11 @@ function createApp(root) {
|
|
|
369
374
|
problems
|
|
370
375
|
});
|
|
371
376
|
});
|
|
377
|
+
app.post("/api/archive", async (c) => {
|
|
378
|
+
const { kind, ids, archived } = await c.req.json();
|
|
379
|
+
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);
|
|
380
|
+
return c.json(setArchivedMany(root, kind, ids, archived));
|
|
381
|
+
});
|
|
372
382
|
app.post("/api/sessions/:id/archive", async (c) => {
|
|
373
383
|
const { archived } = await c.req.json();
|
|
374
384
|
return c.json(setSessionArchived(root, c.req.param("id"), archived));
|