@minonl/dia 0.0.14 → 0.0.16
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/README.md +6 -0
- package/dist/cli.js +1594 -33
- package/docs/cli.md +3 -2
- package/docs/design/graph-prio-dag.md +85 -0
- package/docs/design/graph-ui-m1.md +106 -0
- package/docs/design/graph-web-m1.md +113 -0
- package/docs/design/graph-web-m2.md +92 -0
- package/docs/design/graph-web-mockup.html +667 -133
- package/docs/design/graph-web.md +147 -43
- package/docs/design/seat-scheduling-m2.md +138 -0
- package/docs/index.md +8 -1
- package/docs/install-guard-design.md +148 -0
- package/docs/kit-install.md +7 -0
- package/docs/kit-layer.md +78 -0
- package/docs/migrations/data-root-2026-11.md +3 -2
- package/docs/status.md +3 -0
- package/kits/git/dia.json +10 -0
- package/kits/git/knowledge/hooks.md +30 -0
- package/kits/git/knowledge/worktree.md +34 -0
- package/kits/node/dia.json +10 -0
- package/kits/node/knowledge/hook-managers.md +17 -0
- package/kits/node/knowledge/pnpm.md +34 -0
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +15 -0
- package/packages/wt/tests/adopt.test.sh +8 -0
package/dist/cli.js
CHANGED
|
@@ -1899,6 +1899,9 @@ function assertInside(base, p) {
|
|
|
1899
1899
|
return abs;
|
|
1900
1900
|
}
|
|
1901
1901
|
var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
|
|
1902
|
+
function copyFilter(base) {
|
|
1903
|
+
return (p) => !relative3(base, p).split(sep4).some((s) => TREE_SKIP.has(s));
|
|
1904
|
+
}
|
|
1902
1905
|
function walkFiles(root) {
|
|
1903
1906
|
const out = [];
|
|
1904
1907
|
(function rec(dir) {
|
|
@@ -2109,7 +2112,7 @@ function install(from, opts = {}) {
|
|
|
2109
2112
|
if (!alreadyInstalled || hashTree(src.dir) !== hashTree(target)) {
|
|
2110
2113
|
rmSync3(target, { recursive: true, force: true });
|
|
2111
2114
|
mkdirSync7(target, { recursive: true });
|
|
2112
|
-
cpSync(src.dir, target, { recursive: true, dereference: true, filter: (
|
|
2115
|
+
cpSync(src.dir, target, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2113
2116
|
}
|
|
2114
2117
|
const rendered = renderContent(src.dir, target, m) ?? planned;
|
|
2115
2118
|
const appliedConfig = apply(m.config ?? []);
|
|
@@ -2133,11 +2136,42 @@ function install(from, opts = {}) {
|
|
|
2133
2136
|
}, null, 1));
|
|
2134
2137
|
return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed: true };
|
|
2135
2138
|
}
|
|
2139
|
+
function installed() {
|
|
2140
|
+
const root = join10(dataDir2(), "kits");
|
|
2141
|
+
if (!existsSync8(root))
|
|
2142
|
+
return [];
|
|
2143
|
+
const out = [];
|
|
2144
|
+
for (const e of readdirSync3(root, { withFileTypes: true })) {
|
|
2145
|
+
if (!e.isDirectory())
|
|
2146
|
+
continue;
|
|
2147
|
+
const versions = readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()).sort();
|
|
2148
|
+
const version = versions[versions.length - 1];
|
|
2149
|
+
if (!version)
|
|
2150
|
+
continue;
|
|
2151
|
+
const dir = join10(root, e.name, version);
|
|
2152
|
+
let tags = [];
|
|
2153
|
+
try {
|
|
2154
|
+
tags = readKitManifest(dir).tags ?? [];
|
|
2155
|
+
} catch {}
|
|
2156
|
+
out.push({ id: e.name, version, dir, tags });
|
|
2157
|
+
}
|
|
2158
|
+
return out;
|
|
2159
|
+
}
|
|
2136
2160
|
function list() {
|
|
2137
2161
|
const root = join10(dataDir2(), "kits");
|
|
2138
2162
|
if (!existsSync8(root))
|
|
2139
2163
|
return [];
|
|
2140
|
-
return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) =>
|
|
2164
|
+
return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => {
|
|
2165
|
+
const versions = readdirSync3(join10(root, e.name)).filter((v) => statSync5(join10(root, e.name, v)).isDirectory()).sort();
|
|
2166
|
+
const latest = versions[versions.length - 1];
|
|
2167
|
+
let tags = [];
|
|
2168
|
+
if (latest) {
|
|
2169
|
+
try {
|
|
2170
|
+
tags = readKitManifest(join10(root, e.name, latest)).tags ?? [];
|
|
2171
|
+
} catch {}
|
|
2172
|
+
}
|
|
2173
|
+
return { id: e.name, versions, tags };
|
|
2174
|
+
});
|
|
2141
2175
|
}
|
|
2142
2176
|
function readLock(id, version) {
|
|
2143
2177
|
const idDir = join10(dataDir2(), "kits", id);
|
|
@@ -2881,7 +2915,7 @@ function install2(from, opts = {}) {
|
|
|
2881
2915
|
const staging = join13(idDir, `.staging-${process.pid}-${Date.now().toString(36)}`);
|
|
2882
2916
|
rmSync4(staging, { recursive: true, force: true });
|
|
2883
2917
|
mkdirSync10(staging, { recursive: true });
|
|
2884
|
-
cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: (
|
|
2918
|
+
cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2885
2919
|
const stagingHash = hashTree2(staging);
|
|
2886
2920
|
let reuseTarget = false;
|
|
2887
2921
|
if (existsSync11(target)) {
|
|
@@ -4570,6 +4604,30 @@ import { appendFileSync as appendFileSync6, existsSync as existsSync15, mkdirSyn
|
|
|
4570
4604
|
import { join as join17 } from "node:path";
|
|
4571
4605
|
import { createHash as createHash9 } from "node:crypto";
|
|
4572
4606
|
var DATA5 = () => diaHome();
|
|
4607
|
+
var PRIO_RE = /^P\d{1,2}$/;
|
|
4608
|
+
var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4609
|
+
function normPrio(v) {
|
|
4610
|
+
const s = String(v).trim().toUpperCase();
|
|
4611
|
+
if (!PRIO_RE.test(s))
|
|
4612
|
+
throw new Error(`E_TASK: prio 必须是 P0–P99(收到 ${v})`);
|
|
4613
|
+
return s;
|
|
4614
|
+
}
|
|
4615
|
+
function normDay(v) {
|
|
4616
|
+
const s = String(v).trim();
|
|
4617
|
+
if (DAY_RE.test(s)) {
|
|
4618
|
+
const [y, mo, d] = s.split("-").map(Number);
|
|
4619
|
+
const dt = new Date(y, mo - 1, d);
|
|
4620
|
+
if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d)
|
|
4621
|
+
return s;
|
|
4622
|
+
}
|
|
4623
|
+
throw new Error(`E_TASK: 日期必须是有效 YYYY-MM-DD(收到 ${v})`);
|
|
4624
|
+
}
|
|
4625
|
+
function normProgress(v) {
|
|
4626
|
+
const n = Number(v);
|
|
4627
|
+
if (!Number.isInteger(n) || n < 0 || n > 100)
|
|
4628
|
+
throw new Error(`E_TASK: progress 必须是 0–100 的整数(收到 ${v})`);
|
|
4629
|
+
return n;
|
|
4630
|
+
}
|
|
4573
4631
|
function file2() {
|
|
4574
4632
|
return join17(DATA5(), "tasks", "ledger.jsonl");
|
|
4575
4633
|
}
|
|
@@ -4600,10 +4658,39 @@ function fold2() {
|
|
|
4600
4658
|
deps: uniq(e.deps || []),
|
|
4601
4659
|
touches: uniq(e.touches || []),
|
|
4602
4660
|
created_at: e.ts,
|
|
4603
|
-
updated_at: e.ts
|
|
4661
|
+
updated_at: e.ts,
|
|
4662
|
+
...readPlan(e)
|
|
4604
4663
|
});
|
|
4605
4664
|
continue;
|
|
4606
4665
|
}
|
|
4666
|
+
if (e.event === "split" && e.parent && Array.isArray(e.children)) {
|
|
4667
|
+
const parent = map.get(e.parent);
|
|
4668
|
+
if (parent) {
|
|
4669
|
+
for (const c of e.children) {
|
|
4670
|
+
if (!c || !c.id || map.has(c.id))
|
|
4671
|
+
continue;
|
|
4672
|
+
map.set(c.id, {
|
|
4673
|
+
id: c.id,
|
|
4674
|
+
title: c.title || c.id,
|
|
4675
|
+
role: c.role || "builder",
|
|
4676
|
+
judge: c.judge || "",
|
|
4677
|
+
status: "todo",
|
|
4678
|
+
evidence: "",
|
|
4679
|
+
deps: [],
|
|
4680
|
+
touches: uniq(c.touches || []),
|
|
4681
|
+
created_at: e.ts,
|
|
4682
|
+
updated_at: e.ts,
|
|
4683
|
+
split_from: e.parent,
|
|
4684
|
+
...c.judge ? { judge_inherited: true } : {},
|
|
4685
|
+
...e.slice && e.slice === c.id ? { slice: true } : {}
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
const ids = e.children.map((c) => c.id).filter((id) => map.has(id));
|
|
4689
|
+
parent.deps = uniq([...parent.deps, ...ids]);
|
|
4690
|
+
parent.split_children = uniq([...parent.split_children || [], ...ids]);
|
|
4691
|
+
}
|
|
4692
|
+
continue;
|
|
4693
|
+
}
|
|
4607
4694
|
const t = map.get(e.id);
|
|
4608
4695
|
if (!t)
|
|
4609
4696
|
continue;
|
|
@@ -4611,9 +4698,16 @@ function fold2() {
|
|
|
4611
4698
|
const remove = new Set([...e.remove_deps || [], ...e.remove_touches || []]);
|
|
4612
4699
|
t.deps = uniq([...t.deps, ...e.deps || []]).filter((x) => !remove.has(x));
|
|
4613
4700
|
t.touches = uniq([...t.touches, ...e.touches || []]).filter((x) => !remove.has(x));
|
|
4701
|
+
} else if (e.event === "plan") {
|
|
4702
|
+
Object.assign(t, readPlan(e));
|
|
4614
4703
|
} else {
|
|
4615
|
-
if (e.status)
|
|
4704
|
+
if (e.status) {
|
|
4616
4705
|
t.status = e.status;
|
|
4706
|
+
if (e.status === "done" || e.status === "accepted")
|
|
4707
|
+
t.closed_at = t.closed_at || e.ts;
|
|
4708
|
+
else
|
|
4709
|
+
t.closed_at = undefined;
|
|
4710
|
+
}
|
|
4617
4711
|
if (e.evidence !== undefined)
|
|
4618
4712
|
t.evidence = e.evidence;
|
|
4619
4713
|
if (e.closure_reason !== undefined)
|
|
@@ -4640,10 +4734,87 @@ function add(title, opts = {}) {
|
|
|
4640
4734
|
judge: opts.judge || "",
|
|
4641
4735
|
owner: opts.owner,
|
|
4642
4736
|
deps: uniq(opts.deps || []),
|
|
4643
|
-
touches: uniq(opts.touches || [])
|
|
4737
|
+
touches: uniq(opts.touches || []),
|
|
4738
|
+
...planFields(opts)
|
|
4644
4739
|
});
|
|
4645
4740
|
return fold2().get(id);
|
|
4646
4741
|
}
|
|
4742
|
+
function planFields(o) {
|
|
4743
|
+
const out = {};
|
|
4744
|
+
if (o.prio !== undefined)
|
|
4745
|
+
out.prio = normPrio(o.prio);
|
|
4746
|
+
if (o.start !== undefined)
|
|
4747
|
+
out.start = normDay(o.start);
|
|
4748
|
+
if (o.due !== undefined)
|
|
4749
|
+
out.due = normDay(o.due);
|
|
4750
|
+
if (o.progress !== undefined)
|
|
4751
|
+
out.progress = normProgress(o.progress);
|
|
4752
|
+
return out;
|
|
4753
|
+
}
|
|
4754
|
+
function readPlan(e) {
|
|
4755
|
+
const out = {};
|
|
4756
|
+
try {
|
|
4757
|
+
if (e.prio !== undefined && PRIO_RE.test(String(e.prio).toUpperCase()))
|
|
4758
|
+
out.prio = String(e.prio).toUpperCase();
|
|
4759
|
+
} catch {}
|
|
4760
|
+
try {
|
|
4761
|
+
if (e.start !== undefined && DAY_RE.test(String(e.start)))
|
|
4762
|
+
out.start = String(e.start);
|
|
4763
|
+
} catch {}
|
|
4764
|
+
try {
|
|
4765
|
+
if (e.due !== undefined && DAY_RE.test(String(e.due)))
|
|
4766
|
+
out.due = String(e.due);
|
|
4767
|
+
} catch {}
|
|
4768
|
+
try {
|
|
4769
|
+
const n = Number(e.progress);
|
|
4770
|
+
if (e.progress !== undefined && Number.isInteger(n) && n >= 0 && n <= 100)
|
|
4771
|
+
out.progress = n;
|
|
4772
|
+
} catch {}
|
|
4773
|
+
return out;
|
|
4774
|
+
}
|
|
4775
|
+
function plan(id, patch) {
|
|
4776
|
+
const cur = get2(id);
|
|
4777
|
+
const fields = planFields(patch);
|
|
4778
|
+
if (!Object.keys(fields).length)
|
|
4779
|
+
throw new Error("E_TASK: plan 至少需要一个字段(--prio/--start/--due/--progress)");
|
|
4780
|
+
const start = fields.start ?? cur.start, due = fields.due ?? cur.due;
|
|
4781
|
+
if (start && due && start > due)
|
|
4782
|
+
throw new Error("E_TASK: start 不能晚于 due");
|
|
4783
|
+
append3({ event: "plan", id, ...fields });
|
|
4784
|
+
return get2(id);
|
|
4785
|
+
}
|
|
4786
|
+
var SATISFIED = ["done", "accepted", "dropped"];
|
|
4787
|
+
function isSatisfied(t) {
|
|
4788
|
+
return !!t && SATISFIED.includes(t.status);
|
|
4789
|
+
}
|
|
4790
|
+
function isTerminal2(t) {
|
|
4791
|
+
return !!t && ["done", "accepted", "dropped", "rejected"].includes(t.status);
|
|
4792
|
+
}
|
|
4793
|
+
function split(parentId, titles, opts = {}) {
|
|
4794
|
+
const parent = get2(parentId);
|
|
4795
|
+
if (isTerminal2(parent))
|
|
4796
|
+
throw new Error("E_TASK: 终态任务不能拆分");
|
|
4797
|
+
if (parent.status === "doing" && !opts.force)
|
|
4798
|
+
throw new Error("E_TASK: 进行中的任务拆分需 --force(拆分后会先等子任务)");
|
|
4799
|
+
const names = titles.map((x) => x.trim()).filter(Boolean);
|
|
4800
|
+
if (names.length < 2)
|
|
4801
|
+
throw new Error("E_TASK: 至少拆成 2 个切片");
|
|
4802
|
+
if (new Set(names).size !== names.length)
|
|
4803
|
+
throw new Error("E_TASK: 切片标题重复");
|
|
4804
|
+
if (opts.slice && !names.includes(opts.slice))
|
|
4805
|
+
throw new Error("E_TASK: --slice 必须是某个切片标题");
|
|
4806
|
+
const judge = opts.judge ?? parent.judge;
|
|
4807
|
+
const children = names.map((title) => ({
|
|
4808
|
+
id: "t-" + createHash9("sha1").update(title + Date.now() + Math.random()).digest("hex").slice(0, 8),
|
|
4809
|
+
title,
|
|
4810
|
+
role: opts.role || parent.role,
|
|
4811
|
+
judge,
|
|
4812
|
+
touches: uniq(opts.touches ?? parent.touches)
|
|
4813
|
+
}));
|
|
4814
|
+
const sliceId = opts.slice ? children[names.indexOf(opts.slice)].id : undefined;
|
|
4815
|
+
append3({ event: "split", parent: parentId, children, ...sliceId ? { slice: sliceId } : {} });
|
|
4816
|
+
return get2(parentId);
|
|
4817
|
+
}
|
|
4647
4818
|
function list4() {
|
|
4648
4819
|
return [...fold2().values()];
|
|
4649
4820
|
}
|
|
@@ -4685,11 +4856,258 @@ function status3(id, status, evidence, opts = {}) {
|
|
|
4685
4856
|
return get2(id);
|
|
4686
4857
|
}
|
|
4687
4858
|
|
|
4859
|
+
// src/plan.ts
|
|
4860
|
+
var NO_PRIO = "P99";
|
|
4861
|
+
var TERMINAL3 = new Set(["done", "accepted", "dropped", "rejected"]);
|
|
4862
|
+
var SATISFIED2 = new Set(["done", "accepted", "dropped"]);
|
|
4863
|
+
var PROPAGATE = new Set(["P0", "P1"]);
|
|
4864
|
+
var isTerminal3 = (t) => TERMINAL3.has(t.status);
|
|
4865
|
+
var isSatisfied2 = (t) => !!t && SATISFIED2.has(t.status);
|
|
4866
|
+
function prioIndex(p) {
|
|
4867
|
+
const m = /^P(\d{1,2})$/.exec(String(p ?? "").toUpperCase());
|
|
4868
|
+
return m ? Number(m[1]) : 99;
|
|
4869
|
+
}
|
|
4870
|
+
function prioName(idx) {
|
|
4871
|
+
return idx >= 99 ? NO_PRIO : `P${idx}`;
|
|
4872
|
+
}
|
|
4873
|
+
function graphOf(tasks) {
|
|
4874
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4875
|
+
const dependents = new Map;
|
|
4876
|
+
for (const t of tasks)
|
|
4877
|
+
for (const d of t.deps)
|
|
4878
|
+
if (byId.has(d))
|
|
4879
|
+
(dependents.get(d) ?? dependents.set(d, []).get(d)).push(t.id);
|
|
4880
|
+
return { byId, dependents };
|
|
4881
|
+
}
|
|
4882
|
+
function findCycles(tasks) {
|
|
4883
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4884
|
+
const state = new Map;
|
|
4885
|
+
const stack = [];
|
|
4886
|
+
const cycles = [];
|
|
4887
|
+
const seen = new Set;
|
|
4888
|
+
const visit = (id) => {
|
|
4889
|
+
const st = state.get(id) || 0;
|
|
4890
|
+
if (st === 1) {
|
|
4891
|
+
const i = stack.indexOf(id);
|
|
4892
|
+
const cyc = i >= 0 ? stack.slice(i) : [id];
|
|
4893
|
+
const key = [...cyc].sort().join(",");
|
|
4894
|
+
if (!seen.has(key)) {
|
|
4895
|
+
seen.add(key);
|
|
4896
|
+
cycles.push(cyc);
|
|
4897
|
+
}
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
if (st === 2)
|
|
4901
|
+
return;
|
|
4902
|
+
state.set(id, 1);
|
|
4903
|
+
stack.push(id);
|
|
4904
|
+
for (const dep of byId.get(id)?.deps || [])
|
|
4905
|
+
if (byId.has(dep))
|
|
4906
|
+
visit(dep);
|
|
4907
|
+
stack.pop();
|
|
4908
|
+
state.set(id, 2);
|
|
4909
|
+
};
|
|
4910
|
+
for (const t of tasks)
|
|
4911
|
+
visit(t.id);
|
|
4912
|
+
return cycles.sort((a, b) => a.join(",").localeCompare(b.join(",")));
|
|
4913
|
+
}
|
|
4914
|
+
function computeEff(tasks, dependents) {
|
|
4915
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4916
|
+
const eff = new Map;
|
|
4917
|
+
for (const t of tasks)
|
|
4918
|
+
eff.set(t.id, { idx: prioIndex(t.prio) });
|
|
4919
|
+
for (const src of tasks) {
|
|
4920
|
+
const idx = prioIndex(src.prio);
|
|
4921
|
+
if (isTerminal3(src) || !PROPAGATE.has(prioName(idx)))
|
|
4922
|
+
continue;
|
|
4923
|
+
const seen = new Map([[src.id, 0]]);
|
|
4924
|
+
const queue = [src.id];
|
|
4925
|
+
while (queue.length) {
|
|
4926
|
+
const cur = queue.shift();
|
|
4927
|
+
const dist = seen.get(cur);
|
|
4928
|
+
for (const up of byId.get(cur)?.deps || []) {
|
|
4929
|
+
if (!byId.has(up) || seen.has(up))
|
|
4930
|
+
continue;
|
|
4931
|
+
const upTask = byId.get(up);
|
|
4932
|
+
if (isTerminal3(upTask))
|
|
4933
|
+
continue;
|
|
4934
|
+
seen.set(up, dist + 1);
|
|
4935
|
+
queue.push(up);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
for (const [id, distance] of seen) {
|
|
4939
|
+
if (id === src.id)
|
|
4940
|
+
continue;
|
|
4941
|
+
const cur = eff.get(id);
|
|
4942
|
+
const cand = { id: src.id, declared: prioName(idx), distance };
|
|
4943
|
+
if (idx < cur.idx)
|
|
4944
|
+
eff.set(id, { idx, src: cand });
|
|
4945
|
+
else if (idx === cur.idx && cur.src) {
|
|
4946
|
+
const better = cand.distance < cur.src.distance || cand.distance === cur.src.distance && cand.id < cur.src.id;
|
|
4947
|
+
if (better)
|
|
4948
|
+
eff.set(id, { idx, src: cand });
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
return eff;
|
|
4953
|
+
}
|
|
4954
|
+
function longestDepth(tasks, byId, cyclic) {
|
|
4955
|
+
const memo = new Map;
|
|
4956
|
+
const visit = (id, stack) => {
|
|
4957
|
+
if (memo.has(id))
|
|
4958
|
+
return memo.get(id);
|
|
4959
|
+
if (stack.has(id))
|
|
4960
|
+
return 0;
|
|
4961
|
+
const t = byId.get(id);
|
|
4962
|
+
if (!t)
|
|
4963
|
+
return 0;
|
|
4964
|
+
stack.add(id);
|
|
4965
|
+
let d = 0;
|
|
4966
|
+
for (const dep of t.deps)
|
|
4967
|
+
if (byId.has(dep) && !cyclic.has(dep))
|
|
4968
|
+
d = Math.max(d, visit(dep, stack) + 1);
|
|
4969
|
+
stack.delete(id);
|
|
4970
|
+
memo.set(id, d);
|
|
4971
|
+
return d;
|
|
4972
|
+
};
|
|
4973
|
+
for (const t of tasks)
|
|
4974
|
+
visit(t.id, new Set);
|
|
4975
|
+
return memo;
|
|
4976
|
+
}
|
|
4977
|
+
function transitiveDependents(tasks, dependents) {
|
|
4978
|
+
const out = new Map;
|
|
4979
|
+
for (const t of tasks) {
|
|
4980
|
+
const seen = new Set;
|
|
4981
|
+
const queue = [...dependents.get(t.id) || []];
|
|
4982
|
+
while (queue.length) {
|
|
4983
|
+
const cur = queue.shift();
|
|
4984
|
+
if (seen.has(cur))
|
|
4985
|
+
continue;
|
|
4986
|
+
seen.add(cur);
|
|
4987
|
+
queue.push(...dependents.get(cur) || []);
|
|
4988
|
+
}
|
|
4989
|
+
out.set(t.id, seen);
|
|
4990
|
+
}
|
|
4991
|
+
return out;
|
|
4992
|
+
}
|
|
4993
|
+
function derive(tasks) {
|
|
4994
|
+
const { byId, dependents } = graphOf(tasks);
|
|
4995
|
+
const cycles = findCycles(tasks);
|
|
4996
|
+
const cyclic = new Set(cycles.flat());
|
|
4997
|
+
const eff = computeEff(tasks, dependents);
|
|
4998
|
+
const depth = longestDepth(tasks, byId, cyclic);
|
|
4999
|
+
const down = transitiveDependents(tasks, dependents);
|
|
5000
|
+
const critical = new Set;
|
|
5001
|
+
for (const t of tasks) {
|
|
5002
|
+
const idx = prioIndex(t.prio);
|
|
5003
|
+
if (isTerminal3(t) || !PROPAGATE.has(prioName(idx)))
|
|
5004
|
+
continue;
|
|
5005
|
+
const seen = new Set;
|
|
5006
|
+
const queue = [t.id];
|
|
5007
|
+
while (queue.length) {
|
|
5008
|
+
const cur = queue.shift();
|
|
5009
|
+
for (const dep of byId.get(cur)?.deps || []) {
|
|
5010
|
+
if (seen.has(dep) || !byId.has(dep))
|
|
5011
|
+
continue;
|
|
5012
|
+
seen.add(dep);
|
|
5013
|
+
critical.add(dep);
|
|
5014
|
+
queue.push(dep);
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
const byIdOut = new Map;
|
|
5019
|
+
for (const t of tasks) {
|
|
5020
|
+
const deps = t.deps;
|
|
5021
|
+
const dangling = deps.filter((d) => !byId.has(d));
|
|
5022
|
+
const unsatisfied = deps.filter((d) => byId.has(d) ? !isSatisfied2(byId.get(d)) : true);
|
|
5023
|
+
const released = deps.filter((d) => byId.get(d)?.status === "dropped");
|
|
5024
|
+
const hasCycle = cyclic.has(t.id);
|
|
5025
|
+
const ready = !isTerminal3(t) && !hasCycle && unsatisfied.length === 0;
|
|
5026
|
+
const e = eff.get(t.id);
|
|
5027
|
+
byIdOut.set(t.id, {
|
|
5028
|
+
id: t.id,
|
|
5029
|
+
eff_prio: prioName(e.idx),
|
|
5030
|
+
prio_inherited: e.idx < prioIndex(t.prio),
|
|
5031
|
+
...e.src && e.idx < prioIndex(t.prio) ? { prio_source: e.src } : {},
|
|
5032
|
+
critical: critical.has(t.id),
|
|
5033
|
+
ready,
|
|
5034
|
+
startable: ready && t.status === "todo",
|
|
5035
|
+
blocked_by: unsatisfied,
|
|
5036
|
+
released_by: released,
|
|
5037
|
+
dangling_deps: dangling,
|
|
5038
|
+
depth: depth.get(t.id) ?? 0,
|
|
5039
|
+
unblocks: down.get(t.id)?.size ?? 0,
|
|
5040
|
+
rank: null,
|
|
5041
|
+
...t.split_from ? { split_from: t.split_from } : {},
|
|
5042
|
+
...t.split_children?.length ? { split_children: t.split_children } : {},
|
|
5043
|
+
...t.slice ? { slice: true } : {}
|
|
5044
|
+
});
|
|
5045
|
+
}
|
|
5046
|
+
const nodes = tasks.filter((t) => !isTerminal3(t) && !cyclic.has(t.id));
|
|
5047
|
+
const indeg = new Map;
|
|
5048
|
+
for (const t of nodes)
|
|
5049
|
+
indeg.set(t.id, t.deps.filter((d) => nodes.some((n) => n.id === d)).length);
|
|
5050
|
+
const readyQueue = nodes.filter((t) => (indeg.get(t.id) ?? 0) === 0);
|
|
5051
|
+
const key = (t) => {
|
|
5052
|
+
const d = byIdOut.get(t.id);
|
|
5053
|
+
return [
|
|
5054
|
+
prioIndex(d.eff_prio),
|
|
5055
|
+
t.due ?? "9999-12-31",
|
|
5056
|
+
d.critical ? 0 : 1,
|
|
5057
|
+
d.slice ? 0 : 1,
|
|
5058
|
+
-d.unblocks,
|
|
5059
|
+
d.depth,
|
|
5060
|
+
t.created_at,
|
|
5061
|
+
t.id
|
|
5062
|
+
];
|
|
5063
|
+
};
|
|
5064
|
+
const cmp = (a, b) => {
|
|
5065
|
+
const ka = key(a), kb = key(b);
|
|
5066
|
+
for (let i = 0;i < ka.length; i++) {
|
|
5067
|
+
if (ka[i] < kb[i])
|
|
5068
|
+
return -1;
|
|
5069
|
+
if (ka[i] > kb[i])
|
|
5070
|
+
return 1;
|
|
5071
|
+
}
|
|
5072
|
+
return 0;
|
|
5073
|
+
};
|
|
5074
|
+
let rank = 0;
|
|
5075
|
+
const pending = [...readyQueue].sort(cmp);
|
|
5076
|
+
while (pending.length) {
|
|
5077
|
+
const t = pending.shift();
|
|
5078
|
+
byIdOut.get(t.id).rank = ++rank;
|
|
5079
|
+
for (const d of dependents.get(t.id) || []) {
|
|
5080
|
+
const n = indeg.get(d);
|
|
5081
|
+
if (n === undefined)
|
|
5082
|
+
continue;
|
|
5083
|
+
indeg.set(d, n - 1);
|
|
5084
|
+
if (n - 1 === 0)
|
|
5085
|
+
pending.push(byId.get(d));
|
|
5086
|
+
}
|
|
5087
|
+
pending.sort(cmp);
|
|
5088
|
+
}
|
|
5089
|
+
const lint = [];
|
|
5090
|
+
for (const t of tasks) {
|
|
5091
|
+
const d = byIdOut.get(t.id);
|
|
5092
|
+
for (const x of d.dangling_deps)
|
|
5093
|
+
lint.push({ kind: "dangling_dep", id: t.id, detail: `依赖不存在:${x}` });
|
|
5094
|
+
for (const x of d.released_by)
|
|
5095
|
+
lint.push({ kind: "released_by_drop", id: t.id, detail: `依赖已 dropped:${x}(前提失效,确认是否还需要做)` });
|
|
5096
|
+
if (PROPAGATE.has(t.prio ?? "") && !isTerminal3(t) && d.depth >= 3) {
|
|
5097
|
+
lint.push({ kind: "long_chain", id: t.id, detail: `依赖链 ${d.depth} 跳,考虑拆分` });
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
for (const c of cycles)
|
|
5101
|
+
lint.push({ kind: "cycle", id: c[0], detail: `环:${c.join(" → ")}` });
|
|
5102
|
+
return { byId: byIdOut, cycles, lint, ranking: { policy: "kahn-v1" } };
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4688
5105
|
// src/graph.ts
|
|
5106
|
+
var DERIVED_PROGRESS = { todo: 0, doing: 50, review: 80, done: 100, accepted: 100, blocked: 0, dropped: 0, rejected: 0, escalated: 50, awaiting_acceptance: 90 };
|
|
4689
5107
|
var taskNode = (id) => `task:${id}`;
|
|
4690
5108
|
var sessionNode = (id) => `session:${id}`;
|
|
4691
5109
|
var pathNode = (p) => `path:${p}`;
|
|
4692
|
-
function
|
|
5110
|
+
function findCycles2(tasks) {
|
|
4693
5111
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4694
5112
|
const state = new Map;
|
|
4695
5113
|
const stack = [];
|
|
@@ -4723,13 +5141,46 @@ function findCycles(tasks) {
|
|
|
4723
5141
|
}
|
|
4724
5142
|
function build(opts = {}) {
|
|
4725
5143
|
const tasks = list4();
|
|
5144
|
+
const derived = derive(tasks);
|
|
4726
5145
|
const active = list3();
|
|
4727
5146
|
const nodes = new Map;
|
|
4728
5147
|
const edges = [];
|
|
4729
5148
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4730
5149
|
for (const t of tasks) {
|
|
4731
|
-
const blocked = t.deps.filter((d) => byId.get(d)
|
|
4732
|
-
|
|
5150
|
+
const blocked = t.deps.filter((d) => !isSatisfied(byId.get(d)));
|
|
5151
|
+
const d = derived.byId.get(t.id);
|
|
5152
|
+
nodes.set(taskNode(t.id), {
|
|
5153
|
+
id: taskNode(t.id),
|
|
5154
|
+
kind: "task",
|
|
5155
|
+
label: t.title,
|
|
5156
|
+
status: t.status,
|
|
5157
|
+
owner: t.owner,
|
|
5158
|
+
...blocked.length ? { blocked_by: blocked } : {},
|
|
5159
|
+
...t.prio ? { prio: t.prio } : {},
|
|
5160
|
+
...t.start ? { start: t.start } : {},
|
|
5161
|
+
...t.due ? { due: t.due } : {},
|
|
5162
|
+
progress: t.progress ?? (DERIVED_PROGRESS[t.status] ?? 0),
|
|
5163
|
+
progress_derived: t.progress === undefined,
|
|
5164
|
+
created_at: t.created_at,
|
|
5165
|
+
updated_at: t.updated_at,
|
|
5166
|
+
...t.closed_at ? { closed_at: t.closed_at } : {},
|
|
5167
|
+
deps: t.deps,
|
|
5168
|
+
touches: t.touches,
|
|
5169
|
+
eff_prio: d.eff_prio,
|
|
5170
|
+
prio_inherited: d.prio_inherited,
|
|
5171
|
+
...d.prio_source ? { prio_source: d.prio_source } : {},
|
|
5172
|
+
critical: d.critical,
|
|
5173
|
+
ready: d.ready,
|
|
5174
|
+
startable: d.startable,
|
|
5175
|
+
released_by: d.released_by,
|
|
5176
|
+
dangling_deps: d.dangling_deps,
|
|
5177
|
+
depth: d.depth,
|
|
5178
|
+
unblocks: d.unblocks,
|
|
5179
|
+
rank: d.rank,
|
|
5180
|
+
...d.split_from ? { split_from: d.split_from } : {},
|
|
5181
|
+
...d.split_children ? { split_children: d.split_children } : {},
|
|
5182
|
+
...d.slice ? { slice: true } : {}
|
|
5183
|
+
});
|
|
4733
5184
|
if (t.owner)
|
|
4734
5185
|
nodes.set(sessionNode(t.owner), { id: sessionNode(t.owner), kind: "session", label: t.owner });
|
|
4735
5186
|
for (const d of t.deps) {
|
|
@@ -4770,7 +5221,7 @@ function build(opts = {}) {
|
|
|
4770
5221
|
}
|
|
4771
5222
|
filteredNodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
4772
5223
|
filteredEdges.sort((a, b) => (a.from + "\x00" + a.to + "\x00" + a.kind).localeCompare(b.from + "\x00" + b.to + "\x00" + b.kind));
|
|
4773
|
-
return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles:
|
|
5224
|
+
return { schema: 1, capabilities: ["prio", "timeline", "progress", "eff-prio", "ready", "rank", "split"], ranking: derived.ranking, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles2(tasks) };
|
|
4774
5225
|
}
|
|
4775
5226
|
|
|
4776
5227
|
// src/graph-tui.ts
|
|
@@ -4858,6 +5309,842 @@ function watch(build, opts = {}) {
|
|
|
4858
5309
|
return new Promise(() => {});
|
|
4859
5310
|
}
|
|
4860
5311
|
|
|
5312
|
+
// src/graph-html/template.html
|
|
5313
|
+
var template_default = `<!doctype html>
|
|
5314
|
+
<html lang="zh">
|
|
5315
|
+
<head>
|
|
5316
|
+
<meta charset="utf-8">
|
|
5317
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5318
|
+
<title>dia graph · 快照</title>
|
|
5319
|
+
<style>
|
|
5320
|
+
:root{
|
|
5321
|
+
--bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
|
|
5322
|
+
--blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
|
|
5323
|
+
--shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
|
|
5324
|
+
}
|
|
5325
|
+
@media (prefers-color-scheme: dark){
|
|
5326
|
+
:root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
|
|
5327
|
+
--shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
|
|
5328
|
+
}
|
|
5329
|
+
*{box-sizing:border-box}
|
|
5330
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
5331
|
+
header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
|
|
5332
|
+
header h1{font-size:17px;margin:0;letter-spacing:.2px}
|
|
5333
|
+
header .sub{color:var(--muted);font-size:12px}
|
|
5334
|
+
header .spacer{flex:1}
|
|
5335
|
+
.tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
5336
|
+
.tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
|
|
5337
|
+
.tabs button.on{background:var(--blue);color:#fff}
|
|
5338
|
+
.pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
|
|
5339
|
+
main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
|
|
5340
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
|
|
5341
|
+
.toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
5342
|
+
.seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
5343
|
+
.seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
5344
|
+
.seg button.on{background:var(--blue);color:#fff}
|
|
5345
|
+
.seg button:disabled{opacity:.35;cursor:not-allowed}
|
|
5346
|
+
.win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
|
|
5347
|
+
.search{position:relative;display:flex;align-items:center;gap:4px;flex-wrap:wrap;border:1px solid var(--line);background:var(--bg);border-radius:8px;padding:4px 8px;min-width:240px;max-width:420px;cursor:text}
|
|
5348
|
+
.search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
|
|
5349
|
+
.token{display:inline-flex;align-items:center;gap:4px;background:color-mix(in srgb,var(--blue) 22%,transparent);color:var(--blue);border-radius:999px;padding:2px 4px 2px 8px;font-size:11px;white-space:nowrap}
|
|
5350
|
+
.token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
|
|
5351
|
+
.search .q-list{position:absolute;left:0;top:calc(100% + 4px);z-index:20;width:300px;max-height:250px;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);display:none}
|
|
5352
|
+
.search .q-list.open{display:block}
|
|
5353
|
+
.q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
5354
|
+
.q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
|
|
5355
|
+
.q-item .k{color:var(--muted)}
|
|
5356
|
+
.q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
|
|
5357
|
+
.q-item.sel{color:var(--blue)}
|
|
5358
|
+
.q-item.sel .k{color:var(--blue)}
|
|
5359
|
+
.q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
|
|
5360
|
+
.q-empty{padding:10px;font-size:12px;color:var(--muted)}
|
|
5361
|
+
.node.hit{outline:2px solid var(--blue);outline-offset:1px}
|
|
5362
|
+
.node.dim{opacity:.22}
|
|
5363
|
+
.focus-chip{display:none;align-items:center;gap:4px;border:1px solid var(--blue);color:var(--blue);border-radius:999px;padding:3px 4px 3px 10px;font-size:11px}
|
|
5364
|
+
.focus-chip.on{display:inline-flex}
|
|
5365
|
+
.focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
|
|
5366
|
+
.focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
|
|
5367
|
+
.toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
|
|
5368
|
+
.canvas{position:relative;height:620px;overflow:hidden}
|
|
5369
|
+
.canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
|
|
5370
|
+
.lane-rail{position:absolute;left:0;top:0;bottom:0;width:110px;z-index:6;pointer-events:none;background:linear-gradient(90deg,var(--panel) 86%,transparent)}
|
|
5371
|
+
svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
|
|
5372
|
+
.node{position:absolute;width:190px;border:1px solid var(--line);border-left:1px solid var(--line);border-radius:10px;background:var(--panel);padding:8px 10px;box-shadow:var(--shadow);cursor:pointer}
|
|
5373
|
+
.node.path{border-left-color:var(--violet)}
|
|
5374
|
+
.node.session{border-left-color:var(--cyan)}
|
|
5375
|
+
.node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5376
|
+
.node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5377
|
+
.progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
|
|
5378
|
+
.progress i{display:block;height:100%;background:var(--blue);background-image:linear-gradient(45deg,rgba(255,255,255,.28) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.28) 50%,rgba(255,255,255,.28) 75%,transparent 75%);background-size:12px 12px;animation:flow 1s linear infinite}
|
|
5379
|
+
.progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
|
|
5380
|
+
.progress i.review{background-color:var(--amber)}
|
|
5381
|
+
.progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
|
|
5382
|
+
.progress i.todo{background-color:var(--muted);background-image:none;animation:none}
|
|
5383
|
+
@keyframes flow{to{background-position:12px 0}}
|
|
5384
|
+
.st{font-weight:500}
|
|
5385
|
+
.st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
|
|
5386
|
+
.st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
|
|
5387
|
+
.prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
|
|
5388
|
+
.prio{background:transparent;border:1px solid var(--line)}
|
|
5389
|
+
.prio.p0{background:var(--red);border-color:var(--red);color:#fff}
|
|
5390
|
+
.prio.p1{border-color:var(--amber);color:var(--amber)}
|
|
5391
|
+
.prio.p2{border-color:var(--line);color:var(--muted)}
|
|
5392
|
+
.pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
|
|
5393
|
+
.plane-label{position:absolute;left:20px;width:78px;z-index:6;pointer-events:none;padding:2px 6px 2px 0;background:linear-gradient(90deg,var(--panel) 72%,transparent);transform:translateY(-50%);font-size:13px;font-weight:700;letter-spacing:.1em}
|
|
5394
|
+
.plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
|
|
5395
|
+
.legend-overlay{position:absolute;right:12px;bottom:12px;z-index:5;background:color-mix(in srgb,var(--panel) 92%,transparent);border:1px solid var(--line);border-radius:10px;padding:8px 10px;box-shadow:var(--shadow);font-size:11px;color:var(--muted);display:grid;gap:5px;min-width:190px}
|
|
5396
|
+
.legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
|
|
5397
|
+
.legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
|
|
5398
|
+
.legend-overlay b{color:var(--ink);font-weight:600}
|
|
5399
|
+
.canvas.simple .legend-overlay .detail-row{display:none}
|
|
5400
|
+
aside{display:flex;flex-direction:column;gap:14px}
|
|
5401
|
+
.side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
|
|
5402
|
+
.side-title h2{font-size:13px;margin:0}
|
|
5403
|
+
.side-title span{font-size:11px;color:var(--muted)}
|
|
5404
|
+
.list{padding:6px 14px 12px}
|
|
5405
|
+
.item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5406
|
+
.item:last-child{border-bottom:0}
|
|
5407
|
+
.item .k{color:var(--muted)}
|
|
5408
|
+
.attention{padding:8px 14px 14px}
|
|
5409
|
+
.attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5410
|
+
.attention .row:last-child{border-bottom:0}
|
|
5411
|
+
.tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
|
|
5412
|
+
.tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
|
|
5413
|
+
.attention .row{cursor:pointer}
|
|
5414
|
+
.quick{display:flex;gap:6px;margin-top:6px}
|
|
5415
|
+
.quick button,.btn{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:7px;padding:3px 8px;font:inherit;font-size:11px;cursor:pointer}
|
|
5416
|
+
.quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
5417
|
+
.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
|
|
5418
|
+
.backdrop.show{opacity:1;pointer-events:auto}
|
|
5419
|
+
.drawer{position:fixed;top:0;right:0;bottom:0;width:440px;max-width:92vw;background:var(--panel);border-left:1px solid var(--line);box-shadow:-12px 0 40px rgba(0,0,0,.25);transform:translateX(100%);transition:transform .2s ease;z-index:50;display:flex;flex-direction:column}
|
|
5420
|
+
.drawer.show{transform:none}
|
|
5421
|
+
.drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
|
|
5422
|
+
.drawer-head h3{font-size:15px;margin:0 0 3px}
|
|
5423
|
+
.drawer-head .meta{color:var(--muted);font-size:11px}
|
|
5424
|
+
.drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
|
|
5425
|
+
.drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
|
|
5426
|
+
.drawer-tabs button{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
5427
|
+
.drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
5428
|
+
.drawer-body{padding:14px 16px 24px;overflow:auto}
|
|
5429
|
+
.drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
|
|
5430
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
5431
|
+
.kv .k{color:var(--muted)}
|
|
5432
|
+
.iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
|
|
5433
|
+
.iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
|
|
5434
|
+
.iv-seg button.on{background:var(--blue);color:#fff}
|
|
5435
|
+
.iv-row{display:grid;gap:6px;margin-bottom:10px}
|
|
5436
|
+
.iv-row label{font-size:11px;color:var(--muted)}
|
|
5437
|
+
.iv-row select,.iv-row textarea{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--ink);padding:8px;font:inherit;font-size:12px}
|
|
5438
|
+
.iv-row textarea{min-height:96px;resize:vertical}
|
|
5439
|
+
.iv-status{color:var(--green);font-size:11px;margin-left:8px}
|
|
5440
|
+
.rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5441
|
+
.rel-list .item:last-child{border-bottom:0}
|
|
5442
|
+
.rel-list .k{color:var(--muted)}
|
|
5443
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
5444
|
+
.kv .k{color:var(--muted)}
|
|
5445
|
+
/* timeline */
|
|
5446
|
+
.timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
|
|
5447
|
+
.tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
|
|
5448
|
+
.tl-labels{overflow:hidden;position:relative}
|
|
5449
|
+
.tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
|
|
5450
|
+
.tl-labels-body{will-change:transform}
|
|
5451
|
+
.tl-label-row{height:38px;box-sizing:border-box;padding:6px 8px 4px 0;border-top:1px dashed var(--line);font-size:12px;overflow:hidden}
|
|
5452
|
+
.tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5453
|
+
.tl-label-row span{color:var(--muted);font-size:10px}
|
|
5454
|
+
.tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
|
|
5455
|
+
.scroll-hint{position:absolute;right:18px;top:10px;z-index:6;background:var(--panel);border:1px solid var(--line);border-radius:999px;padding:3px 9px;font-size:10px;color:var(--muted);box-shadow:var(--shadow)}
|
|
5456
|
+
.tl-head{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:end;color:var(--muted);font-size:11px;margin-bottom:8px;position:sticky;top:0;z-index:4;background:var(--panel)}
|
|
5457
|
+
.tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
|
|
5458
|
+
.tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
|
|
5459
|
+
.tl-axis span small{display:block;font-size:9px;opacity:.55}
|
|
5460
|
+
.tl-body{position:relative}
|
|
5461
|
+
.tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
|
|
5462
|
+
.tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
|
|
5463
|
+
.tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
|
|
5464
|
+
.tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
|
|
5465
|
+
.tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
|
|
5466
|
+
.tl-bar.review{background:var(--amber)}
|
|
5467
|
+
.tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
|
|
5468
|
+
.tl-bar.open{border-right:2px dashed var(--muted);opacity:.8}
|
|
5469
|
+
.tl-today{position:absolute;top:0;bottom:0;pointer-events:none;z-index:2;border-left:1px dashed var(--red);color:var(--red);font-size:10px;padding-left:4px}
|
|
5470
|
+
.tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
|
|
5471
|
+
footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
|
|
5472
|
+
@media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
|
|
5473
|
+
</style>
|
|
5474
|
+
</head>
|
|
5475
|
+
<body>
|
|
5476
|
+
<header>
|
|
5477
|
+
<h1>dia graph</h1>
|
|
5478
|
+
<span class="sub">只读依赖图 + 时间线 · 静态快照</span>
|
|
5479
|
+
<div class="tabs" id="tabs">
|
|
5480
|
+
<button class="on" data-v="graph">关系图</button>
|
|
5481
|
+
<button data-v="time">时间线</button>
|
|
5482
|
+
</div>
|
|
5483
|
+
<span class="spacer"></span>
|
|
5484
|
+
<span class="pill" id="trunc-pill" style="display:none"></span>
|
|
5485
|
+
<span class="pill">静态快照 · 无网络</span>
|
|
5486
|
+
<span class="pill" id="route">#/graph</span>
|
|
5487
|
+
<span class="pill" id="updated">-</span>
|
|
5488
|
+
</header>
|
|
5489
|
+
|
|
5490
|
+
<main>
|
|
5491
|
+
<section class="panel">
|
|
5492
|
+
<div class="toolbar">
|
|
5493
|
+
<div id="graph-tools" style="display:flex;gap:8px;align-items:center">
|
|
5494
|
+
<div class="seg" id="layout">
|
|
5495
|
+
<button class="on" data-l="priority">优先级视图</button>
|
|
5496
|
+
<button data-l="dep">依赖视图</button>
|
|
5497
|
+
</div>
|
|
5498
|
+
<div class="seg" id="mode">
|
|
5499
|
+
<button class="on" data-m="simple">简约</button>
|
|
5500
|
+
<button data-m="detail">详细</button>
|
|
5501
|
+
</div>
|
|
5502
|
+
<div class="search" id="search-box">
|
|
5503
|
+
<span id="tokens" style="display:contents"></span>
|
|
5504
|
+
<input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
|
|
5505
|
+
<div class="q-list" id="q-list"></div>
|
|
5506
|
+
</div>
|
|
5507
|
+
<span class="focus-chip" id="focus-chip"><select id="focus-fd" class="fd"><option value="1">1 层</option><option value="2" selected>2 层</option><option value="all">全链路</option></select><button id="focus-exit">清空</button></span>
|
|
5508
|
+
</div>
|
|
5509
|
+
<div id="time-tools" style="display:none;gap:8px;align-items:center">
|
|
5510
|
+
<div class="seg" id="zoom">
|
|
5511
|
+
<button data-z="-">-</button>
|
|
5512
|
+
<button data-z="reset">100%</button>
|
|
5513
|
+
<button data-z="+">+</button>
|
|
5514
|
+
</div>
|
|
5515
|
+
</div>
|
|
5516
|
+
<select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
|
|
5517
|
+
<option value="7">近 7 天</option>
|
|
5518
|
+
<option value="14">近 14 天</option>
|
|
5519
|
+
<option value="30">近 30 天</option>
|
|
5520
|
+
<option value="0">全部时间</option>
|
|
5521
|
+
</select>
|
|
5522
|
+
<span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
|
|
5523
|
+
</div>
|
|
5524
|
+
<div id="view-graph">
|
|
5525
|
+
<div class="canvas" id="canvas">
|
|
5526
|
+
<div class="canvas-scroll" id="canvas-scroll">
|
|
5527
|
+
<svg class="edges" id="edges"></svg>
|
|
5528
|
+
</div>
|
|
5529
|
+
<div class="lane-rail" id="lane-rail"></div>
|
|
5530
|
+
<div class="legend-overlay" id="legend">
|
|
5531
|
+
<div class="row"><svg class="sample" viewBox="0 0 38 10"><defs><marker id="lg-dep" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto"><path d="M0,0 L6,3 L0,6" fill="#2563eb"/></marker></defs><line x1="1" y1="5" x2="30" y2="5" stroke="#2563eb" stroke-width="1.6" marker-end="url(#lg-dep)"/></svg><span><b>→ depends_on</b> 单向</span></div>
|
|
5532
|
+
</div>
|
|
5533
|
+
</div>
|
|
5534
|
+
</div>
|
|
5535
|
+
<div id="view-time" style="display:none">
|
|
5536
|
+
<div class="timeline">
|
|
5537
|
+
<div class="scroll-hint">← 左右滚动 →</div>
|
|
5538
|
+
<div class="tl-grid">
|
|
5539
|
+
<div class="tl-labels">
|
|
5540
|
+
<div class="tl-label-head">任务</div>
|
|
5541
|
+
<div class="tl-labels-body" id="tl-labels-body"></div>
|
|
5542
|
+
</div>
|
|
5543
|
+
<div class="tl-scroll" id="tl-scroll">
|
|
5544
|
+
<div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
|
|
5545
|
+
<div class="tl-body" id="tl-body"></div>
|
|
5546
|
+
</div>
|
|
5547
|
+
</div>
|
|
5548
|
+
<div class="tl-caption">红色虚线 = 今天;虚线右端 = 未录入 due 的开放任务;斜纹 = 进度按状态估算。</div>
|
|
5549
|
+
</div>
|
|
5550
|
+
</div>
|
|
5551
|
+
</section>
|
|
5552
|
+
|
|
5553
|
+
<aside>
|
|
5554
|
+
<div class="panel">
|
|
5555
|
+
<div class="side-title"><h2>概览</h2><span>snapshot</span></div>
|
|
5556
|
+
<div class="list">
|
|
5557
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">0</b></div>
|
|
5558
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
|
|
5559
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
|
|
5560
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
|
|
5561
|
+
<div class="item"><span class="k">claims</span><b id="stat-claims">0</b></div>
|
|
5562
|
+
</div>
|
|
5563
|
+
</div>
|
|
5564
|
+
<div class="panel">
|
|
5565
|
+
<div class="side-title"><h2>需要人</h2><span>attention</span></div>
|
|
5566
|
+
<div class="attention" id="attention-list"></div>
|
|
5567
|
+
</div>
|
|
5568
|
+
</aside>
|
|
5569
|
+
</main>
|
|
5570
|
+
<div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
|
|
5571
|
+
<aside class="drawer" id="drawer" aria-hidden="true">
|
|
5572
|
+
<div class="drawer-head">
|
|
5573
|
+
<div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
|
|
5574
|
+
<button class="close" onclick="closeDrawer()">×</button>
|
|
5575
|
+
</div>
|
|
5576
|
+
<div class="drawer-tabs">
|
|
5577
|
+
<button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
|
|
5578
|
+
<button data-dtab="intervene" onclick="setDrawerTab('intervene')">命令</button>
|
|
5579
|
+
</div>
|
|
5580
|
+
<div class="drawer-body" id="drawer-body"></div>
|
|
5581
|
+
</aside>
|
|
5582
|
+
<footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
|
|
5583
|
+
|
|
5584
|
+
<script type="application/json" id="dia-graph">__DIA_PAYLOAD__</script>
|
|
5585
|
+
<script>
|
|
5586
|
+
const payload=(()=>{ try{ const el=document.getElementById('dia-graph'); return JSON.parse(el?el.textContent:'{}')||{}; }catch{ return {}; } })();
|
|
5587
|
+
const TODAY=payload.today||'1970-01-01';
|
|
5588
|
+
const STATUSES=new Set(['todo','doing','review','blocked','done','accepted','dropped','awaiting_acceptance','rejected','escalated']);
|
|
5589
|
+
const tasks=(payload.tasks||[]).map(t=>{
|
|
5590
|
+
const status=STATUSES.has(t.status)?t.status:'todo';
|
|
5591
|
+
const progress=Number.isFinite(t.progress)?Math.max(0,Math.min(100,Math.round(t.progress))):0;
|
|
5592
|
+
return {
|
|
5593
|
+
id:String(t.id), label:String(t.label||t.id), status,
|
|
5594
|
+
prio:String(t.prio||'P?'), progress, derived:!!t.derived,
|
|
5595
|
+
start:Number.isFinite(t.start)?t.start:null,
|
|
5596
|
+
end:Number.isFinite(t.end)?t.end:null,
|
|
5597
|
+
owner:t.owner?String(t.owner):null,
|
|
5598
|
+
deps:Array.isArray(t.deps)?t.deps.map(String):[],
|
|
5599
|
+
touches:Array.isArray(t.touches)?t.touches.map(String):[],
|
|
5600
|
+
};
|
|
5601
|
+
});
|
|
5602
|
+
const taskEdges=(payload.edges||[]).filter(e=>e&&e.kind==='depends_on').map(e=>({from:String(e.from),to:String(e.to),kind:'depends_on'}));
|
|
5603
|
+
const truncated=payload.truncated||null;
|
|
5604
|
+
const state={view:'graph', layout:'priority', mode:'simple', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
|
|
5605
|
+
const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
|
|
5606
|
+
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
5607
|
+
const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○',dropped:'⨯',rejected:'✗',escalated:'▲',awaiting_acceptance:'…'};
|
|
5608
|
+
const stIconOf=s=>stIcon[s]||'·';
|
|
5609
|
+
|
|
5610
|
+
function focusSet(id,depth){
|
|
5611
|
+
const by=new Map(tasks.map(t=>[t.id,t]));
|
|
5612
|
+
const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
|
|
5613
|
+
const out=new Set([id]); let frontier=[id];
|
|
5614
|
+
for(let step=0; step<max && frontier.length; step++){
|
|
5615
|
+
const next=[];
|
|
5616
|
+
frontier.forEach(cur=>{
|
|
5617
|
+
const t=by.get(cur); if(!t) return;
|
|
5618
|
+
(t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
|
|
5619
|
+
tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
|
|
5620
|
+
});
|
|
5621
|
+
frontier=next;
|
|
5622
|
+
}
|
|
5623
|
+
return out;
|
|
5624
|
+
}
|
|
5625
|
+
function visibleTasks(){
|
|
5626
|
+
const base=tasks.filter(t=>state.window<=0 || t.start===null || t.start>=-state.window);
|
|
5627
|
+
if(!state.sel.length) return base;
|
|
5628
|
+
const keep=new Set();
|
|
5629
|
+
state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
|
|
5630
|
+
return base.filter(t=>keep.has(t.id));
|
|
5631
|
+
}
|
|
5632
|
+
function searchMatches(){
|
|
5633
|
+
const q=(state.q||'').trim().toLowerCase();
|
|
5634
|
+
if(!q) return null;
|
|
5635
|
+
return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
|
|
5636
|
+
}
|
|
5637
|
+
function syncStats(){
|
|
5638
|
+
const list=visibleTasks(), st=id=>document.getElementById(id);
|
|
5639
|
+
if(!st('stat-tasks')) return;
|
|
5640
|
+
st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
|
|
5641
|
+
st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
|
|
5642
|
+
st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
|
|
5643
|
+
st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
|
|
5644
|
+
if(st('stat-claims')) st('stat-claims').textContent=String((payload.claims||[]).length);
|
|
5645
|
+
renderAttention(list);
|
|
5646
|
+
const tp=document.getElementById('trunc-pill');
|
|
5647
|
+
if(tp){ if(truncated&&truncated.shown<truncated.total){ tp.style.display=''; tp.textContent='已截断 '+truncated.shown+' / '+truncated.total+'(--all 全量)'; } else tp.style.display='none'; }
|
|
5648
|
+
}
|
|
5649
|
+
function renderAttention(list){
|
|
5650
|
+
const box=document.getElementById('attention-list'); if(!box) return;
|
|
5651
|
+
const rows=[];
|
|
5652
|
+
list.filter(t=>t.status==='blocked').slice(0,3).forEach(t=>rows.push({tag:'阻塞',cls:'',t,note:'blocked'}));
|
|
5653
|
+
list.filter(t=>t.status==='review').slice(0,3).forEach(t=>rows.push({tag:'评审',cls:' amber',t,note:'等待评审'}));
|
|
5654
|
+
box.innerHTML=rows.length?rows.map(r=>'<div class="row" data-task="'+esc(r.t.id)+'"><span class="tag'+r.cls+'">'+r.tag+'</span><div><b>'+esc(r.t.label)+'</b><br><span class="k">'+esc(r.t.id)+' · '+esc(r.note)+'</span><div class="quick"><button class="primary" onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'intervene\\')">命令</button><button onclick="event.stopPropagation();openDrawer(\\''+esc(r.t.id)+'\\',\\'task\\')">查看</button></div></div></div>').join('')
|
|
5655
|
+
:'<p class="k" style="padding:8px 0">当前没有阻塞或待评审任务。</p>';
|
|
5656
|
+
box.querySelectorAll('.row[data-task]').forEach(el=>el.onclick=()=>openDrawer(el.dataset.task,'task'));
|
|
5657
|
+
}
|
|
5658
|
+
function renderEmpty(){
|
|
5659
|
+
if(tasks.length) return;
|
|
5660
|
+
const el=document.getElementById('canvas-scroll');
|
|
5661
|
+
if(el) el.innerHTML='<div style="padding:60px 20px;text-align:center;color:var(--muted)">账本里还没有任务。<br>先跑 <code>dia task add <title> --prio P1</code>,再重新导出。</div>';
|
|
5662
|
+
}
|
|
5663
|
+
function updateHint(){
|
|
5664
|
+
const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
|
|
5665
|
+
const n=visibleTasks().length;
|
|
5666
|
+
if(state.sel.length){
|
|
5667
|
+
h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
|
|
5671
|
+
}
|
|
5672
|
+
function depths(){
|
|
5673
|
+
const list=visibleTasks();
|
|
5674
|
+
const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
|
|
5675
|
+
const depth=t=>{
|
|
5676
|
+
if(!t) return 0;
|
|
5677
|
+
if(memo.has(t.id)) return memo.get(t.id);
|
|
5678
|
+
memo.set(t.id,0);
|
|
5679
|
+
const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
|
|
5680
|
+
memo.set(t.id,d); return d;
|
|
5681
|
+
};
|
|
5682
|
+
list.forEach(depth); return memo;
|
|
5683
|
+
}
|
|
5684
|
+
function levels(){
|
|
5685
|
+
return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
|
|
5686
|
+
const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
|
|
5687
|
+
return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
|
|
5688
|
+
});
|
|
5689
|
+
}
|
|
5690
|
+
function rowsFor(){
|
|
5691
|
+
const list=visibleTasks(), d=depths();
|
|
5692
|
+
const gap=state.mode==='detail'?230:145;
|
|
5693
|
+
const h=state.mode==='detail'?205:110;
|
|
5694
|
+
return levels().map((prio,i)=>{
|
|
5695
|
+
const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
|
|
5696
|
+
return {prio,y:36+i*gap,h,tasks:rowTasks};
|
|
5697
|
+
}).filter(r=>r.tasks.length);
|
|
5698
|
+
}
|
|
5699
|
+
let cardMetrics={mid:52,h:58}, clickTimer=null;
|
|
5700
|
+
function measureCard(){
|
|
5701
|
+
const probe=card(tasks[0],-9999,-9999);
|
|
5702
|
+
probe.style.visibility='hidden';
|
|
5703
|
+
layer.appendChild(probe);
|
|
5704
|
+
const prog=probe.querySelector('.progress');
|
|
5705
|
+
const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
|
|
5706
|
+
const h=probe.offsetHeight||58;
|
|
5707
|
+
probe.remove();
|
|
5708
|
+
return {mid,h};
|
|
5709
|
+
}
|
|
5710
|
+
function bandCenter(r, bandH){ return r.y-12+bandH/2; }
|
|
5711
|
+
function cardTop(r){
|
|
5712
|
+
const bandH=state.mode==='detail'?230:145;
|
|
5713
|
+
const center=bandCenter(r,bandH);
|
|
5714
|
+
// 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
|
|
5715
|
+
return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
|
|
5716
|
+
}
|
|
5717
|
+
function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
|
|
5718
|
+
function pos(){
|
|
5719
|
+
const rows=rowsFor(), p=new Map();
|
|
5720
|
+
rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
|
|
5721
|
+
return {p,rows};
|
|
5722
|
+
}
|
|
5723
|
+
function card(t,x,y,showPrio=false){
|
|
5724
|
+
const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
|
|
5725
|
+
const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
5726
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
5727
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
5728
|
+
return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
|
|
5729
|
+
}).slice(0,4).join('');
|
|
5730
|
+
const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
|
|
5731
|
+
el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
|
|
5732
|
+
'<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIconOf(t.status)+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
|
|
5733
|
+
'<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
|
|
5734
|
+
(state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
|
|
5735
|
+
el.dataset.id=t.id;
|
|
5736
|
+
el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
|
|
5737
|
+
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
|
|
5738
|
+
return el;
|
|
5739
|
+
}
|
|
5740
|
+
function edgePath(a,b){
|
|
5741
|
+
const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
|
|
5742
|
+
if(x2>=x1) return \`M\${x1} \${y1} H\${x1+Math.max(24,(x2-x1)/2)} V\${y2} H\${x2}\`;
|
|
5743
|
+
return \`M\${x1} \${y1} H\${x1-30} V\${y2} H\${x2}\`;
|
|
5744
|
+
}
|
|
5745
|
+
function drawGraph(){
|
|
5746
|
+
canvas.classList.toggle('simple', state.mode==='simple');
|
|
5747
|
+
if(state.layout==='dep'){ drawDepGraph(); return; }
|
|
5748
|
+
cardMetrics=measureCard();
|
|
5749
|
+
const {p,rows}=pos();
|
|
5750
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
5751
|
+
rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
|
|
5752
|
+
const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
|
|
5753
|
+
const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
|
|
5754
|
+
const bandH=state.mode==='detail'?230:145;
|
|
5755
|
+
rows.forEach((r,i)=>{
|
|
5756
|
+
const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
|
|
5757
|
+
g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
|
|
5758
|
+
const alpha=Math.max(0.02, 0.075-i*0.012);
|
|
5759
|
+
g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
|
|
5760
|
+
layer.appendChild(g);
|
|
5761
|
+
const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
|
|
5762
|
+
label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
|
|
5763
|
+
const meta=r.prio==='P0'?['var(--red)','紧急']:r.prio==='P1'?['var(--amber)','高']:r.prio==='P?'?['var(--muted)','未排期']:['var(--muted)','常规'];
|
|
5764
|
+
label.style.color=meta[0];
|
|
5765
|
+
label.innerHTML=esc(r.prio)+' · '+r.tasks.length+'<small>'+meta[1]+'</small>';
|
|
5766
|
+
rail.appendChild(label);
|
|
5767
|
+
r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
|
|
5768
|
+
});
|
|
5769
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
|
|
5770
|
+
const defs=\`<defs>
|
|
5771
|
+
<marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
|
|
5772
|
+
<marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
|
|
5773
|
+
</defs>\`;
|
|
5774
|
+
const lines=taskEdges.map(e=>{
|
|
5775
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
5776
|
+
return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
|
|
5777
|
+
}).join('');
|
|
5778
|
+
svg.innerHTML=defs+lines;
|
|
5779
|
+
document.getElementById('updated').textContent='快照 '+(payload.generated_at||'-');
|
|
5780
|
+
syncStats(); updateHint(); applySearch();
|
|
5781
|
+
}
|
|
5782
|
+
let drawerTask=null, drawerTab='task';
|
|
5783
|
+
function relations(t){
|
|
5784
|
+
return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
5785
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
5786
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
5787
|
+
return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
|
|
5788
|
+
}).join('') || '<p class="k">无边</p>';
|
|
5789
|
+
}
|
|
5790
|
+
function taskHtml(t){
|
|
5791
|
+
return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
|
|
5792
|
+
'<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
|
|
5793
|
+
'<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
|
|
5794
|
+
'<div class="k">start</div><div>'+(t.start===null?'-(按创建时间)':esc(isoDate(t.start)))+'</div>'+
|
|
5795
|
+
'<div class="k">due</div><div>'+(Number.isFinite(t.end)?esc(isoDate(t.end)):'开放')+'</div>'+
|
|
5796
|
+
'<div class="k">progress</div><div>'+t.progress+'%'+(t.derived?'(估算)':'')+'</div>'+
|
|
5797
|
+
'<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div>'+
|
|
5798
|
+
'<div class="k">touches</div><div>'+esc(t.touches.join(', ')||'-')+'</div></div>'+
|
|
5799
|
+
'<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
|
|
5800
|
+
}
|
|
5801
|
+
function commandHtml(t){
|
|
5802
|
+
const cmds=[
|
|
5803
|
+
['dia task plan '+t.id+' --prio P1', '设置优先级'],
|
|
5804
|
+
['dia task plan '+t.id+' --start '+isoDate(0)+' --due '+isoDate(7), '设置计划起止'],
|
|
5805
|
+
['dia task plan '+t.id+' --progress 50', '更新进度'],
|
|
5806
|
+
['dia msg send <session> "关于 '+t.id+': "', '给负责人发消息'],
|
|
5807
|
+
['dia claim '+t.id, '声明正在处理'],
|
|
5808
|
+
];
|
|
5809
|
+
return '<p class="k">本页是静态快照:命令只供复制,不会发送。</p>'+
|
|
5810
|
+
cmds.map(([c,l])=>'<div class="cmd"><code>'+esc(c)+'</code><button onclick="copyCmd(this)" data-cmd="'+esc(c)+'">复制</button><span class="k">'+esc(l)+'</span></div>').join('');
|
|
5811
|
+
}
|
|
5812
|
+
function copyCmd(btn){
|
|
5813
|
+
const text=btn.dataset.cmd||'';
|
|
5814
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(()=>{ btn.textContent='已复制'; },()=>{ btn.textContent='复制失败'; }); }
|
|
5815
|
+
else { btn.textContent='请手动复制'; }
|
|
5816
|
+
}
|
|
5817
|
+
function openDrawer(id, tab){
|
|
5818
|
+
drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
|
|
5819
|
+
drawerTab=tab||'task';
|
|
5820
|
+
document.getElementById('drawer-title').textContent='任务详情';
|
|
5821
|
+
document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
|
|
5822
|
+
document.getElementById('drawer').classList.add('show');
|
|
5823
|
+
document.getElementById('drawer').setAttribute('aria-hidden','false');
|
|
5824
|
+
document.getElementById('backdrop').classList.add('show');
|
|
5825
|
+
setDrawerTab(drawerTab);
|
|
5826
|
+
}
|
|
5827
|
+
function setDrawerTab(tab){
|
|
5828
|
+
drawerTab=tab;
|
|
5829
|
+
document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
|
|
5830
|
+
const body=document.getElementById('drawer-body');
|
|
5831
|
+
body.innerHTML=tab==='intervene'?commandHtml(drawerTask):taskHtml(drawerTask);
|
|
5832
|
+
}
|
|
5833
|
+
function closeDrawer(){
|
|
5834
|
+
document.getElementById('drawer').classList.remove('show');
|
|
5835
|
+
document.getElementById('drawer').setAttribute('aria-hidden','true');
|
|
5836
|
+
document.getElementById('backdrop').classList.remove('show');
|
|
5837
|
+
}
|
|
5838
|
+
function todayUTC(){ const [y,m,d]=TODAY.split('-').map(Number); return Date.UTC(y,(m||1)-1,d||1); }
|
|
5839
|
+
function dayDate(d){
|
|
5840
|
+
const dt=new Date(todayUTC()+d*86400000);
|
|
5841
|
+
return ('0'+(dt.getUTCMonth()+1)).slice(-2)+'-'+('0'+dt.getUTCDate()).slice(-2);
|
|
5842
|
+
}
|
|
5843
|
+
function isoDate(d){ return new Date(todayUTC()+d*86400000).toISOString().slice(0,10); }
|
|
5844
|
+
function timelineWindow(){
|
|
5845
|
+
if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
|
|
5846
|
+
const list=visibleTasks();
|
|
5847
|
+
const w=state.window>0?state.window:14;
|
|
5848
|
+
const starts=list.map(t=>t.start).filter(Number.isFinite);
|
|
5849
|
+
const ends=list.map(t=>t.end).filter(Number.isFinite);
|
|
5850
|
+
const from=Math.min(-w, starts.length?Math.min(...starts):-w);
|
|
5851
|
+
const to=Math.max(0, ends.length?Math.max(...ends):0)+1;
|
|
5852
|
+
return {from,to};
|
|
5853
|
+
}
|
|
5854
|
+
function drawTimeline(){
|
|
5855
|
+
const win=timelineWindow(), span=Math.max(1,win.to-win.from);
|
|
5856
|
+
const x=v=>((v-win.from)/span)*100;
|
|
5857
|
+
const chartW=Math.max(640, span*state.zoom);
|
|
5858
|
+
document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
|
|
5859
|
+
const axis=document.getElementById('tl-axis'); axis.innerHTML='';
|
|
5860
|
+
const step=span<=10?1:span<=18?2:3;
|
|
5861
|
+
for(let d=win.from;d<=win.to;d+=step){
|
|
5862
|
+
const el=document.createElement('span'); el.style.left=x(d)+'%';
|
|
5863
|
+
el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
|
|
5864
|
+
if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
|
|
5865
|
+
if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
|
|
5866
|
+
}
|
|
5867
|
+
const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML='';
|
|
5868
|
+
visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
|
|
5869
|
+
const row=document.createElement('div'); row.className='tl-row';
|
|
5870
|
+
row.innerHTML='<div class="tl-track"></div>';
|
|
5871
|
+
const label=document.createElement('div'); label.className='tl-label-row'; label.innerHTML='<b>'+esc(t.label)+'</b><span>'+esc(t.id)+' · '+esc(t.prio)+' · '+esc(t.status)+'</span>'; labelsBody.appendChild(label);
|
|
5872
|
+
const track=row.querySelector('.tl-track');
|
|
5873
|
+
const startOff=t.start===null?win.from:t.start;
|
|
5874
|
+
const endOff=Number.isFinite(t.end)?t.end:win.to;
|
|
5875
|
+
const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(Number.isFinite(t.end)?'':' open');
|
|
5876
|
+
bar.style.left=x(startOff)+'%'; bar.style.width=Math.max(1.5,x(endOff)-x(startOff))+'%';
|
|
5877
|
+
bar.title=dayDate(startOff)+' → '+(Number.isFinite(t.end)?dayDate(t.end):'开放')+' ('+t.progress+'%'+(t.derived?' 估算':'')+')';
|
|
5878
|
+
bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
|
|
5879
|
+
if(win.from<=0&&win.to>=0){ const line=document.createElement('div'); line.className='tl-today'; line.style.left=x(0)+'%'; line.style.top='0'; line.style.bottom='0'; line.textContent=''; track.appendChild(line); }
|
|
5880
|
+
body.appendChild(row);
|
|
5881
|
+
});
|
|
5882
|
+
syncStats();
|
|
5883
|
+
}
|
|
5884
|
+
function drawDepGraph(){
|
|
5885
|
+
cardMetrics=measureCard();
|
|
5886
|
+
const list=visibleTasks(), d=depths(), groups=new Map();
|
|
5887
|
+
list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
|
|
5888
|
+
const p=new Map(); const rows=[];
|
|
5889
|
+
const gap=14;
|
|
5890
|
+
[...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
|
|
5891
|
+
const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
|
|
5892
|
+
rows.push({depth,all,x:30+ci*230});
|
|
5893
|
+
});
|
|
5894
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
5895
|
+
rail.innerHTML=''; rail.style.display='none';
|
|
5896
|
+
let maxY=0;
|
|
5897
|
+
// 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
|
|
5898
|
+
rows.forEach(r=>{
|
|
5899
|
+
let y=60;
|
|
5900
|
+
r.all.forEach(t=>{
|
|
5901
|
+
const el=card(t,r.x,y,true);
|
|
5902
|
+
el.style.top=y+'px';
|
|
5903
|
+
layer.appendChild(el);
|
|
5904
|
+
p.set(t.id,{x:r.x,y});
|
|
5905
|
+
y+=el.offsetHeight+gap;
|
|
5906
|
+
});
|
|
5907
|
+
maxY=Math.max(maxY,y);
|
|
5908
|
+
});
|
|
5909
|
+
maxY=Math.max(600,maxY);
|
|
5910
|
+
const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
|
|
5911
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
|
|
5912
|
+
const defs=\`<defs>
|
|
5913
|
+
<marker id="ar-dep" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#2563eb"/></marker>
|
|
5914
|
+
<marker id="ar-collab" markerWidth="7" markerHeight="7" refX="6" refY="3.5" orient="auto"><path d="M0,0 L7,3.5 L0,7" fill="#64748b"/></marker>
|
|
5915
|
+
</defs>\`;
|
|
5916
|
+
const lines=taskEdges.map(e=>{
|
|
5917
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
5918
|
+
return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
|
|
5919
|
+
}).join('');
|
|
5920
|
+
svg.innerHTML=defs+lines;
|
|
5921
|
+
document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
|
|
5922
|
+
syncStats(); updateHint(); applySearch();
|
|
5923
|
+
}
|
|
5924
|
+
function syncLaneLabels(){
|
|
5925
|
+
rail.querySelectorAll('.plane-label').forEach(el=>{
|
|
5926
|
+
el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
|
|
5927
|
+
});
|
|
5928
|
+
}
|
|
5929
|
+
function applySearch(){
|
|
5930
|
+
const ms=searchMatches();
|
|
5931
|
+
layer.querySelectorAll('.node.task').forEach(el=>{
|
|
5932
|
+
el.classList.remove('hit','dim');
|
|
5933
|
+
if(!ms) return;
|
|
5934
|
+
if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
|
|
5935
|
+
});
|
|
5936
|
+
const box=document.getElementById('q-list');
|
|
5937
|
+
if(box && box.classList.contains('open')) renderSearchList(ms);
|
|
5938
|
+
}
|
|
5939
|
+
function renderSearchList(ms){
|
|
5940
|
+
const box=document.getElementById('q-list'); if(!box) return;
|
|
5941
|
+
const q=(state.q||'').trim();
|
|
5942
|
+
const browse=!q;
|
|
5943
|
+
const pool=browse?tasks.filter(t=>state.window<=0||t.start>=-state.window).slice().sort((a,b)=>a.prio.localeCompare(b.prio)||a.start-b.start):ms;
|
|
5944
|
+
if(!pool||!pool.length){
|
|
5945
|
+
box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
|
|
5946
|
+
box.classList.add('open'); return;
|
|
5947
|
+
}
|
|
5948
|
+
const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
|
|
5949
|
+
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
|
|
5950
|
+
box.innerHTML=head+pool.slice(0,20).map(t=>{
|
|
5951
|
+
const on=state.sel.includes(t.id);
|
|
5952
|
+
return '<div class="q-item'+(on?' sel':'')+'" data-id="'+esc(t.id)+'"><span>'+esc(t.label)+'<i class="q-prio">'+esc(t.prio)+'</i></span><span class="k">'+(on?'✓ ':'')+esc(t.id)+'</span></div>';
|
|
5953
|
+
}).join('');
|
|
5954
|
+
box.classList.add('open');
|
|
5955
|
+
box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
|
|
5956
|
+
}
|
|
5957
|
+
function searchListItems(){
|
|
5958
|
+
const box=document.getElementById('q-list');
|
|
5959
|
+
return box?[...box.querySelectorAll('.q-item')]:[];
|
|
5960
|
+
}
|
|
5961
|
+
function moveSearchCursor(delta){
|
|
5962
|
+
let items=searchListItems();
|
|
5963
|
+
if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
|
|
5964
|
+
let idx=items.findIndex(el=>el.classList.contains('active'));
|
|
5965
|
+
idx=idx<0?0:(idx+delta+items.length)%items.length;
|
|
5966
|
+
items.forEach((el,i)=>el.classList.toggle('active',i===idx));
|
|
5967
|
+
items[idx].scrollIntoView({block:'nearest'});
|
|
5968
|
+
}
|
|
5969
|
+
function toggleSel(id){
|
|
5970
|
+
const i=state.sel.indexOf(id);
|
|
5971
|
+
if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
|
|
5972
|
+
state.fd=state.fd||2; state.q='';
|
|
5973
|
+
const qi=document.getElementById('q'); if(qi) qi.value='';
|
|
5974
|
+
go(state.view,state.mode,state.range,state.zoom);
|
|
5975
|
+
}
|
|
5976
|
+
function renderTokens(){
|
|
5977
|
+
const box=document.getElementById('tokens'); if(!box) return;
|
|
5978
|
+
box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
|
|
5979
|
+
box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
|
|
5980
|
+
}
|
|
5981
|
+
function readRoute(){
|
|
5982
|
+
const raw=location.hash.replace(/^#\\/?/,''); const [path,qs]=raw.split('?');
|
|
5983
|
+
const params=new URLSearchParams(qs||location.search);
|
|
5984
|
+
const range=(params.get('range')||'30d');
|
|
5985
|
+
const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
|
|
5986
|
+
const sx=Number(params.get('sx')||0);
|
|
5987
|
+
const fromRaw=params.get('from'), toRaw=params.get('to');
|
|
5988
|
+
const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
|
|
5989
|
+
const winRaw=params.get('w');
|
|
5990
|
+
const rangeRaw=params.get('range');
|
|
5991
|
+
const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
|
|
5992
|
+
const fdRaw=params.get('fd');
|
|
5993
|
+
const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
|
|
5994
|
+
return {view:(path==='timeline'||params.get('view')==='timeline')?'time':'graph', layout:params.get('layout')==='dep'?'dep':'priority', mode:params.get('mode')==='detail'?'detail':'simple', range, zoom, sx, window:win, q:params.get('q')||'', sel:(params.get('sel')||params.get('focus')||'').split(',').map(x=>x.trim()).filter(x=>x&&tasks.some(t=>t.id===x)), fd, from:toOff(fromRaw), to:toOff(toRaw), task:params.get('task'), tab:params.get('tab')==='intervene'?'intervene':'task'};
|
|
5995
|
+
}
|
|
5996
|
+
function routeString(view,mode,range,zoom,from,to){
|
|
5997
|
+
const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
|
|
5998
|
+
if(view==='graph'&&mode==='detail') qs.set('mode','detail');
|
|
5999
|
+
if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
|
|
6000
|
+
if(view==='graph'){
|
|
6001
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
6002
|
+
if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
|
|
6003
|
+
if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
|
|
6004
|
+
}
|
|
6005
|
+
if(view==='time'){
|
|
6006
|
+
if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
|
|
6007
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
6008
|
+
if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
|
|
6009
|
+
}
|
|
6010
|
+
return '#/'+p+(qs.toString()?'?'+qs.toString():'');
|
|
6011
|
+
}
|
|
6012
|
+
function applyRoute(){
|
|
6013
|
+
const stampEl=document.getElementById('updated');
|
|
6014
|
+
if(stampEl) stampEl.textContent='快照 '+(payload.generated_at||'-');
|
|
6015
|
+
const r=readRoute(); state.view=r.view; state.layout=r.layout; state.mode=r.mode; state.range=r.range; state.zoom=r.zoom; state.sx=r.sx||0; state.window=r.window; state.q=r.q; state.sel=r.sel; state.fd=r.fd; state.from=r.from; state.to=r.to;
|
|
6016
|
+
document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
|
|
6017
|
+
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
|
|
6018
|
+
document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
|
|
6019
|
+
document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
|
|
6020
|
+
document.getElementById('view-time').style.display=r.view==='time'?'':'none';
|
|
6021
|
+
document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
|
|
6022
|
+
document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
|
|
6023
|
+
const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
|
|
6024
|
+
const dec=document.querySelector('#zoom button[data-z="-"]'), inc=document.querySelector('#zoom button[data-z="+"]'); if(dec) dec.disabled=r.zoom<=14; if(inc) inc.disabled=r.zoom>=84;
|
|
6025
|
+
if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;红色虚线 = 今天';
|
|
6026
|
+
const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
|
|
6027
|
+
const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
|
|
6028
|
+
const chip=document.getElementById('focus-chip');
|
|
6029
|
+
if(chip) chip.classList.toggle('on',r.sel.length>0);
|
|
6030
|
+
if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
|
|
6031
|
+
const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
|
|
6032
|
+
renderTokens();
|
|
6033
|
+
const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
|
|
6034
|
+
document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
|
|
6035
|
+
if(r.view==='time'){ drawTimeline(); const sc=document.getElementById('tl-scroll'), lb=document.getElementById('tl-labels-body'); sc.scrollTop=0; lb.style.transform='translateY(0px)'; if(state.sx) sc.scrollLeft=state.sx; } else drawGraph();
|
|
6036
|
+
if(r.task) openDrawer(r.task,r.tab);
|
|
6037
|
+
syncLaneLabels();
|
|
6038
|
+
if(r.view==='graph') renderEmpty();
|
|
6039
|
+
}
|
|
6040
|
+
function go(view,mode,range,zoom){ const h=routeString(view,mode,range||state.range,zoom||state.zoom,state.from,state.to); if(location.hash===h) applyRoute(); else location.hash=h; }
|
|
6041
|
+
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
|
|
6042
|
+
document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
|
|
6043
|
+
const winSelEl=document.getElementById('win');
|
|
6044
|
+
if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
6045
|
+
const qInput=document.getElementById('q');
|
|
6046
|
+
if(qInput){
|
|
6047
|
+
qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
|
|
6048
|
+
qInput.onkeydown=e=>{
|
|
6049
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
|
|
6050
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
|
|
6051
|
+
else if(e.key==='Enter'){
|
|
6052
|
+
const items=searchListItems();
|
|
6053
|
+
const active=items.find(el=>el.classList.contains('active'))||items[0];
|
|
6054
|
+
if(active) toggleSel(active.dataset.id);
|
|
6055
|
+
} else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
|
|
6056
|
+
else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
|
|
6057
|
+
};
|
|
6058
|
+
qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
|
|
6059
|
+
qInput.onclick=()=>{ renderSearchList(searchMatches()); };
|
|
6060
|
+
const sbox=document.getElementById('search-box');
|
|
6061
|
+
if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
|
|
6062
|
+
qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
|
|
6063
|
+
}
|
|
6064
|
+
const fdSel=document.getElementById('focus-fd');
|
|
6065
|
+
if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
6066
|
+
const fxBtn=document.getElementById('focus-exit');
|
|
6067
|
+
if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
|
|
6068
|
+
document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
|
|
6069
|
+
document.querySelectorAll('#zoom button').forEach(b=>b.onclick=()=>{ const z=b.dataset.z==='+'?Math.min(84,state.zoom+8):b.dataset.z==='-'?Math.max(14,state.zoom-8):28; go('time',state.mode,state.range,z); });
|
|
6070
|
+
layer.addEventListener('scroll',syncLaneLabels,{passive:true});
|
|
6071
|
+
const tlScroller=document.getElementById('tl-scroll');
|
|
6072
|
+
tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
|
|
6073
|
+
document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
|
|
6074
|
+
window.addEventListener('hashchange',applyRoute);
|
|
6075
|
+
applyRoute();
|
|
6076
|
+
if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
|
|
6077
|
+
</script>
|
|
6078
|
+
</body>
|
|
6079
|
+
</html>
|
|
6080
|
+
`;
|
|
6081
|
+
|
|
6082
|
+
// src/graph-html/render.ts
|
|
6083
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
6084
|
+
function localDay(d) {
|
|
6085
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
6086
|
+
}
|
|
6087
|
+
function dayDiff(from, to) {
|
|
6088
|
+
const a = from.split("-").map(Number), b = to.split("-").map(Number);
|
|
6089
|
+
return Math.round((Date.UTC(b[0], b[1] - 1, b[2]) - Date.UTC(a[0], a[1] - 1, a[2])) / 86400000);
|
|
6090
|
+
}
|
|
6091
|
+
function shortHash(s) {
|
|
6092
|
+
let h = 2166136261;
|
|
6093
|
+
for (let i = 0;i < s.length; i++) {
|
|
6094
|
+
h ^= s.charCodeAt(i);
|
|
6095
|
+
h = Math.imul(h, 16777619);
|
|
6096
|
+
}
|
|
6097
|
+
return (h >>> 0).toString(16).padStart(8, "0").slice(0, 8);
|
|
6098
|
+
}
|
|
6099
|
+
function safeJson(v) {
|
|
6100
|
+
return JSON.stringify(v).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6101
|
+
}
|
|
6102
|
+
var bare = (id) => id.replace(/^task:/, "");
|
|
6103
|
+
function buildPayload(graph, opts = {}) {
|
|
6104
|
+
const now = opts.now ?? new Date;
|
|
6105
|
+
const today = localDay(now);
|
|
6106
|
+
const limit = opts.limit === undefined ? 500 : opts.limit;
|
|
6107
|
+
const redact = !!opts.redact;
|
|
6108
|
+
const taskNodes = graph.nodes.filter((n) => n.kind === "task");
|
|
6109
|
+
const known = new Set(taskNodes.map((n) => n.id));
|
|
6110
|
+
const ordered = [...taskNodes].sort((a, b) => String(a.prio ?? "P9").localeCompare(String(b.prio ?? "P9")) || String(a.start ?? a.created_at ?? "").localeCompare(String(b.start ?? b.created_at ?? "")) || a.id.localeCompare(b.id));
|
|
6111
|
+
const shown = limit > 0 ? ordered.slice(0, limit) : ordered;
|
|
6112
|
+
const tasks = shown.map((n) => {
|
|
6113
|
+
const startDay = n.start ?? (n.created_at ? localDay(new Date(n.created_at)) : null);
|
|
6114
|
+
const endDay = n.due ?? (n.closed_at ? localDay(new Date(n.closed_at)) : null);
|
|
6115
|
+
return {
|
|
6116
|
+
id: bare(n.id),
|
|
6117
|
+
label: n.label,
|
|
6118
|
+
status: n.status ?? "todo",
|
|
6119
|
+
prio: n.prio ?? null,
|
|
6120
|
+
start: startDay ? dayDiff(today, startDay) : null,
|
|
6121
|
+
end: endDay && startDay ? dayDiff(today, endDay) : null,
|
|
6122
|
+
progress: typeof n.progress === "number" ? n.progress : 0,
|
|
6123
|
+
derived: n.progress_derived !== false,
|
|
6124
|
+
owner: n.owner ? redact ? `owner-${shortHash(n.owner)}` : n.owner : null,
|
|
6125
|
+
deps: (n.deps ?? []).filter((d) => known.has(`task:${d}`)),
|
|
6126
|
+
touches: (n.touches ?? []).map((p) => redact ? p.split("/").pop() || p : p)
|
|
6127
|
+
};
|
|
6128
|
+
});
|
|
6129
|
+
const shownIds = new Set(tasks.map((t) => t.id));
|
|
6130
|
+
const edges = graph.edges.filter((e) => e.kind === "depends_on").map((e) => ({ from: bare(e.from), to: bare(e.to), kind: "depends_on" })).filter((e) => shownIds.has(e.from) && shownIds.has(e.to));
|
|
6131
|
+
return {
|
|
6132
|
+
schema: 1,
|
|
6133
|
+
today,
|
|
6134
|
+
generated_at: `${today} ${pad(now.getHours())}:${pad(now.getMinutes())}`,
|
|
6135
|
+
tasks,
|
|
6136
|
+
edges,
|
|
6137
|
+
cycles: graph.cycles.map((c) => c.map(bare)),
|
|
6138
|
+
claims: opts.claims ?? graph.edges.filter((e) => e.kind === "claims").length,
|
|
6139
|
+
truncated: limit > 0 && ordered.length > limit ? { shown: limit, total: ordered.length } : null,
|
|
6140
|
+
capabilities: graph.capabilities ?? []
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
function renderHtml(graph, opts = {}) {
|
|
6144
|
+
const payload = buildPayload(graph, opts);
|
|
6145
|
+
return template_default.replace("__DIA_PAYLOAD__", safeJson(payload));
|
|
6146
|
+
}
|
|
6147
|
+
|
|
4861
6148
|
// packages/cli/src/index.ts
|
|
4862
6149
|
init_paths();
|
|
4863
6150
|
|
|
@@ -5282,9 +6569,9 @@ function scopeEnv(s, extra = {}) {
|
|
|
5282
6569
|
}
|
|
5283
6570
|
|
|
5284
6571
|
// src/doctor.ts
|
|
5285
|
-
import { existsSync as existsSync22, readFileSync as readFileSync19 } from "node:fs";
|
|
6572
|
+
import { closeSync as closeSync3, existsSync as existsSync22, openSync as openSync3, readFileSync as readFileSync19, readSync, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
|
|
5286
6573
|
import { homedir as homedir12 } from "node:os";
|
|
5287
|
-
import { join as join25, resolve as resolve13 } from "node:path";
|
|
6574
|
+
import { delimiter, dirname as dirname13, join as join25, resolve as resolve13 } from "node:path";
|
|
5288
6575
|
|
|
5289
6576
|
// src/dsh.ts
|
|
5290
6577
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
@@ -5681,6 +6968,92 @@ function platformChecks() {
|
|
|
5681
6968
|
function quoteArg(v) {
|
|
5682
6969
|
return /^[\w.@%+=:,\/-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
|
|
5683
6970
|
}
|
|
6971
|
+
function npmOwnedPath(p) {
|
|
6972
|
+
return /(^|[\\/])node_modules[\\/]@minonl[\\/]dia([\\/]|$)/.test(p);
|
|
6973
|
+
}
|
|
6974
|
+
function pkgShimPath(p) {
|
|
6975
|
+
try {
|
|
6976
|
+
const st = statSync12(p);
|
|
6977
|
+
if (!st.isFile() || st.size > 1024 * 1024)
|
|
6978
|
+
return false;
|
|
6979
|
+
const fd = openSync3(p, "r");
|
|
6980
|
+
try {
|
|
6981
|
+
const buf = Buffer.alloc(8192);
|
|
6982
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
6983
|
+
return /@minonl\/dia|node_modules\/(@minonl\/)?dia\//.test(buf.subarray(0, n).toString("utf8"));
|
|
6984
|
+
} finally {
|
|
6985
|
+
closeSync3(fd);
|
|
6986
|
+
}
|
|
6987
|
+
} catch {
|
|
6988
|
+
return false;
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
function pkgBinDirOwned(p) {
|
|
6992
|
+
const d = realOrSelf(dirname13(p));
|
|
6993
|
+
const home = homedir12();
|
|
6994
|
+
const known = [
|
|
6995
|
+
join25(home, ".volta/bin"),
|
|
6996
|
+
join25(home, ".bun/bin"),
|
|
6997
|
+
join25(home, ".yarn/bin"),
|
|
6998
|
+
join25(home, ".pnpm-global/bin"),
|
|
6999
|
+
join25(home, ".local/share/pnpm"),
|
|
7000
|
+
join25(home, ".npm-global/bin"),
|
|
7001
|
+
process.env.PNPM_HOME,
|
|
7002
|
+
process.env.YARN_GLOBAL_FOLDER,
|
|
7003
|
+
process.env.VOLTA_HOME ? join25(process.env.VOLTA_HOME, "bin") : "",
|
|
7004
|
+
process.env.BUN_INSTALL ? join25(process.env.BUN_INSTALL, "bin") : ""
|
|
7005
|
+
].filter(Boolean);
|
|
7006
|
+
return known.some((k) => realOrSelf(k) === d);
|
|
7007
|
+
}
|
|
7008
|
+
function pkgOwnedPath(p) {
|
|
7009
|
+
return npmOwnedPath(p) || pkgShimPath(p) || pkgBinDirOwned(p);
|
|
7010
|
+
}
|
|
7011
|
+
function realOrSelf(p) {
|
|
7012
|
+
try {
|
|
7013
|
+
return realpathSync5(p);
|
|
7014
|
+
} catch {
|
|
7015
|
+
return resolve13(p);
|
|
7016
|
+
}
|
|
7017
|
+
}
|
|
7018
|
+
function whichDia(pathEnv = process.env.PATH || "") {
|
|
7019
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
7020
|
+
if (!dir)
|
|
7021
|
+
continue;
|
|
7022
|
+
const cand = join25(dir, "dia");
|
|
7023
|
+
try {
|
|
7024
|
+
const st = statSync12(cand);
|
|
7025
|
+
if (!st.isFile())
|
|
7026
|
+
continue;
|
|
7027
|
+
if (process.platform !== "win32" && (st.mode & 73) === 0)
|
|
7028
|
+
continue;
|
|
7029
|
+
return cand;
|
|
7030
|
+
} catch {}
|
|
7031
|
+
}
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
function installShadowCheck() {
|
|
7035
|
+
const name = "install_shadow";
|
|
7036
|
+
const install = installDir();
|
|
7037
|
+
const installNpm = npmOwnedPath(realOrSelf(install));
|
|
7038
|
+
if (!installNpm && !existsSync22(join25(install, "install.json"))) {
|
|
7039
|
+
return { name, status: "ok", detail: `源码运行(install=${install} 无 install.json,跳过一致性判定)` };
|
|
7040
|
+
}
|
|
7041
|
+
const bin = whichDia();
|
|
7042
|
+
if (!bin)
|
|
7043
|
+
return { name, status: "ok", detail: `PATH 中没有 dia(跳过一致性判定;install=${install})` };
|
|
7044
|
+
const binReal = realOrSelf(bin);
|
|
7045
|
+
const binNpm = pkgOwnedPath(binReal);
|
|
7046
|
+
const kind = (npm) => npm ? "包管理器全局安装" : "脚本安装";
|
|
7047
|
+
if (installNpm === binNpm) {
|
|
7048
|
+
return { name, status: "ok", detail: `一致(${kind(installNpm)}):install=${install};PATH dia=${binReal}` };
|
|
7049
|
+
}
|
|
7050
|
+
const fix = installNpm ? `ln -sfn "$(npm prefix -g)/bin/dia" ${quoteArg(bin)}` : `ln -sfn ${quoteArg(join25(install, "bin/dia.js"))} ${quoteArg(bin)}`;
|
|
7051
|
+
return {
|
|
7052
|
+
name,
|
|
7053
|
+
status: "fail",
|
|
7054
|
+
detail: `安装类型不一致:install=${install}(${kind(installNpm)})但 PATH dia=${binReal}(${kind(binNpm)});修复:${fix}`
|
|
7055
|
+
};
|
|
7056
|
+
}
|
|
5684
7057
|
function describeStale(st) {
|
|
5685
7058
|
const diff = st.added + st.removed + st.changed;
|
|
5686
7059
|
const parts = [st.added ? `新增 ${st.added}` : "", st.removed ? `删除 ${st.removed}` : "", st.changed ? `修改 ${st.changed}` : ""].filter(Boolean);
|
|
@@ -5826,6 +7199,7 @@ async function run(opts = {}) {
|
|
|
5826
7199
|
status: install === diaHome() ? "warn" : "ok",
|
|
5827
7200
|
detail: `install=${install} data=${diaHome()}${install === diaHome() ? "(安装物与数据同根)" : ""}`
|
|
5828
7201
|
});
|
|
7202
|
+
checks.push(installShadowCheck());
|
|
5829
7203
|
const wt = join25(install, "packages/wt/bin/dia-wt");
|
|
5830
7204
|
checks.push({ name: "wt", status: existsSync22(wt) ? "ok" : "warn", detail: existsSync22(wt) ? wt : "dia wt 未安装(dia install.sh)" });
|
|
5831
7205
|
const jobRoot = jobRootResolved();
|
|
@@ -6178,9 +7552,9 @@ function askBroker(req, timeoutMs = 5000) {
|
|
|
6178
7552
|
|
|
6179
7553
|
// packages/cli/src/index.ts
|
|
6180
7554
|
import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
6181
|
-
import { mkdirSync as mkdirSync20, openSync as
|
|
7555
|
+
import { mkdirSync as mkdirSync20, openSync as openSync4 } from "node:fs";
|
|
6182
7556
|
import { basename as basename10, join as join32 } from "node:path";
|
|
6183
|
-
import { existsSync as existsSync29, readFileSync as readFileSync26 } from "node:fs";
|
|
7557
|
+
import { existsSync as existsSync29, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
|
|
6184
7558
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
6185
7559
|
import { resolve as resolvePath, sep as pathSep } from "node:path";
|
|
6186
7560
|
|
|
@@ -7185,13 +8559,13 @@ import {
|
|
|
7185
8559
|
readdirSync as readdirSync13,
|
|
7186
8560
|
readlinkSync as readlinkSync2,
|
|
7187
8561
|
rmSync as rmSync9,
|
|
7188
|
-
statSync as
|
|
8562
|
+
statSync as statSync13,
|
|
7189
8563
|
symlinkSync as symlinkSync3,
|
|
7190
8564
|
unlinkSync as unlinkSync3,
|
|
7191
8565
|
writeFileSync as writeFileSync17
|
|
7192
8566
|
} from "node:fs";
|
|
7193
8567
|
import { hostname as osHostname } from "node:os";
|
|
7194
|
-
import { basename as basename9, dirname as
|
|
8568
|
+
import { basename as basename9, dirname as dirname14, isAbsolute as isAbsolute4, join as join31, relative as relative6, resolve as resolve15, sep as sep8 } from "node:path";
|
|
7195
8569
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7196
8570
|
init_paths();
|
|
7197
8571
|
var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -7402,7 +8776,7 @@ function resolveEntry(cmd, baseDir) {
|
|
|
7402
8776
|
}
|
|
7403
8777
|
function isExecutableFile(p) {
|
|
7404
8778
|
try {
|
|
7405
|
-
const st =
|
|
8779
|
+
const st = statSync13(p);
|
|
7406
8780
|
if (!st.isFile())
|
|
7407
8781
|
return false;
|
|
7408
8782
|
return process.platform === "win32" ? true : (st.mode & 73) !== 0;
|
|
@@ -7639,9 +9013,9 @@ function diaCmd() {
|
|
|
7639
9013
|
if (onPath)
|
|
7640
9014
|
return "dia";
|
|
7641
9015
|
const entries = [
|
|
7642
|
-
resolve15(
|
|
7643
|
-
resolve15(
|
|
7644
|
-
resolve15(
|
|
9016
|
+
resolve15(dirname14(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
|
|
9017
|
+
resolve15(dirname14(process.argv[1] ?? ""), "..", "src", "cli.ts"),
|
|
9018
|
+
resolve15(dirname14(process.argv[1] ?? ""), "..", "dist", "cli.js")
|
|
7645
9019
|
];
|
|
7646
9020
|
const entry = entries.find((p) => existsSync28(p));
|
|
7647
9021
|
return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
|
|
@@ -7730,7 +9104,7 @@ function readHealthLines(file) {
|
|
|
7730
9104
|
return rows;
|
|
7731
9105
|
}
|
|
7732
9106
|
function appendHealthRecord(file, rec) {
|
|
7733
|
-
mkdirSync19(
|
|
9107
|
+
mkdirSync19(dirname14(file), { recursive: true });
|
|
7734
9108
|
appendFileSync9(file, JSON.stringify(rec) + `
|
|
7735
9109
|
`);
|
|
7736
9110
|
}
|
|
@@ -8111,7 +9485,7 @@ function collectManagedFiles(base, hc) {
|
|
|
8111
9485
|
continue;
|
|
8112
9486
|
let st;
|
|
8113
9487
|
try {
|
|
8114
|
-
st =
|
|
9488
|
+
st = statSync13(abs);
|
|
8115
9489
|
} catch {
|
|
8116
9490
|
continue;
|
|
8117
9491
|
}
|
|
@@ -8332,8 +9706,8 @@ function copyTree(from, to) {
|
|
|
8332
9706
|
function copyAdopted(from, to) {
|
|
8333
9707
|
if (existsSync28(to))
|
|
8334
9708
|
throw new Error(`E_CONFLICT: ${to} 已存在`);
|
|
8335
|
-
mkdirSync19(
|
|
8336
|
-
if (
|
|
9709
|
+
mkdirSync19(dirname14(to), { recursive: true });
|
|
9710
|
+
if (statSync13(from).isDirectory())
|
|
8337
9711
|
copyTree(from, to);
|
|
8338
9712
|
else {
|
|
8339
9713
|
mkdirSync19(to, { recursive: true });
|
|
@@ -8355,7 +9729,7 @@ function remove3(opts) {
|
|
|
8355
9729
|
}
|
|
8356
9730
|
if (lock.entry.kind === "repo" && lock.entry.link && isSymlink2(lock.entry.link)) {
|
|
8357
9731
|
const rel = readlinkSync2(lock.entry.link);
|
|
8358
|
-
if (rel.startsWith(lock.dir) || resolve15(
|
|
9732
|
+
if (rel.startsWith(lock.dir) || resolve15(dirname14(lock.entry.link), rel).startsWith(lock.dir)) {
|
|
8359
9733
|
unlinkSync3(lock.entry.link);
|
|
8360
9734
|
removed.push(lock.entry.link);
|
|
8361
9735
|
} else
|
|
@@ -8588,8 +9962,11 @@ usage:
|
|
|
8588
9962
|
dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
|
|
8589
9963
|
dia agents list|register|unregister|discover
|
|
8590
9964
|
dia msg deliver <agent> <id> [--transport tmux|acp]
|
|
8591
|
-
dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
|
|
9965
|
+
dia task add|list|link|plan|split|status|done [--owner S] [--depends a,b] [--touches p1,p2] [--prio P1] [--start D] [--due D] [--progress 0-100]
|
|
9966
|
+
dia task split <id> --into "切片A,切片B" [--slice 切片A] [--role R] [--judge J] [--touches p] [--force]
|
|
8592
9967
|
dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
|
|
9968
|
+
dia graph --export-html [path] [--out path] [--now ISO] [--redact] [--all] [--limit N] [--force]
|
|
9969
|
+
dia graph --next [n] | --explain <id> | --lint # 只读编排建议(Kahn-v1)
|
|
8593
9970
|
dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
|
|
8594
9971
|
dia team run <id> --mock | --acp|--dsh --workspace DIR
|
|
8595
9972
|
dia team gate|accept|reject|next
|
|
@@ -8633,15 +10010,42 @@ async function kb(argv) {
|
|
|
8633
10010
|
throw e;
|
|
8634
10011
|
}
|
|
8635
10012
|
}
|
|
10013
|
+
const tagFilter = flag(args, "--tag");
|
|
10014
|
+
const rootsByPath = new Map;
|
|
10015
|
+
let kitHits = 0;
|
|
10016
|
+
for (const k of installed()) {
|
|
10017
|
+
if (tagFilter && !k.tags.includes(tagFilter))
|
|
10018
|
+
continue;
|
|
10019
|
+
let kr = [];
|
|
10020
|
+
try {
|
|
10021
|
+
kr = await search(k.dir, q, undefined, limit);
|
|
10022
|
+
} catch {
|
|
10023
|
+
try {
|
|
10024
|
+
await buildIndex(k.dir);
|
|
10025
|
+
kr = await search(k.dir, q, undefined, limit);
|
|
10026
|
+
} catch {
|
|
10027
|
+
kr = [];
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
for (const h of kr) {
|
|
10031
|
+
rootsByPath.set(`${k.id}:${h.path}`, k.dir);
|
|
10032
|
+
rows.push({ ...h, path: `${k.id}:${h.path}`, source: `kit:${k.id}@${k.version}` });
|
|
10033
|
+
kitHits++;
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
if (kitHits)
|
|
10037
|
+
rows.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).splice(limit);
|
|
8636
10038
|
const full = sub === "query" && has(args, "--full");
|
|
8637
10039
|
const bytes = sub === "query" && has(args, "--bytes");
|
|
8638
10040
|
const enrich = rows.map((r) => {
|
|
8639
10041
|
const item = { ...r };
|
|
10042
|
+
const rowRoot = rootsByPath.get(r.path) ?? root;
|
|
10043
|
+
const rowPath = r.source ? r.path.split(":").slice(1).join(":") : r.path;
|
|
8640
10044
|
try {
|
|
8641
10045
|
if (bytes)
|
|
8642
|
-
item.bytes = Buffer.byteLength(getDoc(
|
|
10046
|
+
item.bytes = Buffer.byteLength(getDoc(rowRoot, rowPath), "utf8");
|
|
8643
10047
|
if (full)
|
|
8644
|
-
item.text = getDoc(
|
|
10048
|
+
item.text = getDoc(rowRoot, rowPath);
|
|
8645
10049
|
} catch {
|
|
8646
10050
|
item.stale_index = true;
|
|
8647
10051
|
item.text = null;
|
|
@@ -8653,7 +10057,7 @@ async function kb(argv) {
|
|
|
8653
10057
|
out(JSON.stringify(enrich));
|
|
8654
10058
|
else
|
|
8655
10059
|
for (const r of enrich) {
|
|
8656
|
-
out(`${r.path} ${r.title}
|
|
10060
|
+
out(`${r.source ? `[${r.source}] ` : ""}${r.path} ${r.title}
|
|
8657
10061
|
${r.snippet}`);
|
|
8658
10062
|
if (r.bytes !== undefined)
|
|
8659
10063
|
out(` bytes ${r.bytes}`);
|
|
@@ -8932,7 +10336,18 @@ async function taskCli(argv) {
|
|
|
8932
10336
|
err("missing title");
|
|
8933
10337
|
return 2;
|
|
8934
10338
|
}
|
|
8935
|
-
const
|
|
10339
|
+
const prog = flag(args, "--progress");
|
|
10340
|
+
const t = add(title, {
|
|
10341
|
+
role: flag(args, "--role"),
|
|
10342
|
+
judge: flag(args, "--judge"),
|
|
10343
|
+
owner: flag(args, "--owner"),
|
|
10344
|
+
deps: csv("--depends"),
|
|
10345
|
+
touches: csv("--touches"),
|
|
10346
|
+
prio: flag(args, "--prio"),
|
|
10347
|
+
start: flag(args, "--start"),
|
|
10348
|
+
due: flag(args, "--due"),
|
|
10349
|
+
progress: prog === undefined ? undefined : normProgress(prog)
|
|
10350
|
+
});
|
|
8936
10351
|
out(asJson ? JSON.stringify(t) : `${t.id} ${t.title}`);
|
|
8937
10352
|
return 0;
|
|
8938
10353
|
}
|
|
@@ -8952,6 +10367,45 @@ async function taskCli(argv) {
|
|
|
8952
10367
|
out(asJson ? JSON.stringify(t) : `${t.id} deps=${t.deps.join(",") || "-"} touches=${t.touches.join(",") || "-"}`);
|
|
8953
10368
|
return 0;
|
|
8954
10369
|
}
|
|
10370
|
+
if (sub === "plan") {
|
|
10371
|
+
const id = positional(args)[0];
|
|
10372
|
+
if (!id) {
|
|
10373
|
+
err("usage: dia task plan <id> [--prio P1] [--start YYYY-MM-DD] [--due YYYY-MM-DD] [--progress 0-100]");
|
|
10374
|
+
return 2;
|
|
10375
|
+
}
|
|
10376
|
+
const prog = flag(args, "--progress");
|
|
10377
|
+
const t = plan(id, {
|
|
10378
|
+
prio: flag(args, "--prio"),
|
|
10379
|
+
start: flag(args, "--start"),
|
|
10380
|
+
due: flag(args, "--due"),
|
|
10381
|
+
progress: prog === undefined ? undefined : normProgress(prog)
|
|
10382
|
+
});
|
|
10383
|
+
out(asJson ? JSON.stringify(t) : `${t.id} prio=${t.prio || "-"} start=${t.start || "-"} due=${t.due || "-"} progress=${t.progress ?? "-"}`);
|
|
10384
|
+
return 0;
|
|
10385
|
+
}
|
|
10386
|
+
if (sub === "split") {
|
|
10387
|
+
const id = positional(args)[0];
|
|
10388
|
+
const into = flag(args, "--into");
|
|
10389
|
+
if (!id || !into) {
|
|
10390
|
+
err('usage: dia task split <id> --into "切片A,切片B" [--slice 切片A] [--force]');
|
|
10391
|
+
return 2;
|
|
10392
|
+
}
|
|
10393
|
+
const parent = split(id, into.split(","), {
|
|
10394
|
+
slice: flag(args, "--slice"),
|
|
10395
|
+
role: flag(args, "--role"),
|
|
10396
|
+
judge: flag(args, "--judge"),
|
|
10397
|
+
touches: csv("--touches"),
|
|
10398
|
+
force: has(args, "--force")
|
|
10399
|
+
});
|
|
10400
|
+
const kids = (parent.split_children || []).map((cid) => {
|
|
10401
|
+
const c = get2(cid);
|
|
10402
|
+
return `${c.id} ${c.title}${c.slice ? "\t[slice]" : ""}`;
|
|
10403
|
+
});
|
|
10404
|
+
out(asJson ? JSON.stringify(parent) : `${parent.id} children=${(parent.split_children || []).length}
|
|
10405
|
+
${kids.join(`
|
|
10406
|
+
`)}`);
|
|
10407
|
+
return 0;
|
|
10408
|
+
}
|
|
8955
10409
|
if (sub === "status") {
|
|
8956
10410
|
const id = positional(args)[0];
|
|
8957
10411
|
const st = positional(args)[1];
|
|
@@ -8977,11 +10431,118 @@ async function taskCli(argv) {
|
|
|
8977
10431
|
err(e?.message || String(e));
|
|
8978
10432
|
return 30;
|
|
8979
10433
|
}
|
|
8980
|
-
err("usage: dia task add|list|link|status|done");
|
|
10434
|
+
err("usage: dia task add|list|link|plan|split|status|done");
|
|
8981
10435
|
return 2;
|
|
8982
10436
|
}
|
|
8983
10437
|
async function graphCli(argv) {
|
|
8984
10438
|
const asJson = has(argv, "--json");
|
|
10439
|
+
if (has(argv, "--next") || has(argv, "--explain") || has(argv, "--lint")) {
|
|
10440
|
+
const tasks = list4();
|
|
10441
|
+
const dr = derive(tasks);
|
|
10442
|
+
if (has(argv, "--explain")) {
|
|
10443
|
+
const id = flag(argv, "--explain");
|
|
10444
|
+
if (!id || id.startsWith("--")) {
|
|
10445
|
+
err("usage: dia graph --explain <id>");
|
|
10446
|
+
return 2;
|
|
10447
|
+
}
|
|
10448
|
+
const t = tasks.find((x) => x.id === id);
|
|
10449
|
+
const d = dr.byId.get(id);
|
|
10450
|
+
if (!t || !d) {
|
|
10451
|
+
err(`E_GRAPH: 任务不存在 ${id}`);
|
|
10452
|
+
return 2;
|
|
10453
|
+
}
|
|
10454
|
+
const info = {
|
|
10455
|
+
id,
|
|
10456
|
+
title: t.title,
|
|
10457
|
+
status: t.status,
|
|
10458
|
+
declared: t.prio ?? null,
|
|
10459
|
+
eff: d.eff_prio,
|
|
10460
|
+
inherited: d.prio_inherited,
|
|
10461
|
+
source: d.prio_source ?? null,
|
|
10462
|
+
ready: d.ready,
|
|
10463
|
+
startable: d.startable,
|
|
10464
|
+
rank: d.rank,
|
|
10465
|
+
blocked_by: d.blocked_by,
|
|
10466
|
+
released_by: d.released_by,
|
|
10467
|
+
dangling: d.dangling_deps,
|
|
10468
|
+
unblocks: d.unblocks,
|
|
10469
|
+
critical: d.critical,
|
|
10470
|
+
slice: !!t.slice
|
|
10471
|
+
};
|
|
10472
|
+
out(asJson ? JSON.stringify(info) : [
|
|
10473
|
+
`${id} ${t.title}`,
|
|
10474
|
+
`status=${t.status} declared=${info.declared ?? "-"} eff=${info.eff}${d.prio_inherited ? ` (继承自 ${d.prio_source?.id} ${d.prio_source?.declared}, ${d.prio_source?.distance} 跳)` : ""}`,
|
|
10475
|
+
`ready=${d.ready} startable=${d.startable} rank=${d.rank ?? "-"} unblocks=${d.unblocks} critical=${d.critical}`,
|
|
10476
|
+
`blocked_by=${d.blocked_by.join(",") || "-"} released_by=${d.released_by.join(",") || "-"} dangling=${d.dangling_deps.join(",") || "-"}`
|
|
10477
|
+
].join(`
|
|
10478
|
+
`));
|
|
10479
|
+
return 0;
|
|
10480
|
+
}
|
|
10481
|
+
if (has(argv, "--lint")) {
|
|
10482
|
+
const findings = dr.lint;
|
|
10483
|
+
out(asJson ? JSON.stringify({ findings, cycles: dr.cycles }) : findings.length ? findings.map((f) => `${f.kind} ${f.id} ${f.detail}`).join(`
|
|
10484
|
+
`) : "(clean)");
|
|
10485
|
+
return dr.cycles.length ? 3 : 0;
|
|
10486
|
+
}
|
|
10487
|
+
const raw = flag(argv, "--next");
|
|
10488
|
+
const n = raw && /^\d+$/.test(raw) ? Math.max(1, Number(raw)) : 5;
|
|
10489
|
+
const doing = tasks.filter((t) => t.status === "doing" && dr.byId.get(t.id).rank !== null).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
|
|
10490
|
+
const startable = tasks.filter((t) => dr.byId.get(t.id).startable).sort((a, b) => dr.byId.get(a.id).rank - dr.byId.get(b.id).rank);
|
|
10491
|
+
const picked = [...doing, ...startable].slice(0, n);
|
|
10492
|
+
const items = picked.map((t) => {
|
|
10493
|
+
const d = dr.byId.get(t.id);
|
|
10494
|
+
return {
|
|
10495
|
+
id: t.id,
|
|
10496
|
+
title: t.title,
|
|
10497
|
+
status: t.status,
|
|
10498
|
+
rank: d.rank,
|
|
10499
|
+
declared: t.prio ?? null,
|
|
10500
|
+
eff: d.eff_prio,
|
|
10501
|
+
inherited: d.prio_inherited,
|
|
10502
|
+
ready: d.ready,
|
|
10503
|
+
critical: d.critical,
|
|
10504
|
+
unblocks: d.unblocks,
|
|
10505
|
+
blocked_by: d.blocked_by,
|
|
10506
|
+
why: `${t.status === "doing" ? "进行中" : "可开工"} · eff=${d.eff_prio}${d.prio_inherited ? `(←${d.prio_source?.id})` : ""} · 解锁${d.unblocks}${d.critical ? " · 关键路径" : ""}`
|
|
10507
|
+
};
|
|
10508
|
+
});
|
|
10509
|
+
out(asJson ? JSON.stringify({ policy: dr.ranking.policy, items }) : items.length ? items.map((x, i) => `#${i + 1} ${x.id} ${x.eff}${x.inherited ? "(继承)" : ""} ${x.title} ${x.why}`).join(`
|
|
10510
|
+
`) : "(没有可开工任务)");
|
|
10511
|
+
return dr.cycles.length ? 3 : 0;
|
|
10512
|
+
}
|
|
10513
|
+
if (has(argv, "--export-html") || has(argv, "--out")) {
|
|
10514
|
+
const nowRaw = flag(argv, "--now");
|
|
10515
|
+
const now = nowRaw ? new Date(nowRaw) : new Date;
|
|
10516
|
+
if (Number.isNaN(now.getTime())) {
|
|
10517
|
+
err(`E_GRAPH: --now 不是合法时间(收到 ${nowRaw})`);
|
|
10518
|
+
return 2;
|
|
10519
|
+
}
|
|
10520
|
+
const limitRaw = flag(argv, "--limit");
|
|
10521
|
+
const limit = has(argv, "--all") ? 0 : limitRaw === undefined ? 500 : Number(limitRaw);
|
|
10522
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
10523
|
+
err("E_GRAPH: --limit 必须是 >=0 的整数");
|
|
10524
|
+
return 2;
|
|
10525
|
+
}
|
|
10526
|
+
const stamp = `${localDay(now).replace(/-/g, "")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
10527
|
+
const argPath = flag(argv, "--export-html");
|
|
10528
|
+
const positionalOut = argPath && !argPath.startsWith("-") ? argPath : "";
|
|
10529
|
+
const outPath = flag(argv, "--out") || positionalOut || join32(diaHome(), "export", `graph-${stamp}.html`);
|
|
10530
|
+
if (existsSync29(outPath) && !has(argv, "--force")) {
|
|
10531
|
+
err(`E_GRAPH: 已存在 ${outPath}(用 --force 覆盖)`);
|
|
10532
|
+
return 2;
|
|
10533
|
+
}
|
|
10534
|
+
try {
|
|
10535
|
+
const g = build({ task: flag(argv, "--task") });
|
|
10536
|
+
const html = renderHtml(g, { now, redact: has(argv, "--redact"), limit, claims: list3().length });
|
|
10537
|
+
mkdirSync20(join32(outPath, ".."), { recursive: true });
|
|
10538
|
+
writeFileSync18(outPath, html, { mode: 384 });
|
|
10539
|
+
out(outPath);
|
|
10540
|
+
return 0;
|
|
10541
|
+
} catch (e) {
|
|
10542
|
+
err(e?.message || String(e));
|
|
10543
|
+
return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
8985
10546
|
if (has(argv, "--tui") || has(argv, "--watch")) {
|
|
8986
10547
|
const width = Number(flag(argv, "--width") || 0);
|
|
8987
10548
|
const tuiOpts = {
|
|
@@ -9086,7 +10647,7 @@ function spawnDetachedDelivery(args) {
|
|
|
9086
10647
|
const dir = join32(root, agent);
|
|
9087
10648
|
mkdirSync20(dir, { recursive: true });
|
|
9088
10649
|
const log = join32(dir, `deliver-${id}.log`);
|
|
9089
|
-
const fd =
|
|
10650
|
+
const fd = openSync4(log, "a");
|
|
9090
10651
|
const bin = process.argv[1];
|
|
9091
10652
|
const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
|
|
9092
10653
|
detached: true,
|
|
@@ -9932,7 +11493,7 @@ async function kitCli(argv) {
|
|
|
9932
11493
|
}
|
|
9933
11494
|
if (sub === "list") {
|
|
9934
11495
|
const rows = list();
|
|
9935
|
-
out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}`).join(`
|
|
11496
|
+
out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}${r.tags?.length ? ` [${r.tags.join(", ")}]` : ""}`).join(`
|
|
9936
11497
|
`) || "(none)");
|
|
9937
11498
|
return 0;
|
|
9938
11499
|
}
|