@minonl/dia 0.0.15 → 0.0.17
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 +8 -0
- package/dist/cli.js +1966 -270
- package/docs/builtin-kit-design.md +65 -0
- 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 +49 -25
- package/docs/design/graph-web.md +9 -4
- package/docs/design/seat-scheduling-m2.md +138 -0
- package/docs/index.md +9 -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 +187 -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
|
@@ -11,6 +11,7 @@ var __esm = (fn, res, err) => () => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/paths.ts
|
|
14
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
14
15
|
import { homedir as homedir2 } from "node:os";
|
|
15
16
|
import { dirname as dirname2, join as join3 } from "node:path";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
@@ -20,26 +21,31 @@ function diaHome() {
|
|
|
20
21
|
}
|
|
21
22
|
function installDir() {
|
|
22
23
|
const env = process.env.DIA_INSTALL_DIR || process.env.DIA_HOME_INSTALL;
|
|
23
|
-
if (env)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return diaHome();
|
|
24
|
+
if (env) {
|
|
25
|
+
if (existsSync3(env))
|
|
26
|
+
return env;
|
|
27
|
+
process.stderr.write(`! warn: DIA_INSTALL_DIR=${env} 不存在,忽略该覆盖并自动探测安装目录
|
|
28
|
+
`);
|
|
29
29
|
}
|
|
30
|
+
try {
|
|
31
|
+
const derived = dirname2(dirname2(fileURLToPath(import.meta.url)));
|
|
32
|
+
if (existsSync3(derived))
|
|
33
|
+
return derived;
|
|
34
|
+
} catch {}
|
|
35
|
+
return diaHome();
|
|
30
36
|
}
|
|
31
37
|
var personasDir = () => join3(diaHome(), "personas"), msgRoot = () => join3(diaHome(), "msg"), agentsRoot = () => join3(diaHome(), "agents"), stateDir = () => process.env.DIA_STATE_DIR || join3(diaHome(), "state"), cacheRoot = () => join3(diaHome(), "cache"), jobRoot = () => join3(diaHome(), "job"), ingestDir = () => join3(diaHome(), "ingest"), srcCacheDir = () => process.env.DIA_CACHE_DIR || join3(cacheRoot(), "src");
|
|
32
38
|
var init_paths = () => {};
|
|
33
39
|
|
|
34
40
|
// src/runtime.ts
|
|
35
|
-
import { existsSync as
|
|
41
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
36
42
|
import { join as join5 } from "node:path";
|
|
37
43
|
function callsFile() {
|
|
38
44
|
return join5(DATA2(), "runtime", "calls.jsonl");
|
|
39
45
|
}
|
|
40
46
|
function allCalls() {
|
|
41
47
|
const f = callsFile();
|
|
42
|
-
if (!
|
|
48
|
+
if (!existsSync5(f))
|
|
43
49
|
return [];
|
|
44
50
|
const out = [];
|
|
45
51
|
for (const line of readFileSync3(f, "utf8").split(`
|
|
@@ -69,10 +75,10 @@ var init_runtime = __esm(() => {
|
|
|
69
75
|
});
|
|
70
76
|
|
|
71
77
|
// src/freeze.ts
|
|
72
|
-
import { chmodSync, existsSync as
|
|
78
|
+
import { chmodSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, statSync as statSync3 } from "node:fs";
|
|
73
79
|
import { join as join6 } from "node:path";
|
|
74
80
|
function status() {
|
|
75
|
-
if (!
|
|
81
|
+
if (!existsSync6(FILE))
|
|
76
82
|
return { frozen: false };
|
|
77
83
|
try {
|
|
78
84
|
const s = JSON.parse(readFileSync4(FILE, "utf8"));
|
|
@@ -139,7 +145,7 @@ function thaw(opts) {
|
|
|
139
145
|
}
|
|
140
146
|
function snapshot() {
|
|
141
147
|
const p = join6(STATE, "freeze-snapshot.json");
|
|
142
|
-
if (!
|
|
148
|
+
if (!existsSync6(p))
|
|
143
149
|
return null;
|
|
144
150
|
return JSON.parse(readFileSync4(p, "utf8"));
|
|
145
151
|
}
|
|
@@ -155,7 +161,7 @@ var init_freeze = __esm(() => {
|
|
|
155
161
|
import {
|
|
156
162
|
appendFileSync as appendFileSync2,
|
|
157
163
|
chmodSync as chmodSync2,
|
|
158
|
-
existsSync as
|
|
164
|
+
existsSync as existsSync7,
|
|
159
165
|
mkdirSync as mkdirSync4,
|
|
160
166
|
readFileSync as readFileSync5,
|
|
161
167
|
realpathSync,
|
|
@@ -217,7 +223,7 @@ function checkpointTarget() {
|
|
|
217
223
|
};
|
|
218
224
|
}
|
|
219
225
|
try {
|
|
220
|
-
if (
|
|
226
|
+
if (existsSync7(file) && statSync4(file).isDirectory()) {
|
|
221
227
|
return { ok: false, path: file, error: `${CHECKPOINT_ENV} points at a directory (${file}); expected a file path` };
|
|
222
228
|
}
|
|
223
229
|
} catch {}
|
|
@@ -229,7 +235,7 @@ function readCheckpoint(target = checkpointTarget()) {
|
|
|
229
235
|
if (!target.ok)
|
|
230
236
|
return { state: "misconfigured", path: target.path, error: target.error };
|
|
231
237
|
const file = target.path;
|
|
232
|
-
if (!
|
|
238
|
+
if (!existsSync7(file))
|
|
233
239
|
return { state: "missing", path: file };
|
|
234
240
|
let parsed;
|
|
235
241
|
try {
|
|
@@ -401,7 +407,7 @@ function holderAlive(dir) {
|
|
|
401
407
|
}
|
|
402
408
|
function readHead() {
|
|
403
409
|
const file = headFile();
|
|
404
|
-
if (!
|
|
410
|
+
if (!existsSync7(file))
|
|
405
411
|
return { state: "missing" };
|
|
406
412
|
let parsed;
|
|
407
413
|
try {
|
|
@@ -498,8 +504,8 @@ function append(ev) {
|
|
|
498
504
|
full = { ...base, prev_hash };
|
|
499
505
|
full.hash = hashEvent(full, prev_hash);
|
|
500
506
|
const before = {
|
|
501
|
-
size:
|
|
502
|
-
head:
|
|
507
|
+
size: existsSync7(file) ? statSync4(file).size : 0,
|
|
508
|
+
head: existsSync7(headFile()) ? readFileSync5(headFile()) : null
|
|
503
509
|
};
|
|
504
510
|
const anchored = { last_seq: last.seq ?? 0, last_hash: last.hash ?? GENESIS, row_count: last.count };
|
|
505
511
|
const cpId = checkpoint_id || randomUUID2();
|
|
@@ -568,7 +574,7 @@ function append(ev) {
|
|
|
568
574
|
}
|
|
569
575
|
function rawLines() {
|
|
570
576
|
const file = auditFile();
|
|
571
|
-
if (!
|
|
577
|
+
if (!existsSync7(file))
|
|
572
578
|
return [];
|
|
573
579
|
return readFileSync5(file, "utf8").split(`
|
|
574
580
|
`).filter((l) => l.trim());
|
|
@@ -1288,7 +1294,7 @@ function getDoc(root, rel, section) {
|
|
|
1288
1294
|
// src/ingest.ts
|
|
1289
1295
|
import { execFileSync } from "node:child_process";
|
|
1290
1296
|
import { createHash as createHash2 } from "node:crypto";
|
|
1291
|
-
import { appendFileSync, existsSync as
|
|
1297
|
+
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "node:fs";
|
|
1292
1298
|
import { homedir as homedir3 } from "node:os";
|
|
1293
1299
|
import { basename as basename2, dirname as dirname3, join as join4 } from "node:path";
|
|
1294
1300
|
import { zstdDecompressSync } from "node:zlib";
|
|
@@ -1480,7 +1486,7 @@ async function readOpencode(dbPath, domain, project) {
|
|
|
1480
1486
|
}
|
|
1481
1487
|
function loadState(cacheDir) {
|
|
1482
1488
|
const p = join4(cacheDir || CACHE, "state.json");
|
|
1483
|
-
if (!
|
|
1489
|
+
if (!existsSync4(p))
|
|
1484
1490
|
return {};
|
|
1485
1491
|
try {
|
|
1486
1492
|
return JSON.parse(readFileSync2(p, "utf8"));
|
|
@@ -1490,7 +1496,7 @@ function loadState(cacheDir) {
|
|
|
1490
1496
|
}
|
|
1491
1497
|
function seenSet(file) {
|
|
1492
1498
|
const set = new Set;
|
|
1493
|
-
if (!
|
|
1499
|
+
if (!existsSync4(file))
|
|
1494
1500
|
return set;
|
|
1495
1501
|
for (const line of readFileSync2(file, "utf8").split(`
|
|
1496
1502
|
`)) {
|
|
@@ -1518,7 +1524,7 @@ async function ingest(opts = {}) {
|
|
|
1518
1524
|
let episodes = [];
|
|
1519
1525
|
if (sources.includes("dsh")) {
|
|
1520
1526
|
const root = opts.sessionsRoot || join4(homedir3(), ".dsh", "sessions");
|
|
1521
|
-
if (
|
|
1527
|
+
if (existsSync4(root)) {
|
|
1522
1528
|
const cutoff = opts.full ? 0 : sinceMs ?? Number(state.dsh?.lastMtime || 0);
|
|
1523
1529
|
for (const file of walkDsh(root)) {
|
|
1524
1530
|
const mtime = Math.floor(statSync2(file).mtimeMs);
|
|
@@ -1536,7 +1542,7 @@ async function ingest(opts = {}) {
|
|
|
1536
1542
|
}
|
|
1537
1543
|
if (sources.includes("opencode")) {
|
|
1538
1544
|
const dbPath = opts.opencodeDb || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
|
|
1539
|
-
if (
|
|
1545
|
+
if (existsSync4(dbPath)) {
|
|
1540
1546
|
const cutoff = opts.full ? 0 : sinceMs ?? Number(state.opencode?.lastUpdated || 0);
|
|
1541
1547
|
const all = await readOpencode(dbPath, domain, opts.project);
|
|
1542
1548
|
result.scanned += all.length;
|
|
@@ -1699,7 +1705,7 @@ function resumeCard(session) {
|
|
|
1699
1705
|
|
|
1700
1706
|
// src/persona.ts
|
|
1701
1707
|
import { createHash as createHash7 } from "node:crypto";
|
|
1702
|
-
import { closeSync, cpSync as cpSync2, existsSync as
|
|
1708
|
+
import { closeSync as closeSync2, cpSync as cpSync2, existsSync as existsSync12, lstatSync as lstatSync2, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync10, readdirSync as readdirSync6, realpathSync as realpathSync3, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync8, symlinkSync as symlinkSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
1703
1709
|
import { homedir as homedir8 } from "node:os";
|
|
1704
1710
|
import { basename as basename6, dirname as dirname8, join as join13, relative as relative4, resolve as resolve7, sep as sep5 } from "node:path";
|
|
1705
1711
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
@@ -1707,14 +1713,17 @@ import { execFileSync as execFileSync3 } from "node:child_process";
|
|
|
1707
1713
|
// src/kit.ts
|
|
1708
1714
|
import { createHash as createHash6 } from "node:crypto";
|
|
1709
1715
|
import {
|
|
1716
|
+
closeSync,
|
|
1710
1717
|
cpSync,
|
|
1711
|
-
existsSync as
|
|
1718
|
+
existsSync as existsSync9,
|
|
1712
1719
|
lstatSync,
|
|
1713
1720
|
mkdirSync as mkdirSync7,
|
|
1721
|
+
openSync,
|
|
1714
1722
|
readFileSync as readFileSync7,
|
|
1715
1723
|
readdirSync as readdirSync3,
|
|
1716
1724
|
readlinkSync,
|
|
1717
1725
|
realpathSync as realpathSync2,
|
|
1726
|
+
renameSync as renameSync3,
|
|
1718
1727
|
rmSync as rmSync3,
|
|
1719
1728
|
statSync as statSync5,
|
|
1720
1729
|
symlinkSync,
|
|
@@ -1728,7 +1737,7 @@ import { execFileSync as execFileSync2 } from "node:child_process";
|
|
|
1728
1737
|
// src/config.ts
|
|
1729
1738
|
init_paths();
|
|
1730
1739
|
import { createHash as createHash5 } from "node:crypto";
|
|
1731
|
-
import { existsSync as
|
|
1740
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1732
1741
|
import { homedir as homedir4 } from "node:os";
|
|
1733
1742
|
import { dirname as dirname5, join as join9 } from "node:path";
|
|
1734
1743
|
var home = () => process.env.DIA_HOME || homedir4();
|
|
@@ -1811,7 +1820,7 @@ function apply(entries, opts = {}) {
|
|
|
1811
1820
|
const applied = [];
|
|
1812
1821
|
for (const e of entries) {
|
|
1813
1822
|
const path = expand(e.file);
|
|
1814
|
-
const exists =
|
|
1823
|
+
const exists = existsSync8(path);
|
|
1815
1824
|
const before = exists ? readFileSync6(path, "utf8") : "";
|
|
1816
1825
|
let doc = {};
|
|
1817
1826
|
if (exists && before.trim()) {
|
|
@@ -1846,7 +1855,7 @@ function apply(entries, opts = {}) {
|
|
|
1846
1855
|
function revert(records) {
|
|
1847
1856
|
const restored = [], skipped = [];
|
|
1848
1857
|
for (const r of records) {
|
|
1849
|
-
if (!
|
|
1858
|
+
if (!existsSync8(r.path)) {
|
|
1850
1859
|
skipped.push(r.path);
|
|
1851
1860
|
continue;
|
|
1852
1861
|
}
|
|
@@ -1891,13 +1900,64 @@ function assertInsideHome(p) {
|
|
|
1891
1900
|
function assertInside(base, p) {
|
|
1892
1901
|
const abs = resolve5(base, p);
|
|
1893
1902
|
const rootReal = realpathSync2(base);
|
|
1894
|
-
if (!
|
|
1903
|
+
if (!existsSync9(abs))
|
|
1895
1904
|
throw new Error(`E_MANIFEST: source missing: ${p}`);
|
|
1896
1905
|
const real = realpathSync2(abs);
|
|
1897
1906
|
if (real !== rootReal && !real.startsWith(rootReal + sep4))
|
|
1898
1907
|
throw new Error(`E_MANIFEST: source escapes kit: ${p}`);
|
|
1899
1908
|
return abs;
|
|
1900
1909
|
}
|
|
1910
|
+
var EMPTY_TREE_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
1911
|
+
function cmpVersion(a, b) {
|
|
1912
|
+
const pa = a.split(/[.+-]/), pb = b.split(/[.+-]/);
|
|
1913
|
+
for (let i = 0;i < Math.max(pa.length, pb.length); i++) {
|
|
1914
|
+
const x = pa[i] ?? "0", y = pb[i] ?? "0";
|
|
1915
|
+
const nx = Number(x), ny = Number(y);
|
|
1916
|
+
const both = !Number.isNaN(nx) && !Number.isNaN(ny);
|
|
1917
|
+
if (both ? nx !== ny : x !== y)
|
|
1918
|
+
return both ? nx - ny : x < y ? -1 : 1;
|
|
1919
|
+
}
|
|
1920
|
+
return 0;
|
|
1921
|
+
}
|
|
1922
|
+
function versionDirs(dir) {
|
|
1923
|
+
return readdirSync3(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort(cmpVersion);
|
|
1924
|
+
}
|
|
1925
|
+
function sleepSync2(ms) {
|
|
1926
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1927
|
+
}
|
|
1928
|
+
function withInstallLock(lockFile, fn) {
|
|
1929
|
+
const budget = Number(process.env.DIA_KIT_LOCK_MS || 5000);
|
|
1930
|
+
const start = Date.now();
|
|
1931
|
+
mkdirSync7(dirname6(lockFile), { recursive: true });
|
|
1932
|
+
for (;; ) {
|
|
1933
|
+
try {
|
|
1934
|
+
const fd = openSync(lockFile, "wx");
|
|
1935
|
+
writeFileSync5(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
|
|
1936
|
+
closeSync(fd);
|
|
1937
|
+
break;
|
|
1938
|
+
} catch (e) {
|
|
1939
|
+
if (e?.code !== "EEXIST")
|
|
1940
|
+
throw e;
|
|
1941
|
+
try {
|
|
1942
|
+
const st = statSync5(lockFile);
|
|
1943
|
+
if (Date.now() - st.mtimeMs > 120000) {
|
|
1944
|
+
rmSync3(lockFile, { force: true });
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
} catch {
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (Date.now() - start > budget)
|
|
1951
|
+
throw new Error(`E_KIT_BUSY: 另一个 dia 进程正在安装 kit(锁 ${lockFile};可用 DIA_KIT_LOCK_MS 调整等待)`);
|
|
1952
|
+
sleepSync2(50);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
return fn();
|
|
1957
|
+
} finally {
|
|
1958
|
+
rmSync3(lockFile, { force: true });
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1901
1961
|
var TREE_SKIP = new Set([".git", "node_modules", ".data"]);
|
|
1902
1962
|
function copyFilter(base) {
|
|
1903
1963
|
return (p) => !relative3(base, p).split(sep4).some((s) => TREE_SKIP.has(s));
|
|
@@ -1927,7 +1987,7 @@ function hashTree(root) {
|
|
|
1927
1987
|
}
|
|
1928
1988
|
function readKitManifest(dir) {
|
|
1929
1989
|
const p = join10(dir, "dia.json");
|
|
1930
|
-
if (!
|
|
1990
|
+
if (!existsSync9(p))
|
|
1931
1991
|
throw new Error("E_MANIFEST: dia.json not found");
|
|
1932
1992
|
let m;
|
|
1933
1993
|
try {
|
|
@@ -1951,7 +2011,7 @@ function resolveSource(from, ref) {
|
|
|
1951
2011
|
const key = createHash6("sha1").update(url).digest("hex").slice(0, 12);
|
|
1952
2012
|
const dir = join10(cacheDir2(), key);
|
|
1953
2013
|
const git = (args) => execFileSync2("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1954
|
-
if (!
|
|
2014
|
+
if (!existsSync9(dir)) {
|
|
1955
2015
|
mkdirSync7(cacheDir2(), { recursive: true });
|
|
1956
2016
|
try {
|
|
1957
2017
|
execFileSync2("git", ["clone", "--quiet", url, dir], { stdio: "pipe" });
|
|
@@ -1976,7 +2036,7 @@ function resolveSource(from, ref) {
|
|
|
1976
2036
|
return { dir, commit: git(["rev-parse", "HEAD"]).trim() };
|
|
1977
2037
|
}
|
|
1978
2038
|
const dir = resolve5(from);
|
|
1979
|
-
if (!
|
|
2039
|
+
if (!existsSync9(dir))
|
|
1980
2040
|
throw new Error("E_SOURCE: path not found");
|
|
1981
2041
|
let commit = "local";
|
|
1982
2042
|
try {
|
|
@@ -1985,7 +2045,7 @@ function resolveSource(from, ref) {
|
|
|
1985
2045
|
return { dir, commit };
|
|
1986
2046
|
}
|
|
1987
2047
|
function checkTargetFree(path, kind, expected) {
|
|
1988
|
-
if (!
|
|
2048
|
+
if (!existsSync9(path) && !isSymlink(path))
|
|
1989
2049
|
return;
|
|
1990
2050
|
if (kind === "file") {
|
|
1991
2051
|
if (!isSymlink(path) && statSync5(path).isFile() && shaFile(path) === expected)
|
|
@@ -2051,7 +2111,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
|
|
|
2051
2111
|
try {
|
|
2052
2112
|
if (p.entry.type === "file" && p.content !== undefined) {
|
|
2053
2113
|
checkTargetFree(p.dest, "file", sha256(Buffer.from(p.content, "base64")));
|
|
2054
|
-
} else if (p.linkTarget && (
|
|
2114
|
+
} else if (p.linkTarget && (existsSync9(p.dest) || isSymlink(p.dest))) {
|
|
2055
2115
|
if (!linkManaged(p.dest, p.linkTarget))
|
|
2056
2116
|
conflict = true;
|
|
2057
2117
|
}
|
|
@@ -2065,7 +2125,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
|
|
|
2065
2125
|
throw new Error(`E_CONFLICT: ${p.dest} exists and differs`);
|
|
2066
2126
|
throw new Error(`E_CONFLICT: ${p.dest} exists and is not managed`);
|
|
2067
2127
|
}
|
|
2068
|
-
if (!isSymlink(p.dest) &&
|
|
2128
|
+
if (!isSymlink(p.dest) && existsSync9(p.dest)) {
|
|
2069
2129
|
mkdirSync7(backupDir, { recursive: true });
|
|
2070
2130
|
cpSync(p.dest, join10(backupDir, basename5(p.dest)));
|
|
2071
2131
|
}
|
|
@@ -2079,7 +2139,7 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
|
|
|
2079
2139
|
writeFileSync5(p.dest, Buffer.from(p.content, "base64"));
|
|
2080
2140
|
p.entry.value = shaFile(p.dest);
|
|
2081
2141
|
} else if (p.linkTarget) {
|
|
2082
|
-
if (
|
|
2142
|
+
if (existsSync9(p.dest) || isSymlink(p.dest))
|
|
2083
2143
|
unlinkSync(p.dest);
|
|
2084
2144
|
symlinkSync(resolve5(p.linkTarget), p.dest);
|
|
2085
2145
|
p.entry.value = relative3(targetDir, resolve5(p.linkTarget));
|
|
@@ -2095,8 +2155,9 @@ function renderContent(baseDir, targetDir, m, opts = {}) {
|
|
|
2095
2155
|
function install(from, opts = {}) {
|
|
2096
2156
|
const src = resolveSource(from, opts.ref);
|
|
2097
2157
|
const m = readKitManifest(src.dir);
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2158
|
+
const kitsRoot = resolve5(dataDir2(), "kits");
|
|
2159
|
+
const target = resolve5(kitsRoot, m.id, m.version);
|
|
2160
|
+
if (!target.startsWith(kitsRoot + sep4))
|
|
2100
2161
|
throw new Error("E_MANIFEST: version escapes install dir");
|
|
2101
2162
|
const kitRootRel = m.root ?? ".";
|
|
2102
2163
|
const kitRoot = resolve5(target, kitRootRel);
|
|
@@ -2108,47 +2169,113 @@ function install(from, opts = {}) {
|
|
|
2108
2169
|
const planned = renderContent(src.dir, target, m, { dryRun: true }) ?? [];
|
|
2109
2170
|
if (opts.dryRun)
|
|
2110
2171
|
return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered: planned, hash: null, changed: false };
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2172
|
+
return withInstallLock(join10(kitsRoot, ".install.lock"), () => {
|
|
2173
|
+
const alreadyInstalled = existsSync9(join10(target, "kit.lock"));
|
|
2174
|
+
const changed = !alreadyInstalled || hashTree(src.dir) !== hashTree(target);
|
|
2175
|
+
if (changed) {
|
|
2176
|
+
const staging = join10(kitsRoot, ".staging", `${m.id}-${m.version}-${process.pid}-${Date.now()}`);
|
|
2177
|
+
mkdirSync7(staging, { recursive: true });
|
|
2178
|
+
try {
|
|
2179
|
+
cpSync(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2180
|
+
const stagedHash = hashTree(staging);
|
|
2181
|
+
if (stagedHash === EMPTY_TREE_HASH)
|
|
2182
|
+
throw new Error(`E_KIT_EMPTY: kit ${m.id}@${m.version} 内容为空(检查 source 与清单路径)`);
|
|
2183
|
+
writeFileSync5(join10(staging, "kit.lock"), JSON.stringify({
|
|
2184
|
+
id: m.id,
|
|
2185
|
+
version: m.version,
|
|
2186
|
+
commit: src.commit,
|
|
2187
|
+
root: kitRootRel,
|
|
2188
|
+
hash: stagedHash,
|
|
2189
|
+
rendered: [],
|
|
2190
|
+
commands: m.commands ?? [],
|
|
2191
|
+
knowledge: m.knowledge ?? [],
|
|
2192
|
+
dataRoot: m.data_root ?? null,
|
|
2193
|
+
env: m.env ?? null,
|
|
2194
|
+
config: []
|
|
2195
|
+
}, null, 1));
|
|
2196
|
+
mkdirSync7(dirname6(target), { recursive: true });
|
|
2197
|
+
if (existsSync9(target)) {
|
|
2198
|
+
const bak = `${target}.old-${process.pid}-${Date.now()}`;
|
|
2199
|
+
renameSync3(target, bak);
|
|
2200
|
+
try {
|
|
2201
|
+
renameSync3(staging, target);
|
|
2202
|
+
rmSync3(bak, { recursive: true, force: true });
|
|
2203
|
+
} catch (e) {
|
|
2204
|
+
try {
|
|
2205
|
+
renameSync3(bak, target);
|
|
2206
|
+
} catch {}
|
|
2207
|
+
throw e;
|
|
2208
|
+
}
|
|
2209
|
+
} else {
|
|
2210
|
+
renameSync3(staging, target);
|
|
2211
|
+
}
|
|
2212
|
+
} finally {
|
|
2213
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
const rendered = renderContent(src.dir, target, m) ?? planned;
|
|
2217
|
+
const appliedConfig = apply(m.config ?? []);
|
|
2218
|
+
const hash = hashTree(target);
|
|
2219
|
+
writeFileSync5(join10(target, "kit.lock"), JSON.stringify({
|
|
2220
|
+
id: m.id,
|
|
2221
|
+
version: m.version,
|
|
2222
|
+
commit: src.commit,
|
|
2223
|
+
root: kitRootRel,
|
|
2224
|
+
hash,
|
|
2225
|
+
rendered,
|
|
2226
|
+
commands: m.commands ?? [],
|
|
2227
|
+
knowledge: m.knowledge ?? [],
|
|
2228
|
+
dataRoot: m.data_root ?? null,
|
|
2229
|
+
env: m.env ?? null,
|
|
2230
|
+
config: appliedConfig
|
|
2231
|
+
}, null, 1));
|
|
2232
|
+
return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed };
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
function installed() {
|
|
2236
|
+
const root = join10(dataDir2(), "kits");
|
|
2237
|
+
if (!existsSync9(root))
|
|
2238
|
+
return [];
|
|
2239
|
+
const out = [];
|
|
2240
|
+
for (const e of readdirSync3(root, { withFileTypes: true })) {
|
|
2241
|
+
if (!e.isDirectory() || e.name.startsWith("."))
|
|
2242
|
+
continue;
|
|
2243
|
+
const versions = versionDirs(join10(root, e.name));
|
|
2244
|
+
const version = versions[versions.length - 1];
|
|
2245
|
+
if (!version)
|
|
2246
|
+
continue;
|
|
2247
|
+
const dir = join10(root, e.name, version);
|
|
2248
|
+
let tags = [];
|
|
2249
|
+
try {
|
|
2250
|
+
tags = readKitManifest(dir).tags ?? [];
|
|
2251
|
+
} catch {}
|
|
2252
|
+
out.push({ id: e.name, version, dir, tags });
|
|
2123
2253
|
}
|
|
2124
|
-
|
|
2125
|
-
id: m.id,
|
|
2126
|
-
version: m.version,
|
|
2127
|
-
commit: src.commit,
|
|
2128
|
-
root: kitRootRel,
|
|
2129
|
-
hash,
|
|
2130
|
-
rendered,
|
|
2131
|
-
commands: m.commands ?? [],
|
|
2132
|
-
knowledge: m.knowledge ?? [],
|
|
2133
|
-
dataRoot: m.data_root ?? null,
|
|
2134
|
-
env: m.env ?? null,
|
|
2135
|
-
config: appliedConfig
|
|
2136
|
-
}, null, 1));
|
|
2137
|
-
return { id: m.id, version: m.version, dir: target, root: kitRoot, rendered, hash, changed: true };
|
|
2254
|
+
return out;
|
|
2138
2255
|
}
|
|
2139
2256
|
function list() {
|
|
2140
2257
|
const root = join10(dataDir2(), "kits");
|
|
2141
|
-
if (!
|
|
2258
|
+
if (!existsSync9(root))
|
|
2142
2259
|
return [];
|
|
2143
|
-
return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory()
|
|
2260
|
+
return readdirSync3(root, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => {
|
|
2261
|
+
const versions = versionDirs(join10(root, e.name));
|
|
2262
|
+
const latest = versions[versions.length - 1];
|
|
2263
|
+
let tags = [];
|
|
2264
|
+
if (latest) {
|
|
2265
|
+
try {
|
|
2266
|
+
tags = readKitManifest(join10(root, e.name, latest)).tags ?? [];
|
|
2267
|
+
} catch {}
|
|
2268
|
+
}
|
|
2269
|
+
return { id: e.name, versions, tags };
|
|
2270
|
+
});
|
|
2144
2271
|
}
|
|
2145
2272
|
function readLock(id, version) {
|
|
2146
2273
|
const idDir = join10(dataDir2(), "kits", id);
|
|
2147
|
-
if (!
|
|
2274
|
+
if (!existsSync9(idDir))
|
|
2148
2275
|
throw new Error("E_NOT_INSTALLED");
|
|
2149
|
-
const v = version ||
|
|
2276
|
+
const v = version || versionDirs(idDir).pop();
|
|
2150
2277
|
const lockFile = join10(idDir, v, "kit.lock");
|
|
2151
|
-
if (!
|
|
2278
|
+
if (!existsSync9(lockFile))
|
|
2152
2279
|
throw new Error("E_NOT_INSTALLED");
|
|
2153
2280
|
return { version: v, lock: JSON.parse(readFileSync7(lockFile, "utf8")) };
|
|
2154
2281
|
}
|
|
@@ -2165,7 +2292,7 @@ function verify2(id, version) {
|
|
|
2165
2292
|
for (const r of lock.rendered) {
|
|
2166
2293
|
if (r.type === "dir")
|
|
2167
2294
|
continue;
|
|
2168
|
-
if (!
|
|
2295
|
+
if (!existsSync9(r.path)) {
|
|
2169
2296
|
drift.push(`missing: ${r.path}`);
|
|
2170
2297
|
continue;
|
|
2171
2298
|
}
|
|
@@ -2189,19 +2316,19 @@ function verify2(id, version) {
|
|
|
2189
2316
|
}
|
|
2190
2317
|
function remove(id) {
|
|
2191
2318
|
const idDir = join10(dataDir2(), "kits", id);
|
|
2192
|
-
if (!
|
|
2319
|
+
if (!existsSync9(idDir))
|
|
2193
2320
|
throw new Error("E_NOT_INSTALLED");
|
|
2194
2321
|
const removed = [];
|
|
2195
2322
|
const skipped = [];
|
|
2196
2323
|
for (const v of readdirSync3(idDir)) {
|
|
2197
2324
|
const dir = join10(idDir, v);
|
|
2198
2325
|
const lockFile = join10(dir, "kit.lock");
|
|
2199
|
-
if (
|
|
2326
|
+
if (existsSync9(lockFile)) {
|
|
2200
2327
|
const lock = JSON.parse(readFileSync7(lockFile, "utf8"));
|
|
2201
2328
|
for (const r of lock.rendered) {
|
|
2202
2329
|
if (r.type === "dir")
|
|
2203
2330
|
continue;
|
|
2204
|
-
if (!
|
|
2331
|
+
if (!existsSync9(r.path) && !isSymlink(r.path))
|
|
2205
2332
|
continue;
|
|
2206
2333
|
if (r.type === "file") {
|
|
2207
2334
|
if (!isSymlink(r.path) && shaFile(r.path) === r.value) {
|
|
@@ -2218,7 +2345,7 @@ function remove(id) {
|
|
|
2218
2345
|
continue;
|
|
2219
2346
|
}
|
|
2220
2347
|
const want = resolve5(dir, r.value);
|
|
2221
|
-
if (
|
|
2348
|
+
if (existsSync9(want) && cur === realpathSync2(want)) {
|
|
2222
2349
|
unlinkSync(r.path);
|
|
2223
2350
|
removed.push(r.path);
|
|
2224
2351
|
} else
|
|
@@ -2233,7 +2360,7 @@ function remove(id) {
|
|
|
2233
2360
|
}
|
|
2234
2361
|
|
|
2235
2362
|
// src/default-msg.ts
|
|
2236
|
-
import { existsSync as
|
|
2363
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync4, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2237
2364
|
import { homedir as homedir6 } from "node:os";
|
|
2238
2365
|
import { dirname as dirname7, join as join11, resolve as resolve6 } from "node:path";
|
|
2239
2366
|
init_paths();
|
|
@@ -2259,7 +2386,7 @@ function writeConfig(c) {
|
|
|
2259
2386
|
const tmp = `${file}.tmp.${process.pid}`;
|
|
2260
2387
|
writeFileSync6(tmp, JSON.stringify(next, null, 1) + `
|
|
2261
2388
|
`, { mode: 384 });
|
|
2262
|
-
|
|
2389
|
+
renameSync4(tmp, file);
|
|
2263
2390
|
return next;
|
|
2264
2391
|
}
|
|
2265
2392
|
function setEnabled(enabled, personaId) {
|
|
@@ -2286,7 +2413,7 @@ function isEnabled(personaId) {
|
|
|
2286
2413
|
}
|
|
2287
2414
|
function installedVersion() {
|
|
2288
2415
|
const dir = join11(diaHome(), "kits", "dia-msg");
|
|
2289
|
-
if (!
|
|
2416
|
+
if (!existsSync10(dir))
|
|
2290
2417
|
return null;
|
|
2291
2418
|
try {
|
|
2292
2419
|
const rows = readdirSync4(dir).filter((v) => statSync6(join11(dir, v)).isDirectory()).sort();
|
|
@@ -2302,6 +2429,27 @@ function autoInstallAllowed() {
|
|
|
2302
2429
|
return false;
|
|
2303
2430
|
return resolve6(diaHome()) === resolve6(join11(homedir6(), ".dia"));
|
|
2304
2431
|
}
|
|
2432
|
+
function pkgVersion() {
|
|
2433
|
+
try {
|
|
2434
|
+
return String(JSON.parse(readFileSync8(join11(installDir(), "package.json"), "utf8")).version || "unknown");
|
|
2435
|
+
} catch {
|
|
2436
|
+
return "unknown";
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
var stampFile = () => join11(stateDir(), "bootstrap.json");
|
|
2440
|
+
function readStamp() {
|
|
2441
|
+
try {
|
|
2442
|
+
return JSON.parse(readFileSync8(stampFile(), "utf8"));
|
|
2443
|
+
} catch {
|
|
2444
|
+
return null;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
function writeStamp(kitId) {
|
|
2448
|
+
try {
|
|
2449
|
+
mkdirSync8(stateDir(), { recursive: true });
|
|
2450
|
+
writeFileSync6(stampFile(), JSON.stringify({ pkg: pkgVersion(), kit: kitId ?? null, at: new Date().toISOString() }, null, 1));
|
|
2451
|
+
} catch {}
|
|
2452
|
+
}
|
|
2305
2453
|
function ensure(personaId, opts = {}) {
|
|
2306
2454
|
const gate = isEnabled(personaId);
|
|
2307
2455
|
if (!gate.enabled && !opts.force)
|
|
@@ -2309,10 +2457,11 @@ function ensure(personaId, opts = {}) {
|
|
|
2309
2457
|
if (!opts.force && !autoInstallAllowed())
|
|
2310
2458
|
return { ok: false, reason: "auto-install-skipped:non-default-root(dia collab enable 可强制安装)" };
|
|
2311
2459
|
const source = builtinKitDir();
|
|
2312
|
-
if (!
|
|
2460
|
+
if (!existsSync10(join11(source, "dia.json")))
|
|
2313
2461
|
return { ok: false, reason: `missing-kit:${source}` };
|
|
2314
2462
|
try {
|
|
2315
2463
|
const r = install(source);
|
|
2464
|
+
writeStamp(r.id);
|
|
2316
2465
|
return { ok: true, kit: `${r.id}@${r.version}` };
|
|
2317
2466
|
} catch (e) {
|
|
2318
2467
|
return { ok: false, reason: `kit-install:${e?.message || String(e)}` };
|
|
@@ -2341,7 +2490,7 @@ function ensureForPersona(id, capabilities) {
|
|
|
2341
2490
|
|
|
2342
2491
|
// src/active.ts
|
|
2343
2492
|
init_paths();
|
|
2344
|
-
import { existsSync as
|
|
2493
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync9, readdirSync as readdirSync5, statSync as statSync7, appendFileSync as appendFileSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2345
2494
|
import { userInfo } from "node:os";
|
|
2346
2495
|
import { join as join12 } from "node:path";
|
|
2347
2496
|
var dataDir3 = () => diaHome();
|
|
@@ -2357,10 +2506,10 @@ function readAll() {
|
|
|
2357
2506
|
}
|
|
2358
2507
|
function installedVersions(id) {
|
|
2359
2508
|
const dir = join12(dataDir3(), "personas", id);
|
|
2360
|
-
if (!
|
|
2509
|
+
if (!existsSync11(dir))
|
|
2361
2510
|
return [];
|
|
2362
2511
|
const verRoot = join12(dir, "versions");
|
|
2363
|
-
if (
|
|
2512
|
+
if (existsSync11(verRoot))
|
|
2364
2513
|
return readdirSync5(verRoot).filter((v) => statSync7(join12(verRoot, v)).isDirectory()).sort();
|
|
2365
2514
|
const skip = new Set(["current", "latest", "data", "repo", "repo.git", "worktrees", "versions", ".lock"]);
|
|
2366
2515
|
return readdirSync5(dir).filter((v) => !skip.has(v) && !v.startsWith(".staging-") && statSync7(join12(dir, v)).isDirectory()).sort();
|
|
@@ -2408,7 +2557,7 @@ function use(id, version, opts = {}) {
|
|
|
2408
2557
|
return { previous, current: entry };
|
|
2409
2558
|
}
|
|
2410
2559
|
function history(id, limit = 20) {
|
|
2411
|
-
if (!
|
|
2560
|
+
if (!existsSync11(auditFile2()))
|
|
2412
2561
|
return [];
|
|
2413
2562
|
const rows = readFileSync9(auditFile2(), "utf8").split(`
|
|
2414
2563
|
`).filter(Boolean).map((l) => {
|
|
@@ -2705,7 +2854,7 @@ var opencodeConfigDir = () => process.env.DIA_OPENCODE_CONFIG_DIR || join13(home
|
|
|
2705
2854
|
var opencodeConfigCandidates = () => process.env.DIA_OPENCODE_CONFIG ? [process.env.DIA_OPENCODE_CONFIG] : OPENCODE_CONFIG_NAMES.map((n) => join13(opencodeConfigDir(), n));
|
|
2706
2855
|
var opencodeConfig = () => {
|
|
2707
2856
|
const candidates = opencodeConfigCandidates();
|
|
2708
|
-
return candidates.find((p) =>
|
|
2857
|
+
return candidates.find((p) => existsSync12(p)) ?? candidates[0];
|
|
2709
2858
|
};
|
|
2710
2859
|
function shaFile2(p) {
|
|
2711
2860
|
return createHash7("sha256").update(readFileSync10(p)).digest("hex");
|
|
@@ -2727,7 +2876,7 @@ function walkFiles2(root) {
|
|
|
2727
2876
|
}
|
|
2728
2877
|
function readManifest(dir) {
|
|
2729
2878
|
const p = join13(dir, "dia.json");
|
|
2730
|
-
if (!
|
|
2879
|
+
if (!existsSync12(p))
|
|
2731
2880
|
throw new Error("E_MANIFEST: dia.json not found");
|
|
2732
2881
|
let m;
|
|
2733
2882
|
try {
|
|
@@ -2751,7 +2900,7 @@ function resolveSource2(from, ref) {
|
|
|
2751
2900
|
const key = createHash7("sha1").update(url).digest("hex").slice(0, 12);
|
|
2752
2901
|
const dir = join13(cacheDir3(), key);
|
|
2753
2902
|
const git = (args) => execFileSync3("git", ["-C", dir, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2754
|
-
if (!
|
|
2903
|
+
if (!existsSync12(dir)) {
|
|
2755
2904
|
mkdirSync10(cacheDir3(), { recursive: true });
|
|
2756
2905
|
try {
|
|
2757
2906
|
execFileSync3("git", ["clone", "--quiet", url, dir], { stdio: "pipe" });
|
|
@@ -2777,7 +2926,7 @@ function resolveSource2(from, ref) {
|
|
|
2777
2926
|
return { dir, commit };
|
|
2778
2927
|
}
|
|
2779
2928
|
const dir = resolve7(from);
|
|
2780
|
-
if (!
|
|
2929
|
+
if (!existsSync12(dir))
|
|
2781
2930
|
throw new Error("E_SOURCE: path not found");
|
|
2782
2931
|
let commit = "local";
|
|
2783
2932
|
try {
|
|
@@ -2816,7 +2965,7 @@ function install2(from, opts = {}) {
|
|
|
2816
2965
|
if (!entry)
|
|
2817
2966
|
throw new Error("E_MANIFEST: missing entry.opencode");
|
|
2818
2967
|
const entryAbs = resolve7(src.dir, entry);
|
|
2819
|
-
if (!
|
|
2968
|
+
if (!existsSync12(entryAbs))
|
|
2820
2969
|
throw new Error("E_MANIFEST: entry.opencode file missing");
|
|
2821
2970
|
if (lstatSync2(entryAbs).isSymbolicLink())
|
|
2822
2971
|
throw new Error("E_MANIFEST: entry must not be a symlink");
|
|
@@ -2825,7 +2974,7 @@ function install2(from, opts = {}) {
|
|
|
2825
2974
|
if (!entryReal.startsWith(srcReal + sep5))
|
|
2826
2975
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
2827
2976
|
const dest = join13(opencodeAgents(), `${m.id}.md`);
|
|
2828
|
-
if (
|
|
2977
|
+
if (existsSync12(dest) && !readFileSync10(dest).equals(readFileSync10(entryAbs))) {
|
|
2829
2978
|
if (!opts.force)
|
|
2830
2979
|
throw new Error("E_CONFLICT: agent file exists and differs; remove it first (或 --force 覆盖,覆盖前会备份)");
|
|
2831
2980
|
overwrites.push(dest);
|
|
@@ -2836,7 +2985,7 @@ function install2(from, opts = {}) {
|
|
|
2836
2985
|
if (!dshEntry)
|
|
2837
2986
|
throw new Error("E_MANIFEST: missing entry.dsh");
|
|
2838
2987
|
const dshSrc = resolve7(src.dir, dshEntry);
|
|
2839
|
-
if (!
|
|
2988
|
+
if (!existsSync12(dshSrc) || !statSync8(dshSrc).isDirectory())
|
|
2840
2989
|
throw new Error("E_MANIFEST: entry.dsh dir missing");
|
|
2841
2990
|
if (!realpathSync3(dshSrc).startsWith(realpathSync3(src.dir) + sep5) && realpathSync3(dshSrc) !== realpathSync3(src.dir))
|
|
2842
2991
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
@@ -2847,12 +2996,12 @@ function install2(from, opts = {}) {
|
|
|
2847
2996
|
if (!codexEntry)
|
|
2848
2997
|
throw new Error("E_MANIFEST: missing entry.codex");
|
|
2849
2998
|
const abs = resolve7(src.dir, codexEntry);
|
|
2850
|
-
if (!
|
|
2999
|
+
if (!existsSync12(abs) || !statSync8(abs).isFile())
|
|
2851
3000
|
throw new Error("E_MANIFEST: entry.codex file missing");
|
|
2852
3001
|
if (!realpathSync3(abs).startsWith(realpathSync3(src.dir) + sep5))
|
|
2853
3002
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
2854
3003
|
const dest = join13(codexHome, ".codex", "AGENTS.md");
|
|
2855
|
-
if (
|
|
3004
|
+
if (existsSync12(dest) && !readFileSync10(dest).equals(readFileSync10(abs))) {
|
|
2856
3005
|
if (!opts.force)
|
|
2857
3006
|
throw new Error("E_CONFLICT: codex AGENTS.md exists and differs; use --force (备份后再覆盖)");
|
|
2858
3007
|
overwrites.push(dest);
|
|
@@ -2871,12 +3020,12 @@ function install2(from, opts = {}) {
|
|
|
2871
3020
|
let lockFd = null;
|
|
2872
3021
|
try {
|
|
2873
3022
|
try {
|
|
2874
|
-
lockFd =
|
|
3023
|
+
lockFd = openSync2(lockPath, "wx");
|
|
2875
3024
|
} catch {
|
|
2876
3025
|
const st = statSync8(lockPath);
|
|
2877
3026
|
if (Date.now() - st.mtimeMs > 10 * 60000) {
|
|
2878
3027
|
rmSync4(lockPath, { force: true });
|
|
2879
|
-
lockFd =
|
|
3028
|
+
lockFd = openSync2(lockPath, "wx");
|
|
2880
3029
|
} else
|
|
2881
3030
|
throw new Error("E_LOCKED: 另一个安装正在进行(10 分钟后视为陈旧)");
|
|
2882
3031
|
}
|
|
@@ -2887,7 +3036,7 @@ function install2(from, opts = {}) {
|
|
|
2887
3036
|
cpSync2(src.dir, staging, { recursive: true, dereference: true, filter: copyFilter(src.dir) });
|
|
2888
3037
|
const stagingHash = hashTree2(staging);
|
|
2889
3038
|
let reuseTarget = false;
|
|
2890
|
-
if (
|
|
3039
|
+
if (existsSync12(target)) {
|
|
2891
3040
|
const existing = (() => {
|
|
2892
3041
|
try {
|
|
2893
3042
|
return JSON.parse(readFileSync10(join13(target, "dia.lock"), "utf8"));
|
|
@@ -2906,11 +3055,11 @@ function install2(from, opts = {}) {
|
|
|
2906
3055
|
}
|
|
2907
3056
|
}
|
|
2908
3057
|
if (!reuseTarget)
|
|
2909
|
-
|
|
3058
|
+
renameSync5(staging, target);
|
|
2910
3059
|
} finally {
|
|
2911
3060
|
if (lockFd !== null) {
|
|
2912
3061
|
try {
|
|
2913
|
-
|
|
3062
|
+
closeSync2(lockFd);
|
|
2914
3063
|
} catch {}
|
|
2915
3064
|
}
|
|
2916
3065
|
try {
|
|
@@ -2936,7 +3085,7 @@ function install2(from, opts = {}) {
|
|
|
2936
3085
|
const renderedDirs = [];
|
|
2937
3086
|
if (hosts.includes("dsh")) {
|
|
2938
3087
|
const dshDest = resolve7(dshProfiles(), m.id);
|
|
2939
|
-
if (
|
|
3088
|
+
if (existsSync12(dshDest) && !existsSync12(join13(dshDest, ".dia-managed"))) {
|
|
2940
3089
|
throw new Error("E_CONFLICT: dsh profile exists and is not managed by dia");
|
|
2941
3090
|
}
|
|
2942
3091
|
rmSync4(dshDest, { recursive: true, force: true });
|
|
@@ -2957,7 +3106,7 @@ function install2(from, opts = {}) {
|
|
|
2957
3106
|
const srcSkill = resolve7(target, sk.from);
|
|
2958
3107
|
if (!srcSkill.startsWith(target + sep5))
|
|
2959
3108
|
throw new Error("E_MANIFEST: skill escapes persona dir: " + sk.from);
|
|
2960
|
-
if (!
|
|
3109
|
+
if (!existsSync12(srcSkill))
|
|
2961
3110
|
continue;
|
|
2962
3111
|
const name = sk.name || basename6(srcSkill);
|
|
2963
3112
|
mkdirSync10(skillRoot, { recursive: true });
|
|
@@ -2979,7 +3128,7 @@ function install2(from, opts = {}) {
|
|
|
2979
3128
|
const kitDir = resolve7(target, ref);
|
|
2980
3129
|
if (!kitDir.startsWith(target + sep5))
|
|
2981
3130
|
throw new Error(`E_MANIFEST: kit escapes persona dir: ${ref}`);
|
|
2982
|
-
if (!
|
|
3131
|
+
if (!existsSync12(join13(kitDir, "dia.json")))
|
|
2983
3132
|
throw new Error(`E_MANIFEST: kit manifest missing: ${ref}`);
|
|
2984
3133
|
const r = install(kitDir);
|
|
2985
3134
|
kitRefs.push({ id: r.id, version: r.version });
|
|
@@ -3007,13 +3156,13 @@ function install2(from, opts = {}) {
|
|
|
3007
3156
|
}
|
|
3008
3157
|
function list2() {
|
|
3009
3158
|
const root = join13(dataDir4(), "personas");
|
|
3010
|
-
if (!
|
|
3159
|
+
if (!existsSync12(root))
|
|
3011
3160
|
return [];
|
|
3012
|
-
return readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions:
|
|
3161
|
+
return readdirSync6(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ id: e.name, versions: versionDirs2(join13(root, e.name)) }));
|
|
3013
3162
|
}
|
|
3014
|
-
function
|
|
3163
|
+
function versionDirs2(idDir) {
|
|
3015
3164
|
const verRoot = join13(idDir, "versions");
|
|
3016
|
-
const scan = (root, keep) =>
|
|
3165
|
+
const scan = (root, keep) => existsSync12(root) ? readdirSync6(root).filter((n) => {
|
|
3017
3166
|
if (!keep(n))
|
|
3018
3167
|
return false;
|
|
3019
3168
|
try {
|
|
@@ -3022,14 +3171,14 @@ function versionDirs(idDir) {
|
|
|
3022
3171
|
return false;
|
|
3023
3172
|
}
|
|
3024
3173
|
}).sort() : [];
|
|
3025
|
-
if (
|
|
3174
|
+
if (existsSync12(verRoot))
|
|
3026
3175
|
return scan(verRoot, () => true);
|
|
3027
3176
|
const keep = (n) => !["current", "latest", "data", "repo", "repo.git", "worktrees", "versions"].includes(n) && !n.startsWith(".staging-") && n !== ".lock";
|
|
3028
3177
|
return scan(idDir, keep);
|
|
3029
3178
|
}
|
|
3030
3179
|
function resolveVersion(id, sel) {
|
|
3031
3180
|
const dir = join13(dataDir4(), "personas", id);
|
|
3032
|
-
const all =
|
|
3181
|
+
const all = versionDirs2(dir);
|
|
3033
3182
|
if (all.includes(sel))
|
|
3034
3183
|
return sel;
|
|
3035
3184
|
const hits = all.filter((v) => v === sel || v.startsWith(sel + "+") || v.startsWith(sel + "-"));
|
|
@@ -3045,12 +3194,12 @@ function dirOf(id, version) {
|
|
|
3045
3194
|
return null;
|
|
3046
3195
|
const flat = join13(dataDir4(), "personas", id, v);
|
|
3047
3196
|
const nested = join13(dataDir4(), "personas", id, "versions", v);
|
|
3048
|
-
const dir =
|
|
3049
|
-
return
|
|
3197
|
+
const dir = existsSync12(nested) ? nested : flat;
|
|
3198
|
+
return existsSync12(dir) ? { dir, version: v } : null;
|
|
3050
3199
|
}
|
|
3051
3200
|
function readMarker(dir) {
|
|
3052
3201
|
const p = join13(dir, ".dia-managed");
|
|
3053
|
-
if (!
|
|
3202
|
+
if (!existsSync12(p))
|
|
3054
3203
|
return null;
|
|
3055
3204
|
try {
|
|
3056
3205
|
return JSON.parse(readFileSync10(p, "utf8"));
|
|
@@ -3079,9 +3228,9 @@ function cmpSemver(a, b) {
|
|
|
3079
3228
|
}
|
|
3080
3229
|
function gc(id, opts = {}) {
|
|
3081
3230
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3082
|
-
if (!
|
|
3231
|
+
if (!existsSync12(idDir))
|
|
3083
3232
|
throw new Error("E_NOT_INSTALLED");
|
|
3084
|
-
const all =
|
|
3233
|
+
const all = versionDirs2(idDir);
|
|
3085
3234
|
const activeVer = effective(id)?.version;
|
|
3086
3235
|
const keep = Math.max(1, opts.keep ?? 3);
|
|
3087
3236
|
const sorted = [...all].sort((a, b) => cmpSemver(b, a));
|
|
@@ -3105,12 +3254,12 @@ function gc(id, opts = {}) {
|
|
|
3105
3254
|
}
|
|
3106
3255
|
function verify3(id, version) {
|
|
3107
3256
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3108
|
-
if (!
|
|
3257
|
+
if (!existsSync12(idDir))
|
|
3109
3258
|
throw new Error("E_NOT_INSTALLED");
|
|
3110
|
-
const v = version || effective(id)?.version ||
|
|
3259
|
+
const v = version || effective(id)?.version || versionDirs2(idDir).sort((a, b) => cmpSemver(b, a))[0];
|
|
3111
3260
|
if (!v)
|
|
3112
3261
|
throw new Error("E_NOT_INSTALLED");
|
|
3113
|
-
const dir =
|
|
3262
|
+
const dir = existsSync12(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
|
|
3114
3263
|
const lock = JSON.parse(readFileSync10(join13(dir, "dia.lock"), "utf8"));
|
|
3115
3264
|
const files = walkFiles2(dir).filter((f) => basename6(f) !== "dia.lock");
|
|
3116
3265
|
const h = createHash7("sha256");
|
|
@@ -3122,13 +3271,13 @@ function verify3(id, version) {
|
|
|
3122
3271
|
if (real !== lock.hash)
|
|
3123
3272
|
issues.push(`内容哈希不一致(期望 ${lock.hash.slice(0, 8)} 实得 ${real.slice(0, 8)})`);
|
|
3124
3273
|
for (const [f, expectedHash] of Object.entries(lock.renderedHashes || {})) {
|
|
3125
|
-
if (!
|
|
3274
|
+
if (!existsSync12(f))
|
|
3126
3275
|
issues.push(`产物缺失:${f}`);
|
|
3127
3276
|
else if (shaFile2(f) !== expectedHash)
|
|
3128
3277
|
issues.push(`产物被改动:${f}`);
|
|
3129
3278
|
}
|
|
3130
3279
|
for (const d of lock.renderedDirs || []) {
|
|
3131
|
-
if (!
|
|
3280
|
+
if (!existsSync12(d.path)) {
|
|
3132
3281
|
issues.push(`宿主 profile 缺失:${d.path}`);
|
|
3133
3282
|
continue;
|
|
3134
3283
|
}
|
|
@@ -3141,7 +3290,7 @@ function verify3(id, version) {
|
|
|
3141
3290
|
for (const r of lock.content?.rendered ?? []) {
|
|
3142
3291
|
if (r.type === "dir")
|
|
3143
3292
|
continue;
|
|
3144
|
-
if (!
|
|
3293
|
+
if (!existsSync12(r.path)) {
|
|
3145
3294
|
issues.push(`内容产物缺失:${r.path}`);
|
|
3146
3295
|
continue;
|
|
3147
3296
|
}
|
|
@@ -3157,7 +3306,7 @@ function verify3(id, version) {
|
|
|
3157
3306
|
continue;
|
|
3158
3307
|
}
|
|
3159
3308
|
const want = resolve7(dir, r.value);
|
|
3160
|
-
if (!
|
|
3309
|
+
if (!existsSync12(want) || cur !== realpathSync3(want))
|
|
3161
3310
|
issues.push(`内容产物指向不符:${r.path}`);
|
|
3162
3311
|
}
|
|
3163
3312
|
}
|
|
@@ -3274,7 +3423,7 @@ function create(id, opts = {}) {
|
|
|
3274
3423
|
const hosts = opts.hosts?.length ? opts.hosts : ["opencode", "dsh"];
|
|
3275
3424
|
for (const h of hosts)
|
|
3276
3425
|
assertHost(h);
|
|
3277
|
-
if (
|
|
3426
|
+
if (existsSync12(dir) && readdirSync6(dir).length)
|
|
3278
3427
|
throw new Error(`E_CONFLICT: ${dir} 非空`);
|
|
3279
3428
|
const files = skeleton(id, hosts, opts.msg !== false);
|
|
3280
3429
|
if (!opts.dryRun) {
|
|
@@ -3302,7 +3451,7 @@ function compareDir(expected, actual) {
|
|
|
3302
3451
|
if (HOST_OWNED_FILES.has(rel))
|
|
3303
3452
|
continue;
|
|
3304
3453
|
const dest = join13(actual, rel);
|
|
3305
|
-
if (!
|
|
3454
|
+
if (!existsSync12(dest))
|
|
3306
3455
|
out.push({ path: dest, action: "missing" });
|
|
3307
3456
|
else if (shaFile2(dest) !== shaFile2(f))
|
|
3308
3457
|
out.push({ path: dest, action: "changed" });
|
|
@@ -3311,7 +3460,7 @@ function compareDir(expected, actual) {
|
|
|
3311
3460
|
}
|
|
3312
3461
|
function render(id, opts = {}) {
|
|
3313
3462
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3314
|
-
if (!
|
|
3463
|
+
if (!existsSync12(idDir))
|
|
3315
3464
|
throw new Error(`E_NOT_INSTALLED: persona ${id}`);
|
|
3316
3465
|
const version = effective(id)?.version ?? readdirSync6(idDir).filter((v) => statSync8(join13(idDir, v)).isDirectory()).sort().pop();
|
|
3317
3466
|
const dir = join13(idDir, version);
|
|
@@ -3330,10 +3479,10 @@ function render(id, opts = {}) {
|
|
|
3330
3479
|
agentSrc = resolve7(dir, entry);
|
|
3331
3480
|
if (!agentSrc.startsWith(dir + sep5))
|
|
3332
3481
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
3333
|
-
if (!
|
|
3482
|
+
if (!existsSync12(agentSrc))
|
|
3334
3483
|
throw new Error("E_MANIFEST: entry.opencode file missing");
|
|
3335
3484
|
agentDest = join13(opencodeAgents(), `${id}.md`);
|
|
3336
|
-
if (!
|
|
3485
|
+
if (!existsSync12(agentDest))
|
|
3337
3486
|
drift.push({ path: agentDest, action: "missing" });
|
|
3338
3487
|
else if (shaFile2(agentDest) !== shaFile2(agentSrc))
|
|
3339
3488
|
drift.push({ path: agentDest, action: "changed" });
|
|
@@ -3345,10 +3494,10 @@ function render(id, opts = {}) {
|
|
|
3345
3494
|
codexSrc = resolve7(dir, entry);
|
|
3346
3495
|
if (!codexSrc.startsWith(dir + sep5))
|
|
3347
3496
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
3348
|
-
if (!
|
|
3497
|
+
if (!existsSync12(codexSrc))
|
|
3349
3498
|
throw new Error("E_MANIFEST: entry.codex file missing");
|
|
3350
3499
|
codexDest = join13(codexHomeFor(opts, m), ".codex", "AGENTS.md");
|
|
3351
|
-
if (!
|
|
3500
|
+
if (!existsSync12(codexDest))
|
|
3352
3501
|
drift.push({ path: codexDest, action: "missing" });
|
|
3353
3502
|
else if (shaFile2(codexDest) !== shaFile2(codexSrc))
|
|
3354
3503
|
drift.push({ path: codexDest, action: "changed" });
|
|
@@ -3360,15 +3509,15 @@ function render(id, opts = {}) {
|
|
|
3360
3509
|
dshSrc = resolve7(dir, entry);
|
|
3361
3510
|
if (!dshSrc.startsWith(dir + sep5))
|
|
3362
3511
|
throw new Error("E_MANIFEST: entry escapes persona dir");
|
|
3363
|
-
if (!
|
|
3512
|
+
if (!existsSync12(dshSrc))
|
|
3364
3513
|
throw new Error("E_MANIFEST: entry.dsh dir missing");
|
|
3365
3514
|
dshDest = resolve7(dshProfiles(), id);
|
|
3366
|
-
if (!
|
|
3515
|
+
if (!existsSync12(dshDest))
|
|
3367
3516
|
drift.push({ path: dshDest, action: "missing" });
|
|
3368
3517
|
else {
|
|
3369
3518
|
drift.push(...compareDir(dshSrc, dshDest));
|
|
3370
3519
|
const marker = join13(dshDest, ".dia-managed");
|
|
3371
|
-
if (!
|
|
3520
|
+
if (!existsSync12(marker))
|
|
3372
3521
|
drift.push({ path: marker, action: "missing" });
|
|
3373
3522
|
else {
|
|
3374
3523
|
try {
|
|
@@ -3388,7 +3537,7 @@ function render(id, opts = {}) {
|
|
|
3388
3537
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
3389
3538
|
const bdir = join13(dataDir4(), "backups", `render-${stamp}`);
|
|
3390
3539
|
for (const d of drift) {
|
|
3391
|
-
if (!
|
|
3540
|
+
if (!existsSync12(d.path))
|
|
3392
3541
|
continue;
|
|
3393
3542
|
mkdirSync10(bdir, { recursive: true });
|
|
3394
3543
|
const dest = join13(bdir, basename6(d.path));
|
|
@@ -3439,14 +3588,14 @@ function render(id, opts = {}) {
|
|
|
3439
3588
|
}
|
|
3440
3589
|
function renderDrift(id, opts = {}) {
|
|
3441
3590
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3442
|
-
if (!
|
|
3591
|
+
if (!existsSync12(idDir))
|
|
3443
3592
|
throw new Error(`E_NOT_INSTALLED: persona ${id}`);
|
|
3444
3593
|
const version = effective(id)?.version ?? readdirSync6(idDir).filter((v) => statSync8(join13(idDir, v)).isDirectory()).sort().pop();
|
|
3445
3594
|
if (!version)
|
|
3446
3595
|
return { id, version: "", drift: [], skipped: `无已安装版本:${idDir}` };
|
|
3447
3596
|
const dir = join13(idDir, version);
|
|
3448
3597
|
const lockPath = join13(dir, "dia.lock");
|
|
3449
|
-
if (!
|
|
3598
|
+
if (!existsSync12(lockPath))
|
|
3450
3599
|
return { id, version, drift: [], skipped: `无 dia.lock(安装未完成):${dir}` };
|
|
3451
3600
|
let lock;
|
|
3452
3601
|
try {
|
|
@@ -3464,18 +3613,18 @@ function renderDrift(id, opts = {}) {
|
|
|
3464
3613
|
}
|
|
3465
3614
|
};
|
|
3466
3615
|
for (const [f, expected] of Object.entries(lock.renderedHashes ?? {})) {
|
|
3467
|
-
if (!
|
|
3616
|
+
if (!existsSync12(f))
|
|
3468
3617
|
push({ path: f, action: "missing" });
|
|
3469
3618
|
else if (shaFile2(f) !== expected)
|
|
3470
3619
|
push({ path: f, action: "changed" });
|
|
3471
3620
|
}
|
|
3472
3621
|
for (const d of lock.renderedDirs ?? []) {
|
|
3473
|
-
if (!
|
|
3622
|
+
if (!existsSync12(d.path)) {
|
|
3474
3623
|
push({ path: d.path, action: "missing" });
|
|
3475
3624
|
continue;
|
|
3476
3625
|
}
|
|
3477
3626
|
const marker = join13(d.path, ".dia-managed");
|
|
3478
|
-
if (!
|
|
3627
|
+
if (!existsSync12(marker)) {
|
|
3479
3628
|
push({ path: marker, action: "missing" });
|
|
3480
3629
|
continue;
|
|
3481
3630
|
}
|
|
@@ -3489,7 +3638,7 @@ function renderDrift(id, opts = {}) {
|
|
|
3489
3638
|
for (const c of lock.content?.rendered ?? []) {
|
|
3490
3639
|
if (c.type === "dir")
|
|
3491
3640
|
continue;
|
|
3492
|
-
if (!
|
|
3641
|
+
if (!existsSync12(c.path)) {
|
|
3493
3642
|
push({ path: c.path, action: "missing" });
|
|
3494
3643
|
continue;
|
|
3495
3644
|
}
|
|
@@ -3506,22 +3655,22 @@ function renderDrift(id, opts = {}) {
|
|
|
3506
3655
|
continue;
|
|
3507
3656
|
}
|
|
3508
3657
|
const want = resolve7(dir, c.value);
|
|
3509
|
-
if (!
|
|
3658
|
+
if (!existsSync12(want) || cur !== realpathSync3(want))
|
|
3510
3659
|
push({ path: c.path, action: "changed" });
|
|
3511
3660
|
}
|
|
3512
3661
|
return { id, version: r.version, drift };
|
|
3513
3662
|
}
|
|
3514
3663
|
function remove2(id) {
|
|
3515
3664
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3516
|
-
if (!
|
|
3665
|
+
if (!existsSync12(idDir))
|
|
3517
3666
|
throw new Error("E_NOT_INSTALLED");
|
|
3518
3667
|
const removed = [];
|
|
3519
3668
|
const skipped = [];
|
|
3520
3669
|
const allowed = resolve7(opencodeAgents(), `${id}.md`);
|
|
3521
|
-
for (const v of
|
|
3522
|
-
const dir =
|
|
3670
|
+
for (const v of versionDirs2(idDir)) {
|
|
3671
|
+
const dir = existsSync12(join13(idDir, "versions", v)) ? join13(idDir, "versions", v) : join13(idDir, v);
|
|
3523
3672
|
const lockFile = join13(dir, "dia.lock");
|
|
3524
|
-
if (
|
|
3673
|
+
if (existsSync12(lockFile)) {
|
|
3525
3674
|
const lock = JSON.parse(readFileSync10(lockFile, "utf8"));
|
|
3526
3675
|
for (const d of lock.renderedDirs || []) {
|
|
3527
3676
|
const abs = resolve7(d.path);
|
|
@@ -3529,7 +3678,7 @@ function remove2(id) {
|
|
|
3529
3678
|
skipped.push(abs);
|
|
3530
3679
|
continue;
|
|
3531
3680
|
}
|
|
3532
|
-
if (!
|
|
3681
|
+
if (!existsSync12(abs))
|
|
3533
3682
|
continue;
|
|
3534
3683
|
const m = readMarker(abs);
|
|
3535
3684
|
if (!m || m.id !== id) {
|
|
@@ -3542,10 +3691,10 @@ function remove2(id) {
|
|
|
3542
3691
|
for (const r of lock.content?.rendered ?? []) {
|
|
3543
3692
|
if (r.type === "dir")
|
|
3544
3693
|
continue;
|
|
3545
|
-
if (!
|
|
3694
|
+
if (!existsSync12(r.path))
|
|
3546
3695
|
continue;
|
|
3547
3696
|
if (r.type === "file") {
|
|
3548
|
-
if (!
|
|
3697
|
+
if (!existsSync12(r.path) || shaFile2(r.path) !== r.value) {
|
|
3549
3698
|
skipped.push(r.path);
|
|
3550
3699
|
continue;
|
|
3551
3700
|
}
|
|
@@ -3560,7 +3709,7 @@ function remove2(id) {
|
|
|
3560
3709
|
continue;
|
|
3561
3710
|
}
|
|
3562
3711
|
const want = resolve7(dir, r.value);
|
|
3563
|
-
if (
|
|
3712
|
+
if (existsSync12(want) && cur === realpathSync3(want)) {
|
|
3564
3713
|
unlinkSync2(r.path);
|
|
3565
3714
|
removed.push(r.path);
|
|
3566
3715
|
} else
|
|
@@ -3575,7 +3724,7 @@ function remove2(id) {
|
|
|
3575
3724
|
continue;
|
|
3576
3725
|
}
|
|
3577
3726
|
const expectedHash = lock.renderedHashes?.[f];
|
|
3578
|
-
if (!expectedHash || !
|
|
3727
|
+
if (!expectedHash || !existsSync12(abs) || shaFile2(abs) !== expectedHash) {
|
|
3579
3728
|
skipped.push(abs);
|
|
3580
3729
|
continue;
|
|
3581
3730
|
}
|
|
@@ -3606,7 +3755,7 @@ function showDefaultAgent(opts = {}) {
|
|
|
3606
3755
|
if (host !== "opencode")
|
|
3607
3756
|
throw new Error("E_HOST: only opencode has default_agent");
|
|
3608
3757
|
const file = opencodeConfig();
|
|
3609
|
-
if (!
|
|
3758
|
+
if (!existsSync12(file))
|
|
3610
3759
|
return { file, current: null };
|
|
3611
3760
|
const cfg = readOpencodeConfig(file);
|
|
3612
3761
|
return { file, current: typeof cfg.default_agent === "string" ? cfg.default_agent : null };
|
|
@@ -3624,13 +3773,13 @@ function setDefaultAgent(id, opts = {}) {
|
|
|
3624
3773
|
if (host !== "opencode")
|
|
3625
3774
|
throw new Error("E_HOST: only opencode has default_agent");
|
|
3626
3775
|
const file = opencodeConfig();
|
|
3627
|
-
if (!
|
|
3776
|
+
if (!existsSync12(file))
|
|
3628
3777
|
throw new Error(`E_CONFIG: ${file} 不存在(先装宿主,或设 DIA_OPENCODE_CONFIG)`);
|
|
3629
3778
|
const raw = readFileSync10(file, "utf8");
|
|
3630
3779
|
const cfg = readOpencodeConfig(file);
|
|
3631
3780
|
const stateFile = join13(dataDir4(), "state", "default-agent.json");
|
|
3632
3781
|
let state = { file, previous: null, set_at: null };
|
|
3633
|
-
if (
|
|
3782
|
+
if (existsSync12(stateFile)) {
|
|
3634
3783
|
try {
|
|
3635
3784
|
state = JSON.parse(readFileSync10(stateFile, "utf8"));
|
|
3636
3785
|
} catch {}
|
|
@@ -3653,7 +3802,7 @@ function setDefaultAgent(id, opts = {}) {
|
|
|
3653
3802
|
}
|
|
3654
3803
|
function checkUpstream(id, version) {
|
|
3655
3804
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3656
|
-
if (!
|
|
3805
|
+
if (!existsSync12(idDir))
|
|
3657
3806
|
throw new Error("E_NOT_INSTALLED");
|
|
3658
3807
|
const v = version || (effective(id)?.version ?? readdirSync6(idDir).sort().pop());
|
|
3659
3808
|
const lock = JSON.parse(readFileSync10(join13(idDir, v, "dia.lock"), "utf8"));
|
|
@@ -3687,7 +3836,7 @@ function checkUpstream(id, version) {
|
|
|
3687
3836
|
}
|
|
3688
3837
|
function update(id, opts = {}) {
|
|
3689
3838
|
const idDir = join13(dataDir4(), "personas", id);
|
|
3690
|
-
if (!
|
|
3839
|
+
if (!existsSync12(idDir))
|
|
3691
3840
|
throw new Error("E_NOT_INSTALLED");
|
|
3692
3841
|
const cur = effective(id)?.version ?? readdirSync6(idDir).sort().pop();
|
|
3693
3842
|
const lock = JSON.parse(readFileSync10(join13(idDir, cur, "dia.lock"), "utf8"));
|
|
@@ -3701,7 +3850,7 @@ function update(id, opts = {}) {
|
|
|
3701
3850
|
|
|
3702
3851
|
// src/msg.ts
|
|
3703
3852
|
init_paths();
|
|
3704
|
-
import { existsSync as
|
|
3853
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync11, renameSync as renameSync6, statSync as statSync9, writeFileSync as writeFileSync9 } from "node:fs";
|
|
3705
3854
|
import { join as join14 } from "node:path";
|
|
3706
3855
|
import { randomBytes } from "node:crypto";
|
|
3707
3856
|
var msgRoot2 = () => msgRoot();
|
|
@@ -3713,11 +3862,11 @@ function atomicWrite(path, data) {
|
|
|
3713
3862
|
mkdirSync11(join14(path, ".."), { recursive: true });
|
|
3714
3863
|
const tmp = `${path}.tmp.${process.pid}.${randomBytes(2).toString("hex")}`;
|
|
3715
3864
|
writeFileSync9(tmp, data);
|
|
3716
|
-
|
|
3865
|
+
renameSync6(tmp, path);
|
|
3717
3866
|
}
|
|
3718
3867
|
function readAll2(agent) {
|
|
3719
3868
|
const dir = dirOf2(agent);
|
|
3720
|
-
if (!
|
|
3869
|
+
if (!existsSync13(dir))
|
|
3721
3870
|
return [];
|
|
3722
3871
|
return readdirSync7(dir).filter((f) => f.endsWith(".json")).map((f) => {
|
|
3723
3872
|
try {
|
|
@@ -3736,7 +3885,7 @@ function newId(prefix) {
|
|
|
3736
3885
|
}
|
|
3737
3886
|
function allInboxes() {
|
|
3738
3887
|
const root = msgRoot2();
|
|
3739
|
-
if (!
|
|
3888
|
+
if (!existsSync13(root))
|
|
3740
3889
|
return [];
|
|
3741
3890
|
return readdirSync7(root, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ agent: e.name, messages: readAll2(e.name) }));
|
|
3742
3891
|
}
|
|
@@ -3749,7 +3898,7 @@ function send(opts) {
|
|
|
3749
3898
|
throw new Error("E_USAGE: --subject 必填");
|
|
3750
3899
|
if (opts.body && opts.bodyFile)
|
|
3751
3900
|
throw new Error("E_USAGE: --body 与 --body-file 只能给一个");
|
|
3752
|
-
if (opts.bodyFile && !
|
|
3901
|
+
if (opts.bodyFile && !existsSync13(opts.bodyFile))
|
|
3753
3902
|
throw new Error(`E_USAGE: body file 不存在: ${opts.bodyFile}`);
|
|
3754
3903
|
const threadId = opts.threadId || newId("th");
|
|
3755
3904
|
const existing = [...readAll2(opts.to), ...readAll2(opts.from)].filter((m) => m.thread_id === threadId);
|
|
@@ -3792,7 +3941,7 @@ function inbox(agent, opts = {}) {
|
|
|
3792
3941
|
}
|
|
3793
3942
|
function findInbox(agent, id) {
|
|
3794
3943
|
const dir = dirOf2(agent);
|
|
3795
|
-
if (!
|
|
3944
|
+
if (!existsSync13(dir))
|
|
3796
3945
|
return null;
|
|
3797
3946
|
for (const f of readdirSync7(dir)) {
|
|
3798
3947
|
if (!f.endsWith(".json"))
|
|
@@ -3901,7 +4050,7 @@ function stuck(minutes = 30, now = new Date) {
|
|
|
3901
4050
|
}
|
|
3902
4051
|
function agents() {
|
|
3903
4052
|
const root = msgRoot2();
|
|
3904
|
-
if (!
|
|
4053
|
+
if (!existsSync13(root))
|
|
3905
4054
|
return [];
|
|
3906
4055
|
return readdirSync7(root, { withFileTypes: true }).filter((e) => e.isDirectory() && statSync9(join14(root, e.name)).isDirectory()).map((e) => e.name);
|
|
3907
4056
|
}
|
|
@@ -3909,13 +4058,13 @@ function agents() {
|
|
|
3909
4058
|
// src/collab.ts
|
|
3910
4059
|
init_paths();
|
|
3911
4060
|
import {
|
|
3912
|
-
closeSync as
|
|
3913
|
-
existsSync as
|
|
4061
|
+
closeSync as closeSync3,
|
|
4062
|
+
existsSync as existsSync14,
|
|
3914
4063
|
mkdirSync as mkdirSync12,
|
|
3915
|
-
openSync as
|
|
4064
|
+
openSync as openSync3,
|
|
3916
4065
|
readdirSync as readdirSync8,
|
|
3917
4066
|
readFileSync as readFileSync12,
|
|
3918
|
-
renameSync as
|
|
4067
|
+
renameSync as renameSync7,
|
|
3919
4068
|
rmSync as rmSync5,
|
|
3920
4069
|
statSync as statSync10,
|
|
3921
4070
|
writeFileSync as writeFileSync10
|
|
@@ -3954,7 +4103,7 @@ function writeAtomic(path, data) {
|
|
|
3954
4103
|
ensureDir(dirname9(path));
|
|
3955
4104
|
const tmp = `${path}.tmp.${process.pid}.${randomBytes2(2).toString("hex")}`;
|
|
3956
4105
|
writeFileSync10(tmp, data, { mode: 384 });
|
|
3957
|
-
|
|
4106
|
+
renameSync7(tmp, path);
|
|
3958
4107
|
}
|
|
3959
4108
|
function sleep(ms) {
|
|
3960
4109
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
@@ -3967,7 +4116,7 @@ function withLock(name, fn) {
|
|
|
3967
4116
|
let fd;
|
|
3968
4117
|
for (let i = 0;i < 150; i++) {
|
|
3969
4118
|
try {
|
|
3970
|
-
fd =
|
|
4119
|
+
fd = openSync3(lock, "wx", 384);
|
|
3971
4120
|
break;
|
|
3972
4121
|
} catch (e) {
|
|
3973
4122
|
if (e?.code !== "EEXIST")
|
|
@@ -3985,7 +4134,7 @@ function withLock(name, fn) {
|
|
|
3985
4134
|
return fn();
|
|
3986
4135
|
} finally {
|
|
3987
4136
|
try {
|
|
3988
|
-
|
|
4137
|
+
closeSync3(fd);
|
|
3989
4138
|
} catch {}
|
|
3990
4139
|
try {
|
|
3991
4140
|
rmSync5(lock, { force: true });
|
|
@@ -4053,7 +4202,7 @@ function messageFile(sessionId, id) {
|
|
|
4053
4202
|
}
|
|
4054
4203
|
function readAll3(sessionId) {
|
|
4055
4204
|
const dir = join15(inboxRoot(), normalizeSessionId(sessionId));
|
|
4056
|
-
if (!
|
|
4205
|
+
if (!existsSync14(dir))
|
|
4057
4206
|
return [];
|
|
4058
4207
|
const out = [];
|
|
4059
4208
|
for (const f of readdirSync8(dir)) {
|
|
@@ -4067,7 +4216,7 @@ function readAll3(sessionId) {
|
|
|
4067
4216
|
return out.sort((a, b) => a.msg.created_at.localeCompare(b.msg.created_at) || a.msg.seq - b.msg.seq);
|
|
4068
4217
|
}
|
|
4069
4218
|
function allSessionsWithInbox() {
|
|
4070
|
-
if (!
|
|
4219
|
+
if (!existsSync14(inboxRoot()))
|
|
4071
4220
|
return [];
|
|
4072
4221
|
return readdirSync8(inboxRoot(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
4073
4222
|
}
|
|
@@ -4090,7 +4239,7 @@ function legacySlug(agent) {
|
|
|
4090
4239
|
return `${base}-${createHash8("sha1").update(agent).digest("hex").slice(0, 6)}`;
|
|
4091
4240
|
}
|
|
4092
4241
|
function legacyAll() {
|
|
4093
|
-
if (!
|
|
4242
|
+
if (!existsSync14(legacyRoot()))
|
|
4094
4243
|
return [];
|
|
4095
4244
|
const out = [];
|
|
4096
4245
|
for (const slug of readdirSync8(legacyRoot(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) {
|
|
@@ -4132,7 +4281,7 @@ function readBody(opts) {
|
|
|
4132
4281
|
throw new Error(`E_USAGE: body 超过 ${MAX_BODY} 字节,请拆分或引用文件`);
|
|
4133
4282
|
return { body: opts.body, body_bytes: bytes };
|
|
4134
4283
|
}
|
|
4135
|
-
if (!
|
|
4284
|
+
if (!existsSync14(opts.bodyFile))
|
|
4136
4285
|
throw new Error(`E_USAGE: body file 不存在: ${opts.bodyFile}`);
|
|
4137
4286
|
const buf = readFileSync12(opts.bodyFile);
|
|
4138
4287
|
if (buf.byteLength > MAX_BODY)
|
|
@@ -4312,13 +4461,13 @@ function close(sessionId, id, reason) {
|
|
|
4312
4461
|
}
|
|
4313
4462
|
function migrateLegacy(opts = {}) {
|
|
4314
4463
|
const oldRoot = join15(diaHome(), "msg");
|
|
4315
|
-
if (!
|
|
4464
|
+
if (!existsSync14(oldRoot))
|
|
4316
4465
|
return { total: 0, migrated: 0, skipped: 0, messages: [] };
|
|
4317
4466
|
const rows = [];
|
|
4318
4467
|
let migrated = 0, skipped = 0, total = 0;
|
|
4319
4468
|
for (const agent of readdirSync8(oldRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)) {
|
|
4320
4469
|
const dir = join15(oldRoot, agent, "inbox");
|
|
4321
|
-
if (!
|
|
4470
|
+
if (!existsSync14(dir))
|
|
4322
4471
|
continue;
|
|
4323
4472
|
const slug = legacySlug(agent);
|
|
4324
4473
|
for (const f of readdirSync8(dir)) {
|
|
@@ -4336,7 +4485,7 @@ function migrateLegacy(opts = {}) {
|
|
|
4336
4485
|
continue;
|
|
4337
4486
|
}
|
|
4338
4487
|
const target = join15(legacyRoot(), slug, `${legacyId}.json`);
|
|
4339
|
-
if (
|
|
4488
|
+
if (existsSync14(target)) {
|
|
4340
4489
|
skipped++;
|
|
4341
4490
|
continue;
|
|
4342
4491
|
}
|
|
@@ -4348,7 +4497,7 @@ function migrateLegacy(opts = {}) {
|
|
|
4348
4497
|
const allowed = [resolve8(oldRoot), resolve8(tmpdir())].some((base) => abs === base || abs.startsWith(base + sep6));
|
|
4349
4498
|
if (!allowed) {
|
|
4350
4499
|
bodyMissing = true;
|
|
4351
|
-
} else if (
|
|
4500
|
+
} else if (existsSync14(abs)) {
|
|
4352
4501
|
const buf = readFileSync12(abs);
|
|
4353
4502
|
bodyBytes = buf.byteLength;
|
|
4354
4503
|
if (bodyBytes > MAX_BODY) {
|
|
@@ -4396,7 +4545,7 @@ function migrateLegacy(opts = {}) {
|
|
|
4396
4545
|
|
|
4397
4546
|
// src/claims.ts
|
|
4398
4547
|
init_paths();
|
|
4399
|
-
import { appendFileSync as appendFileSync5, existsSync as
|
|
4548
|
+
import { appendFileSync as appendFileSync5, existsSync as existsSync15, mkdirSync as mkdirSync13, readFileSync as readFileSync13, renameSync as renameSync8, writeFileSync as writeFileSync11 } from "node:fs";
|
|
4400
4549
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4401
4550
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4402
4551
|
import { basename as basename7, join as join16, relative as relative5, resolve as resolve9, sep as sep7 } from "node:path";
|
|
@@ -4408,7 +4557,7 @@ function append2(e) {
|
|
|
4408
4557
|
`, { mode: 384 });
|
|
4409
4558
|
}
|
|
4410
4559
|
function fold() {
|
|
4411
|
-
if (!
|
|
4560
|
+
if (!existsSync15(file()))
|
|
4412
4561
|
return [];
|
|
4413
4562
|
const map = new Map;
|
|
4414
4563
|
for (const line of readFileSync13(file(), "utf8").split(`
|
|
@@ -4520,7 +4669,7 @@ function compact(opts = {}) {
|
|
|
4520
4669
|
const cutoff = now.getTime() - days * 86400000;
|
|
4521
4670
|
return withLock("claims", () => {
|
|
4522
4671
|
const f = file();
|
|
4523
|
-
if (!
|
|
4672
|
+
if (!existsSync15(f))
|
|
4524
4673
|
return { before: 0, after: 0, dropped: 0 };
|
|
4525
4674
|
const raw = readFileSync13(f, "utf8");
|
|
4526
4675
|
const lines = raw.split(`
|
|
@@ -4560,26 +4709,50 @@ function compact(opts = {}) {
|
|
|
4560
4709
|
writeFileSync11(tmp, kept.join(`
|
|
4561
4710
|
`) + (kept.length ? `
|
|
4562
4711
|
` : ""), { mode: 384 });
|
|
4563
|
-
|
|
4712
|
+
renameSync8(tmp, f);
|
|
4564
4713
|
}
|
|
4565
|
-
const after =
|
|
4714
|
+
const after = existsSync15(f) ? readFileSync13(f, "utf8").length : 0;
|
|
4566
4715
|
return { before: raw.length, after, dropped };
|
|
4567
4716
|
});
|
|
4568
4717
|
}
|
|
4569
4718
|
|
|
4570
4719
|
// src/task.ts
|
|
4571
4720
|
init_paths();
|
|
4572
|
-
import { appendFileSync as appendFileSync6, existsSync as
|
|
4721
|
+
import { appendFileSync as appendFileSync6, existsSync as existsSync16, mkdirSync as mkdirSync14, readFileSync as readFileSync14 } from "node:fs";
|
|
4573
4722
|
import { join as join17 } from "node:path";
|
|
4574
4723
|
import { createHash as createHash9 } from "node:crypto";
|
|
4575
4724
|
var DATA5 = () => diaHome();
|
|
4725
|
+
var PRIO_RE = /^P\d{1,2}$/;
|
|
4726
|
+
var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
4727
|
+
function normPrio(v) {
|
|
4728
|
+
const s = String(v).trim().toUpperCase();
|
|
4729
|
+
if (!PRIO_RE.test(s))
|
|
4730
|
+
throw new Error(`E_TASK: prio 必须是 P0–P99(收到 ${v})`);
|
|
4731
|
+
return s;
|
|
4732
|
+
}
|
|
4733
|
+
function normDay(v) {
|
|
4734
|
+
const s = String(v).trim();
|
|
4735
|
+
if (DAY_RE.test(s)) {
|
|
4736
|
+
const [y, mo, d] = s.split("-").map(Number);
|
|
4737
|
+
const dt = new Date(y, mo - 1, d);
|
|
4738
|
+
if (dt.getFullYear() === y && dt.getMonth() === mo - 1 && dt.getDate() === d)
|
|
4739
|
+
return s;
|
|
4740
|
+
}
|
|
4741
|
+
throw new Error(`E_TASK: 日期必须是有效 YYYY-MM-DD(收到 ${v})`);
|
|
4742
|
+
}
|
|
4743
|
+
function normProgress(v) {
|
|
4744
|
+
const n = Number(v);
|
|
4745
|
+
if (!Number.isInteger(n) || n < 0 || n > 100)
|
|
4746
|
+
throw new Error(`E_TASK: progress 必须是 0–100 的整数(收到 ${v})`);
|
|
4747
|
+
return n;
|
|
4748
|
+
}
|
|
4576
4749
|
function file2() {
|
|
4577
4750
|
return join17(DATA5(), "tasks", "ledger.jsonl");
|
|
4578
4751
|
}
|
|
4579
4752
|
var uniq = (xs) => [...new Set(xs.filter(Boolean))];
|
|
4580
4753
|
function fold2() {
|
|
4581
4754
|
const map = new Map;
|
|
4582
|
-
if (!
|
|
4755
|
+
if (!existsSync16(file2()))
|
|
4583
4756
|
return map;
|
|
4584
4757
|
for (const line of readFileSync14(file2(), "utf8").split(`
|
|
4585
4758
|
`)) {
|
|
@@ -4603,10 +4776,39 @@ function fold2() {
|
|
|
4603
4776
|
deps: uniq(e.deps || []),
|
|
4604
4777
|
touches: uniq(e.touches || []),
|
|
4605
4778
|
created_at: e.ts,
|
|
4606
|
-
updated_at: e.ts
|
|
4779
|
+
updated_at: e.ts,
|
|
4780
|
+
...readPlan(e)
|
|
4607
4781
|
});
|
|
4608
4782
|
continue;
|
|
4609
4783
|
}
|
|
4784
|
+
if (e.event === "split" && e.parent && Array.isArray(e.children)) {
|
|
4785
|
+
const parent = map.get(e.parent);
|
|
4786
|
+
if (parent) {
|
|
4787
|
+
for (const c of e.children) {
|
|
4788
|
+
if (!c || !c.id || map.has(c.id))
|
|
4789
|
+
continue;
|
|
4790
|
+
map.set(c.id, {
|
|
4791
|
+
id: c.id,
|
|
4792
|
+
title: c.title || c.id,
|
|
4793
|
+
role: c.role || "builder",
|
|
4794
|
+
judge: c.judge || "",
|
|
4795
|
+
status: "todo",
|
|
4796
|
+
evidence: "",
|
|
4797
|
+
deps: [],
|
|
4798
|
+
touches: uniq(c.touches || []),
|
|
4799
|
+
created_at: e.ts,
|
|
4800
|
+
updated_at: e.ts,
|
|
4801
|
+
split_from: e.parent,
|
|
4802
|
+
...c.judge ? { judge_inherited: true } : {},
|
|
4803
|
+
...e.slice && e.slice === c.id ? { slice: true } : {}
|
|
4804
|
+
});
|
|
4805
|
+
}
|
|
4806
|
+
const ids = e.children.map((c) => c.id).filter((id) => map.has(id));
|
|
4807
|
+
parent.deps = uniq([...parent.deps, ...ids]);
|
|
4808
|
+
parent.split_children = uniq([...parent.split_children || [], ...ids]);
|
|
4809
|
+
}
|
|
4810
|
+
continue;
|
|
4811
|
+
}
|
|
4610
4812
|
const t = map.get(e.id);
|
|
4611
4813
|
if (!t)
|
|
4612
4814
|
continue;
|
|
@@ -4614,9 +4816,16 @@ function fold2() {
|
|
|
4614
4816
|
const remove = new Set([...e.remove_deps || [], ...e.remove_touches || []]);
|
|
4615
4817
|
t.deps = uniq([...t.deps, ...e.deps || []]).filter((x) => !remove.has(x));
|
|
4616
4818
|
t.touches = uniq([...t.touches, ...e.touches || []]).filter((x) => !remove.has(x));
|
|
4819
|
+
} else if (e.event === "plan") {
|
|
4820
|
+
Object.assign(t, readPlan(e));
|
|
4617
4821
|
} else {
|
|
4618
|
-
if (e.status)
|
|
4822
|
+
if (e.status) {
|
|
4619
4823
|
t.status = e.status;
|
|
4824
|
+
if (e.status === "done" || e.status === "accepted")
|
|
4825
|
+
t.closed_at = t.closed_at || e.ts;
|
|
4826
|
+
else
|
|
4827
|
+
t.closed_at = undefined;
|
|
4828
|
+
}
|
|
4620
4829
|
if (e.evidence !== undefined)
|
|
4621
4830
|
t.evidence = e.evidence;
|
|
4622
4831
|
if (e.closure_reason !== undefined)
|
|
@@ -4643,10 +4852,87 @@ function add(title, opts = {}) {
|
|
|
4643
4852
|
judge: opts.judge || "",
|
|
4644
4853
|
owner: opts.owner,
|
|
4645
4854
|
deps: uniq(opts.deps || []),
|
|
4646
|
-
touches: uniq(opts.touches || [])
|
|
4855
|
+
touches: uniq(opts.touches || []),
|
|
4856
|
+
...planFields(opts)
|
|
4647
4857
|
});
|
|
4648
4858
|
return fold2().get(id);
|
|
4649
4859
|
}
|
|
4860
|
+
function planFields(o) {
|
|
4861
|
+
const out = {};
|
|
4862
|
+
if (o.prio !== undefined)
|
|
4863
|
+
out.prio = normPrio(o.prio);
|
|
4864
|
+
if (o.start !== undefined)
|
|
4865
|
+
out.start = normDay(o.start);
|
|
4866
|
+
if (o.due !== undefined)
|
|
4867
|
+
out.due = normDay(o.due);
|
|
4868
|
+
if (o.progress !== undefined)
|
|
4869
|
+
out.progress = normProgress(o.progress);
|
|
4870
|
+
return out;
|
|
4871
|
+
}
|
|
4872
|
+
function readPlan(e) {
|
|
4873
|
+
const out = {};
|
|
4874
|
+
try {
|
|
4875
|
+
if (e.prio !== undefined && PRIO_RE.test(String(e.prio).toUpperCase()))
|
|
4876
|
+
out.prio = String(e.prio).toUpperCase();
|
|
4877
|
+
} catch {}
|
|
4878
|
+
try {
|
|
4879
|
+
if (e.start !== undefined && DAY_RE.test(String(e.start)))
|
|
4880
|
+
out.start = String(e.start);
|
|
4881
|
+
} catch {}
|
|
4882
|
+
try {
|
|
4883
|
+
if (e.due !== undefined && DAY_RE.test(String(e.due)))
|
|
4884
|
+
out.due = String(e.due);
|
|
4885
|
+
} catch {}
|
|
4886
|
+
try {
|
|
4887
|
+
const n = Number(e.progress);
|
|
4888
|
+
if (e.progress !== undefined && Number.isInteger(n) && n >= 0 && n <= 100)
|
|
4889
|
+
out.progress = n;
|
|
4890
|
+
} catch {}
|
|
4891
|
+
return out;
|
|
4892
|
+
}
|
|
4893
|
+
function plan(id, patch) {
|
|
4894
|
+
const cur = get2(id);
|
|
4895
|
+
const fields = planFields(patch);
|
|
4896
|
+
if (!Object.keys(fields).length)
|
|
4897
|
+
throw new Error("E_TASK: plan 至少需要一个字段(--prio/--start/--due/--progress)");
|
|
4898
|
+
const start = fields.start ?? cur.start, due = fields.due ?? cur.due;
|
|
4899
|
+
if (start && due && start > due)
|
|
4900
|
+
throw new Error("E_TASK: start 不能晚于 due");
|
|
4901
|
+
append3({ event: "plan", id, ...fields });
|
|
4902
|
+
return get2(id);
|
|
4903
|
+
}
|
|
4904
|
+
var SATISFIED = ["done", "accepted", "dropped"];
|
|
4905
|
+
function isSatisfied(t) {
|
|
4906
|
+
return !!t && SATISFIED.includes(t.status);
|
|
4907
|
+
}
|
|
4908
|
+
function isTerminal2(t) {
|
|
4909
|
+
return !!t && ["done", "accepted", "dropped", "rejected"].includes(t.status);
|
|
4910
|
+
}
|
|
4911
|
+
function split(parentId, titles, opts = {}) {
|
|
4912
|
+
const parent = get2(parentId);
|
|
4913
|
+
if (isTerminal2(parent))
|
|
4914
|
+
throw new Error("E_TASK: 终态任务不能拆分");
|
|
4915
|
+
if (parent.status === "doing" && !opts.force)
|
|
4916
|
+
throw new Error("E_TASK: 进行中的任务拆分需 --force(拆分后会先等子任务)");
|
|
4917
|
+
const names = titles.map((x) => x.trim()).filter(Boolean);
|
|
4918
|
+
if (names.length < 2)
|
|
4919
|
+
throw new Error("E_TASK: 至少拆成 2 个切片");
|
|
4920
|
+
if (new Set(names).size !== names.length)
|
|
4921
|
+
throw new Error("E_TASK: 切片标题重复");
|
|
4922
|
+
if (opts.slice && !names.includes(opts.slice))
|
|
4923
|
+
throw new Error("E_TASK: --slice 必须是某个切片标题");
|
|
4924
|
+
const judge = opts.judge ?? parent.judge;
|
|
4925
|
+
const children = names.map((title) => ({
|
|
4926
|
+
id: "t-" + createHash9("sha1").update(title + Date.now() + Math.random()).digest("hex").slice(0, 8),
|
|
4927
|
+
title,
|
|
4928
|
+
role: opts.role || parent.role,
|
|
4929
|
+
judge,
|
|
4930
|
+
touches: uniq(opts.touches ?? parent.touches)
|
|
4931
|
+
}));
|
|
4932
|
+
const sliceId = opts.slice ? children[names.indexOf(opts.slice)].id : undefined;
|
|
4933
|
+
append3({ event: "split", parent: parentId, children, ...sliceId ? { slice: sliceId } : {} });
|
|
4934
|
+
return get2(parentId);
|
|
4935
|
+
}
|
|
4650
4936
|
function list4() {
|
|
4651
4937
|
return [...fold2().values()];
|
|
4652
4938
|
}
|
|
@@ -4688,11 +4974,258 @@ function status3(id, status, evidence, opts = {}) {
|
|
|
4688
4974
|
return get2(id);
|
|
4689
4975
|
}
|
|
4690
4976
|
|
|
4977
|
+
// src/plan.ts
|
|
4978
|
+
var NO_PRIO = "P99";
|
|
4979
|
+
var TERMINAL3 = new Set(["done", "accepted", "dropped", "rejected"]);
|
|
4980
|
+
var SATISFIED2 = new Set(["done", "accepted", "dropped"]);
|
|
4981
|
+
var PROPAGATE = new Set(["P0", "P1"]);
|
|
4982
|
+
var isTerminal3 = (t) => TERMINAL3.has(t.status);
|
|
4983
|
+
var isSatisfied2 = (t) => !!t && SATISFIED2.has(t.status);
|
|
4984
|
+
function prioIndex(p) {
|
|
4985
|
+
const m = /^P(\d{1,2})$/.exec(String(p ?? "").toUpperCase());
|
|
4986
|
+
return m ? Number(m[1]) : 99;
|
|
4987
|
+
}
|
|
4988
|
+
function prioName(idx) {
|
|
4989
|
+
return idx >= 99 ? NO_PRIO : `P${idx}`;
|
|
4990
|
+
}
|
|
4991
|
+
function graphOf(tasks) {
|
|
4992
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4993
|
+
const dependents = new Map;
|
|
4994
|
+
for (const t of tasks)
|
|
4995
|
+
for (const d of t.deps)
|
|
4996
|
+
if (byId.has(d))
|
|
4997
|
+
(dependents.get(d) ?? dependents.set(d, []).get(d)).push(t.id);
|
|
4998
|
+
return { byId, dependents };
|
|
4999
|
+
}
|
|
5000
|
+
function findCycles(tasks) {
|
|
5001
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
5002
|
+
const state = new Map;
|
|
5003
|
+
const stack = [];
|
|
5004
|
+
const cycles = [];
|
|
5005
|
+
const seen = new Set;
|
|
5006
|
+
const visit = (id) => {
|
|
5007
|
+
const st = state.get(id) || 0;
|
|
5008
|
+
if (st === 1) {
|
|
5009
|
+
const i = stack.indexOf(id);
|
|
5010
|
+
const cyc = i >= 0 ? stack.slice(i) : [id];
|
|
5011
|
+
const key = [...cyc].sort().join(",");
|
|
5012
|
+
if (!seen.has(key)) {
|
|
5013
|
+
seen.add(key);
|
|
5014
|
+
cycles.push(cyc);
|
|
5015
|
+
}
|
|
5016
|
+
return;
|
|
5017
|
+
}
|
|
5018
|
+
if (st === 2)
|
|
5019
|
+
return;
|
|
5020
|
+
state.set(id, 1);
|
|
5021
|
+
stack.push(id);
|
|
5022
|
+
for (const dep of byId.get(id)?.deps || [])
|
|
5023
|
+
if (byId.has(dep))
|
|
5024
|
+
visit(dep);
|
|
5025
|
+
stack.pop();
|
|
5026
|
+
state.set(id, 2);
|
|
5027
|
+
};
|
|
5028
|
+
for (const t of tasks)
|
|
5029
|
+
visit(t.id);
|
|
5030
|
+
return cycles.sort((a, b) => a.join(",").localeCompare(b.join(",")));
|
|
5031
|
+
}
|
|
5032
|
+
function computeEff(tasks, dependents) {
|
|
5033
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
5034
|
+
const eff = new Map;
|
|
5035
|
+
for (const t of tasks)
|
|
5036
|
+
eff.set(t.id, { idx: prioIndex(t.prio) });
|
|
5037
|
+
for (const src of tasks) {
|
|
5038
|
+
const idx = prioIndex(src.prio);
|
|
5039
|
+
if (isTerminal3(src) || !PROPAGATE.has(prioName(idx)))
|
|
5040
|
+
continue;
|
|
5041
|
+
const seen = new Map([[src.id, 0]]);
|
|
5042
|
+
const queue = [src.id];
|
|
5043
|
+
while (queue.length) {
|
|
5044
|
+
const cur = queue.shift();
|
|
5045
|
+
const dist = seen.get(cur);
|
|
5046
|
+
for (const up of byId.get(cur)?.deps || []) {
|
|
5047
|
+
if (!byId.has(up) || seen.has(up))
|
|
5048
|
+
continue;
|
|
5049
|
+
const upTask = byId.get(up);
|
|
5050
|
+
if (isTerminal3(upTask))
|
|
5051
|
+
continue;
|
|
5052
|
+
seen.set(up, dist + 1);
|
|
5053
|
+
queue.push(up);
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
for (const [id, distance] of seen) {
|
|
5057
|
+
if (id === src.id)
|
|
5058
|
+
continue;
|
|
5059
|
+
const cur = eff.get(id);
|
|
5060
|
+
const cand = { id: src.id, declared: prioName(idx), distance };
|
|
5061
|
+
if (idx < cur.idx)
|
|
5062
|
+
eff.set(id, { idx, src: cand });
|
|
5063
|
+
else if (idx === cur.idx && cur.src) {
|
|
5064
|
+
const better = cand.distance < cur.src.distance || cand.distance === cur.src.distance && cand.id < cur.src.id;
|
|
5065
|
+
if (better)
|
|
5066
|
+
eff.set(id, { idx, src: cand });
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
return eff;
|
|
5071
|
+
}
|
|
5072
|
+
function longestDepth(tasks, byId, cyclic) {
|
|
5073
|
+
const memo = new Map;
|
|
5074
|
+
const visit = (id, stack) => {
|
|
5075
|
+
if (memo.has(id))
|
|
5076
|
+
return memo.get(id);
|
|
5077
|
+
if (stack.has(id))
|
|
5078
|
+
return 0;
|
|
5079
|
+
const t = byId.get(id);
|
|
5080
|
+
if (!t)
|
|
5081
|
+
return 0;
|
|
5082
|
+
stack.add(id);
|
|
5083
|
+
let d = 0;
|
|
5084
|
+
for (const dep of t.deps)
|
|
5085
|
+
if (byId.has(dep) && !cyclic.has(dep))
|
|
5086
|
+
d = Math.max(d, visit(dep, stack) + 1);
|
|
5087
|
+
stack.delete(id);
|
|
5088
|
+
memo.set(id, d);
|
|
5089
|
+
return d;
|
|
5090
|
+
};
|
|
5091
|
+
for (const t of tasks)
|
|
5092
|
+
visit(t.id, new Set);
|
|
5093
|
+
return memo;
|
|
5094
|
+
}
|
|
5095
|
+
function transitiveDependents(tasks, dependents) {
|
|
5096
|
+
const out = new Map;
|
|
5097
|
+
for (const t of tasks) {
|
|
5098
|
+
const seen = new Set;
|
|
5099
|
+
const queue = [...dependents.get(t.id) || []];
|
|
5100
|
+
while (queue.length) {
|
|
5101
|
+
const cur = queue.shift();
|
|
5102
|
+
if (seen.has(cur))
|
|
5103
|
+
continue;
|
|
5104
|
+
seen.add(cur);
|
|
5105
|
+
queue.push(...dependents.get(cur) || []);
|
|
5106
|
+
}
|
|
5107
|
+
out.set(t.id, seen);
|
|
5108
|
+
}
|
|
5109
|
+
return out;
|
|
5110
|
+
}
|
|
5111
|
+
function derive(tasks) {
|
|
5112
|
+
const { byId, dependents } = graphOf(tasks);
|
|
5113
|
+
const cycles = findCycles(tasks);
|
|
5114
|
+
const cyclic = new Set(cycles.flat());
|
|
5115
|
+
const eff = computeEff(tasks, dependents);
|
|
5116
|
+
const depth = longestDepth(tasks, byId, cyclic);
|
|
5117
|
+
const down = transitiveDependents(tasks, dependents);
|
|
5118
|
+
const critical = new Set;
|
|
5119
|
+
for (const t of tasks) {
|
|
5120
|
+
const idx = prioIndex(t.prio);
|
|
5121
|
+
if (isTerminal3(t) || !PROPAGATE.has(prioName(idx)))
|
|
5122
|
+
continue;
|
|
5123
|
+
const seen = new Set;
|
|
5124
|
+
const queue = [t.id];
|
|
5125
|
+
while (queue.length) {
|
|
5126
|
+
const cur = queue.shift();
|
|
5127
|
+
for (const dep of byId.get(cur)?.deps || []) {
|
|
5128
|
+
if (seen.has(dep) || !byId.has(dep))
|
|
5129
|
+
continue;
|
|
5130
|
+
seen.add(dep);
|
|
5131
|
+
critical.add(dep);
|
|
5132
|
+
queue.push(dep);
|
|
5133
|
+
}
|
|
5134
|
+
}
|
|
5135
|
+
}
|
|
5136
|
+
const byIdOut = new Map;
|
|
5137
|
+
for (const t of tasks) {
|
|
5138
|
+
const deps = t.deps;
|
|
5139
|
+
const dangling = deps.filter((d) => !byId.has(d));
|
|
5140
|
+
const unsatisfied = deps.filter((d) => byId.has(d) ? !isSatisfied2(byId.get(d)) : true);
|
|
5141
|
+
const released = deps.filter((d) => byId.get(d)?.status === "dropped");
|
|
5142
|
+
const hasCycle = cyclic.has(t.id);
|
|
5143
|
+
const ready = !isTerminal3(t) && !hasCycle && unsatisfied.length === 0;
|
|
5144
|
+
const e = eff.get(t.id);
|
|
5145
|
+
byIdOut.set(t.id, {
|
|
5146
|
+
id: t.id,
|
|
5147
|
+
eff_prio: prioName(e.idx),
|
|
5148
|
+
prio_inherited: e.idx < prioIndex(t.prio),
|
|
5149
|
+
...e.src && e.idx < prioIndex(t.prio) ? { prio_source: e.src } : {},
|
|
5150
|
+
critical: critical.has(t.id),
|
|
5151
|
+
ready,
|
|
5152
|
+
startable: ready && t.status === "todo",
|
|
5153
|
+
blocked_by: unsatisfied,
|
|
5154
|
+
released_by: released,
|
|
5155
|
+
dangling_deps: dangling,
|
|
5156
|
+
depth: depth.get(t.id) ?? 0,
|
|
5157
|
+
unblocks: down.get(t.id)?.size ?? 0,
|
|
5158
|
+
rank: null,
|
|
5159
|
+
...t.split_from ? { split_from: t.split_from } : {},
|
|
5160
|
+
...t.split_children?.length ? { split_children: t.split_children } : {},
|
|
5161
|
+
...t.slice ? { slice: true } : {}
|
|
5162
|
+
});
|
|
5163
|
+
}
|
|
5164
|
+
const nodes = tasks.filter((t) => !isTerminal3(t) && !cyclic.has(t.id));
|
|
5165
|
+
const indeg = new Map;
|
|
5166
|
+
for (const t of nodes)
|
|
5167
|
+
indeg.set(t.id, t.deps.filter((d) => nodes.some((n) => n.id === d)).length);
|
|
5168
|
+
const readyQueue = nodes.filter((t) => (indeg.get(t.id) ?? 0) === 0);
|
|
5169
|
+
const key = (t) => {
|
|
5170
|
+
const d = byIdOut.get(t.id);
|
|
5171
|
+
return [
|
|
5172
|
+
prioIndex(d.eff_prio),
|
|
5173
|
+
t.due ?? "9999-12-31",
|
|
5174
|
+
d.critical ? 0 : 1,
|
|
5175
|
+
d.slice ? 0 : 1,
|
|
5176
|
+
-d.unblocks,
|
|
5177
|
+
d.depth,
|
|
5178
|
+
t.created_at,
|
|
5179
|
+
t.id
|
|
5180
|
+
];
|
|
5181
|
+
};
|
|
5182
|
+
const cmp = (a, b) => {
|
|
5183
|
+
const ka = key(a), kb = key(b);
|
|
5184
|
+
for (let i = 0;i < ka.length; i++) {
|
|
5185
|
+
if (ka[i] < kb[i])
|
|
5186
|
+
return -1;
|
|
5187
|
+
if (ka[i] > kb[i])
|
|
5188
|
+
return 1;
|
|
5189
|
+
}
|
|
5190
|
+
return 0;
|
|
5191
|
+
};
|
|
5192
|
+
let rank = 0;
|
|
5193
|
+
const pending = [...readyQueue].sort(cmp);
|
|
5194
|
+
while (pending.length) {
|
|
5195
|
+
const t = pending.shift();
|
|
5196
|
+
byIdOut.get(t.id).rank = ++rank;
|
|
5197
|
+
for (const d of dependents.get(t.id) || []) {
|
|
5198
|
+
const n = indeg.get(d);
|
|
5199
|
+
if (n === undefined)
|
|
5200
|
+
continue;
|
|
5201
|
+
indeg.set(d, n - 1);
|
|
5202
|
+
if (n - 1 === 0)
|
|
5203
|
+
pending.push(byId.get(d));
|
|
5204
|
+
}
|
|
5205
|
+
pending.sort(cmp);
|
|
5206
|
+
}
|
|
5207
|
+
const lint = [];
|
|
5208
|
+
for (const t of tasks) {
|
|
5209
|
+
const d = byIdOut.get(t.id);
|
|
5210
|
+
for (const x of d.dangling_deps)
|
|
5211
|
+
lint.push({ kind: "dangling_dep", id: t.id, detail: `依赖不存在:${x}` });
|
|
5212
|
+
for (const x of d.released_by)
|
|
5213
|
+
lint.push({ kind: "released_by_drop", id: t.id, detail: `依赖已 dropped:${x}(前提失效,确认是否还需要做)` });
|
|
5214
|
+
if (PROPAGATE.has(t.prio ?? "") && !isTerminal3(t) && d.depth >= 3) {
|
|
5215
|
+
lint.push({ kind: "long_chain", id: t.id, detail: `依赖链 ${d.depth} 跳,考虑拆分` });
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
for (const c of cycles)
|
|
5219
|
+
lint.push({ kind: "cycle", id: c[0], detail: `环:${c.join(" → ")}` });
|
|
5220
|
+
return { byId: byIdOut, cycles, lint, ranking: { policy: "kahn-v1" } };
|
|
5221
|
+
}
|
|
5222
|
+
|
|
4691
5223
|
// src/graph.ts
|
|
5224
|
+
var DERIVED_PROGRESS = { todo: 0, doing: 50, review: 80, done: 100, accepted: 100, blocked: 0, dropped: 0, rejected: 0, escalated: 50, awaiting_acceptance: 90 };
|
|
4692
5225
|
var taskNode = (id) => `task:${id}`;
|
|
4693
5226
|
var sessionNode = (id) => `session:${id}`;
|
|
4694
5227
|
var pathNode = (p) => `path:${p}`;
|
|
4695
|
-
function
|
|
5228
|
+
function findCycles2(tasks) {
|
|
4696
5229
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4697
5230
|
const state = new Map;
|
|
4698
5231
|
const stack = [];
|
|
@@ -4726,13 +5259,46 @@ function findCycles(tasks) {
|
|
|
4726
5259
|
}
|
|
4727
5260
|
function build(opts = {}) {
|
|
4728
5261
|
const tasks = list4();
|
|
5262
|
+
const derived = derive(tasks);
|
|
4729
5263
|
const active = list3();
|
|
4730
5264
|
const nodes = new Map;
|
|
4731
5265
|
const edges = [];
|
|
4732
5266
|
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
4733
5267
|
for (const t of tasks) {
|
|
4734
|
-
const blocked = t.deps.filter((d) => byId.get(d)
|
|
4735
|
-
|
|
5268
|
+
const blocked = t.deps.filter((d) => !isSatisfied(byId.get(d)));
|
|
5269
|
+
const d = derived.byId.get(t.id);
|
|
5270
|
+
nodes.set(taskNode(t.id), {
|
|
5271
|
+
id: taskNode(t.id),
|
|
5272
|
+
kind: "task",
|
|
5273
|
+
label: t.title,
|
|
5274
|
+
status: t.status,
|
|
5275
|
+
owner: t.owner,
|
|
5276
|
+
...blocked.length ? { blocked_by: blocked } : {},
|
|
5277
|
+
...t.prio ? { prio: t.prio } : {},
|
|
5278
|
+
...t.start ? { start: t.start } : {},
|
|
5279
|
+
...t.due ? { due: t.due } : {},
|
|
5280
|
+
progress: t.progress ?? (DERIVED_PROGRESS[t.status] ?? 0),
|
|
5281
|
+
progress_derived: t.progress === undefined,
|
|
5282
|
+
created_at: t.created_at,
|
|
5283
|
+
updated_at: t.updated_at,
|
|
5284
|
+
...t.closed_at ? { closed_at: t.closed_at } : {},
|
|
5285
|
+
deps: t.deps,
|
|
5286
|
+
touches: t.touches,
|
|
5287
|
+
eff_prio: d.eff_prio,
|
|
5288
|
+
prio_inherited: d.prio_inherited,
|
|
5289
|
+
...d.prio_source ? { prio_source: d.prio_source } : {},
|
|
5290
|
+
critical: d.critical,
|
|
5291
|
+
ready: d.ready,
|
|
5292
|
+
startable: d.startable,
|
|
5293
|
+
released_by: d.released_by,
|
|
5294
|
+
dangling_deps: d.dangling_deps,
|
|
5295
|
+
depth: d.depth,
|
|
5296
|
+
unblocks: d.unblocks,
|
|
5297
|
+
rank: d.rank,
|
|
5298
|
+
...d.split_from ? { split_from: d.split_from } : {},
|
|
5299
|
+
...d.split_children ? { split_children: d.split_children } : {},
|
|
5300
|
+
...d.slice ? { slice: true } : {}
|
|
5301
|
+
});
|
|
4736
5302
|
if (t.owner)
|
|
4737
5303
|
nodes.set(sessionNode(t.owner), { id: sessionNode(t.owner), kind: "session", label: t.owner });
|
|
4738
5304
|
for (const d of t.deps) {
|
|
@@ -4773,7 +5339,7 @@ function build(opts = {}) {
|
|
|
4773
5339
|
}
|
|
4774
5340
|
filteredNodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
4775
5341
|
filteredEdges.sort((a, b) => (a.from + "\x00" + a.to + "\x00" + a.kind).localeCompare(b.from + "\x00" + b.to + "\x00" + b.kind));
|
|
4776
|
-
return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles:
|
|
5342
|
+
return { schema: 1, capabilities: ["prio", "timeline", "progress", "eff-prio", "ready", "rank", "split"], ranking: derived.ranking, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles2(tasks) };
|
|
4777
5343
|
}
|
|
4778
5344
|
|
|
4779
5345
|
// src/graph-tui.ts
|
|
@@ -4861,12 +5427,848 @@ function watch(build, opts = {}) {
|
|
|
4861
5427
|
return new Promise(() => {});
|
|
4862
5428
|
}
|
|
4863
5429
|
|
|
5430
|
+
// src/graph-html/template.html
|
|
5431
|
+
var template_default = `<!doctype html>
|
|
5432
|
+
<html lang="zh">
|
|
5433
|
+
<head>
|
|
5434
|
+
<meta charset="utf-8">
|
|
5435
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5436
|
+
<title>dia graph · 快照</title>
|
|
5437
|
+
<style>
|
|
5438
|
+
:root{
|
|
5439
|
+
--bg:#f6f7f9; --panel:#fff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb;
|
|
5440
|
+
--blue:#2563eb; --violet:#7c3aed; --cyan:#0891b2; --green:#16a34a; --red:#dc2626; --amber:#d97706;
|
|
5441
|
+
--shadow:0 1px 2px rgba(17,24,39,.06),0 8px 24px rgba(17,24,39,.06);
|
|
5442
|
+
}
|
|
5443
|
+
@media (prefers-color-scheme: dark){
|
|
5444
|
+
:root{ --bg:#0f1115; --panel:#171a21; --ink:#e5e7eb; --muted:#9ca3af; --line:#2a2f3a;
|
|
5445
|
+
--shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.25); }
|
|
5446
|
+
}
|
|
5447
|
+
*{box-sizing:border-box}
|
|
5448
|
+
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
5449
|
+
header{display:flex;align-items:center;gap:16px;padding:14px 22px;border-bottom:1px solid var(--line);background:var(--panel);flex-wrap:wrap}
|
|
5450
|
+
header h1{font-size:17px;margin:0;letter-spacing:.2px}
|
|
5451
|
+
header .sub{color:var(--muted);font-size:12px}
|
|
5452
|
+
header .spacer{flex:1}
|
|
5453
|
+
.tabs{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
5454
|
+
.tabs button{border:0;background:transparent;color:var(--muted);padding:6px 12px;font:inherit;font-size:12px;cursor:pointer}
|
|
5455
|
+
.tabs button.on{background:var(--blue);color:#fff}
|
|
5456
|
+
.pill{border:1px solid var(--line);border-radius:999px;padding:4px 10px;font-size:12px;color:var(--muted);background:var(--bg)}
|
|
5457
|
+
main{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:18px;padding:18px;max-width:1480px;margin:0 auto}
|
|
5458
|
+
.panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)}
|
|
5459
|
+
.toolbar{display:flex;gap:8px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--line);flex-wrap:wrap}
|
|
5460
|
+
.seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden}
|
|
5461
|
+
.seg button{border:0;background:transparent;color:var(--muted);padding:6px 10px;font:inherit;font-size:12px;cursor:pointer}
|
|
5462
|
+
.seg button.on{background:var(--blue);color:#fff}
|
|
5463
|
+
.seg button:disabled{opacity:.35;cursor:not-allowed}
|
|
5464
|
+
.win-select{border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px}
|
|
5465
|
+
.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}
|
|
5466
|
+
.search input{border:0;background:transparent;color:var(--ink);padding:2px 0;font:inherit;font-size:12px;flex:1;min-width:80px;outline:none}
|
|
5467
|
+
.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}
|
|
5468
|
+
.token button{border:0;background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:12px;line-height:1;padding:0 2px}
|
|
5469
|
+
.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}
|
|
5470
|
+
.search .q-list.open{display:block}
|
|
5471
|
+
.q-item{padding:6px 10px;font-size:12px;cursor:pointer;display:flex;justify-content:space-between;gap:10px}
|
|
5472
|
+
.q-item:hover,.q-item.active{background:color-mix(in srgb,var(--blue) 18%,transparent)}
|
|
5473
|
+
.q-item .k{color:var(--muted)}
|
|
5474
|
+
.q-item .q-prio{font-style:normal;font-size:10px;color:var(--muted);margin-left:6px}
|
|
5475
|
+
.q-item.sel{color:var(--blue)}
|
|
5476
|
+
.q-item.sel .k{color:var(--blue)}
|
|
5477
|
+
.q-head{padding:6px 10px;font-size:10px;color:var(--muted);border-bottom:1px solid var(--line)}
|
|
5478
|
+
.q-empty{padding:10px;font-size:12px;color:var(--muted)}
|
|
5479
|
+
.node.hit{outline:2px solid var(--blue);outline-offset:1px}
|
|
5480
|
+
.node.dim{opacity:.22}
|
|
5481
|
+
.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}
|
|
5482
|
+
.focus-chip.on{display:inline-flex}
|
|
5483
|
+
.focus-chip button{border:0;background:transparent;color:var(--blue);cursor:pointer;font:inherit;font-size:11px;padding:0 6px}
|
|
5484
|
+
.focus-chip .fd{border:0;background:transparent;color:var(--blue);font:inherit;font-size:11px}
|
|
5485
|
+
.toolbar .hint{margin-left:auto;color:var(--muted);font-size:12px}
|
|
5486
|
+
.canvas{position:relative;height:620px;overflow:hidden}
|
|
5487
|
+
.canvas-scroll{position:absolute;inset:0;overflow:auto;padding:10px}
|
|
5488
|
+
.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)}
|
|
5489
|
+
svg.edges{position:absolute;inset:0;pointer-events:none;overflow:visible}
|
|
5490
|
+
.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}
|
|
5491
|
+
.node.path{border-left-color:var(--violet)}
|
|
5492
|
+
.node.session{border-left-color:var(--cyan)}
|
|
5493
|
+
.node b{display:block;font-size:13px;line-height:1.3;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5494
|
+
.node .meta{color:var(--muted);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5495
|
+
.progress{height:4px;border-radius:99px;background:var(--line);margin-top:6px;overflow:hidden}
|
|
5496
|
+
.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}
|
|
5497
|
+
.progress i.blocked{background-color:var(--red);animation-play-state:paused;opacity:.65}
|
|
5498
|
+
.progress i.review{background-color:var(--amber)}
|
|
5499
|
+
.progress i.done,.progress i.accepted{background-color:var(--muted);background-image:none;animation:none}
|
|
5500
|
+
.progress i.todo{background-color:var(--muted);background-image:none;animation:none}
|
|
5501
|
+
@keyframes flow{to{background-position:12px 0}}
|
|
5502
|
+
.st{font-weight:500}
|
|
5503
|
+
.st.doing{color:var(--ink)} .st.blocked{color:var(--red)} .st.review{color:var(--amber)}
|
|
5504
|
+
.st.done,.st.accepted{color:var(--muted)} .st.todo{color:var(--muted)}
|
|
5505
|
+
.prio{float:right;font-size:10px;font-weight:600;border-radius:5px;padding:0 6px;color:#fff;margin-left:4px}
|
|
5506
|
+
.prio{background:transparent;border:1px solid var(--line)}
|
|
5507
|
+
.prio.p0{background:var(--red);border-color:var(--red);color:#fff}
|
|
5508
|
+
.prio.p1{border-color:var(--amber);color:var(--amber)}
|
|
5509
|
+
.prio.p2{border-color:var(--line);color:var(--muted)}
|
|
5510
|
+
.pgroup{position:absolute;left:0;pointer-events:none;border-bottom:1px dashed var(--line)}
|
|
5511
|
+
.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}
|
|
5512
|
+
.plane-label small{display:block;font-size:9px;font-weight:400;letter-spacing:0;opacity:.7;margin-top:2px}
|
|
5513
|
+
.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}
|
|
5514
|
+
.legend-overlay .row{display:flex;align-items:center;gap:8px;white-space:nowrap}
|
|
5515
|
+
.legend-overlay .sample{width:38px;height:10px;flex:0 0 38px}
|
|
5516
|
+
.legend-overlay b{color:var(--ink);font-weight:600}
|
|
5517
|
+
.canvas.simple .legend-overlay .detail-row{display:none}
|
|
5518
|
+
aside{display:flex;flex-direction:column;gap:14px}
|
|
5519
|
+
.side-title{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-bottom:1px solid var(--line)}
|
|
5520
|
+
.side-title h2{font-size:13px;margin:0}
|
|
5521
|
+
.side-title span{font-size:11px;color:var(--muted)}
|
|
5522
|
+
.list{padding:6px 14px 12px}
|
|
5523
|
+
.item{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5524
|
+
.item:last-child{border-bottom:0}
|
|
5525
|
+
.item .k{color:var(--muted)}
|
|
5526
|
+
.attention{padding:8px 14px 14px}
|
|
5527
|
+
.attention .row{display:flex;gap:8px;align-items:flex-start;padding:8px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5528
|
+
.attention .row:last-child{border-bottom:0}
|
|
5529
|
+
.tag{font-size:10px;border-radius:6px;padding:1px 6px;background:rgba(220,38,38,.12);color:var(--red);white-space:nowrap}
|
|
5530
|
+
.tag.amber{background:rgba(217,119,6,.12);color:var(--amber)}
|
|
5531
|
+
.attention .row{cursor:pointer}
|
|
5532
|
+
.quick{display:flex;gap:6px;margin-top:6px}
|
|
5533
|
+
.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}
|
|
5534
|
+
.quick button.primary,.btn.primary{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
5535
|
+
.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.35);opacity:0;pointer-events:none;transition:opacity .18s;z-index:40}
|
|
5536
|
+
.backdrop.show{opacity:1;pointer-events:auto}
|
|
5537
|
+
.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}
|
|
5538
|
+
.drawer.show{transform:none}
|
|
5539
|
+
.drawer-head{display:flex;align-items:flex-start;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
|
|
5540
|
+
.drawer-head h3{font-size:15px;margin:0 0 3px}
|
|
5541
|
+
.drawer-head .meta{color:var(--muted);font-size:11px}
|
|
5542
|
+
.drawer-head .close{margin-left:auto;border:0;background:transparent;color:var(--muted);font-size:18px;cursor:pointer}
|
|
5543
|
+
.drawer-tabs{display:flex;gap:6px;padding:10px 16px;border-bottom:1px solid var(--line)}
|
|
5544
|
+
.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}
|
|
5545
|
+
.drawer-tabs button.on{background:var(--blue);border-color:var(--blue);color:#fff}
|
|
5546
|
+
.drawer-body{padding:14px 16px 24px;overflow:auto}
|
|
5547
|
+
.drawer-body h4{font-size:12px;margin:14px 0 6px;color:var(--muted);letter-spacing:.05em;text-transform:uppercase}
|
|
5548
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
5549
|
+
.kv .k{color:var(--muted)}
|
|
5550
|
+
.iv-seg{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;margin-bottom:12px}
|
|
5551
|
+
.iv-seg button{flex:1;border:0;background:transparent;color:var(--muted);padding:7px 8px;font:inherit;font-size:12px;cursor:pointer}
|
|
5552
|
+
.iv-seg button.on{background:var(--blue);color:#fff}
|
|
5553
|
+
.iv-row{display:grid;gap:6px;margin-bottom:10px}
|
|
5554
|
+
.iv-row label{font-size:11px;color:var(--muted)}
|
|
5555
|
+
.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}
|
|
5556
|
+
.iv-row textarea{min-height:96px;resize:vertical}
|
|
5557
|
+
.iv-status{color:var(--green);font-size:11px;margin-left:8px}
|
|
5558
|
+
.rel-list .item{display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px dashed var(--line);font-size:12px}
|
|
5559
|
+
.rel-list .item:last-child{border-bottom:0}
|
|
5560
|
+
.rel-list .k{color:var(--muted)}
|
|
5561
|
+
.kv{display:grid;grid-template-columns:70px 1fr;gap:4px 10px;font-size:12px}
|
|
5562
|
+
.kv .k{color:var(--muted)}
|
|
5563
|
+
/* timeline */
|
|
5564
|
+
.timeline{padding:14px 16px 18px;overflow:hidden;position:relative}
|
|
5565
|
+
.tl-grid{display:grid;grid-template-columns:190px minmax(0,1fr);gap:8px;align-items:start}
|
|
5566
|
+
.tl-labels{overflow:hidden;position:relative}
|
|
5567
|
+
.tl-label-head{height:34px;display:flex;align-items:flex-end;font-size:11px;color:var(--muted);padding-bottom:6px}
|
|
5568
|
+
.tl-labels-body{will-change:transform}
|
|
5569
|
+
.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}
|
|
5570
|
+
.tl-label-row b{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
5571
|
+
.tl-label-row span{color:var(--muted);font-size:10px}
|
|
5572
|
+
.tl-scroll{overflow:auto;max-height:620px;scrollbar-width:thin}
|
|
5573
|
+
.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)}
|
|
5574
|
+
.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)}
|
|
5575
|
+
.tl-axis{position:relative;height:34px;width:var(--tlw,900px)}
|
|
5576
|
+
.tl-axis span{position:absolute;transform:translateX(-50%);white-space:nowrap;line-height:1.2}
|
|
5577
|
+
.tl-axis span small{display:block;font-size:9px;opacity:.55}
|
|
5578
|
+
.tl-body{position:relative}
|
|
5579
|
+
.tl-row{display:grid;grid-template-columns:var(--tlw,900px);gap:8px;align-items:start;min-height:38px;border-top:1px dashed var(--line)}
|
|
5580
|
+
.tl-track{position:relative;height:28px;overflow:hidden;width:var(--tlw,900px);margin-top:4px}
|
|
5581
|
+
.tl-bar{position:absolute;top:7px;height:14px;border-radius:5px;background:var(--blue);opacity:.9;cursor:pointer}
|
|
5582
|
+
.tl-bar .fill{position:absolute;left:0;top:0;bottom:0;border-radius:5px;background:rgba(255,255,255,.45)}
|
|
5583
|
+
.tl-bar.blocked{background:var(--red)} .tl-bar.done,.tl-bar.accepted,.tl-bar.todo{background:var(--muted)}
|
|
5584
|
+
.tl-bar.review{background:var(--amber)}
|
|
5585
|
+
.tl-bar.critical{outline:2px solid var(--amber);outline-offset:2px}
|
|
5586
|
+
.tl-bar.open{border-right:2px dashed var(--muted);opacity:.8}
|
|
5587
|
+
.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}
|
|
5588
|
+
.tl-caption{color:var(--muted);font-size:12px;padding:10px 0 2px}
|
|
5589
|
+
footer{padding:0 22px 22px;color:var(--muted);font-size:11px}
|
|
5590
|
+
@media (max-width:980px){ main{grid-template-columns:1fr} .canvas{height:560px} }
|
|
5591
|
+
</style>
|
|
5592
|
+
</head>
|
|
5593
|
+
<body>
|
|
5594
|
+
<header>
|
|
5595
|
+
<h1>dia graph</h1>
|
|
5596
|
+
<span class="sub">只读依赖图 + 时间线 · 静态快照</span>
|
|
5597
|
+
<div class="tabs" id="tabs">
|
|
5598
|
+
<button class="on" data-v="graph">关系图</button>
|
|
5599
|
+
<button data-v="time">时间线</button>
|
|
5600
|
+
</div>
|
|
5601
|
+
<span class="spacer"></span>
|
|
5602
|
+
<span class="pill" id="trunc-pill" style="display:none"></span>
|
|
5603
|
+
<span class="pill">静态快照 · 无网络</span>
|
|
5604
|
+
<span class="pill" id="route">#/graph</span>
|
|
5605
|
+
<span class="pill" id="updated">-</span>
|
|
5606
|
+
</header>
|
|
5607
|
+
|
|
5608
|
+
<main>
|
|
5609
|
+
<section class="panel">
|
|
5610
|
+
<div class="toolbar">
|
|
5611
|
+
<div id="graph-tools" style="display:flex;gap:8px;align-items:center">
|
|
5612
|
+
<div class="seg" id="layout">
|
|
5613
|
+
<button class="on" data-l="priority">优先级视图</button>
|
|
5614
|
+
<button data-l="dep">依赖视图</button>
|
|
5615
|
+
</div>
|
|
5616
|
+
<div class="seg" id="mode">
|
|
5617
|
+
<button class="on" data-m="simple">简约</button>
|
|
5618
|
+
<button data-m="detail">详细</button>
|
|
5619
|
+
</div>
|
|
5620
|
+
<div class="search" id="search-box">
|
|
5621
|
+
<span id="tokens" style="display:contents"></span>
|
|
5622
|
+
<input id="q" type="search" placeholder="搜索任务,选中后变成 tag" autocomplete="off">
|
|
5623
|
+
<div class="q-list" id="q-list"></div>
|
|
5624
|
+
</div>
|
|
5625
|
+
<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>
|
|
5626
|
+
</div>
|
|
5627
|
+
<div id="time-tools" style="display:none;gap:8px;align-items:center">
|
|
5628
|
+
<div class="seg" id="zoom">
|
|
5629
|
+
<button data-z="-">-</button>
|
|
5630
|
+
<button data-z="reset">100%</button>
|
|
5631
|
+
<button data-z="+">+</button>
|
|
5632
|
+
</div>
|
|
5633
|
+
</div>
|
|
5634
|
+
<select class="win-select" id="win" title="按任务开始时间过滤,结束时间不限(两个视图共用)">
|
|
5635
|
+
<option value="7">近 7 天</option>
|
|
5636
|
+
<option value="14">近 14 天</option>
|
|
5637
|
+
<option value="30">近 30 天</option>
|
|
5638
|
+
<option value="0">全部时间</option>
|
|
5639
|
+
</select>
|
|
5640
|
+
<span class="hint" id="hint">按优先级分行(P0 最高);行内从左到右 = 依赖顺序。简约只看任务和依赖,详细展开具体关联。</span>
|
|
5641
|
+
</div>
|
|
5642
|
+
<div id="view-graph">
|
|
5643
|
+
<div class="canvas" id="canvas">
|
|
5644
|
+
<div class="canvas-scroll" id="canvas-scroll">
|
|
5645
|
+
<svg class="edges" id="edges"></svg>
|
|
5646
|
+
</div>
|
|
5647
|
+
<div class="lane-rail" id="lane-rail"></div>
|
|
5648
|
+
<div class="legend-overlay" id="legend">
|
|
5649
|
+
<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>
|
|
5650
|
+
</div>
|
|
5651
|
+
</div>
|
|
5652
|
+
</div>
|
|
5653
|
+
<div id="view-time" style="display:none">
|
|
5654
|
+
<div class="timeline">
|
|
5655
|
+
<div class="scroll-hint">← 左右滚动 →</div>
|
|
5656
|
+
<div class="tl-grid">
|
|
5657
|
+
<div class="tl-labels">
|
|
5658
|
+
<div class="tl-label-head">任务</div>
|
|
5659
|
+
<div class="tl-labels-body" id="tl-labels-body"></div>
|
|
5660
|
+
</div>
|
|
5661
|
+
<div class="tl-scroll" id="tl-scroll">
|
|
5662
|
+
<div class="tl-head"><div class="tl-axis" id="tl-axis"></div></div>
|
|
5663
|
+
<div class="tl-body" id="tl-body"></div>
|
|
5664
|
+
</div>
|
|
5665
|
+
</div>
|
|
5666
|
+
<div class="tl-caption">红色虚线 = 今天;虚线右端 = 未录入 due 的开放任务;斜纹 = 进度按状态估算。</div>
|
|
5667
|
+
</div>
|
|
5668
|
+
</div>
|
|
5669
|
+
</section>
|
|
5670
|
+
|
|
5671
|
+
<aside>
|
|
5672
|
+
<div class="panel">
|
|
5673
|
+
<div class="side-title"><h2>概览</h2><span>snapshot</span></div>
|
|
5674
|
+
<div class="list">
|
|
5675
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>任务</span><b id="stat-tasks">0</b></div>
|
|
5676
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--red)"></i>阻塞</span><b id="stat-blocked">0</b></div>
|
|
5677
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--amber)"></i>待评审</span><b id="stat-review">0</b></div>
|
|
5678
|
+
<div class="item"><span class="k"><i class="dot" style="background:var(--muted)"></i>完成</span><b id="stat-done">0</b></div>
|
|
5679
|
+
<div class="item"><span class="k">claims</span><b id="stat-claims">0</b></div>
|
|
5680
|
+
</div>
|
|
5681
|
+
</div>
|
|
5682
|
+
<div class="panel">
|
|
5683
|
+
<div class="side-title"><h2>需要人</h2><span>attention</span></div>
|
|
5684
|
+
<div class="attention" id="attention-list"></div>
|
|
5685
|
+
</div>
|
|
5686
|
+
</aside>
|
|
5687
|
+
</main>
|
|
5688
|
+
<div class="backdrop" id="backdrop" onclick="closeDrawer()"></div>
|
|
5689
|
+
<aside class="drawer" id="drawer" aria-hidden="true">
|
|
5690
|
+
<div class="drawer-head">
|
|
5691
|
+
<div><h3 id="drawer-title">任务详情</h3><div class="meta" id="drawer-sub"></div></div>
|
|
5692
|
+
<button class="close" onclick="closeDrawer()">×</button>
|
|
5693
|
+
</div>
|
|
5694
|
+
<div class="drawer-tabs">
|
|
5695
|
+
<button data-dtab="task" onclick="setDrawerTab('task')">任务</button>
|
|
5696
|
+
<button data-dtab="intervene" onclick="setDrawerTab('intervene')">命令</button>
|
|
5697
|
+
</div>
|
|
5698
|
+
<div class="drawer-body" id="drawer-body"></div>
|
|
5699
|
+
</aside>
|
|
5700
|
+
<footer>颜色只表达警告/异常:红=阻塞/P0,橙=待评审/P1;其余状态用中性灰 + 单一蓝色进度。关系图按依赖深度分层,时间线独立展示执行情况。</footer>
|
|
5701
|
+
|
|
5702
|
+
<script type="application/json" id="dia-graph">__DIA_PAYLOAD__</script>
|
|
5703
|
+
<script>
|
|
5704
|
+
const payload=(()=>{ try{ const el=document.getElementById('dia-graph'); return JSON.parse(el?el.textContent:'{}')||{}; }catch{ return {}; } })();
|
|
5705
|
+
const TODAY=payload.today||'1970-01-01';
|
|
5706
|
+
const STATUSES=new Set(['todo','doing','review','blocked','done','accepted','dropped','awaiting_acceptance','rejected','escalated']);
|
|
5707
|
+
const tasks=(payload.tasks||[]).map(t=>{
|
|
5708
|
+
const status=STATUSES.has(t.status)?t.status:'todo';
|
|
5709
|
+
const progress=Number.isFinite(t.progress)?Math.max(0,Math.min(100,Math.round(t.progress))):0;
|
|
5710
|
+
return {
|
|
5711
|
+
id:String(t.id), label:String(t.label||t.id), status,
|
|
5712
|
+
prio:String(t.prio||'P?'), progress, derived:!!t.derived,
|
|
5713
|
+
start:Number.isFinite(t.start)?t.start:null,
|
|
5714
|
+
end:Number.isFinite(t.end)?t.end:null,
|
|
5715
|
+
owner:t.owner?String(t.owner):null,
|
|
5716
|
+
deps:Array.isArray(t.deps)?t.deps.map(String):[],
|
|
5717
|
+
touches:Array.isArray(t.touches)?t.touches.map(String):[],
|
|
5718
|
+
};
|
|
5719
|
+
});
|
|
5720
|
+
const taskEdges=(payload.edges||[]).filter(e=>e&&e.kind==='depends_on').map(e=>({from:String(e.from),to:String(e.to),kind:'depends_on'}));
|
|
5721
|
+
const truncated=payload.truncated||null;
|
|
5722
|
+
const state={view:'graph', layout:'priority', mode:'simple', zoom:28, window:7, q:'', sel:[], fd:2, from:null, to:null};
|
|
5723
|
+
const canvas=document.getElementById('canvas'), layer=document.getElementById('canvas-scroll'), rail=document.getElementById('lane-rail'), svg=document.getElementById('edges');
|
|
5724
|
+
const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
5725
|
+
const stIcon={doing:'▶',blocked:'⏸',review:'◐',done:'✓',accepted:'✓',todo:'○',dropped:'⨯',rejected:'✗',escalated:'▲',awaiting_acceptance:'…'};
|
|
5726
|
+
const stIconOf=s=>stIcon[s]||'·';
|
|
5727
|
+
|
|
5728
|
+
function focusSet(id,depth){
|
|
5729
|
+
const by=new Map(tasks.map(t=>[t.id,t]));
|
|
5730
|
+
const max=depth==='all'?Infinity:Math.max(1,Number(depth)||2);
|
|
5731
|
+
const out=new Set([id]); let frontier=[id];
|
|
5732
|
+
for(let step=0; step<max && frontier.length; step++){
|
|
5733
|
+
const next=[];
|
|
5734
|
+
frontier.forEach(cur=>{
|
|
5735
|
+
const t=by.get(cur); if(!t) return;
|
|
5736
|
+
(t.deps||[]).forEach(x=>{ if(by.has(x)&&!out.has(x)){ out.add(x); next.push(x); } });
|
|
5737
|
+
tasks.forEach(o=>{ if((o.deps||[]).includes(cur)&&!out.has(o.id)){ out.add(o.id); next.push(o.id); } });
|
|
5738
|
+
});
|
|
5739
|
+
frontier=next;
|
|
5740
|
+
}
|
|
5741
|
+
return out;
|
|
5742
|
+
}
|
|
5743
|
+
function visibleTasks(){
|
|
5744
|
+
const base=tasks.filter(t=>state.window<=0 || t.start===null || t.start>=-state.window);
|
|
5745
|
+
if(!state.sel.length) return base;
|
|
5746
|
+
const keep=new Set();
|
|
5747
|
+
state.sel.forEach(id=>focusSet(id,state.fd).forEach(x=>keep.add(x)));
|
|
5748
|
+
return base.filter(t=>keep.has(t.id));
|
|
5749
|
+
}
|
|
5750
|
+
function searchMatches(){
|
|
5751
|
+
const q=(state.q||'').trim().toLowerCase();
|
|
5752
|
+
if(!q) return null;
|
|
5753
|
+
return tasks.filter(t=>t.id.toLowerCase().includes(q)||t.label.toLowerCase().includes(q)||String(t.owner||'').toLowerCase().includes(q));
|
|
5754
|
+
}
|
|
5755
|
+
function syncStats(){
|
|
5756
|
+
const list=visibleTasks(), st=id=>document.getElementById(id);
|
|
5757
|
+
if(!st('stat-tasks')) return;
|
|
5758
|
+
st('stat-tasks').textContent = list.length===tasks.length ? String(tasks.length) : list.length+' / '+tasks.length;
|
|
5759
|
+
st('stat-blocked').textContent=String(list.filter(t=>t.status==='blocked').length);
|
|
5760
|
+
st('stat-review').textContent=String(list.filter(t=>t.status==='review').length);
|
|
5761
|
+
st('stat-done').textContent=String(list.filter(t=>t.status==='done'||t.status==='accepted').length);
|
|
5762
|
+
if(st('stat-claims')) st('stat-claims').textContent=String((payload.claims||[]).length);
|
|
5763
|
+
renderAttention(list);
|
|
5764
|
+
const tp=document.getElementById('trunc-pill');
|
|
5765
|
+
if(tp){ if(truncated&&truncated.shown<truncated.total){ tp.style.display=''; tp.textContent='已截断 '+truncated.shown+' / '+truncated.total+'(--all 全量)'; } else tp.style.display='none'; }
|
|
5766
|
+
}
|
|
5767
|
+
function renderAttention(list){
|
|
5768
|
+
const box=document.getElementById('attention-list'); if(!box) return;
|
|
5769
|
+
const rows=[];
|
|
5770
|
+
list.filter(t=>t.status==='blocked').slice(0,3).forEach(t=>rows.push({tag:'阻塞',cls:'',t,note:'blocked'}));
|
|
5771
|
+
list.filter(t=>t.status==='review').slice(0,3).forEach(t=>rows.push({tag:'评审',cls:' amber',t,note:'等待评审'}));
|
|
5772
|
+
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('')
|
|
5773
|
+
:'<p class="k" style="padding:8px 0">当前没有阻塞或待评审任务。</p>';
|
|
5774
|
+
box.querySelectorAll('.row[data-task]').forEach(el=>el.onclick=()=>openDrawer(el.dataset.task,'task'));
|
|
5775
|
+
}
|
|
5776
|
+
function renderEmpty(){
|
|
5777
|
+
if(tasks.length) return;
|
|
5778
|
+
const el=document.getElementById('canvas-scroll');
|
|
5779
|
+
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>';
|
|
5780
|
+
}
|
|
5781
|
+
function updateHint(){
|
|
5782
|
+
const h=document.getElementById('hint'); if(!h||state.view!=='graph') return;
|
|
5783
|
+
const n=visibleTasks().length;
|
|
5784
|
+
if(state.sel.length){
|
|
5785
|
+
h.textContent='显示 '+n+' 个任务 · '+(state.fd==='all'?'全链路':state.fd+' 层');
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5788
|
+
h.textContent='显示 '+n+' / '+tasks.length+' 个任务 · '+(state.layout==='dep'?'从左到右 = 依赖深度':'按优先级分行(P0 最高)');
|
|
5789
|
+
}
|
|
5790
|
+
function depths(){
|
|
5791
|
+
const list=visibleTasks();
|
|
5792
|
+
const by=new Map(list.map(t=>[t.id,t])), memo=new Map();
|
|
5793
|
+
const depth=t=>{
|
|
5794
|
+
if(!t) return 0;
|
|
5795
|
+
if(memo.has(t.id)) return memo.get(t.id);
|
|
5796
|
+
memo.set(t.id,0);
|
|
5797
|
+
const d=t.deps.length?Math.max(...t.deps.map(x=>{ const dt=by.get(x); return dt?depth(dt):-1; }))+1:0;
|
|
5798
|
+
memo.set(t.id,d); return d;
|
|
5799
|
+
};
|
|
5800
|
+
list.forEach(depth); return memo;
|
|
5801
|
+
}
|
|
5802
|
+
function levels(){
|
|
5803
|
+
return [...new Set(visibleTasks().map(t=>t.prio))].sort((a,b)=>{
|
|
5804
|
+
const na=parseInt(String(a).replace(/^P/i,''),10), nb=parseInt(String(b).replace(/^P/i,''),10);
|
|
5805
|
+
return (Number.isFinite(na)?na:99)-(Number.isFinite(nb)?nb:99)||String(a).localeCompare(String(b));
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5808
|
+
function rowsFor(){
|
|
5809
|
+
const list=visibleTasks(), d=depths();
|
|
5810
|
+
const gap=state.mode==='detail'?230:145;
|
|
5811
|
+
const h=state.mode==='detail'?205:110;
|
|
5812
|
+
return levels().map((prio,i)=>{
|
|
5813
|
+
const rowTasks=list.filter(t=>t.prio===prio).sort((a,b)=>(d.get(a.id)-d.get(b.id))||a.label.localeCompare(b.label));
|
|
5814
|
+
return {prio,y:36+i*gap,h,tasks:rowTasks};
|
|
5815
|
+
}).filter(r=>r.tasks.length);
|
|
5816
|
+
}
|
|
5817
|
+
let cardMetrics={mid:52,h:58}, clickTimer=null;
|
|
5818
|
+
function measureCard(){
|
|
5819
|
+
const probe=card(tasks[0],-9999,-9999);
|
|
5820
|
+
probe.style.visibility='hidden';
|
|
5821
|
+
layer.appendChild(probe);
|
|
5822
|
+
const prog=probe.querySelector('.progress');
|
|
5823
|
+
const mid=prog?prog.offsetTop+prog.offsetHeight/2+probe.clientTop:44;
|
|
5824
|
+
const h=probe.offsetHeight||58;
|
|
5825
|
+
probe.remove();
|
|
5826
|
+
return {mid,h};
|
|
5827
|
+
}
|
|
5828
|
+
function bandCenter(r, bandH){ return r.y-12+bandH/2; }
|
|
5829
|
+
function cardTop(r){
|
|
5830
|
+
const bandH=state.mode==='detail'?230:145;
|
|
5831
|
+
const center=bandCenter(r,bandH);
|
|
5832
|
+
// 详细:进度条中心线对齐泳道中心;简约:卡片盒整体垂直居中
|
|
5833
|
+
return state.mode==='detail'?center-cardMetrics.mid:center-cardMetrics.h/2;
|
|
5834
|
+
}
|
|
5835
|
+
function edgeOffset(){ return state.mode==='detail'?cardMetrics.mid:cardMetrics.h/2; }
|
|
5836
|
+
function pos(){
|
|
5837
|
+
const rows=rowsFor(), p=new Map();
|
|
5838
|
+
rows.forEach(r=>r.tasks.forEach((t,i)=>p.set(t.id,{x:170+i*230,y:cardTop(r)})));
|
|
5839
|
+
return {p,rows};
|
|
5840
|
+
}
|
|
5841
|
+
function card(t,x,y,showPrio=false){
|
|
5842
|
+
const el=document.createElement('div'); el.className='node task '+t.status; el.style.left=x+'px'; el.style.top=y+'px';
|
|
5843
|
+
const rel=taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
5844
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
5845
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
5846
|
+
return '<div class="meta" style="margin-top:2px">↳ '+esc(kind)+': '+esc(other)+'</div>';
|
|
5847
|
+
}).slice(0,4).join('');
|
|
5848
|
+
const badge=showPrio?'<span class="prio '+esc(t.prio.toLowerCase())+'">'+esc(t.prio)+'</span>':'';
|
|
5849
|
+
el.innerHTML='<b>'+esc(t.label)+badge+'</b>'+
|
|
5850
|
+
'<div class="meta">'+esc(t.id)+' · <span class="st '+esc(t.status)+'">'+stIconOf(t.status)+' '+esc(t.status)+'</span> · '+t.progress+'%</div>'+
|
|
5851
|
+
'<div class="progress"><i class="'+esc(t.status)+'" style="width:'+t.progress+'%"></i></div>'+
|
|
5852
|
+
(state.mode==='detail'?'<div class="meta" style="margin-top:5px">'+(t.owner?'@'+esc(t.owner):'unassigned')+' · dep '+t.deps.length+'</div>'+rel:'');
|
|
5853
|
+
el.dataset.id=t.id;
|
|
5854
|
+
el.onclick=()=>{ clearTimeout(clickTimer); clickTimer=setTimeout(()=>openDrawer(t.id,'task'),180); };
|
|
5855
|
+
el.ondblclick=e=>{ e.preventDefault(); clearTimeout(clickTimer); toggleSel(t.id); };
|
|
5856
|
+
return el;
|
|
5857
|
+
}
|
|
5858
|
+
function edgePath(a,b){
|
|
5859
|
+
const o=edgeOffset(), x1=a.x+190,y1=a.y+o,x2=b.x,y2=b.y+o;
|
|
5860
|
+
if(x2>=x1) return \`M\${x1} \${y1} H\${x1+Math.max(24,(x2-x1)/2)} V\${y2} H\${x2}\`;
|
|
5861
|
+
return \`M\${x1} \${y1} H\${x1-30} V\${y2} H\${x2}\`;
|
|
5862
|
+
}
|
|
5863
|
+
function drawGraph(){
|
|
5864
|
+
canvas.classList.toggle('simple', state.mode==='simple');
|
|
5865
|
+
if(state.layout==='dep'){ drawDepGraph(); return; }
|
|
5866
|
+
cardMetrics=measureCard();
|
|
5867
|
+
const {p,rows}=pos();
|
|
5868
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
5869
|
+
rail.style.display=''; rail.querySelectorAll('.plane-label').forEach(e=>e.remove());
|
|
5870
|
+
const maxX=Math.max(600, ...rows.map(r=>170+(r.tasks.length-1)*230+190+10));
|
|
5871
|
+
const maxY=rows.length?Math.max(...rows.map(r=>r.y+r.h))+40:600;
|
|
5872
|
+
const bandH=state.mode==='detail'?230:145;
|
|
5873
|
+
rows.forEach((r,i)=>{
|
|
5874
|
+
const g=document.createElement('div'); g.className='pgroup '+r.prio.toLowerCase();
|
|
5875
|
+
g.style.top=(r.y-12)+'px'; g.style.width=maxX+'px'; g.style.height=bandH+'px';
|
|
5876
|
+
const alpha=Math.max(0.02, 0.075-i*0.012);
|
|
5877
|
+
g.style.background='color-mix(in srgb, var(--blue) '+Math.round(alpha*100)+'%, transparent)';
|
|
5878
|
+
layer.appendChild(g);
|
|
5879
|
+
const label=document.createElement('div'); label.className='plane-label '+r.prio.toLowerCase();
|
|
5880
|
+
label.dataset.y=String(r.y-12+bandH/2); label.style.top=(r.y-12+bandH/2-layer.scrollTop)+'px';
|
|
5881
|
+
const meta=r.prio==='P0'?['var(--red)','紧急']:r.prio==='P1'?['var(--amber)','高']:r.prio==='P?'?['var(--muted)','未排期']:['var(--muted)','常规'];
|
|
5882
|
+
label.style.color=meta[0];
|
|
5883
|
+
label.innerHTML=esc(r.prio)+' · '+r.tasks.length+'<small>'+meta[1]+'</small>';
|
|
5884
|
+
rail.appendChild(label);
|
|
5885
|
+
r.tasks.forEach((t,j)=>layer.appendChild(card(t,170+j*230,cardTop(r))));
|
|
5886
|
+
});
|
|
5887
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY);
|
|
5888
|
+
const defs=\`<defs>
|
|
5889
|
+
<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>
|
|
5890
|
+
<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>
|
|
5891
|
+
</defs>\`;
|
|
5892
|
+
const lines=taskEdges.map(e=>{
|
|
5893
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
5894
|
+
return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
|
|
5895
|
+
}).join('');
|
|
5896
|
+
svg.innerHTML=defs+lines;
|
|
5897
|
+
document.getElementById('updated').textContent='快照 '+(payload.generated_at||'-');
|
|
5898
|
+
syncStats(); updateHint(); applySearch();
|
|
5899
|
+
}
|
|
5900
|
+
let drawerTask=null, drawerTab='task';
|
|
5901
|
+
function relations(t){
|
|
5902
|
+
return taskEdges.filter(e=>e.from===t.id||e.to===t.id).map(e=>{
|
|
5903
|
+
const outgoing=e.from===t.id, other=outgoing?e.to:e.from;
|
|
5904
|
+
const kind=e.kind==='depends_on'?(outgoing?'depends_on':'depended_by'):e.kind;
|
|
5905
|
+
return '<div class="item"><span class="k">'+esc(kind)+'</span><span>'+esc(other)+'</span></div>';
|
|
5906
|
+
}).join('') || '<p class="k">无边</p>';
|
|
5907
|
+
}
|
|
5908
|
+
function taskHtml(t){
|
|
5909
|
+
return '<div class="kv"><div class="k">status</div><div>'+esc(t.status)+'</div>'+
|
|
5910
|
+
'<div class="k">priority</div><div>'+esc(t.prio)+'</div>'+
|
|
5911
|
+
'<div class="k">owner</div><div>'+esc(t.owner||'-')+'</div>'+
|
|
5912
|
+
'<div class="k">start</div><div>'+(t.start===null?'-(按创建时间)':esc(isoDate(t.start)))+'</div>'+
|
|
5913
|
+
'<div class="k">due</div><div>'+(Number.isFinite(t.end)?esc(isoDate(t.end)):'开放')+'</div>'+
|
|
5914
|
+
'<div class="k">progress</div><div>'+t.progress+'%'+(t.derived?'(估算)':'')+'</div>'+
|
|
5915
|
+
'<div class="k">deps</div><div>'+esc(t.deps.join(', ')||'-')+'</div>'+
|
|
5916
|
+
'<div class="k">touches</div><div>'+esc(t.touches.join(', ')||'-')+'</div></div>'+
|
|
5917
|
+
'<h4>关联</h4><div class="rel-list">'+relations(t)+'</div>';
|
|
5918
|
+
}
|
|
5919
|
+
function commandHtml(t){
|
|
5920
|
+
const cmds=[
|
|
5921
|
+
['dia task plan '+t.id+' --prio P1', '设置优先级'],
|
|
5922
|
+
['dia task plan '+t.id+' --start '+isoDate(0)+' --due '+isoDate(7), '设置计划起止'],
|
|
5923
|
+
['dia task plan '+t.id+' --progress 50', '更新进度'],
|
|
5924
|
+
['dia msg send <session> "关于 '+t.id+': "', '给负责人发消息'],
|
|
5925
|
+
['dia claim '+t.id, '声明正在处理'],
|
|
5926
|
+
];
|
|
5927
|
+
return '<p class="k">本页是静态快照:命令只供复制,不会发送。</p>'+
|
|
5928
|
+
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('');
|
|
5929
|
+
}
|
|
5930
|
+
function copyCmd(btn){
|
|
5931
|
+
const text=btn.dataset.cmd||'';
|
|
5932
|
+
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(()=>{ btn.textContent='已复制'; },()=>{ btn.textContent='复制失败'; }); }
|
|
5933
|
+
else { btn.textContent='请手动复制'; }
|
|
5934
|
+
}
|
|
5935
|
+
function openDrawer(id, tab){
|
|
5936
|
+
drawerTask=tasks.find(t=>t.id===id); if(!drawerTask) return;
|
|
5937
|
+
drawerTab=tab||'task';
|
|
5938
|
+
document.getElementById('drawer-title').textContent='任务详情';
|
|
5939
|
+
document.getElementById('drawer-sub').textContent=drawerTask.label+' · '+drawerTask.id+' · '+drawerTask.prio+' · '+drawerTask.status;
|
|
5940
|
+
document.getElementById('drawer').classList.add('show');
|
|
5941
|
+
document.getElementById('drawer').setAttribute('aria-hidden','false');
|
|
5942
|
+
document.getElementById('backdrop').classList.add('show');
|
|
5943
|
+
setDrawerTab(drawerTab);
|
|
5944
|
+
}
|
|
5945
|
+
function setDrawerTab(tab){
|
|
5946
|
+
drawerTab=tab;
|
|
5947
|
+
document.querySelectorAll('[data-dtab]').forEach(b=>b.classList.toggle('on',b.dataset.dtab===tab));
|
|
5948
|
+
const body=document.getElementById('drawer-body');
|
|
5949
|
+
body.innerHTML=tab==='intervene'?commandHtml(drawerTask):taskHtml(drawerTask);
|
|
5950
|
+
}
|
|
5951
|
+
function closeDrawer(){
|
|
5952
|
+
document.getElementById('drawer').classList.remove('show');
|
|
5953
|
+
document.getElementById('drawer').setAttribute('aria-hidden','true');
|
|
5954
|
+
document.getElementById('backdrop').classList.remove('show');
|
|
5955
|
+
}
|
|
5956
|
+
function todayUTC(){ const [y,m,d]=TODAY.split('-').map(Number); return Date.UTC(y,(m||1)-1,d||1); }
|
|
5957
|
+
function dayDate(d){
|
|
5958
|
+
const dt=new Date(todayUTC()+d*86400000);
|
|
5959
|
+
return ('0'+(dt.getUTCMonth()+1)).slice(-2)+'-'+('0'+dt.getUTCDate()).slice(-2);
|
|
5960
|
+
}
|
|
5961
|
+
function isoDate(d){ return new Date(todayUTC()+d*86400000).toISOString().slice(0,10); }
|
|
5962
|
+
function timelineWindow(){
|
|
5963
|
+
if(state.from!==null&&state.to!==null) return {from:state.from,to:state.to};
|
|
5964
|
+
const list=visibleTasks();
|
|
5965
|
+
const w=state.window>0?state.window:14;
|
|
5966
|
+
const starts=list.map(t=>t.start).filter(Number.isFinite);
|
|
5967
|
+
const ends=list.map(t=>t.end).filter(Number.isFinite);
|
|
5968
|
+
const from=Math.min(-w, starts.length?Math.min(...starts):-w);
|
|
5969
|
+
const to=Math.max(0, ends.length?Math.max(...ends):0)+1;
|
|
5970
|
+
return {from,to};
|
|
5971
|
+
}
|
|
5972
|
+
function drawTimeline(){
|
|
5973
|
+
const win=timelineWindow(), span=Math.max(1,win.to-win.from);
|
|
5974
|
+
const x=v=>((v-win.from)/span)*100;
|
|
5975
|
+
const chartW=Math.max(640, span*state.zoom);
|
|
5976
|
+
document.getElementById('view-time').style.setProperty('--tlw', chartW+'px');
|
|
5977
|
+
const axis=document.getElementById('tl-axis'); axis.innerHTML='';
|
|
5978
|
+
const step=span<=10?1:span<=18?2:3;
|
|
5979
|
+
for(let d=win.from;d<=win.to;d+=step){
|
|
5980
|
+
const el=document.createElement('span'); el.style.left=x(d)+'%';
|
|
5981
|
+
el.innerHTML=dayDate(d)+'<small>'+(d===0?'今天':(d>0?'+'+d+'d':d+'d'))+'</small>';
|
|
5982
|
+
if(d===win.from) el.style.transform='translateX(0)'; else if(d+step>win.to) el.style.transform='translateX(-100%)';
|
|
5983
|
+
if(d===0)el.style.color='var(--red)'; axis.appendChild(el);
|
|
5984
|
+
}
|
|
5985
|
+
const body=document.getElementById('tl-body'); body.innerHTML=''; const labelsBody=document.getElementById('tl-labels-body'); labelsBody.innerHTML='';
|
|
5986
|
+
visibleTasks().slice().sort((a,b)=>a.start-b.start).forEach(t=>{
|
|
5987
|
+
const row=document.createElement('div'); row.className='tl-row';
|
|
5988
|
+
row.innerHTML='<div class="tl-track"></div>';
|
|
5989
|
+
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);
|
|
5990
|
+
const track=row.querySelector('.tl-track');
|
|
5991
|
+
const startOff=t.start===null?win.from:t.start;
|
|
5992
|
+
const endOff=Number.isFinite(t.end)?t.end:win.to;
|
|
5993
|
+
const bar=document.createElement('div'); bar.className='tl-bar '+t.status+(Number.isFinite(t.end)?'':' open');
|
|
5994
|
+
bar.style.left=x(startOff)+'%'; bar.style.width=Math.max(1.5,x(endOff)-x(startOff))+'%';
|
|
5995
|
+
bar.title=dayDate(startOff)+' → '+(Number.isFinite(t.end)?dayDate(t.end):'开放')+' ('+t.progress+'%'+(t.derived?' 估算':'')+')';
|
|
5996
|
+
bar.innerHTML='<div class="fill" style="width:'+t.progress+'%"></div>'; bar.onclick=()=>openDrawer(t.id,'task'); track.appendChild(bar);
|
|
5997
|
+
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); }
|
|
5998
|
+
body.appendChild(row);
|
|
5999
|
+
});
|
|
6000
|
+
syncStats();
|
|
6001
|
+
}
|
|
6002
|
+
function drawDepGraph(){
|
|
6003
|
+
cardMetrics=measureCard();
|
|
6004
|
+
const list=visibleTasks(), d=depths(), groups=new Map();
|
|
6005
|
+
list.forEach(t=>{ const k=d.get(t.id)||0; groups.set(k,[...(groups.get(k)||[]),t]); });
|
|
6006
|
+
const p=new Map(); const rows=[];
|
|
6007
|
+
const gap=14;
|
|
6008
|
+
[...groups.keys()].sort((a,b)=>a-b).forEach((depth,ci)=>{
|
|
6009
|
+
const all=groups.get(depth).sort((a,b)=>a.prio.localeCompare(b.prio)||a.label.localeCompare(b.label));
|
|
6010
|
+
rows.push({depth,all,x:30+ci*230});
|
|
6011
|
+
});
|
|
6012
|
+
[...layer.querySelectorAll('.node,.pgroup')].forEach(e=>e.remove());
|
|
6013
|
+
rail.innerHTML=''; rail.style.display='none';
|
|
6014
|
+
let maxY=0;
|
|
6015
|
+
// 先渲染卡片,再按实测高度逐列堆叠,避免详细模式卡片相互重叠
|
|
6016
|
+
rows.forEach(r=>{
|
|
6017
|
+
let y=60;
|
|
6018
|
+
r.all.forEach(t=>{
|
|
6019
|
+
const el=card(t,r.x,y,true);
|
|
6020
|
+
el.style.top=y+'px';
|
|
6021
|
+
layer.appendChild(el);
|
|
6022
|
+
p.set(t.id,{x:r.x,y});
|
|
6023
|
+
y+=el.offsetHeight+gap;
|
|
6024
|
+
});
|
|
6025
|
+
maxY=Math.max(maxY,y);
|
|
6026
|
+
});
|
|
6027
|
+
maxY=Math.max(600,maxY);
|
|
6028
|
+
const maxX=Math.max(600,...rows.map(r=>r.x+200+10));
|
|
6029
|
+
svg.setAttribute('width',maxX); svg.setAttribute('height',maxY+40);
|
|
6030
|
+
const defs=\`<defs>
|
|
6031
|
+
<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>
|
|
6032
|
+
<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>
|
|
6033
|
+
</defs>\`;
|
|
6034
|
+
const lines=taskEdges.map(e=>{
|
|
6035
|
+
const a=p.get(e.from), b=p.get(e.to); if(!a||!b) return '';
|
|
6036
|
+
return \`<path d="\${edgePath(a,b)}" fill="none" stroke-width="1.6" stroke="#2563eb" marker-end="url(#ar-dep)"/>\`;
|
|
6037
|
+
}).join('');
|
|
6038
|
+
svg.innerHTML=defs+lines;
|
|
6039
|
+
document.getElementById('updated').textContent='updated '+new Date().toLocaleTimeString();
|
|
6040
|
+
syncStats(); updateHint(); applySearch();
|
|
6041
|
+
}
|
|
6042
|
+
function syncLaneLabels(){
|
|
6043
|
+
rail.querySelectorAll('.plane-label').forEach(el=>{
|
|
6044
|
+
el.style.top=(Number(el.dataset.y||0)-layer.scrollTop)+'px';
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
function applySearch(){
|
|
6048
|
+
const ms=searchMatches();
|
|
6049
|
+
layer.querySelectorAll('.node.task').forEach(el=>{
|
|
6050
|
+
el.classList.remove('hit','dim');
|
|
6051
|
+
if(!ms) return;
|
|
6052
|
+
if(ms.some(t=>t.id===el.dataset.id)) el.classList.add('hit'); else el.classList.add('dim');
|
|
6053
|
+
});
|
|
6054
|
+
const box=document.getElementById('q-list');
|
|
6055
|
+
if(box && box.classList.contains('open')) renderSearchList(ms);
|
|
6056
|
+
}
|
|
6057
|
+
function renderSearchList(ms){
|
|
6058
|
+
const box=document.getElementById('q-list'); if(!box) return;
|
|
6059
|
+
const q=(state.q||'').trim();
|
|
6060
|
+
const browse=!q;
|
|
6061
|
+
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;
|
|
6062
|
+
if(!pool||!pool.length){
|
|
6063
|
+
box.innerHTML='<div class="q-empty">'+(browse?'当前时间窗内没有任务':'没有匹配的任务')+'</div>';
|
|
6064
|
+
box.classList.add('open'); return;
|
|
6065
|
+
}
|
|
6066
|
+
const head=browse?('<div class="q-head">'+pool.length+' 个任务 · ↑↓ 选择,Enter 加入</div>')
|
|
6067
|
+
:('<div class="q-head">匹配 '+pool.length+' 个 · ↑↓ 选择,Enter 加入</div>');
|
|
6068
|
+
box.innerHTML=head+pool.slice(0,20).map(t=>{
|
|
6069
|
+
const on=state.sel.includes(t.id);
|
|
6070
|
+
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>';
|
|
6071
|
+
}).join('');
|
|
6072
|
+
box.classList.add('open');
|
|
6073
|
+
box.querySelectorAll('.q-item').forEach(el=>el.onclick=e=>{ e.stopPropagation(); toggleSel(el.dataset.id); });
|
|
6074
|
+
}
|
|
6075
|
+
function searchListItems(){
|
|
6076
|
+
const box=document.getElementById('q-list');
|
|
6077
|
+
return box?[...box.querySelectorAll('.q-item')]:[];
|
|
6078
|
+
}
|
|
6079
|
+
function moveSearchCursor(delta){
|
|
6080
|
+
let items=searchListItems();
|
|
6081
|
+
if(!items.length){ renderSearchList(searchMatches()); items=searchListItems(); if(!items.length) return; }
|
|
6082
|
+
let idx=items.findIndex(el=>el.classList.contains('active'));
|
|
6083
|
+
idx=idx<0?0:(idx+delta+items.length)%items.length;
|
|
6084
|
+
items.forEach((el,i)=>el.classList.toggle('active',i===idx));
|
|
6085
|
+
items[idx].scrollIntoView({block:'nearest'});
|
|
6086
|
+
}
|
|
6087
|
+
function toggleSel(id){
|
|
6088
|
+
const i=state.sel.indexOf(id);
|
|
6089
|
+
if(i>=0) state.sel.splice(i,1); else state.sel.push(id);
|
|
6090
|
+
state.fd=state.fd||2; state.q='';
|
|
6091
|
+
const qi=document.getElementById('q'); if(qi) qi.value='';
|
|
6092
|
+
go(state.view,state.mode,state.range,state.zoom);
|
|
6093
|
+
}
|
|
6094
|
+
function renderTokens(){
|
|
6095
|
+
const box=document.getElementById('tokens'); if(!box) return;
|
|
6096
|
+
box.innerHTML=state.sel.map(id=>'<span class="token">'+esc(id)+'<button data-id="'+esc(id)+'" title="移除">×</button></span>').join('');
|
|
6097
|
+
box.querySelectorAll('button').forEach(b=>b.onclick=e=>{ e.stopPropagation(); toggleSel(b.dataset.id); });
|
|
6098
|
+
}
|
|
6099
|
+
function readRoute(){
|
|
6100
|
+
const raw=location.hash.replace(/^#\\/?/,''); const [path,qs]=raw.split('?');
|
|
6101
|
+
const params=new URLSearchParams(qs||location.search);
|
|
6102
|
+
const range=(params.get('range')||'30d');
|
|
6103
|
+
const zoom=Math.min(84,Math.max(14,parseInt(params.get('zoom')||'28',10)||28));
|
|
6104
|
+
const sx=Number(params.get('sx')||0);
|
|
6105
|
+
const fromRaw=params.get('from'), toRaw=params.get('to');
|
|
6106
|
+
const toOff=v=>v?Math.round((new Date(v+'T00:00:00').getTime()-new Date('2026-11-06T00:00:00').getTime())/86400000):null;
|
|
6107
|
+
const winRaw=params.get('w');
|
|
6108
|
+
const rangeRaw=params.get('range');
|
|
6109
|
+
const win=winRaw!==null?Math.max(0,parseInt(winRaw,10)||0):(rangeRaw?Math.max(0,parseInt(rangeRaw,10)||0):7);
|
|
6110
|
+
const fdRaw=params.get('fd');
|
|
6111
|
+
const fd=fdRaw==='all'?'all':Math.min(9,Math.max(1,parseInt(fdRaw||'2',10)||2));
|
|
6112
|
+
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'};
|
|
6113
|
+
}
|
|
6114
|
+
function routeString(view,mode,range,zoom,from,to){
|
|
6115
|
+
const p=view==='time'?'timeline':'graph'; const qs=new URLSearchParams();
|
|
6116
|
+
if(view==='graph'&&mode==='detail') qs.set('mode','detail');
|
|
6117
|
+
if(view==='graph'&&state.layout==='dep') qs.set('layout','dep');
|
|
6118
|
+
if(view==='graph'){
|
|
6119
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
6120
|
+
if(state.q&&state.q.trim()) qs.set('q',state.q.trim());
|
|
6121
|
+
if(state.sel.length){ qs.set('sel',state.sel.join(',')); if(state.fd!==2) qs.set('fd',String(state.fd)); }
|
|
6122
|
+
}
|
|
6123
|
+
if(view==='time'){
|
|
6124
|
+
if(from!==null&&to!==null){ qs.set('from',isoDate(from)); qs.set('to',isoDate(to)); }
|
|
6125
|
+
if(state.window!==7) qs.set('w',String(state.window));
|
|
6126
|
+
if(zoom&&zoom!==28) qs.set('zoom',String(zoom));
|
|
6127
|
+
}
|
|
6128
|
+
return '#/'+p+(qs.toString()?'?'+qs.toString():'');
|
|
6129
|
+
}
|
|
6130
|
+
function applyRoute(){
|
|
6131
|
+
const stampEl=document.getElementById('updated');
|
|
6132
|
+
if(stampEl) stampEl.textContent='快照 '+(payload.generated_at||'-');
|
|
6133
|
+
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;
|
|
6134
|
+
document.querySelectorAll('#tabs button').forEach(x=>x.classList.toggle('on',x.dataset.v===r.view));
|
|
6135
|
+
document.querySelectorAll('#mode button').forEach(x=>x.classList.toggle('on',x.dataset.m===r.mode));
|
|
6136
|
+
document.querySelectorAll('#layout button').forEach(x=>x.classList.toggle('on',x.dataset.l===r.layout));
|
|
6137
|
+
document.getElementById('view-graph').style.display=r.view==='graph'?'':'none';
|
|
6138
|
+
document.getElementById('view-time').style.display=r.view==='time'?'':'none';
|
|
6139
|
+
document.getElementById('graph-tools').style.display=r.view==='graph'?'flex':'none';
|
|
6140
|
+
document.getElementById('time-tools').style.display=r.view==='time'?'flex':'none';
|
|
6141
|
+
const zbtn=document.querySelector('#zoom button[data-z="reset"]'); if(zbtn) zbtn.textContent=Math.round(r.zoom/28*100)+'%';
|
|
6142
|
+
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;
|
|
6143
|
+
if(r.view!=='graph') document.getElementById('hint').textContent='时间线:左右滚动查看;-/+缩放;红色虚线 = 今天';
|
|
6144
|
+
const winSel=document.getElementById('win'); if(winSel) winSel.value=String(r.window);
|
|
6145
|
+
const qIn=document.getElementById('q'); if(qIn && qIn.value!==r.q) qIn.value=r.q;
|
|
6146
|
+
const chip=document.getElementById('focus-chip');
|
|
6147
|
+
if(chip) chip.classList.toggle('on',r.sel.length>0);
|
|
6148
|
+
if(chip) chip.title='按依赖邻域聚焦;清空即回全量';
|
|
6149
|
+
const fdEl=document.getElementById('focus-fd'); if(fdEl) fdEl.value=String(r.fd);
|
|
6150
|
+
renderTokens();
|
|
6151
|
+
const rp=document.getElementById('route'); if(rp) rp.textContent=location.hash||routeString(r.view,r.mode,r.range,state.zoom,state.from,state.to);
|
|
6152
|
+
document.title='dia graph · '+(r.view==='time'?'时间线':'关系图');
|
|
6153
|
+
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();
|
|
6154
|
+
if(r.task) openDrawer(r.task,r.tab);
|
|
6155
|
+
syncLaneLabels();
|
|
6156
|
+
if(r.view==='graph') renderEmpty();
|
|
6157
|
+
}
|
|
6158
|
+
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; }
|
|
6159
|
+
document.querySelectorAll('#tabs button').forEach(b=>b.onclick=()=>go(b.dataset.v==='time'?'time':'graph',state.mode,state.range));
|
|
6160
|
+
document.querySelectorAll('#layout button').forEach(b=>b.onclick=()=>{state.layout=b.dataset.l;go(state.view,state.mode,state.range,state.zoom);});
|
|
6161
|
+
const winSelEl=document.getElementById('win');
|
|
6162
|
+
if(winSelEl) winSelEl.onchange=()=>{ state.window=Number(winSelEl.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
6163
|
+
const qInput=document.getElementById('q');
|
|
6164
|
+
if(qInput){
|
|
6165
|
+
qInput.oninput=()=>{ state.q=qInput.value; applySearch(); renderSearchList(searchMatches()); };
|
|
6166
|
+
qInput.onkeydown=e=>{
|
|
6167
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); moveSearchCursor(1); }
|
|
6168
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); moveSearchCursor(-1); }
|
|
6169
|
+
else if(e.key==='Enter'){
|
|
6170
|
+
const items=searchListItems();
|
|
6171
|
+
const active=items.find(el=>el.classList.contains('active'))||items[0];
|
|
6172
|
+
if(active) toggleSel(active.dataset.id);
|
|
6173
|
+
} else if(e.key==='Escape'){ state.q=''; qInput.value=''; applySearch(); qInput.blur(); }
|
|
6174
|
+
else if(e.key==='Backspace' && !qInput.value && state.sel.length){ e.preventDefault(); toggleSel(state.sel[state.sel.length-1]); }
|
|
6175
|
+
};
|
|
6176
|
+
qInput.onfocus=()=>{ renderSearchList(searchMatches()); };
|
|
6177
|
+
qInput.onclick=()=>{ renderSearchList(searchMatches()); };
|
|
6178
|
+
const sbox=document.getElementById('search-box');
|
|
6179
|
+
if(sbox) sbox.onclick=e=>{ if(e.target.closest('.q-item')||e.target.closest('.token')) return; qInput.focus(); };
|
|
6180
|
+
qInput.onblur=()=>{ setTimeout(()=>{ const box=document.getElementById('q-list'); if(box && !document.getElementById('search-box').contains(document.activeElement)) box.classList.remove('open'); },160); };
|
|
6181
|
+
}
|
|
6182
|
+
const fdSel=document.getElementById('focus-fd');
|
|
6183
|
+
if(fdSel) fdSel.onchange=()=>{ state.fd=fdSel.value==='all'?'all':Number(fdSel.value); go(state.view,state.mode,state.range,state.zoom); };
|
|
6184
|
+
const fxBtn=document.getElementById('focus-exit');
|
|
6185
|
+
if(fxBtn) fxBtn.onclick=()=>{ state.sel=[]; go(state.view,state.mode,state.range,state.zoom); };
|
|
6186
|
+
document.querySelectorAll('#mode button').forEach(b=>b.onclick=()=>go(state.view,b.dataset.m,state.range,state.zoom));
|
|
6187
|
+
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); });
|
|
6188
|
+
layer.addEventListener('scroll',syncLaneLabels,{passive:true});
|
|
6189
|
+
const tlScroller=document.getElementById('tl-scroll');
|
|
6190
|
+
tlScroller.addEventListener('scroll',()=>{ const lb=document.getElementById('tl-labels-body'); if(lb) lb.style.transform='translateY(-'+tlScroller.scrollTop+'px)'; },{passive:true});
|
|
6191
|
+
document.querySelectorAll('.attention .row[data-task]').forEach(r=>r.onclick=()=>openDrawer(r.dataset.task,'intervene'));
|
|
6192
|
+
window.addEventListener('hashchange',applyRoute);
|
|
6193
|
+
applyRoute();
|
|
6194
|
+
if(document.fonts&&document.fonts.ready){ document.fonts.ready.then(()=>{ if(state.view==='graph'){ if(state.layout==='dep') drawDepGraph(); else drawGraph(); } }); }
|
|
6195
|
+
</script>
|
|
6196
|
+
</body>
|
|
6197
|
+
</html>
|
|
6198
|
+
`;
|
|
6199
|
+
|
|
6200
|
+
// src/graph-html/render.ts
|
|
6201
|
+
var pad = (n) => String(n).padStart(2, "0");
|
|
6202
|
+
function localDay(d) {
|
|
6203
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
6204
|
+
}
|
|
6205
|
+
function dayDiff(from, to) {
|
|
6206
|
+
const a = from.split("-").map(Number), b = to.split("-").map(Number);
|
|
6207
|
+
return Math.round((Date.UTC(b[0], b[1] - 1, b[2]) - Date.UTC(a[0], a[1] - 1, a[2])) / 86400000);
|
|
6208
|
+
}
|
|
6209
|
+
function shortHash(s) {
|
|
6210
|
+
let h = 2166136261;
|
|
6211
|
+
for (let i = 0;i < s.length; i++) {
|
|
6212
|
+
h ^= s.charCodeAt(i);
|
|
6213
|
+
h = Math.imul(h, 16777619);
|
|
6214
|
+
}
|
|
6215
|
+
return (h >>> 0).toString(16).padStart(8, "0").slice(0, 8);
|
|
6216
|
+
}
|
|
6217
|
+
function safeJson(v) {
|
|
6218
|
+
return JSON.stringify(v).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6219
|
+
}
|
|
6220
|
+
var bare = (id) => id.replace(/^task:/, "");
|
|
6221
|
+
function buildPayload(graph, opts = {}) {
|
|
6222
|
+
const now = opts.now ?? new Date;
|
|
6223
|
+
const today = localDay(now);
|
|
6224
|
+
const limit = opts.limit === undefined ? 500 : opts.limit;
|
|
6225
|
+
const redact = !!opts.redact;
|
|
6226
|
+
const taskNodes = graph.nodes.filter((n) => n.kind === "task");
|
|
6227
|
+
const known = new Set(taskNodes.map((n) => n.id));
|
|
6228
|
+
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));
|
|
6229
|
+
const shown = limit > 0 ? ordered.slice(0, limit) : ordered;
|
|
6230
|
+
const tasks = shown.map((n) => {
|
|
6231
|
+
const startDay = n.start ?? (n.created_at ? localDay(new Date(n.created_at)) : null);
|
|
6232
|
+
const endDay = n.due ?? (n.closed_at ? localDay(new Date(n.closed_at)) : null);
|
|
6233
|
+
return {
|
|
6234
|
+
id: bare(n.id),
|
|
6235
|
+
label: n.label,
|
|
6236
|
+
status: n.status ?? "todo",
|
|
6237
|
+
prio: n.prio ?? null,
|
|
6238
|
+
start: startDay ? dayDiff(today, startDay) : null,
|
|
6239
|
+
end: endDay && startDay ? dayDiff(today, endDay) : null,
|
|
6240
|
+
progress: typeof n.progress === "number" ? n.progress : 0,
|
|
6241
|
+
derived: n.progress_derived !== false,
|
|
6242
|
+
owner: n.owner ? redact ? `owner-${shortHash(n.owner)}` : n.owner : null,
|
|
6243
|
+
deps: (n.deps ?? []).filter((d) => known.has(`task:${d}`)),
|
|
6244
|
+
touches: (n.touches ?? []).map((p) => redact ? p.split("/").pop() || p : p)
|
|
6245
|
+
};
|
|
6246
|
+
});
|
|
6247
|
+
const shownIds = new Set(tasks.map((t) => t.id));
|
|
6248
|
+
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));
|
|
6249
|
+
return {
|
|
6250
|
+
schema: 1,
|
|
6251
|
+
today,
|
|
6252
|
+
generated_at: `${today} ${pad(now.getHours())}:${pad(now.getMinutes())}`,
|
|
6253
|
+
tasks,
|
|
6254
|
+
edges,
|
|
6255
|
+
cycles: graph.cycles.map((c) => c.map(bare)),
|
|
6256
|
+
claims: opts.claims ?? graph.edges.filter((e) => e.kind === "claims").length,
|
|
6257
|
+
truncated: limit > 0 && ordered.length > limit ? { shown: limit, total: ordered.length } : null,
|
|
6258
|
+
capabilities: graph.capabilities ?? []
|
|
6259
|
+
};
|
|
6260
|
+
}
|
|
6261
|
+
function renderHtml(graph, opts = {}) {
|
|
6262
|
+
const payload = buildPayload(graph, opts);
|
|
6263
|
+
return template_default.replace("__DIA_PAYLOAD__", safeJson(payload));
|
|
6264
|
+
}
|
|
6265
|
+
|
|
4864
6266
|
// packages/cli/src/index.ts
|
|
4865
6267
|
init_paths();
|
|
4866
6268
|
|
|
4867
6269
|
// src/agents.ts
|
|
4868
6270
|
init_paths();
|
|
4869
|
-
import { existsSync as
|
|
6271
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync15, readdirSync as readdirSync9, readFileSync as readFileSync15, renameSync as renameSync9, rmSync as rmSync6, writeFileSync as writeFileSync12 } from "node:fs";
|
|
4870
6272
|
import { basename as basename8, join as join18, resolve as resolve10 } from "node:path";
|
|
4871
6273
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
4872
6274
|
var agentsRoot2 = () => agentsRoot();
|
|
@@ -4875,7 +6277,7 @@ function atomicWrite2(path, data) {
|
|
|
4875
6277
|
mkdirSync15(join18(path, ".."), { recursive: true });
|
|
4876
6278
|
const tmp = `${path}.tmp.${process.pid}`;
|
|
4877
6279
|
writeFileSync12(tmp, data);
|
|
4878
|
-
|
|
6280
|
+
renameSync9(tmp, path);
|
|
4879
6281
|
}
|
|
4880
6282
|
function sh(cmd, args) {
|
|
4881
6283
|
try {
|
|
@@ -4896,14 +6298,14 @@ function register(rec) {
|
|
|
4896
6298
|
}
|
|
4897
6299
|
function unregister(agent) {
|
|
4898
6300
|
const p = fileOf(agent);
|
|
4899
|
-
if (!
|
|
6301
|
+
if (!existsSync17(p))
|
|
4900
6302
|
return false;
|
|
4901
6303
|
rmSync6(p, { force: true });
|
|
4902
6304
|
return true;
|
|
4903
6305
|
}
|
|
4904
6306
|
function read2(agent) {
|
|
4905
6307
|
const p = fileOf(agent);
|
|
4906
|
-
if (!
|
|
6308
|
+
if (!existsSync17(p))
|
|
4907
6309
|
return null;
|
|
4908
6310
|
try {
|
|
4909
6311
|
return JSON.parse(readFileSync15(p, "utf8"));
|
|
@@ -4913,7 +6315,7 @@ function read2(agent) {
|
|
|
4913
6315
|
}
|
|
4914
6316
|
function all() {
|
|
4915
6317
|
const root = agentsRoot2();
|
|
4916
|
-
if (!
|
|
6318
|
+
if (!existsSync17(root))
|
|
4917
6319
|
return [];
|
|
4918
6320
|
return readdirSync9(root).filter((f) => f.endsWith(".json")).map((f) => {
|
|
4919
6321
|
try {
|
|
@@ -5179,7 +6581,7 @@ async function deliverAcp(agent, id, opts = {}) {
|
|
|
5179
6581
|
}
|
|
5180
6582
|
|
|
5181
6583
|
// src/scope.ts
|
|
5182
|
-
import { existsSync as
|
|
6584
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16, readdirSync as readdirSync10, statSync as statSync11 } from "node:fs";
|
|
5183
6585
|
init_paths();
|
|
5184
6586
|
import { homedir as homedir9 } from "node:os";
|
|
5185
6587
|
import { join as join20, resolve as resolve11 } from "node:path";
|
|
@@ -5219,7 +6621,7 @@ function loadKit(id, version) {
|
|
|
5219
6621
|
}
|
|
5220
6622
|
function installedPersonas() {
|
|
5221
6623
|
const p = join20(dataDir5(), "personas");
|
|
5222
|
-
if (!
|
|
6624
|
+
if (!existsSync18(p))
|
|
5223
6625
|
return [];
|
|
5224
6626
|
return readdirSync10(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
5225
6627
|
}
|
|
@@ -5235,7 +6637,7 @@ function current(explicit) {
|
|
|
5235
6637
|
if (!id)
|
|
5236
6638
|
return null;
|
|
5237
6639
|
const idDir = join20(dataDir5(), "personas", id);
|
|
5238
|
-
if (!
|
|
6640
|
+
if (!existsSync18(idDir))
|
|
5239
6641
|
throw new Error(`E_SCOPE: persona not installed: ${id}`);
|
|
5240
6642
|
const version = effective(id)?.version ?? latest(idDir);
|
|
5241
6643
|
const dir = join20(idDir, version);
|
|
@@ -5285,13 +6687,13 @@ function scopeEnv(s, extra = {}) {
|
|
|
5285
6687
|
}
|
|
5286
6688
|
|
|
5287
6689
|
// src/doctor.ts
|
|
5288
|
-
import { existsSync as
|
|
6690
|
+
import { closeSync as closeSync4, existsSync as existsSync23, openSync as openSync4, readFileSync as readFileSync19, readSync, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
|
|
5289
6691
|
import { homedir as homedir12 } from "node:os";
|
|
5290
|
-
import { join as join25, resolve as resolve13 } from "node:path";
|
|
6692
|
+
import { delimiter, dirname as dirname13, join as join25, resolve as resolve13 } from "node:path";
|
|
5291
6693
|
|
|
5292
6694
|
// src/dsh.ts
|
|
5293
6695
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
5294
|
-
import { existsSync as
|
|
6696
|
+
import { existsSync as existsSync19, realpathSync as realpathSync4 } from "node:fs";
|
|
5295
6697
|
import { dirname as dirname10, join as join21 } from "node:path";
|
|
5296
6698
|
var dshBin = () => process.env.DIA_DSH_BIN || "dsh";
|
|
5297
6699
|
function which(cmd) {
|
|
@@ -5299,7 +6701,7 @@ function which(cmd) {
|
|
|
5299
6701
|
if (!dir)
|
|
5300
6702
|
continue;
|
|
5301
6703
|
const p = join21(dir, cmd);
|
|
5302
|
-
if (
|
|
6704
|
+
if (existsSync19(p))
|
|
5303
6705
|
return p;
|
|
5304
6706
|
}
|
|
5305
6707
|
return null;
|
|
@@ -5322,7 +6724,7 @@ var packageFor = (id) => `@deepseek-ai/dsh-${id}`;
|
|
|
5322
6724
|
function requireConfig(root, id) {
|
|
5323
6725
|
const pkg = packageFor(id);
|
|
5324
6726
|
const dir = join21(root, "node_modules", ...pkg.split("/"), "lib", "index.js");
|
|
5325
|
-
if (!
|
|
6727
|
+
if (!existsSync19(dir))
|
|
5326
6728
|
return { required: false };
|
|
5327
6729
|
try {
|
|
5328
6730
|
const out = execFileSync7(process.execPath, [
|
|
@@ -5367,7 +6769,7 @@ function pluginsWithoutConfig(dump, personaId) {
|
|
|
5367
6769
|
return missing;
|
|
5368
6770
|
}
|
|
5369
6771
|
function checkPreset(personaId, profileDir) {
|
|
5370
|
-
if (!
|
|
6772
|
+
if (!existsSync19(profileDir))
|
|
5371
6773
|
return { ok: true, skipped: "未渲染 dsh profile", issues: [] };
|
|
5372
6774
|
let dump;
|
|
5373
6775
|
try {
|
|
@@ -5395,7 +6797,7 @@ function checkPreset(personaId, profileDir) {
|
|
|
5395
6797
|
}
|
|
5396
6798
|
|
|
5397
6799
|
// src/platforms.ts
|
|
5398
|
-
import { existsSync as
|
|
6800
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
|
|
5399
6801
|
init_paths();
|
|
5400
6802
|
import { join as join22 } from "node:path";
|
|
5401
6803
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
@@ -5410,7 +6812,7 @@ function registryCandidates() {
|
|
|
5410
6812
|
return out;
|
|
5411
6813
|
}
|
|
5412
6814
|
function registryPath() {
|
|
5413
|
-
return registryCandidates().find((p) =>
|
|
6815
|
+
return registryCandidates().find((p) => existsSync20(p)) ?? null;
|
|
5414
6816
|
}
|
|
5415
6817
|
function load() {
|
|
5416
6818
|
const p = registryPath();
|
|
@@ -5484,7 +6886,7 @@ function checks(reg) {
|
|
|
5484
6886
|
|
|
5485
6887
|
// src/deps.ts
|
|
5486
6888
|
init_paths();
|
|
5487
|
-
import { existsSync as
|
|
6889
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18, mkdirSync as mkdirSync16, writeFileSync as writeFileSync14, chmodSync as chmodSync3 } from "node:fs";
|
|
5488
6890
|
import { homedir as homedir10 } from "node:os";
|
|
5489
6891
|
import { dirname as dirname11, join as join23 } from "node:path";
|
|
5490
6892
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
@@ -5500,7 +6902,7 @@ function candidates() {
|
|
|
5500
6902
|
return out;
|
|
5501
6903
|
}
|
|
5502
6904
|
function file3() {
|
|
5503
|
-
return candidates().find((p) =>
|
|
6905
|
+
return candidates().find((p) => existsSync21(p)) ?? null;
|
|
5504
6906
|
}
|
|
5505
6907
|
function load2(p) {
|
|
5506
6908
|
const path = p ?? file3();
|
|
@@ -5562,7 +6964,7 @@ function check(e) {
|
|
|
5562
6964
|
return { id: e.id, status: "ok", detail: `${e.id}:无固定路径(跳过探测)` };
|
|
5563
6965
|
}
|
|
5564
6966
|
const path = expand3(r.replace(/^link:/, ""));
|
|
5565
|
-
return
|
|
6967
|
+
return existsSync21(path) ? { id: e.id, status: "ok", detail: `${e.id}:${path}` } : missing(`${path} 不存在`);
|
|
5566
6968
|
}
|
|
5567
6969
|
return { id: e.id, status: "ok", detail: `${e.id}:kind=${e.kind} 无探测规则` };
|
|
5568
6970
|
}
|
|
@@ -5590,7 +6992,7 @@ function append4(entry, p) {
|
|
|
5590
6992
|
mkdirSync16(dirname11(path), { recursive: true });
|
|
5591
6993
|
const line = JSON.stringify(entry) + `
|
|
5592
6994
|
`;
|
|
5593
|
-
const prev =
|
|
6995
|
+
const prev = existsSync21(path) ? readFileSync18(path, "utf8") : "";
|
|
5594
6996
|
writeFileSync14(path, prev.endsWith(`
|
|
5595
6997
|
`) || !prev ? prev + line : prev + `
|
|
5596
6998
|
` + line);
|
|
@@ -5606,7 +7008,7 @@ init_paths();
|
|
|
5606
7008
|
// src/job.ts
|
|
5607
7009
|
init_paths();
|
|
5608
7010
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5609
|
-
import { existsSync as
|
|
7011
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
5610
7012
|
import { homedir as homedir11 } from "node:os";
|
|
5611
7013
|
import { dirname as dirname12, join as join24, resolve as resolve12 } from "node:path";
|
|
5612
7014
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
@@ -5614,7 +7016,7 @@ function jobBin() {
|
|
|
5614
7016
|
const env = process.env.DIA_JOB_BIN;
|
|
5615
7017
|
if (env) {
|
|
5616
7018
|
const abs = resolve12(env);
|
|
5617
|
-
return
|
|
7019
|
+
return existsSync22(abs) ? { bin: abs } : { bin: null, error: `DIA_JOB_BIN 不存在:${abs}` };
|
|
5618
7020
|
}
|
|
5619
7021
|
const here = dirname12(fileURLToPath2(import.meta.url));
|
|
5620
7022
|
const candidates = [
|
|
@@ -5622,7 +7024,7 @@ function jobBin() {
|
|
|
5622
7024
|
join24(here, "..", "..", "packages", "job", "bin", "dia-job"),
|
|
5623
7025
|
join24(homedir11(), ".local", "bin", "dia-job")
|
|
5624
7026
|
];
|
|
5625
|
-
const found = candidates.find((p) =>
|
|
7027
|
+
const found = candidates.find((p) => existsSync22(p));
|
|
5626
7028
|
return found ? { bin: found } : { bin: null };
|
|
5627
7029
|
}
|
|
5628
7030
|
function jobRootResolved() {
|
|
@@ -5630,7 +7032,7 @@ function jobRootResolved() {
|
|
|
5630
7032
|
return process.env.DIA_JOB_ROOT;
|
|
5631
7033
|
const next = jobRoot();
|
|
5632
7034
|
const legacy = join24(homedir11(), ".local", "share", "dia-job");
|
|
5633
|
-
return !
|
|
7035
|
+
return !existsSync22(next) && existsSync22(legacy) ? legacy : next;
|
|
5634
7036
|
}
|
|
5635
7037
|
function runJobCapture(argv) {
|
|
5636
7038
|
const { bin, error } = jobBin();
|
|
@@ -5684,6 +7086,102 @@ function platformChecks() {
|
|
|
5684
7086
|
function quoteArg(v) {
|
|
5685
7087
|
return /^[\w.@%+=:,\/-]+$/.test(v) ? v : `'${v.replace(/'/g, "'\\''")}'`;
|
|
5686
7088
|
}
|
|
7089
|
+
function npmOwnedPath(p) {
|
|
7090
|
+
return /(^|[\\/])node_modules[\\/]@minonl[\\/]dia([\\/]|$)/.test(p);
|
|
7091
|
+
}
|
|
7092
|
+
function pkgShimPath(p) {
|
|
7093
|
+
try {
|
|
7094
|
+
const st = statSync12(p);
|
|
7095
|
+
if (!st.isFile() || st.size > 1024 * 1024)
|
|
7096
|
+
return false;
|
|
7097
|
+
const fd = openSync4(p, "r");
|
|
7098
|
+
try {
|
|
7099
|
+
const buf = Buffer.alloc(8192);
|
|
7100
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
7101
|
+
return /@minonl\/dia|node_modules\/(@minonl\/)?dia\//.test(buf.subarray(0, n).toString("utf8"));
|
|
7102
|
+
} finally {
|
|
7103
|
+
closeSync4(fd);
|
|
7104
|
+
}
|
|
7105
|
+
} catch {
|
|
7106
|
+
return false;
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
function pkgBinDirOwned(p) {
|
|
7110
|
+
const d = realOrSelf(dirname13(p));
|
|
7111
|
+
const home = homedir12();
|
|
7112
|
+
const known = [
|
|
7113
|
+
join25(home, ".volta/bin"),
|
|
7114
|
+
join25(home, ".bun/bin"),
|
|
7115
|
+
join25(home, ".yarn/bin"),
|
|
7116
|
+
join25(home, ".pnpm-global/bin"),
|
|
7117
|
+
join25(home, ".local/share/pnpm"),
|
|
7118
|
+
join25(home, ".npm-global/bin"),
|
|
7119
|
+
process.env.PNPM_HOME,
|
|
7120
|
+
process.env.YARN_GLOBAL_FOLDER,
|
|
7121
|
+
process.env.VOLTA_HOME ? join25(process.env.VOLTA_HOME, "bin") : "",
|
|
7122
|
+
process.env.BUN_INSTALL ? join25(process.env.BUN_INSTALL, "bin") : ""
|
|
7123
|
+
].filter(Boolean);
|
|
7124
|
+
return known.some((k) => realOrSelf(k) === d);
|
|
7125
|
+
}
|
|
7126
|
+
function pkgOwnedPath(p) {
|
|
7127
|
+
return npmOwnedPath(p) || pkgShimPath(p) || pkgBinDirOwned(p);
|
|
7128
|
+
}
|
|
7129
|
+
function realOrSelf(p) {
|
|
7130
|
+
try {
|
|
7131
|
+
return realpathSync5(p);
|
|
7132
|
+
} catch {
|
|
7133
|
+
return resolve13(p);
|
|
7134
|
+
}
|
|
7135
|
+
}
|
|
7136
|
+
function whichDia(pathEnv = process.env.PATH || "") {
|
|
7137
|
+
for (const dir of pathEnv.split(delimiter)) {
|
|
7138
|
+
if (!dir)
|
|
7139
|
+
continue;
|
|
7140
|
+
const cand = join25(dir, "dia");
|
|
7141
|
+
try {
|
|
7142
|
+
const st = statSync12(cand);
|
|
7143
|
+
if (!st.isFile())
|
|
7144
|
+
continue;
|
|
7145
|
+
if (process.platform !== "win32" && (st.mode & 73) === 0)
|
|
7146
|
+
continue;
|
|
7147
|
+
return cand;
|
|
7148
|
+
} catch {}
|
|
7149
|
+
}
|
|
7150
|
+
return null;
|
|
7151
|
+
}
|
|
7152
|
+
function installShadowCheck() {
|
|
7153
|
+
const name = "install_shadow";
|
|
7154
|
+
const install = installDir();
|
|
7155
|
+
const installNpm = npmOwnedPath(realOrSelf(install));
|
|
7156
|
+
if (!installNpm && !existsSync23(join25(install, "install.json"))) {
|
|
7157
|
+
return { name, status: "ok", detail: `源码运行(install=${install} 无 install.json,跳过一致性判定)` };
|
|
7158
|
+
}
|
|
7159
|
+
const bin = whichDia();
|
|
7160
|
+
if (!bin)
|
|
7161
|
+
return { name, status: "ok", detail: `PATH 中没有 dia(跳过一致性判定;install=${install})` };
|
|
7162
|
+
const binReal = realOrSelf(bin);
|
|
7163
|
+
const binNpm = pkgOwnedPath(binReal);
|
|
7164
|
+
const kind = (npm) => npm ? "包管理器全局安装" : "脚本安装";
|
|
7165
|
+
if (installNpm === binNpm) {
|
|
7166
|
+
return { name, status: "ok", detail: `一致(${kind(installNpm)}):install=${install};PATH dia=${binReal}` };
|
|
7167
|
+
}
|
|
7168
|
+
const fix = installNpm ? `ln -sfn "$(npm prefix -g)/bin/dia" ${quoteArg(bin)}` : `ln -sfn ${quoteArg(join25(install, "bin/dia.js"))} ${quoteArg(bin)}`;
|
|
7169
|
+
return {
|
|
7170
|
+
name,
|
|
7171
|
+
status: "warn",
|
|
7172
|
+
detail: `安装类型不一致:install=${install}(${kind(installNpm)})但 PATH dia=${binReal}(${kind(binNpm)});修复:${fix}`
|
|
7173
|
+
};
|
|
7174
|
+
}
|
|
7175
|
+
function bootstrapCheck() {
|
|
7176
|
+
const stamp = readStamp();
|
|
7177
|
+
const pkg = pkgVersion();
|
|
7178
|
+
if (!stamp)
|
|
7179
|
+
return { name: "bootstrap", status: "warn", detail: "内置能力尚未装配(首次 persona add/render 或 dia collab enable 会自动装)" };
|
|
7180
|
+
if (stamp.pkg !== pkg) {
|
|
7181
|
+
return { name: "bootstrap", status: "warn", detail: `内置能力可能过期:自举戳 pkg=${stamp.pkg},当前 ${pkg};修复:dia collab enable` };
|
|
7182
|
+
}
|
|
7183
|
+
return { name: "bootstrap", status: "ok", detail: `内置能力已装配(pkg=${pkg},kit=${stamp.kit ?? "-"})` };
|
|
7184
|
+
}
|
|
5687
7185
|
function describeStale(st) {
|
|
5688
7186
|
const diff = st.added + st.removed + st.changed;
|
|
5689
7187
|
const parts = [st.added ? `新增 ${st.added}` : "", st.removed ? `删除 ${st.removed}` : "", st.changed ? `修改 ${st.changed}` : ""].filter(Boolean);
|
|
@@ -5692,7 +7190,7 @@ function describeStale(st) {
|
|
|
5692
7190
|
}
|
|
5693
7191
|
async function knowledgeCheck(id, prefix, root) {
|
|
5694
7192
|
const name = `${prefix}knowledge`;
|
|
5695
|
-
if (!
|
|
7193
|
+
if (!existsSync23(root))
|
|
5696
7194
|
return { name, status: "fail", detail: root };
|
|
5697
7195
|
const rebuild = `dia kb index --persona ${quoteArg(id)}`;
|
|
5698
7196
|
let st;
|
|
@@ -5829,20 +7327,22 @@ async function run(opts = {}) {
|
|
|
5829
7327
|
status: install === diaHome() ? "warn" : "ok",
|
|
5830
7328
|
detail: `install=${install} data=${diaHome()}${install === diaHome() ? "(安装物与数据同根)" : ""}`
|
|
5831
7329
|
});
|
|
7330
|
+
checks.push(installShadowCheck());
|
|
7331
|
+
checks.push(bootstrapCheck());
|
|
5832
7332
|
const wt = join25(install, "packages/wt/bin/dia-wt");
|
|
5833
|
-
checks.push({ name: "wt", status:
|
|
7333
|
+
checks.push({ name: "wt", status: existsSync23(wt) ? "ok" : "warn", detail: existsSync23(wt) ? wt : "dia wt 未安装(dia install.sh)" });
|
|
5834
7334
|
const jobRoot = jobRootResolved();
|
|
5835
7335
|
checks.push({
|
|
5836
7336
|
name: "job",
|
|
5837
|
-
status:
|
|
5838
|
-
detail: `${jobRoot}${
|
|
7337
|
+
status: existsSync23(jobRoot) ? "ok" : "warn",
|
|
7338
|
+
detail: `${jobRoot}${existsSync23(jobRoot) ? "" : "(未创建;dia job install 首次运行会建)"}`
|
|
5839
7339
|
});
|
|
5840
7340
|
const verdict = checks.some((c) => c.status === "fail") ? "fail" : checks.some((c) => c.status === "warn") ? "warn" : "ok";
|
|
5841
7341
|
return { persona: ids.length === 1 ? ids[0] : null, personas: ids, checks, verdict };
|
|
5842
7342
|
}
|
|
5843
7343
|
|
|
5844
7344
|
// src/env.ts
|
|
5845
|
-
import { existsSync as
|
|
7345
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20 } from "node:fs";
|
|
5846
7346
|
import { homedir as homedir13 } from "node:os";
|
|
5847
7347
|
import { join as join26 } from "node:path";
|
|
5848
7348
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
@@ -5886,7 +7386,7 @@ function loadValues(cfg) {
|
|
|
5886
7386
|
return all;
|
|
5887
7387
|
}
|
|
5888
7388
|
function list6(cfg) {
|
|
5889
|
-
const files = cfg.files.map((p) => ({ path: p, exists:
|
|
7389
|
+
const files = cfg.files.map((p) => ({ path: p, exists: existsSync24(p), keys: existsSync24(p) ? Object.keys(parseFile(p)).length : 0 }));
|
|
5890
7390
|
const values = loadValues(cfg);
|
|
5891
7391
|
return { files, keys: cfg.keys.map((k) => ({ key: k, found: !!values[k] })) };
|
|
5892
7392
|
}
|
|
@@ -5927,7 +7427,7 @@ function run2(cfg, cmd, opts = {}) {
|
|
|
5927
7427
|
}
|
|
5928
7428
|
|
|
5929
7429
|
// src/bw.ts
|
|
5930
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
7430
|
+
import { chmodSync as chmodSync4, existsSync as existsSync25, mkdirSync as mkdirSync17, readFileSync as readFileSync21, rmSync as rmSync7, writeFileSync as writeFileSync15 } from "node:fs";
|
|
5931
7431
|
init_paths();
|
|
5932
7432
|
import { join as join27 } from "node:path";
|
|
5933
7433
|
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
@@ -6142,7 +7642,7 @@ function serve(opts = {}) {
|
|
|
6142
7642
|
}
|
|
6143
7643
|
function askBroker(req, timeoutMs = 5000) {
|
|
6144
7644
|
const sock = socketFile();
|
|
6145
|
-
if (!
|
|
7645
|
+
if (!existsSync25(sock))
|
|
6146
7646
|
return Promise.resolve(null);
|
|
6147
7647
|
return new Promise((resolve, reject) => {
|
|
6148
7648
|
const conn = connect(sock);
|
|
@@ -6181,19 +7681,19 @@ function askBroker(req, timeoutMs = 5000) {
|
|
|
6181
7681
|
|
|
6182
7682
|
// packages/cli/src/index.ts
|
|
6183
7683
|
import { execFileSync as execFileSync12, spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
|
|
6184
|
-
import { mkdirSync as mkdirSync20, openSync as
|
|
7684
|
+
import { mkdirSync as mkdirSync20, openSync as openSync5 } from "node:fs";
|
|
6185
7685
|
import { basename as basename10, join as join32 } from "node:path";
|
|
6186
|
-
import { existsSync as
|
|
7686
|
+
import { existsSync as existsSync30, readFileSync as readFileSync26, writeFileSync as writeFileSync18 } from "node:fs";
|
|
6187
7687
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
6188
7688
|
import { resolve as resolvePath, sep as pathSep } from "node:path";
|
|
6189
7689
|
|
|
6190
7690
|
// src/team.ts
|
|
6191
|
-
import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as
|
|
7691
|
+
import { appendFileSync as appendFileSync8, cpSync as cpSync3, existsSync as existsSync27, mkdirSync as mkdirSync18, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync8, writeFileSync as writeFileSync16 } from "node:fs";
|
|
6192
7692
|
import { homedir as homedir15, tmpdir as tmpdir3 } from "node:os";
|
|
6193
7693
|
import { join as join29, resolve as resolve14 } from "node:path";
|
|
6194
7694
|
|
|
6195
7695
|
// src/pricing.ts
|
|
6196
|
-
import { existsSync as
|
|
7696
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "node:fs";
|
|
6197
7697
|
import { homedir as homedir14 } from "node:os";
|
|
6198
7698
|
import { join as join28 } from "node:path";
|
|
6199
7699
|
var DEFAULT = {
|
|
@@ -6205,7 +7705,7 @@ function pricesFile() {
|
|
|
6205
7705
|
return process.env.DIA_PRICES_FILE || join28(homedir14(), ".config", "dia", "prices.json");
|
|
6206
7706
|
}
|
|
6207
7707
|
function loadPrices() {
|
|
6208
|
-
if (!
|
|
7708
|
+
if (!existsSync26(pricesFile()))
|
|
6209
7709
|
return { ...DEFAULT };
|
|
6210
7710
|
try {
|
|
6211
7711
|
const user = JSON.parse(readFileSync22(pricesFile(), "utf8"));
|
|
@@ -6291,7 +7791,7 @@ function judgeWorkspace(dir, fallback) {
|
|
|
6291
7791
|
function gate(id) {
|
|
6292
7792
|
const dir = runDir(id);
|
|
6293
7793
|
const need = ["plan.md", "diff.patch", "test.log", "verify.json", "redteam.json", "packet.md", "run.json"];
|
|
6294
|
-
const missing = need.filter((f) => !
|
|
7794
|
+
const missing = need.filter((f) => !existsSync27(join29(dir, f)));
|
|
6295
7795
|
if (missing.length)
|
|
6296
7796
|
return { ok: false, reason: "missing: " + missing.join(",") };
|
|
6297
7797
|
let verify, redteam, run;
|
|
@@ -6337,7 +7837,7 @@ function gate(id) {
|
|
|
6337
7837
|
return { ok: false, reason: "mock run 不能作为验收证据(请用 --acp/--dsh 真跑判据)" };
|
|
6338
7838
|
}
|
|
6339
7839
|
const workspace = String(run.workspace || "");
|
|
6340
|
-
if (!workspace || !
|
|
7840
|
+
if (!workspace || !existsSync27(workspace))
|
|
6341
7841
|
return { ok: false, reason: "run.json workspace 不存在" };
|
|
6342
7842
|
const started = Date.now();
|
|
6343
7843
|
let exit = 1, out = "";
|
|
@@ -6745,11 +8245,11 @@ Return only the JSON object.`);
|
|
|
6745
8245
|
}
|
|
6746
8246
|
function dshSessionFile(sessionId) {
|
|
6747
8247
|
const root = join29(homedir15(), ".dsh", "sessions");
|
|
6748
|
-
if (!
|
|
8248
|
+
if (!existsSync27(root))
|
|
6749
8249
|
return null;
|
|
6750
8250
|
for (const d of readdirSync11(root)) {
|
|
6751
8251
|
const f = join29(root, d, sessionId, "session.v4.jsonl.zstd");
|
|
6752
|
-
if (
|
|
8252
|
+
if (existsSync27(f))
|
|
6753
8253
|
return f;
|
|
6754
8254
|
}
|
|
6755
8255
|
return null;
|
|
@@ -7039,7 +8539,7 @@ function reject(id, reason) {
|
|
|
7039
8539
|
var DEFAULT_ACP_TIMEOUT = 600000;
|
|
7040
8540
|
|
|
7041
8541
|
// src/proposal.ts
|
|
7042
|
-
import { existsSync as
|
|
8542
|
+
import { existsSync as existsSync28, readFileSync as readFileSync24, readdirSync as readdirSync12 } from "node:fs";
|
|
7043
8543
|
import { join as join30 } from "node:path";
|
|
7044
8544
|
import { createHash as createHash10 } from "node:crypto";
|
|
7045
8545
|
init_paths();
|
|
@@ -7062,7 +8562,7 @@ function readJson3(path) {
|
|
|
7062
8562
|
}
|
|
7063
8563
|
function collectRuns(out) {
|
|
7064
8564
|
const root = join30(DATA7(), "runs");
|
|
7065
|
-
if (!
|
|
8565
|
+
if (!existsSync28(root))
|
|
7066
8566
|
return;
|
|
7067
8567
|
for (const id of readdirSync12(root)) {
|
|
7068
8568
|
const dir = join30(root, id);
|
|
@@ -7106,7 +8606,7 @@ function collectTasks(out) {
|
|
|
7106
8606
|
}
|
|
7107
8607
|
}
|
|
7108
8608
|
function collectDeps(out, path) {
|
|
7109
|
-
if (!
|
|
8609
|
+
if (!existsSync28(path))
|
|
7110
8610
|
return;
|
|
7111
8611
|
for (const e of load2(path)) {
|
|
7112
8612
|
if (e.result === "unknown" || e.support === "unknown") {
|
|
@@ -7139,7 +8639,7 @@ function expandIds(cell) {
|
|
|
7139
8639
|
return out;
|
|
7140
8640
|
}
|
|
7141
8641
|
function collectHypotheses(out, path) {
|
|
7142
|
-
if (!
|
|
8642
|
+
if (!existsSync28(path))
|
|
7143
8643
|
return;
|
|
7144
8644
|
const lines = readFileSync24(path, "utf8").split(`
|
|
7145
8645
|
`);
|
|
@@ -7181,20 +8681,20 @@ import { createHash as createHash11, randomBytes as randomBytes4 } from "node:cr
|
|
|
7181
8681
|
import {
|
|
7182
8682
|
appendFileSync as appendFileSync9,
|
|
7183
8683
|
cpSync as cpSync4,
|
|
7184
|
-
existsSync as
|
|
8684
|
+
existsSync as existsSync29,
|
|
7185
8685
|
lstatSync as lstatSync3,
|
|
7186
8686
|
mkdirSync as mkdirSync19,
|
|
7187
8687
|
readFileSync as readFileSync25,
|
|
7188
8688
|
readdirSync as readdirSync13,
|
|
7189
8689
|
readlinkSync as readlinkSync2,
|
|
7190
8690
|
rmSync as rmSync9,
|
|
7191
|
-
statSync as
|
|
8691
|
+
statSync as statSync13,
|
|
7192
8692
|
symlinkSync as symlinkSync3,
|
|
7193
8693
|
unlinkSync as unlinkSync3,
|
|
7194
8694
|
writeFileSync as writeFileSync17
|
|
7195
8695
|
} from "node:fs";
|
|
7196
8696
|
import { hostname as osHostname } from "node:os";
|
|
7197
|
-
import { basename as basename9, dirname as
|
|
8697
|
+
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";
|
|
7198
8698
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7199
8699
|
init_paths();
|
|
7200
8700
|
var ID_RE2 = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -7375,7 +8875,7 @@ function validateServiceManifest(m) {
|
|
|
7375
8875
|
}
|
|
7376
8876
|
function readServiceManifest(dir) {
|
|
7377
8877
|
const p = join31(dir, "dia.json");
|
|
7378
|
-
if (!
|
|
8878
|
+
if (!existsSync29(p))
|
|
7379
8879
|
throw new Error(`E_NO_MANIFEST: dia.json not found in ${dir}(adopted 服务需先补 dia.json)`);
|
|
7380
8880
|
let raw;
|
|
7381
8881
|
try {
|
|
@@ -7405,7 +8905,7 @@ function resolveEntry(cmd, baseDir) {
|
|
|
7405
8905
|
}
|
|
7406
8906
|
function isExecutableFile(p) {
|
|
7407
8907
|
try {
|
|
7408
|
-
const st =
|
|
8908
|
+
const st = statSync13(p);
|
|
7409
8909
|
if (!st.isFile())
|
|
7410
8910
|
return false;
|
|
7411
8911
|
return process.platform === "win32" ? true : (st.mode & 73) !== 0;
|
|
@@ -7642,11 +9142,11 @@ function diaCmd() {
|
|
|
7642
9142
|
if (onPath)
|
|
7643
9143
|
return "dia";
|
|
7644
9144
|
const entries = [
|
|
7645
|
-
resolve15(
|
|
7646
|
-
resolve15(
|
|
7647
|
-
resolve15(
|
|
9145
|
+
resolve15(dirname14(fileURLToPath3(import.meta.url)), "..", "packages", "cli", "src", "index.ts"),
|
|
9146
|
+
resolve15(dirname14(process.argv[1] ?? ""), "..", "src", "cli.ts"),
|
|
9147
|
+
resolve15(dirname14(process.argv[1] ?? ""), "..", "dist", "cli.js")
|
|
7648
9148
|
];
|
|
7649
|
-
const entry = entries.find((p) =>
|
|
9149
|
+
const entry = entries.find((p) => existsSync29(p));
|
|
7650
9150
|
return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
|
|
7651
9151
|
}
|
|
7652
9152
|
function decorateStatus(text, view) {
|
|
@@ -7674,10 +9174,10 @@ var serviceDir = (id) => join31(servicesRoot(), id);
|
|
|
7674
9174
|
var serviceLockPath = (id) => join31(serviceDir(id), "service.lock.json");
|
|
7675
9175
|
var healthStatePath = (id) => process.env.DIA_SERVICE_HEALTH_FILE || join31(jobRoot2(), "tasks", `${healthJobName(id)}.state.jsonl`);
|
|
7676
9176
|
function readLockFile(id) {
|
|
7677
|
-
if (!
|
|
9177
|
+
if (!existsSync29(serviceDir(id)))
|
|
7678
9178
|
throw new Error(`E_NOT_INSTALLED: service ${id}`);
|
|
7679
9179
|
const p = serviceLockPath(id);
|
|
7680
|
-
if (!
|
|
9180
|
+
if (!existsSync29(p))
|
|
7681
9181
|
throw new Error(`E_NOT_INSTALLED: service ${id}(缺 lock)`);
|
|
7682
9182
|
try {
|
|
7683
9183
|
return JSON.parse(readFileSync25(p, "utf8"));
|
|
@@ -7698,7 +9198,7 @@ function generationOf(id) {
|
|
|
7698
9198
|
return `g-sess${(++ephemeralGen).toString(16).padStart(4, "0")}`;
|
|
7699
9199
|
}
|
|
7700
9200
|
function readHealthLines(file) {
|
|
7701
|
-
if (!
|
|
9201
|
+
if (!existsSync29(file))
|
|
7702
9202
|
return [];
|
|
7703
9203
|
const rows = [];
|
|
7704
9204
|
const lines = readFileSync25(file, "utf8").split(`
|
|
@@ -7733,7 +9233,7 @@ function readHealthLines(file) {
|
|
|
7733
9233
|
return rows;
|
|
7734
9234
|
}
|
|
7735
9235
|
function appendHealthRecord(file, rec) {
|
|
7736
|
-
mkdirSync19(
|
|
9236
|
+
mkdirSync19(dirname14(file), { recursive: true });
|
|
7737
9237
|
appendFileSync9(file, JSON.stringify(rec) + `
|
|
7738
9238
|
`);
|
|
7739
9239
|
}
|
|
@@ -7780,7 +9280,7 @@ function listServices(opts = {}) {
|
|
|
7780
9280
|
const reg = opts.registry === null ? null : readRegistryTasks(opts.registry ?? registryPath2());
|
|
7781
9281
|
const rows = [];
|
|
7782
9282
|
const seen = new Set;
|
|
7783
|
-
if (
|
|
9283
|
+
if (existsSync29(root)) {
|
|
7784
9284
|
for (const e of readdirSync13(root, { withFileTypes: true })) {
|
|
7785
9285
|
if (!e.isDirectory() || e.name.startsWith("."))
|
|
7786
9286
|
continue;
|
|
@@ -7789,7 +9289,7 @@ function listServices(opts = {}) {
|
|
|
7789
9289
|
}
|
|
7790
9290
|
}
|
|
7791
9291
|
const adoptRoot = join31(root, "adopted");
|
|
7792
|
-
if (
|
|
9292
|
+
if (existsSync29(adoptRoot)) {
|
|
7793
9293
|
for (const e of readdirSync13(adoptRoot, { withFileTypes: true })) {
|
|
7794
9294
|
if (!e.isDirectory() || seen.has(e.name))
|
|
7795
9295
|
continue;
|
|
@@ -8077,7 +9577,7 @@ function whichTimeout1() {
|
|
|
8077
9577
|
var serviceJobName = (id) => `svc-${id}`;
|
|
8078
9578
|
var healthJobName = (id) => `svc-${id}-health`;
|
|
8079
9579
|
function readRegistryTasks(file = registryPath2()) {
|
|
8080
|
-
if (!
|
|
9580
|
+
if (!existsSync29(file))
|
|
8081
9581
|
return null;
|
|
8082
9582
|
try {
|
|
8083
9583
|
const d = JSON.parse(readFileSync25(file, "utf8"));
|
|
@@ -8099,7 +9599,7 @@ function collectManagedFiles(base, hc) {
|
|
|
8099
9599
|
const out = {};
|
|
8100
9600
|
const rel = "dia.json";
|
|
8101
9601
|
const p = join31(base, rel);
|
|
8102
|
-
if (
|
|
9602
|
+
if (existsSync29(p))
|
|
8103
9603
|
out[rel] = shaFile3(p);
|
|
8104
9604
|
if (!hc)
|
|
8105
9605
|
return out;
|
|
@@ -8114,7 +9614,7 @@ function collectManagedFiles(base, hc) {
|
|
|
8114
9614
|
continue;
|
|
8115
9615
|
let st;
|
|
8116
9616
|
try {
|
|
8117
|
-
st =
|
|
9617
|
+
st = statSync13(abs);
|
|
8118
9618
|
} catch {
|
|
8119
9619
|
continue;
|
|
8120
9620
|
}
|
|
@@ -8128,11 +9628,11 @@ function verify4(dir, opts = {}) {
|
|
|
8128
9628
|
const svc = readServiceManifest(dir);
|
|
8129
9629
|
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
8130
9630
|
const drift = [];
|
|
8131
|
-
if (!
|
|
9631
|
+
if (!existsSync29(entry.path))
|
|
8132
9632
|
drift.push({ code: "entry_missing", detail: `entry 不存在:${entry.path}` });
|
|
8133
9633
|
else if (!isExecutableFile(entry.path))
|
|
8134
9634
|
drift.push({ code: "entry_not_executable", detail: `entry 不可执行:${entry.path}` });
|
|
8135
|
-
const lock = opts.lock === undefined ?
|
|
9635
|
+
const lock = opts.lock === undefined ? existsSync29(serviceDir(svc.id)) ? tryLock(svc.id) : null : opts.lock;
|
|
8136
9636
|
if (lock) {
|
|
8137
9637
|
if (entry.kind === "ref") {
|
|
8138
9638
|
const want = lock.entry.link ? resolve15(lock.entry.link) : null;
|
|
@@ -8169,7 +9669,7 @@ function tryLock(id) {
|
|
|
8169
9669
|
return null;
|
|
8170
9670
|
}
|
|
8171
9671
|
}
|
|
8172
|
-
var looksLikeRepo = (from) =>
|
|
9672
|
+
var looksLikeRepo = (from) => existsSync29(join31(from, "dia.json")) || existsSync29(join31(from, ".git")) || existsSync29(join31(from, "package.json"));
|
|
8173
9673
|
function linkEntry(path) {
|
|
8174
9674
|
const dir = entryDir();
|
|
8175
9675
|
mkdirSync19(dir, { recursive: true });
|
|
@@ -8177,7 +9677,7 @@ function linkEntry(path) {
|
|
|
8177
9677
|
const dest = join31(dir, name);
|
|
8178
9678
|
if (isSymlink2(dest))
|
|
8179
9679
|
unlinkSync3(dest);
|
|
8180
|
-
else if (
|
|
9680
|
+
else if (existsSync29(dest))
|
|
8181
9681
|
throw new Error(`E_CONFLICT: 稳定入口 ${dest} 已存在且不是软链`);
|
|
8182
9682
|
symlinkSync3(resolve15(path), dest);
|
|
8183
9683
|
return dest;
|
|
@@ -8202,7 +9702,7 @@ function add2(opts) {
|
|
|
8202
9702
|
let copied = false;
|
|
8203
9703
|
if (opts.from !== undefined) {
|
|
8204
9704
|
const src = resolve15(opts.from);
|
|
8205
|
-
if (!
|
|
9705
|
+
if (!existsSync29(src))
|
|
8206
9706
|
throw new Error(`E_SOURCE: path not found: ${src}`);
|
|
8207
9707
|
if (looksLikeRepo(src))
|
|
8208
9708
|
cloneInto(src, rawDir);
|
|
@@ -8230,7 +9730,7 @@ function add2(opts) {
|
|
|
8230
9730
|
if (svc.healthcheck)
|
|
8231
9731
|
healthJobInterval(svc);
|
|
8232
9732
|
const entry = resolveEntry(entryCmdOf(svc), dir);
|
|
8233
|
-
if (!
|
|
9733
|
+
if (!existsSync29(entry.path))
|
|
8234
9734
|
throw new Error(`E_UNSTABLE_ENTRY: entry 不存在:${entry.path}`);
|
|
8235
9735
|
if (!isExecutableFile(entry.path))
|
|
8236
9736
|
throw new Error(`E_UNSTABLE_ENTRY: entry 不可执行:${entry.path}`);
|
|
@@ -8262,7 +9762,7 @@ function add2(opts) {
|
|
|
8262
9762
|
err.code = code;
|
|
8263
9763
|
throw err;
|
|
8264
9764
|
};
|
|
8265
|
-
if (
|
|
9765
|
+
if (existsSync29(serviceLockPath(svc.id)))
|
|
8266
9766
|
remove3({ id: svc.id, runner });
|
|
8267
9767
|
let entryPath = entry.path;
|
|
8268
9768
|
if (entry.kind === "repo") {
|
|
@@ -8333,10 +9833,10 @@ function copyTree(from, to) {
|
|
|
8333
9833
|
cpSync4(from, to, { recursive: true, dereference: true, filter: (p) => !isRuntimeNoise(relative6(from, p)) });
|
|
8334
9834
|
}
|
|
8335
9835
|
function copyAdopted(from, to) {
|
|
8336
|
-
if (
|
|
9836
|
+
if (existsSync29(to))
|
|
8337
9837
|
throw new Error(`E_CONFLICT: ${to} 已存在`);
|
|
8338
|
-
mkdirSync19(
|
|
8339
|
-
if (
|
|
9838
|
+
mkdirSync19(dirname14(to), { recursive: true });
|
|
9839
|
+
if (statSync13(from).isDirectory())
|
|
8340
9840
|
copyTree(from, to);
|
|
8341
9841
|
else {
|
|
8342
9842
|
mkdirSync19(to, { recursive: true });
|
|
@@ -8344,7 +9844,7 @@ function copyAdopted(from, to) {
|
|
|
8344
9844
|
}
|
|
8345
9845
|
}
|
|
8346
9846
|
function isGitRepo(p) {
|
|
8347
|
-
return
|
|
9847
|
+
return existsSync29(join31(p, ".git"));
|
|
8348
9848
|
}
|
|
8349
9849
|
function remove3(opts) {
|
|
8350
9850
|
const lock = readLockFile(opts.id);
|
|
@@ -8358,7 +9858,7 @@ function remove3(opts) {
|
|
|
8358
9858
|
}
|
|
8359
9859
|
if (lock.entry.kind === "repo" && lock.entry.link && isSymlink2(lock.entry.link)) {
|
|
8360
9860
|
const rel = readlinkSync2(lock.entry.link);
|
|
8361
|
-
if (rel.startsWith(lock.dir) || resolve15(
|
|
9861
|
+
if (rel.startsWith(lock.dir) || resolve15(dirname14(lock.entry.link), rel).startsWith(lock.dir)) {
|
|
8362
9862
|
unlinkSync3(lock.entry.link);
|
|
8363
9863
|
removed.push(lock.entry.link);
|
|
8364
9864
|
} else
|
|
@@ -8371,7 +9871,7 @@ function remove3(opts) {
|
|
|
8371
9871
|
function runServiceHealth(id, opts = {}) {
|
|
8372
9872
|
const root = opts.servicesRoot ?? servicesRoot();
|
|
8373
9873
|
const dir = join31(root, id);
|
|
8374
|
-
if (!
|
|
9874
|
+
if (!existsSync29(dir))
|
|
8375
9875
|
throw usageError(`E_NOT_INSTALLED: service ${id}`);
|
|
8376
9876
|
const svc = readServiceManifest(dir);
|
|
8377
9877
|
if (!svc.healthcheck)
|
|
@@ -8591,8 +10091,11 @@ usage:
|
|
|
8591
10091
|
dia msg send|show|inbox|ack|delivered|deliver [--wait]|reply|needs-info|status|stuck|thread|escalate|agents|migrate
|
|
8592
10092
|
dia agents list|register|unregister|discover
|
|
8593
10093
|
dia msg deliver <agent> <id> [--transport tmux|acp]
|
|
8594
|
-
dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
|
|
10094
|
+
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]
|
|
10095
|
+
dia task split <id> --into "切片A,切片B" [--slice 切片A] [--role R] [--judge J] [--touches p] [--force]
|
|
8595
10096
|
dia graph [--json] [--task t-...] [--tui [--watch] [--status S] [--owner S] [--width N] [--no-color]]
|
|
10097
|
+
dia graph --export-html [path] [--out path] [--now ISO] [--redact] [--all] [--limit N] [--force]
|
|
10098
|
+
dia graph --next [n] | --explain <id> | --lint # 只读编排建议(Kahn-v1)
|
|
8596
10099
|
dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] [--gc [--days 7]] | dia unclaim <path|id> [--force]
|
|
8597
10100
|
dia team run <id> --mock | --acp|--dsh --workspace DIR
|
|
8598
10101
|
dia team gate|accept|reject|next
|
|
@@ -8636,15 +10139,49 @@ async function kb(argv) {
|
|
|
8636
10139
|
throw e;
|
|
8637
10140
|
}
|
|
8638
10141
|
}
|
|
10142
|
+
const tagFilter = flag(args, "--tag");
|
|
10143
|
+
const rootsByPath = new Map;
|
|
10144
|
+
let kitHits = 0;
|
|
10145
|
+
for (const k of installed()) {
|
|
10146
|
+
if (tagFilter && !k.tags.includes(tagFilter))
|
|
10147
|
+
continue;
|
|
10148
|
+
let kr = [];
|
|
10149
|
+
try {
|
|
10150
|
+
if ((await indexStatus(k.dir)).state !== "ok")
|
|
10151
|
+
await buildIndex(k.dir);
|
|
10152
|
+
kr = await search(k.dir, q, undefined, limit);
|
|
10153
|
+
} catch {
|
|
10154
|
+
try {
|
|
10155
|
+
await buildIndex(k.dir);
|
|
10156
|
+
kr = await search(k.dir, q, undefined, limit);
|
|
10157
|
+
} catch {
|
|
10158
|
+
kr = [];
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
10161
|
+
for (const h of kr) {
|
|
10162
|
+
rootsByPath.set(`${k.id}:${h.path}`, k.dir);
|
|
10163
|
+
rows.push({ ...h, path: `${k.id}:${h.path}`, source: `kit:${k.id}@${k.version}` });
|
|
10164
|
+
kitHits++;
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
10167
|
+
if (kitHits) {
|
|
10168
|
+
const kitRows = rows.filter((r) => r.source);
|
|
10169
|
+
const personaRows = rows.filter((r) => !r.source);
|
|
10170
|
+
const kitQuota = Math.min(kitRows.length, Math.max(1, Math.floor(limit / 2)));
|
|
10171
|
+
const picked = [...personaRows.slice(0, limit - kitQuota), ...kitRows.slice(0, kitQuota)];
|
|
10172
|
+
rows = picked.sort((a, b) => (b.score ?? 0) - (a.score ?? 0)).slice(0, limit);
|
|
10173
|
+
}
|
|
8639
10174
|
const full = sub === "query" && has(args, "--full");
|
|
8640
10175
|
const bytes = sub === "query" && has(args, "--bytes");
|
|
8641
10176
|
const enrich = rows.map((r) => {
|
|
8642
10177
|
const item = { ...r };
|
|
10178
|
+
const rowRoot = rootsByPath.get(r.path) ?? root;
|
|
10179
|
+
const rowPath = r.source ? r.path.split(":").slice(1).join(":") : r.path;
|
|
8643
10180
|
try {
|
|
8644
10181
|
if (bytes)
|
|
8645
|
-
item.bytes = Buffer.byteLength(getDoc(
|
|
10182
|
+
item.bytes = Buffer.byteLength(getDoc(rowRoot, rowPath), "utf8");
|
|
8646
10183
|
if (full)
|
|
8647
|
-
item.text = getDoc(
|
|
10184
|
+
item.text = getDoc(rowRoot, rowPath);
|
|
8648
10185
|
} catch {
|
|
8649
10186
|
item.stale_index = true;
|
|
8650
10187
|
item.text = null;
|
|
@@ -8656,7 +10193,7 @@ async function kb(argv) {
|
|
|
8656
10193
|
out(JSON.stringify(enrich));
|
|
8657
10194
|
else
|
|
8658
10195
|
for (const r of enrich) {
|
|
8659
|
-
out(`${r.path} ${r.title}
|
|
10196
|
+
out(`${r.source ? `[${r.source}] ` : ""}${r.path} ${r.title}
|
|
8660
10197
|
${r.snippet}`);
|
|
8661
10198
|
if (r.bytes !== undefined)
|
|
8662
10199
|
out(` bytes ${r.bytes}`);
|
|
@@ -8935,7 +10472,18 @@ async function taskCli(argv) {
|
|
|
8935
10472
|
err("missing title");
|
|
8936
10473
|
return 2;
|
|
8937
10474
|
}
|
|
8938
|
-
const
|
|
10475
|
+
const prog = flag(args, "--progress");
|
|
10476
|
+
const t = add(title, {
|
|
10477
|
+
role: flag(args, "--role"),
|
|
10478
|
+
judge: flag(args, "--judge"),
|
|
10479
|
+
owner: flag(args, "--owner"),
|
|
10480
|
+
deps: csv("--depends"),
|
|
10481
|
+
touches: csv("--touches"),
|
|
10482
|
+
prio: flag(args, "--prio"),
|
|
10483
|
+
start: flag(args, "--start"),
|
|
10484
|
+
due: flag(args, "--due"),
|
|
10485
|
+
progress: prog === undefined ? undefined : normProgress(prog)
|
|
10486
|
+
});
|
|
8939
10487
|
out(asJson ? JSON.stringify(t) : `${t.id} ${t.title}`);
|
|
8940
10488
|
return 0;
|
|
8941
10489
|
}
|
|
@@ -8955,6 +10503,45 @@ async function taskCli(argv) {
|
|
|
8955
10503
|
out(asJson ? JSON.stringify(t) : `${t.id} deps=${t.deps.join(",") || "-"} touches=${t.touches.join(",") || "-"}`);
|
|
8956
10504
|
return 0;
|
|
8957
10505
|
}
|
|
10506
|
+
if (sub === "plan") {
|
|
10507
|
+
const id = positional(args)[0];
|
|
10508
|
+
if (!id) {
|
|
10509
|
+
err("usage: dia task plan <id> [--prio P1] [--start YYYY-MM-DD] [--due YYYY-MM-DD] [--progress 0-100]");
|
|
10510
|
+
return 2;
|
|
10511
|
+
}
|
|
10512
|
+
const prog = flag(args, "--progress");
|
|
10513
|
+
const t = plan(id, {
|
|
10514
|
+
prio: flag(args, "--prio"),
|
|
10515
|
+
start: flag(args, "--start"),
|
|
10516
|
+
due: flag(args, "--due"),
|
|
10517
|
+
progress: prog === undefined ? undefined : normProgress(prog)
|
|
10518
|
+
});
|
|
10519
|
+
out(asJson ? JSON.stringify(t) : `${t.id} prio=${t.prio || "-"} start=${t.start || "-"} due=${t.due || "-"} progress=${t.progress ?? "-"}`);
|
|
10520
|
+
return 0;
|
|
10521
|
+
}
|
|
10522
|
+
if (sub === "split") {
|
|
10523
|
+
const id = positional(args)[0];
|
|
10524
|
+
const into = flag(args, "--into");
|
|
10525
|
+
if (!id || !into) {
|
|
10526
|
+
err('usage: dia task split <id> --into "切片A,切片B" [--slice 切片A] [--force]');
|
|
10527
|
+
return 2;
|
|
10528
|
+
}
|
|
10529
|
+
const parent = split(id, into.split(","), {
|
|
10530
|
+
slice: flag(args, "--slice"),
|
|
10531
|
+
role: flag(args, "--role"),
|
|
10532
|
+
judge: flag(args, "--judge"),
|
|
10533
|
+
touches: csv("--touches"),
|
|
10534
|
+
force: has(args, "--force")
|
|
10535
|
+
});
|
|
10536
|
+
const kids = (parent.split_children || []).map((cid) => {
|
|
10537
|
+
const c = get2(cid);
|
|
10538
|
+
return `${c.id} ${c.title}${c.slice ? "\t[slice]" : ""}`;
|
|
10539
|
+
});
|
|
10540
|
+
out(asJson ? JSON.stringify(parent) : `${parent.id} children=${(parent.split_children || []).length}
|
|
10541
|
+
${kids.join(`
|
|
10542
|
+
`)}`);
|
|
10543
|
+
return 0;
|
|
10544
|
+
}
|
|
8958
10545
|
if (sub === "status") {
|
|
8959
10546
|
const id = positional(args)[0];
|
|
8960
10547
|
const st = positional(args)[1];
|
|
@@ -8980,11 +10567,118 @@ async function taskCli(argv) {
|
|
|
8980
10567
|
err(e?.message || String(e));
|
|
8981
10568
|
return 30;
|
|
8982
10569
|
}
|
|
8983
|
-
err("usage: dia task add|list|link|status|done");
|
|
10570
|
+
err("usage: dia task add|list|link|plan|split|status|done");
|
|
8984
10571
|
return 2;
|
|
8985
10572
|
}
|
|
8986
10573
|
async function graphCli(argv) {
|
|
8987
10574
|
const asJson = has(argv, "--json");
|
|
10575
|
+
if (has(argv, "--next") || has(argv, "--explain") || has(argv, "--lint")) {
|
|
10576
|
+
const tasks = list4();
|
|
10577
|
+
const dr = derive(tasks);
|
|
10578
|
+
if (has(argv, "--explain")) {
|
|
10579
|
+
const id = flag(argv, "--explain");
|
|
10580
|
+
if (!id || id.startsWith("--")) {
|
|
10581
|
+
err("usage: dia graph --explain <id>");
|
|
10582
|
+
return 2;
|
|
10583
|
+
}
|
|
10584
|
+
const t = tasks.find((x) => x.id === id);
|
|
10585
|
+
const d = dr.byId.get(id);
|
|
10586
|
+
if (!t || !d) {
|
|
10587
|
+
err(`E_GRAPH: 任务不存在 ${id}`);
|
|
10588
|
+
return 2;
|
|
10589
|
+
}
|
|
10590
|
+
const info = {
|
|
10591
|
+
id,
|
|
10592
|
+
title: t.title,
|
|
10593
|
+
status: t.status,
|
|
10594
|
+
declared: t.prio ?? null,
|
|
10595
|
+
eff: d.eff_prio,
|
|
10596
|
+
inherited: d.prio_inherited,
|
|
10597
|
+
source: d.prio_source ?? null,
|
|
10598
|
+
ready: d.ready,
|
|
10599
|
+
startable: d.startable,
|
|
10600
|
+
rank: d.rank,
|
|
10601
|
+
blocked_by: d.blocked_by,
|
|
10602
|
+
released_by: d.released_by,
|
|
10603
|
+
dangling: d.dangling_deps,
|
|
10604
|
+
unblocks: d.unblocks,
|
|
10605
|
+
critical: d.critical,
|
|
10606
|
+
slice: !!t.slice
|
|
10607
|
+
};
|
|
10608
|
+
out(asJson ? JSON.stringify(info) : [
|
|
10609
|
+
`${id} ${t.title}`,
|
|
10610
|
+
`status=${t.status} declared=${info.declared ?? "-"} eff=${info.eff}${d.prio_inherited ? ` (继承自 ${d.prio_source?.id} ${d.prio_source?.declared}, ${d.prio_source?.distance} 跳)` : ""}`,
|
|
10611
|
+
`ready=${d.ready} startable=${d.startable} rank=${d.rank ?? "-"} unblocks=${d.unblocks} critical=${d.critical}`,
|
|
10612
|
+
`blocked_by=${d.blocked_by.join(",") || "-"} released_by=${d.released_by.join(",") || "-"} dangling=${d.dangling_deps.join(",") || "-"}`
|
|
10613
|
+
].join(`
|
|
10614
|
+
`));
|
|
10615
|
+
return 0;
|
|
10616
|
+
}
|
|
10617
|
+
if (has(argv, "--lint")) {
|
|
10618
|
+
const findings = dr.lint;
|
|
10619
|
+
out(asJson ? JSON.stringify({ findings, cycles: dr.cycles }) : findings.length ? findings.map((f) => `${f.kind} ${f.id} ${f.detail}`).join(`
|
|
10620
|
+
`) : "(clean)");
|
|
10621
|
+
return dr.cycles.length ? 3 : 0;
|
|
10622
|
+
}
|
|
10623
|
+
const raw = flag(argv, "--next");
|
|
10624
|
+
const n = raw && /^\d+$/.test(raw) ? Math.max(1, Number(raw)) : 5;
|
|
10625
|
+
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);
|
|
10626
|
+
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);
|
|
10627
|
+
const picked = [...doing, ...startable].slice(0, n);
|
|
10628
|
+
const items = picked.map((t) => {
|
|
10629
|
+
const d = dr.byId.get(t.id);
|
|
10630
|
+
return {
|
|
10631
|
+
id: t.id,
|
|
10632
|
+
title: t.title,
|
|
10633
|
+
status: t.status,
|
|
10634
|
+
rank: d.rank,
|
|
10635
|
+
declared: t.prio ?? null,
|
|
10636
|
+
eff: d.eff_prio,
|
|
10637
|
+
inherited: d.prio_inherited,
|
|
10638
|
+
ready: d.ready,
|
|
10639
|
+
critical: d.critical,
|
|
10640
|
+
unblocks: d.unblocks,
|
|
10641
|
+
blocked_by: d.blocked_by,
|
|
10642
|
+
why: `${t.status === "doing" ? "进行中" : "可开工"} · eff=${d.eff_prio}${d.prio_inherited ? `(←${d.prio_source?.id})` : ""} · 解锁${d.unblocks}${d.critical ? " · 关键路径" : ""}`
|
|
10643
|
+
};
|
|
10644
|
+
});
|
|
10645
|
+
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(`
|
|
10646
|
+
`) : "(没有可开工任务)");
|
|
10647
|
+
return dr.cycles.length ? 3 : 0;
|
|
10648
|
+
}
|
|
10649
|
+
if (has(argv, "--export-html") || has(argv, "--out")) {
|
|
10650
|
+
const nowRaw = flag(argv, "--now");
|
|
10651
|
+
const now = nowRaw ? new Date(nowRaw) : new Date;
|
|
10652
|
+
if (Number.isNaN(now.getTime())) {
|
|
10653
|
+
err(`E_GRAPH: --now 不是合法时间(收到 ${nowRaw})`);
|
|
10654
|
+
return 2;
|
|
10655
|
+
}
|
|
10656
|
+
const limitRaw = flag(argv, "--limit");
|
|
10657
|
+
const limit = has(argv, "--all") ? 0 : limitRaw === undefined ? 500 : Number(limitRaw);
|
|
10658
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
10659
|
+
err("E_GRAPH: --limit 必须是 >=0 的整数");
|
|
10660
|
+
return 2;
|
|
10661
|
+
}
|
|
10662
|
+
const stamp = `${localDay(now).replace(/-/g, "")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
10663
|
+
const argPath = flag(argv, "--export-html");
|
|
10664
|
+
const positionalOut = argPath && !argPath.startsWith("-") ? argPath : "";
|
|
10665
|
+
const outPath = flag(argv, "--out") || positionalOut || join32(diaHome(), "export", `graph-${stamp}.html`);
|
|
10666
|
+
if (existsSync30(outPath) && !has(argv, "--force")) {
|
|
10667
|
+
err(`E_GRAPH: 已存在 ${outPath}(用 --force 覆盖)`);
|
|
10668
|
+
return 2;
|
|
10669
|
+
}
|
|
10670
|
+
try {
|
|
10671
|
+
const g = build({ task: flag(argv, "--task") });
|
|
10672
|
+
const html = renderHtml(g, { now, redact: has(argv, "--redact"), limit, claims: list3().length });
|
|
10673
|
+
mkdirSync20(join32(outPath, ".."), { recursive: true });
|
|
10674
|
+
writeFileSync18(outPath, html, { mode: 384 });
|
|
10675
|
+
out(outPath);
|
|
10676
|
+
return 0;
|
|
10677
|
+
} catch (e) {
|
|
10678
|
+
err(e?.message || String(e));
|
|
10679
|
+
return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
|
|
10680
|
+
}
|
|
10681
|
+
}
|
|
8988
10682
|
if (has(argv, "--tui") || has(argv, "--watch")) {
|
|
8989
10683
|
const width = Number(flag(argv, "--width") || 0);
|
|
8990
10684
|
const tuiOpts = {
|
|
@@ -9089,7 +10783,7 @@ function spawnDetachedDelivery(args) {
|
|
|
9089
10783
|
const dir = join32(root, agent);
|
|
9090
10784
|
mkdirSync20(dir, { recursive: true });
|
|
9091
10785
|
const log = join32(dir, `deliver-${id}.log`);
|
|
9092
|
-
const fd =
|
|
10786
|
+
const fd = openSync5(log, "a");
|
|
9093
10787
|
const bin = process.argv[1];
|
|
9094
10788
|
const child = spawn3(process.execPath, [bin, "msg", "deliver", ...args, "--wait"], {
|
|
9095
10789
|
detached: true,
|
|
@@ -9454,6 +11148,8 @@ async function collabCli(argv) {
|
|
|
9454
11148
|
}
|
|
9455
11149
|
const kitInfo = sub === "enable" ? ensure(personaId, { force: true }) : null;
|
|
9456
11150
|
out(asJson ? JSON.stringify({ ...c, removed, kit: kitInfo }, null, 1) : `${sub}d: enabled=${c.enabled}${c.disabled_personas.length ? ` disabled_personas=${c.disabled_personas.join(",")}` : ""}` + (removed.length ? ` removed=${removed.length}` : "") + (kitInfo ? kitInfo.ok ? ` kit=${kitInfo.kit}` : ` ⚠ ${kitInfo.reason}` : ""));
|
|
11151
|
+
if (kitInfo && !kitInfo.ok)
|
|
11152
|
+
return 32;
|
|
9457
11153
|
return 0;
|
|
9458
11154
|
}
|
|
9459
11155
|
if (sub === "whoami") {
|
|
@@ -9935,7 +11631,7 @@ async function kitCli(argv) {
|
|
|
9935
11631
|
}
|
|
9936
11632
|
if (sub === "list") {
|
|
9937
11633
|
const rows = list();
|
|
9938
|
-
out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}`).join(`
|
|
11634
|
+
out(asJson ? JSON.stringify(rows, null, 1) : rows.map((r) => `${r.id}: ${r.versions.join(", ")}${r.tags?.length ? ` [${r.tags.join(", ")}]` : ""}`).join(`
|
|
9939
11635
|
`) || "(none)");
|
|
9940
11636
|
return 0;
|
|
9941
11637
|
}
|
|
@@ -9980,7 +11676,7 @@ function wtCli(argv) {
|
|
|
9980
11676
|
home,
|
|
9981
11677
|
resolvePath(fileURLToPath4(new URL("../", import.meta.url)), "packages/wt/bin/dia-wt")
|
|
9982
11678
|
].filter(Boolean);
|
|
9983
|
-
const bin = candidates.find((c) =>
|
|
11679
|
+
const bin = candidates.find((c) => existsSync30(c));
|
|
9984
11680
|
if (!bin) {
|
|
9985
11681
|
err("E_WT: dia-wt 未安装(先跑 dia install.sh,或设 DIA_WT_BIN)");
|
|
9986
11682
|
return 33;
|