@minonl/dia 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +193 -6
- package/docs/msg-collab-m1.md +14 -0
- package/docs/service-noun.md +76 -0
- package/package.json +1 -1
- package/packages/wt/bin/dia-wt +158 -101
- package/packages/wt/tests/adopt.test.sh +67 -143
package/dist/cli.js
CHANGED
|
@@ -4720,6 +4720,67 @@ function build(opts = {}) {
|
|
|
4720
4720
|
return { schema: 1, nodes: filteredNodes, edges: filteredEdges, cycles: findCycles(tasks) };
|
|
4721
4721
|
}
|
|
4722
4722
|
|
|
4723
|
+
// src/graph-tui.ts
|
|
4724
|
+
var ORDER = ["doing", "blocked", "review", "todo", "awaiting_acceptance", "accepted", "done", "dropped", "rejected", "escalated"];
|
|
4725
|
+
var color = (s, code, on) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
4726
|
+
function render2(g, opts = {}) {
|
|
4727
|
+
const on = opts.color ?? false;
|
|
4728
|
+
const tasks = g.nodes.filter((n) => n.kind === "task");
|
|
4729
|
+
const taskById = new Map(tasks.map((t) => [t.id, t]));
|
|
4730
|
+
const edgesFrom = (id, kind) => g.edges.filter((e) => e.from === id && e.kind === kind);
|
|
4731
|
+
const lines = [];
|
|
4732
|
+
const groups = new Map;
|
|
4733
|
+
for (const t of tasks) {
|
|
4734
|
+
const st = t.status || "todo";
|
|
4735
|
+
groups.set(st, [...groups.get(st) || [], t]);
|
|
4736
|
+
}
|
|
4737
|
+
for (const st of [...ORDER.filter((s) => groups.has(s)), ...[...groups.keys()].filter((s) => !ORDER.includes(s))]) {
|
|
4738
|
+
lines.push(color(`[${st}]`, "1;36", on));
|
|
4739
|
+
for (const t of groups.get(st).sort((a, b) => a.label.localeCompare(b.label))) {
|
|
4740
|
+
lines.push(` ${color(t.label, "1", on)} ${color(t.id.replace(/^task:/, ""), "2", on)}${t.owner ? ` owner=${t.owner}` : ""}`);
|
|
4741
|
+
for (const e of edgesFrom(t.id, "depends_on")) {
|
|
4742
|
+
const dep = taskById.get(e.to);
|
|
4743
|
+
lines.push(` ${color("↳ depends", "34", on)} ${dep?.label || e.to}${dep ? ` [${dep.status}]` : ""}`);
|
|
4744
|
+
}
|
|
4745
|
+
for (const e of edgesFrom(t.id, "touches"))
|
|
4746
|
+
lines.push(` ${color("↳ touches", "35", on)} ${e.to.replace(/^path:/, "")}`);
|
|
4747
|
+
if (t.blocked_by?.length)
|
|
4748
|
+
lines.push(` ${color(`⚠ blocked_by ${t.blocked_by.join(", ")}`, "31", on)}`);
|
|
4749
|
+
}
|
|
4750
|
+
lines.push("");
|
|
4751
|
+
}
|
|
4752
|
+
if (!tasks.length)
|
|
4753
|
+
lines.push("(no tasks)");
|
|
4754
|
+
const claimEdges = g.edges.filter((e) => e.kind === "claims");
|
|
4755
|
+
if (claimEdges.length) {
|
|
4756
|
+
lines.push(color("claims", "1;36", on));
|
|
4757
|
+
for (const e of claimEdges)
|
|
4758
|
+
lines.push(` ${e.from.replace(/^session:/, "")} → ${e.to.replace(/^path:/, "")}`);
|
|
4759
|
+
lines.push("");
|
|
4760
|
+
}
|
|
4761
|
+
for (const c of g.cycles)
|
|
4762
|
+
lines.push(color(`⚠ cycle: ${c.join(" -> ")}`, "31", on));
|
|
4763
|
+
return lines.join(`
|
|
4764
|
+
`);
|
|
4765
|
+
}
|
|
4766
|
+
function watch(build, opts = {}) {
|
|
4767
|
+
const paint = () => process.stdout.write("\x1B[2J\x1B[H" + render2(build(), { color: opts.color ?? process.stdout.isTTY }) + `
|
|
4768
|
+
`);
|
|
4769
|
+
paint();
|
|
4770
|
+
const timer = setInterval(() => {
|
|
4771
|
+
try {
|
|
4772
|
+
paint();
|
|
4773
|
+
} catch {}
|
|
4774
|
+
}, opts.intervalMs ?? 2000);
|
|
4775
|
+
const stop = () => {
|
|
4776
|
+
clearInterval(timer);
|
|
4777
|
+
process.exit(0);
|
|
4778
|
+
};
|
|
4779
|
+
process.on("SIGINT", stop);
|
|
4780
|
+
process.on("SIGTERM", stop);
|
|
4781
|
+
return new Promise(() => {});
|
|
4782
|
+
}
|
|
4783
|
+
|
|
4723
4784
|
// packages/cli/src/index.ts
|
|
4724
4785
|
init_paths();
|
|
4725
4786
|
|
|
@@ -5491,6 +5552,25 @@ function jobRootResolved() {
|
|
|
5491
5552
|
const legacy = join24(homedir11(), ".local", "share", "dia-job");
|
|
5492
5553
|
return !existsSync21(next) && existsSync21(legacy) ? legacy : next;
|
|
5493
5554
|
}
|
|
5555
|
+
function runJobCapture(argv) {
|
|
5556
|
+
const { bin, error } = jobBin();
|
|
5557
|
+
if (!bin) {
|
|
5558
|
+
process.stderr.write(`dia job: ${error || "未找到 dia-job;先跑 packages/job/install.sh 或设置 DIA_JOB_BIN"}
|
|
5559
|
+
`);
|
|
5560
|
+
return { status: 2, stdout: "", stderr: "" };
|
|
5561
|
+
}
|
|
5562
|
+
const r = spawnSync2(bin, argv, {
|
|
5563
|
+
encoding: "utf8",
|
|
5564
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
5565
|
+
env: { ...process.env, DIA_JOB_ROOT: jobRootResolved() }
|
|
5566
|
+
});
|
|
5567
|
+
if (r.error) {
|
|
5568
|
+
process.stderr.write(`dia job: ${r.error.message}
|
|
5569
|
+
`);
|
|
5570
|
+
return { status: 1, stdout: "", stderr: "" };
|
|
5571
|
+
}
|
|
5572
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
5573
|
+
}
|
|
5494
5574
|
function runJob(argv) {
|
|
5495
5575
|
const { bin, error } = jobBin();
|
|
5496
5576
|
if (!bin) {
|
|
@@ -7237,7 +7317,18 @@ var STALE_FACTOR = 1.5;
|
|
|
7237
7317
|
var MIN_HEALTH_INTERVAL = 60;
|
|
7238
7318
|
var healthInterval = (hc) => hc?.interval ?? DEFAULT_HEALTH_INTERVAL;
|
|
7239
7319
|
var staleAfter = (hc) => Math.floor(healthInterval(hc) * STALE_FACTOR);
|
|
7240
|
-
var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" &&
|
|
7320
|
+
var isRecord = (v) => !!v && typeof v === "object" && v.kind === "health" && (v.state === "ok" || v.state === "degraded" || v.state === "fail");
|
|
7321
|
+
var TS_RE = /"ts"\s*:\s*(-?\d+)/;
|
|
7322
|
+
var KIND_RE = /"kind"\s*:\s*"([A-Za-z0-9_-]+)"/;
|
|
7323
|
+
function parseStateLine(line) {
|
|
7324
|
+
const head = line.slice(0, 256), tail = line.slice(-256);
|
|
7325
|
+
const ts = TS_RE.exec(head)?.[1] ?? TS_RE.exec(tail)?.[1];
|
|
7326
|
+
const kind = KIND_RE.exec(head)?.[1] ?? KIND_RE.exec(tail)?.[1];
|
|
7327
|
+
if (!ts || !kind)
|
|
7328
|
+
return null;
|
|
7329
|
+
const n = Number(ts);
|
|
7330
|
+
return Number.isFinite(n) ? { ts: n, kind } : null;
|
|
7331
|
+
}
|
|
7241
7332
|
function findLastHealth(lines, service, gen) {
|
|
7242
7333
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
7243
7334
|
const r = lines[i];
|
|
@@ -7422,6 +7513,24 @@ function diaCmd() {
|
|
|
7422
7513
|
const entry = entries.find((p) => existsSync28(p));
|
|
7423
7514
|
return entry ? `${q(process.execPath)} ${q(entry)}` : "dia";
|
|
7424
7515
|
}
|
|
7516
|
+
function decorateStatus(text, view) {
|
|
7517
|
+
const lines = String(text ?? "").split(`
|
|
7518
|
+
`);
|
|
7519
|
+
const outLines = [];
|
|
7520
|
+
for (const line of lines) {
|
|
7521
|
+
outLines.push(line);
|
|
7522
|
+
const m = line.match(/local\.dia-job-svc-([A-Za-z0-9._-]+)/) || line.match(/(?:^|\s)svc-([A-Za-z0-9._-]+)\s/);
|
|
7523
|
+
if (!m)
|
|
7524
|
+
continue;
|
|
7525
|
+
try {
|
|
7526
|
+
const v = view(m[1]);
|
|
7527
|
+
if (v?.label)
|
|
7528
|
+
outLines.push(` health=${v.label}`);
|
|
7529
|
+
} catch {}
|
|
7530
|
+
}
|
|
7531
|
+
return outLines.join(`
|
|
7532
|
+
`);
|
|
7533
|
+
}
|
|
7425
7534
|
function servicesRoot() {
|
|
7426
7535
|
return process.env.DIA_SERVICE_HOME || join31(diaHome(), "services");
|
|
7427
7536
|
}
|
|
@@ -7456,15 +7565,34 @@ function readHealthLines(file) {
|
|
|
7456
7565
|
if (!existsSync28(file))
|
|
7457
7566
|
return [];
|
|
7458
7567
|
const rows = [];
|
|
7459
|
-
|
|
7460
|
-
`)
|
|
7568
|
+
const lines = readFileSync25(file, "utf8").split(`
|
|
7569
|
+
`);
|
|
7570
|
+
for (const line of lines) {
|
|
7461
7571
|
if (!line.trim())
|
|
7462
7572
|
continue;
|
|
7463
7573
|
try {
|
|
7464
7574
|
const d = JSON.parse(line);
|
|
7465
7575
|
if (isRecord(d))
|
|
7466
7576
|
rows.push(d);
|
|
7577
|
+
continue;
|
|
7467
7578
|
} catch {}
|
|
7579
|
+
const head = parseStateLine(line);
|
|
7580
|
+
const state = head ? /"state"\s*:\s*"(ok|degraded|fail)"/.exec(line)?.[1] : undefined;
|
|
7581
|
+
if (head && state) {
|
|
7582
|
+
rows.push({
|
|
7583
|
+
...head,
|
|
7584
|
+
state,
|
|
7585
|
+
service: "",
|
|
7586
|
+
gen: "",
|
|
7587
|
+
source: "schedule",
|
|
7588
|
+
candidate: state,
|
|
7589
|
+
streak: 0,
|
|
7590
|
+
raw: state,
|
|
7591
|
+
exit: null,
|
|
7592
|
+
timeout: false,
|
|
7593
|
+
detail: ""
|
|
7594
|
+
});
|
|
7595
|
+
}
|
|
7468
7596
|
}
|
|
7469
7597
|
return rows;
|
|
7470
7598
|
}
|
|
@@ -7473,6 +7601,41 @@ function appendHealthRecord(file, rec) {
|
|
|
7473
7601
|
appendFileSync9(file, JSON.stringify(rec) + `
|
|
7474
7602
|
`);
|
|
7475
7603
|
}
|
|
7604
|
+
function readServiceHealth(id, opts = {}) {
|
|
7605
|
+
const { healthFile } = opts;
|
|
7606
|
+
const now = opts.now ?? Math.floor(Date.now() / 1000);
|
|
7607
|
+
const jr = opts.jobRoot ?? jobRoot2();
|
|
7608
|
+
const dir = opts.servicesRoot ?? (process.env.DIA_SERVICE_HOME || join31(jr, "services"));
|
|
7609
|
+
const installDir = join31(dir, id);
|
|
7610
|
+
let lock = null;
|
|
7611
|
+
try {
|
|
7612
|
+
lock = readLockFile(id);
|
|
7613
|
+
} catch {}
|
|
7614
|
+
let svc = null;
|
|
7615
|
+
try {
|
|
7616
|
+
svc = readServiceManifest(lock?.dir ?? installDir);
|
|
7617
|
+
} catch {}
|
|
7618
|
+
if (!svc)
|
|
7619
|
+
return healthView({ adopted: true, hasHealthcheck: false, now });
|
|
7620
|
+
const hc = svc?.healthcheck;
|
|
7621
|
+
if (!hc)
|
|
7622
|
+
return healthView({ hasHealthcheck: false, now });
|
|
7623
|
+
const ledger = healthFile ?? healthStatePath(id);
|
|
7624
|
+
const recs = readHealthLines(ledger).filter(isRecord);
|
|
7625
|
+
let rec = lock ? findLastHealth(recs, id, lock.gen) : null;
|
|
7626
|
+
if (!rec) {
|
|
7627
|
+
for (let i = recs.length - 1;i >= 0; i--) {
|
|
7628
|
+
const r = recs[i];
|
|
7629
|
+
if (r.state !== "ok" && r.state !== "degraded" && r.state !== "fail")
|
|
7630
|
+
continue;
|
|
7631
|
+
if (r.service && r.service !== id)
|
|
7632
|
+
continue;
|
|
7633
|
+
if (!rec || r.ts > rec.ts)
|
|
7634
|
+
rec = r;
|
|
7635
|
+
}
|
|
7636
|
+
}
|
|
7637
|
+
return healthView({ record: rec, hasHealthcheck: true, hc, now });
|
|
7638
|
+
}
|
|
7476
7639
|
function listServices(opts = {}) {
|
|
7477
7640
|
const root = opts.servicesRoot ?? servicesRoot();
|
|
7478
7641
|
const jr = opts.jobRoot ?? jobRoot2();
|
|
@@ -8165,7 +8328,7 @@ usage:
|
|
|
8165
8328
|
dia agents list|register|unregister|discover
|
|
8166
8329
|
dia msg deliver <agent> <id> [--transport tmux|acp]
|
|
8167
8330
|
dia task add|list|link|status|done [--owner S] [--depends a,b] [--touches p1,p2]
|
|
8168
|
-
dia graph [--json] [--task t-...]
|
|
8331
|
+
dia graph [--json] [--task t-...] [--tui [--watch] [--no-color]]
|
|
8169
8332
|
dia claim <path> [--reason R] [--ttl 4h] [--session S] | dia claims [--all] [--mine] | dia unclaim <path|id> [--force]
|
|
8170
8333
|
dia team run <id> --mock | --acp|--dsh --workspace DIR
|
|
8171
8334
|
dia team gate|accept|reject|next
|
|
@@ -8558,6 +8721,19 @@ async function taskCli(argv) {
|
|
|
8558
8721
|
}
|
|
8559
8722
|
async function graphCli(argv) {
|
|
8560
8723
|
const asJson = has(argv, "--json");
|
|
8724
|
+
if (has(argv, "--tui") || has(argv, "--watch")) {
|
|
8725
|
+
const paint = (g) => out(render2(g, { color: !!process.stdout.isTTY && !has(argv, "--no-color") }));
|
|
8726
|
+
try {
|
|
8727
|
+
if (has(argv, "--watch"))
|
|
8728
|
+
return await watch(() => build({ task: flag(argv, "--task") }), { color: !!process.stdout.isTTY && !has(argv, "--no-color") });
|
|
8729
|
+
const g = build({ task: flag(argv, "--task") });
|
|
8730
|
+
paint(g);
|
|
8731
|
+
return g.cycles.length ? 3 : 0;
|
|
8732
|
+
} catch (e) {
|
|
8733
|
+
err(e?.message || String(e));
|
|
8734
|
+
return String(e?.message || e).startsWith("E_GRAPH") ? 2 : 31;
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8561
8737
|
try {
|
|
8562
8738
|
const g = build({ task: flag(argv, "--task") });
|
|
8563
8739
|
if (asJson)
|
|
@@ -10086,8 +10262,19 @@ async function main(rawArgv) {
|
|
|
10086
10262
|
return agentsCli(argv.slice(1));
|
|
10087
10263
|
if (cmd === "team")
|
|
10088
10264
|
return teamCli(argv.slice(1));
|
|
10089
|
-
if (cmd === "job")
|
|
10090
|
-
|
|
10265
|
+
if (cmd === "job") {
|
|
10266
|
+
const rest = argv.slice(1);
|
|
10267
|
+
if (rest[0] === "status" || rest[0] === "list") {
|
|
10268
|
+
const r = runJobCapture(rest);
|
|
10269
|
+
if (r.stderr)
|
|
10270
|
+
process.stderr.write(r.stderr);
|
|
10271
|
+
const text = decorateStatus(r.stdout, (id) => readServiceHealth(id));
|
|
10272
|
+
if (text)
|
|
10273
|
+
out(text.replace(/\n$/, ""));
|
|
10274
|
+
return r.status;
|
|
10275
|
+
}
|
|
10276
|
+
return runJob(rest);
|
|
10277
|
+
}
|
|
10091
10278
|
if (cmd === "price") {
|
|
10092
10279
|
const table = loadPrices();
|
|
10093
10280
|
if (has(argv.slice(1), "--json"))
|
package/docs/msg-collab-m1.md
CHANGED
|
@@ -64,3 +64,17 @@ dia graph --json --task t-xxx
|
|
|
64
64
|
- `blocked_by` 只在 task 节点里做派生提示,不是边;
|
|
65
65
|
- 任务依赖成环:`cycles` 非空,退出码 **3**(参数错仍是 2);
|
|
66
66
|
- `--task` 只输出该任务的直接依赖/touches/claim 邻居,不做全图闭包。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# 只读 TUI 图(M2 第一步)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dia graph --tui # 打印一次(有环 exit 3)
|
|
74
|
+
dia graph --watch # 每 2s 刷新,Ctrl-C 退出
|
|
75
|
+
dia graph --tui --no-color # 关 ANSI 颜色
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- 按状态分组显示 task → deps / touches / blocked_by;claims 单列;
|
|
79
|
+
- 不启 HTTP/daemon,遵守框架“无网络模块”约束;
|
|
80
|
+
- `--task t-...` 只显示该任务的直接邻居。
|
package/docs/service-noun.md
CHANGED
|
@@ -352,3 +352,79 @@ unknown(stale)**不单独返回**——`health` 是实时执行,不存在
|
|
|
352
352
|
|
|
353
353
|
1. 判据写成**可判定的检查**:`node scripts/docs-index.mjs --check && node scripts/docs-link-check.mjs`(+ 可选 checklist 计数);
|
|
354
354
|
2. 或者把它当**评审工具**用:接受 `escalated` 是预期结果,只取 `verify.json` 的 findings 作为 grill 产出。
|
|
355
|
+
|
|
356
|
+
## 14. P2:agent 型服务(entry = persona 会话)
|
|
357
|
+
|
|
358
|
+
> 状态:**草案(待实现)**。§0 已定「本版只做脚本型」;本节把 P2 的形状先定下来,避免实现时二次发明语义。
|
|
359
|
+
|
|
360
|
+
### 14.1 与脚本型的关系
|
|
361
|
+
|
|
362
|
+
```mermaid
|
|
363
|
+
flowchart LR
|
|
364
|
+
S["service"] --> A["脚本型(已实现)<br/>entry = bin/*"]
|
|
365
|
+
S --> B["agent 型(P2)<br/>entry = ACP 会话"]
|
|
366
|
+
B --> J["job:把 ACP 端点拉起并保持<br/>(dsh --profile dia-acp / opencode acp)"]
|
|
367
|
+
B --> H["healthcheck:端点可应答 + 最近一轮成功<br/>(不判断“想得对不对”)"]
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**不变的**:`lifecycle`(job 托管)、`healthcheck` 契约(专用码域)、台账(`kind:"health"`)、`gen` 代际。
|
|
371
|
+
**不同的**:没有 `stdin` 事件契约 —— 输入是 ACP 的 `prompt`,输出是会话事件;所以 `contract` 换成 ACP 语义。
|
|
372
|
+
|
|
373
|
+
### 14.2 清单
|
|
374
|
+
|
|
375
|
+
```jsonc
|
|
376
|
+
{
|
|
377
|
+
"kind": "service",
|
|
378
|
+
"id": "example-agent",
|
|
379
|
+
"entry": {
|
|
380
|
+
"agent": {
|
|
381
|
+
"host": "dsh", // dsh | opencode
|
|
382
|
+
"profile": "dia-acp", // 由 persona/宿主提供;缺省用宿主默认
|
|
383
|
+
"persona": "yan" // 可选:仅做归属校验,不注入内容
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
"lifecycle": { "type": "service", "target": "local" },
|
|
387
|
+
"healthcheck": { "kind": "acp", "interval": 1800, "timeout": 60 },
|
|
388
|
+
"kits": []
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
| 字段 | 语义 |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `entry.agent.host` | 用哪套 ACP 实现(`dsh --profile <p>` 或 `opencode acp`) |
|
|
395
|
+
| `entry.agent.profile` | 宿主的 profile 名(不复制 persona 内容,只引用) |
|
|
396
|
+
| `entry.agent.persona` | 可选;`verify` 时校验「该 profile 是否属于此 persona」,防止挂错人 |
|
|
397
|
+
| `healthcheck.kind: "acp"` | 走 ACP 健康检查(见 §14.3),**不再要求 `cmd`** |
|
|
398
|
+
|
|
399
|
+
### 14.3 健康检查(ACP 版)
|
|
400
|
+
|
|
401
|
+
| 检查 | 判据 | 失败归类 |
|
|
402
|
+
|---|---|---|
|
|
403
|
+
| 端点存活 | `initialize` 能返回 `protocolVersion`(JSON-RPC 1.0) | 连不上/超时 → `degraded` |
|
|
404
|
+
| 会话可用 | `session/new` 成功返回 `sessionId` | 返回错误 → `fail` |
|
|
405
|
+
| 最近一轮 | 台账里最近一次 `prompt` 的成功/失败(由调用方写事件) | 连续失败 `debounce` 次 → `degraded` |
|
|
406
|
+
|
|
407
|
+
**明确不做**:不判断「回答质量」、不重放 prompt、不在 healthcheck 里跑真实业务(成本 + 副作用)。
|
|
408
|
+
|
|
409
|
+
### 14.4 与脚本型的复用
|
|
410
|
+
|
|
411
|
+
- `healthView` / `transition` / `gen` / 台账格式**完全复用**(`state.jsonl` + `kind:"health"`)
|
|
412
|
+
- 差异只在「怎么产生一次 judgement」:`runHealth()` 换成 `runAcpHealth()`(`initialize` + `session/new`)
|
|
413
|
+
- `dia service health <id>` 对 agent 型走 `runAcpHealth`,退出码沿用 §5
|
|
414
|
+
|
|
415
|
+
### 14.5 边界
|
|
416
|
+
|
|
417
|
+
| 场景 | 行为 |
|
|
418
|
+
|---|---|
|
|
419
|
+
| 宿主未安装 / profile 不存在 | `add` 期 `E_BACKEND`(不装半套) |
|
|
420
|
+
| 端点活着但 `session/new` 失败 | `fail`(配置/鉴权问题,不是 infra 抖动) |
|
|
421
|
+
| 长任务(一轮 prompt 很久) | **不算失败**;只有「端点不可应答」才算 |
|
|
422
|
+
| 多实例 | `lifecycle.target` 声明归属;ACP 端点同机只允许一个 |
|
|
423
|
+
| 权限/凭据 | 由宿主 profile 自己管;`dia` 不读取、不注入 |
|
|
424
|
+
|
|
425
|
+
### 14.6 验收(P2)
|
|
426
|
+
|
|
427
|
+
- [ ] 单测:清单校验(`entry.agent` 与 `entry.cmd` **互斥**,同时给 → `E_MANIFEST`)
|
|
428
|
+
- [ ] 单测:`runAcpHealth` 用 `tests/fixtures/fake-acp.mjs` 覆盖 `initialize` 失败 / `session/new` 失败 / 正常三态
|
|
429
|
+
- [ ] 单测:`healthcheck.kind` 缺省时按脚本型(向后兼容)
|
|
430
|
+
- [ ] e2e:装一个 agent 型 demo(fake ACP)→ `dia job status` 显示 `health=ok…`,杀掉端点后变 `degraded(stale)`
|
package/package.json
CHANGED
package/packages/wt/bin/dia-wt
CHANGED
|
@@ -1167,22 +1167,31 @@ wt_default_branch() {
|
|
|
1167
1167
|
printf '%s' "$b"
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
cat > "$hooksdir/$name" <<'HOOK'
|
|
1173
|
-
#!/usr/bin/env bash
|
|
1174
|
-
# dia-wt guard(managed by `dia wt adopt`;勿手改)
|
|
1170
|
+
wt_guard_body() {
|
|
1171
|
+
cat <<'HOOKBODY'
|
|
1175
1172
|
set -uo pipefail
|
|
1176
|
-
|
|
1173
|
+
# 钩子名:生成的钩子用 basename $0;`dia wt guard` 用环境变量指定
|
|
1174
|
+
name="${DIA_WT_HOOK_NAME:-$(basename "$0")}"
|
|
1177
1175
|
top="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
1178
1176
|
common="$(git rev-parse --git-common-dir 2>/dev/null || true)"
|
|
1179
|
-
case "$common" in /*) ;; *) common="$(cd "$top" && cd "$common" 2>/dev/null && pwd -P)" ;; esac
|
|
1177
|
+
case "$common" in /*) ;; *) common="$(cd "$top" 2>/dev/null && cd "$common" 2>/dev/null && pwd -P)" ;; esac
|
|
1178
|
+
[ -n "$common" ] || exit 0
|
|
1180
1179
|
gd="$(git rev-parse --git-dir 2>/dev/null || true)"
|
|
1181
|
-
case "$gd" in /*) ;; *) gd="$(cd "$top" && cd "$gd" 2>/dev/null && pwd -P)" ;; esac
|
|
1180
|
+
case "$gd" in /*) ;; *) gd="$(cd "$top" 2>/dev/null && cd "$gd" 2>/dev/null && pwd -P)" ;; esac
|
|
1182
1181
|
is_main=0; [ "$gd" = "$common" ] && is_main=1
|
|
1183
1182
|
branch="$(git symbolic-ref --short HEAD 2>/dev/null || true)"
|
|
1184
|
-
prot=" main master develop dev "
|
|
1185
1183
|
probe="${DIA_WT_GUARD_PROBE:-0}"
|
|
1184
|
+
|
|
1185
|
+
# 只对已 adopt 的仓生效;未 adopt → 纯透传(保持本机其它仓行为不变)
|
|
1186
|
+
cfg="$common/dia/config.json"
|
|
1187
|
+
adopted=0; [ -f "$cfg" ] && adopted=1
|
|
1188
|
+
def=""
|
|
1189
|
+
if [ "$adopted" = 1 ]; then
|
|
1190
|
+
def="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1191
|
+
fi
|
|
1192
|
+
prot=" main master develop dev $def "
|
|
1193
|
+
prot="${prot// / }"
|
|
1194
|
+
|
|
1186
1195
|
wt_guard_scan() {
|
|
1187
1196
|
local ls="$1" rs="$2" range="" base="" hits=""
|
|
1188
1197
|
if [ -n "$rs" ] && [ "$rs" != "0000000000000000000000000000000000000000" ] && git cat-file -e "$rs" 2>/dev/null; then
|
|
@@ -1197,72 +1206,81 @@ hits="$(git diff -U0 "$range" -- . 2>/dev/null | grep -E '^\+' | grep -vE '^\+\+
|
|
|
1197
1206
|
if [ -n "$hits" ]; then
|
|
1198
1207
|
echo "[dia-wt] 推送包含敏感内容(已阻断):" >&2
|
|
1199
1208
|
printf '%s\n' "$hits" >&2
|
|
1200
|
-
echo "[dia-wt] 误报请修扫描规则(hooks 由 dia wt adopt
|
|
1209
|
+
echo "[dia-wt] 误报请修扫描规则(hooks 由 dia wt adopt 安装),不提供关闭开关" >&2
|
|
1201
1210
|
return 1
|
|
1202
1211
|
fi
|
|
1203
1212
|
}
|
|
1204
1213
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
[ "$is_main" = 1 ] &&
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
esac
|
|
1242
|
-
|
|
1214
|
+
if [ "$probe" = 1 ]; then
|
|
1215
|
+
case "$name" in
|
|
1216
|
+
pre-commit) { [ "$adopted" = 1 ] && [ "$is_main" = 1 ] && [[ "$prot" == *" $branch "* ]]; } && exit 1 || exit 0 ;;
|
|
1217
|
+
pre-push) { [ "$adopted" = 1 ] && [ "$is_main" = 1 ]; } && exit 1 || exit 0 ;;
|
|
1218
|
+
*) exit 0 ;;
|
|
1219
|
+
esac
|
|
1220
|
+
fi
|
|
1221
|
+
|
|
1222
|
+
if [ "$adopted" = 1 ]; then
|
|
1223
|
+
case "$name" in
|
|
1224
|
+
pre-commit)
|
|
1225
|
+
if [ "$is_main" = 1 ] && [[ "$prot" == *" $branch "* ]]; then
|
|
1226
|
+
echo "[dia-wt] 主 checkout 禁止在 $branch 直接提交;请先:dia wt create <repo> <feature>" >&2
|
|
1227
|
+
exit 1
|
|
1228
|
+
fi ;;
|
|
1229
|
+
pre-push)
|
|
1230
|
+
if [ "$is_main" = 1 ]; then
|
|
1231
|
+
echo "[dia-wt] 主 checkout 禁止直接 push;请走 worktree 分支 + MR" >&2
|
|
1232
|
+
exit 1
|
|
1233
|
+
fi
|
|
1234
|
+
# 先把 stdin 全读进来:既要逐条判 ref / 扫敏感内容,也要原样喂给仓库自己的钩子
|
|
1235
|
+
push_lines=()
|
|
1236
|
+
while IFS= read -r line; do push_lines+=("$line"); done
|
|
1237
|
+
for line in "${push_lines[@]}"; do
|
|
1238
|
+
read -r _lr ls rr rs <<<"$line"
|
|
1239
|
+
case "${rr:-}" in
|
|
1240
|
+
refs/heads/main|refs/heads/master|refs/heads/develop|refs/heads/dev)
|
|
1241
|
+
echo "[dia-wt] 禁止直接 push ${rr#refs/heads/};请走 MR" >&2; exit 1 ;;
|
|
1242
|
+
esac
|
|
1243
|
+
[ -n "${ls:-}" ] || continue
|
|
1244
|
+
wt_guard_scan "$ls" "${rs:-}" || exit 1
|
|
1245
|
+
done ;;
|
|
1246
|
+
post-checkout)
|
|
1247
|
+
if [ "$is_main" = 1 ] && [[ "$prot" != *" $branch "* ]]; then
|
|
1248
|
+
echo "[dia-wt] 提示:主 checkout 当前在 $branch(建议回到默认分支)" >&2
|
|
1249
|
+
fi ;;
|
|
1250
|
+
esac
|
|
1251
|
+
fi
|
|
1252
|
+
|
|
1253
|
+
# 链式:仓库自己的钩子(simple-git-hooks 等管理器生成在 <common>/hooks)
|
|
1243
1254
|
orig="$common/hooks/$name"
|
|
1244
|
-
if [ -x "$orig" ] && [ "$orig" != "$0" ]; then
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
python3 - "$hooksdir/$name" "$chain" <<'PYCHAIN'
|
|
1249
|
-
import sys
|
|
1250
|
-
p, chain = sys.argv[1], sys.argv[2]
|
|
1251
|
-
s = open(p).read()
|
|
1252
|
-
s = s.replace('chain="${DIA_WT_CHAIN_DIR:-}"', 'chain="${DIA_WT_CHAIN_DIR:-%s}"' % chain.replace('\\', '\\\\').replace('"', '\\"'))
|
|
1253
|
-
open(p, "w").write(s)
|
|
1254
|
-
PYCHAIN
|
|
1255
|
-
fi
|
|
1256
|
-
if [ -n "$extra" ]; then
|
|
1257
|
-
python3 - "$hooksdir/$name" "$extra" <<'PYGT'
|
|
1258
|
-
import sys, re
|
|
1259
|
-
p, extra = sys.argv[1], sys.argv[2]
|
|
1260
|
-
s = open(p).read()
|
|
1261
|
-
s = re.sub(r'^prot=" main master develop dev "$', f'prot=" main master develop dev {extra} "', s, flags=re.M)
|
|
1262
|
-
open(p, "w").write(s)
|
|
1263
|
-
PYGT
|
|
1255
|
+
if [ -x "$orig" ] && [ "$orig" != "$0" ]; then
|
|
1256
|
+
if [ "$name" = "pre-push" ] && [ ${#push_lines[@]} -gt 0 ]; then
|
|
1257
|
+
printf '%s\n' "${push_lines[@]}" | "$orig" "$@"
|
|
1258
|
+
exit $?
|
|
1264
1259
|
fi
|
|
1265
|
-
|
|
1260
|
+
exec "$orig" "$@"
|
|
1261
|
+
fi
|
|
1262
|
+
exit 0
|
|
1263
|
+
HOOKBODY
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
wt_global_hooks_dir() { printf '%s/hooks' "$(dia_wt_data_root 2>/dev/null || echo "${DIA_ROOT:-$HOME/.dia}")"; }
|
|
1267
|
+
wt_write_global_hooks() {
|
|
1268
|
+
local dir; dir="$(wt_global_hooks_dir)"
|
|
1269
|
+
mkdir -p "$dir"
|
|
1270
|
+
local h
|
|
1271
|
+
for h in pre-commit pre-push post-checkout; do
|
|
1272
|
+
{ printf '#!/usr/bin/env bash\n# dia-wt guard(本机全局钩子;由 `dia wt adopt` 安装,勿手改)\n'; wt_guard_body; } > "$dir/$h"
|
|
1273
|
+
chmod +x "$dir/$h"
|
|
1274
|
+
done
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
cmd_guard() {
|
|
1278
|
+
local name="${1:-}"
|
|
1279
|
+
case "$name" in
|
|
1280
|
+
pre-commit|pre-push|post-checkout) ;;
|
|
1281
|
+
*) echo "usage: dia wt guard <pre-commit|pre-push|post-checkout>" >&2; return 2 ;;
|
|
1282
|
+
esac
|
|
1283
|
+
DIA_WT_HOOK_NAME="$name" eval "$(wt_guard_body)"
|
|
1266
1284
|
}
|
|
1267
1285
|
|
|
1268
1286
|
cmd_adopt() {
|
|
@@ -1305,19 +1323,18 @@ cmd_adopt() {
|
|
|
1305
1323
|
local common cfgdir cfg hooksdir prev
|
|
1306
1324
|
common="$(wt_common_dir "$repo")" || die "无法解析 git-common-dir"
|
|
1307
1325
|
cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1308
|
-
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1326
|
+
prev="$(git -C "$repo" config --local --get core.hooksPath 2>/dev/null || true)"
|
|
1309
1327
|
[ -n "$template" ] || template='{{parent}}/{{repo}}-worktree--{{feature}}'
|
|
1310
1328
|
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1311
1329
|
|
|
1312
1330
|
if [ "$mode" = "revert" ]; then
|
|
1313
1331
|
[ -f "$cfg" ] || die "未 adopt(缺 $cfg)"
|
|
1314
1332
|
local saved
|
|
1315
|
-
saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg")"
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1333
|
+
saved="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
1334
|
+
# 只还原 adopt 之前就存在的仓库级 hooksPath;当前若被别人改过则不动
|
|
1335
|
+
if [ -n "$saved" ] && [ "$prev" = "$hooksdir" ]; then
|
|
1336
|
+
git -C "$repo" config --local core.hooksPath "$saved"
|
|
1319
1337
|
fi
|
|
1320
|
-
if [ -n "$saved" ]; then git -C "$repo" config --local core.hooksPath "$saved"; else git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; fi
|
|
1321
1338
|
rm -rf "$cfgdir"
|
|
1322
1339
|
echo "reverted:$cfgdir"
|
|
1323
1340
|
return 0
|
|
@@ -1331,17 +1348,16 @@ cmd_adopt() {
|
|
|
1331
1348
|
local cfgDef; cfgDef="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
1332
1349
|
[ -n "$cfgDef" ] && def="$cfgDef"
|
|
1333
1350
|
fi
|
|
1334
|
-
|
|
1351
|
+
local ghd ghp
|
|
1352
|
+
ghd="$(wt_global_hooks_dir)"
|
|
1353
|
+
ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
|
|
1354
|
+
[ "$ghp" = "$ghd" ] || { echo "FAIL D02 全局 core.hooksPath 未指向 $ghd(当前:${ghp:-未设置})"; bad=1; }
|
|
1335
1355
|
for h in pre-commit pre-push post-checkout; do
|
|
1336
|
-
[ -x "$
|
|
1356
|
+
[ -x "$ghd/$h" ] || { echo "FAIL D02 缺全局钩子 $h"; bad=1; }
|
|
1337
1357
|
done
|
|
1338
|
-
if [ -n "$def" ] && [ -
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1342
|
-
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1343
|
-
&& { echo "FAIL D03 守卫探针未拦截主 checkout 的 $def 提交"; bad=1; }
|
|
1344
|
-
fi
|
|
1358
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1359
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$ghd/pre-commit" >/dev/null 2>&1 ) \
|
|
1360
|
+
&& { echo "FAIL D03 守卫探针未拦截 $def 提交"; bad=1; }
|
|
1345
1361
|
fi
|
|
1346
1362
|
if [ "$json" = 1 ]; then
|
|
1347
1363
|
python3 -c 'import json,sys;print(json.dumps({"repo":sys.argv[1],"ok":sys.argv[2]=="0","defaultBranch":sys.argv[3]},ensure_ascii=False,separators=(",",":")))' "$repo" "$bad" "$def"
|
|
@@ -1366,6 +1382,36 @@ cmd_adopt() {
|
|
|
1366
1382
|
|
|
1367
1383
|
[ -n "$def" ] || die "默认分支不可解析;请 --default-branch main"
|
|
1368
1384
|
mkdir -p "$hooksdir"
|
|
1385
|
+
# 安装/刷新本机全局钩子,并让本仓走全局钩子(不改 package.json、不占用仓库钩子目录)
|
|
1386
|
+
local ghd ghp
|
|
1387
|
+
ghd="$(wt_global_hooks_dir)"
|
|
1388
|
+
ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
|
|
1389
|
+
# 配置外溢防线:全局 hooksPath 若已被别人设置,绝不覆盖
|
|
1390
|
+
if [ -n "$ghp" ] && [ "$ghp" != "$ghd" ]; then
|
|
1391
|
+
die "全局 core.hooksPath 已指向 $ghp(非 dia),拒绝覆盖;请先自行处理该配置"
|
|
1392
|
+
fi
|
|
1393
|
+
wt_write_global_hooks
|
|
1394
|
+
git config --global core.hooksPath "$ghd"
|
|
1395
|
+
# 仓库级 hooksPath:旧版 dia 目录 → 撤掉迁移;别人的管理器(husky/.githooks)→ 保留并提示
|
|
1396
|
+
if [ -n "$prev" ] && [ "$prev" != "$ghd" ]; then
|
|
1397
|
+
if [ "$prev" = "$hooksdir" ]; then
|
|
1398
|
+
git -C "$repo" config --local --unset core.hooksPath 2>/dev/null || true; prev=""
|
|
1399
|
+
else
|
|
1400
|
+
echo "[dia-wt] ⚠️ 该仓本地 core.hooksPath=$prev(由 hook manager 接管),全局守卫对本仓不生效" >&2
|
|
1401
|
+
fi
|
|
1402
|
+
fi
|
|
1403
|
+
rm -rf "$hooksdir" 2>/dev/null || true
|
|
1404
|
+
python3 - "$cfg" "$repo" "$def" "$template" "$prev" <<'PYG'
|
|
1405
|
+
import json, sys, datetime
|
|
1406
|
+
cfg, repo, br, tmpl, prev = sys.argv[1:6]
|
|
1407
|
+
json.dump({
|
|
1408
|
+
"schema": 2, "repo": repo, "defaultBranch": br, "template": tmpl,
|
|
1409
|
+
"hooksManager": "global", "hooksPrevious": prev or None,
|
|
1410
|
+
"adoptedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
1411
|
+
}, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1412
|
+
PYG
|
|
1413
|
+
echo "adopted:$repo(守卫 = 本机全局钩子 $(wt_global_hooks_dir))"
|
|
1414
|
+
return 0
|
|
1369
1415
|
# 保留首次 adopt 记下的 hooksPrevious(重复 apply 时 prev 已是我们自己的 hooksdir)
|
|
1370
1416
|
local savedPrev=""
|
|
1371
1417
|
[ -f "$cfg" ] && savedPrev="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$cfg" 2>/dev/null || true)"
|
|
@@ -1384,11 +1430,6 @@ data = {
|
|
|
1384
1430
|
}
|
|
1385
1431
|
json.dump(data, open(cfg, "w"), ensure_ascii=False, indent=1)
|
|
1386
1432
|
PY
|
|
1387
|
-
# 提示:simple-git-hooks / husky 的 postinstall 会重写 core.hooksPath 下的钩子,
|
|
1388
|
-
# 每次 `pnpm/npm install` 后可能把守卫冲掉 → 用 `dia wt doctor --fix` 或重跑 adopt --apply 修复。
|
|
1389
|
-
if grep -qs '"simple-git-hooks"\|"husky"\|lefthook' "$repo/package.json" "$repo/.lefthook.yml" 2>/dev/null; then
|
|
1390
|
-
echo "[dia-wt] 注意:该仓使用 hook manager(simple-git-hooks/husky/lefthook),install 后若守卫被重写,跑 dia wt doctor --fix" >&2
|
|
1391
|
-
fi
|
|
1392
1433
|
wt_write_hook "$hooksdir" pre-commit "$def" "$chain"
|
|
1393
1434
|
wt_write_hook "$hooksdir" pre-push "$def" "$chain"
|
|
1394
1435
|
wt_write_hook "$hooksdir" post-checkout "$def" "$chain"
|
|
@@ -1419,7 +1460,7 @@ cmd_doctor() {
|
|
|
1419
1460
|
git -C "$repo" rev-parse --git-dir >/dev/null 2>&1 || continue
|
|
1420
1461
|
local common cfgdir cfg hooksdir prev def
|
|
1421
1462
|
common="$(wt_common_dir "$repo")"; cfgdir="$common/dia"; cfg="$cfgdir/config.json"; hooksdir="$cfgdir/hooks"
|
|
1422
|
-
prev="$(git -C "$repo" config --get core.hooksPath 2>/dev/null || true)"
|
|
1463
|
+
prev="$(git -C "$repo" config --local --get core.hooksPath 2>/dev/null || true)"
|
|
1423
1464
|
def=""
|
|
1424
1465
|
if [ -f "$cfg" ]; then
|
|
1425
1466
|
def="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("defaultBranch") or "")' "$cfg" 2>/dev/null || true)"
|
|
@@ -1427,6 +1468,13 @@ cmd_doctor() {
|
|
|
1427
1468
|
[ -n "$def" ] || def="$(wt_default_branch "$repo")"
|
|
1428
1469
|
# --fix:无损修复(重写三个守卫钩子 + hooksPath 绝对路径)。只对已 adopt 的仓生效。
|
|
1429
1470
|
if [ "$fix" = 1 ] && [ -f "$cfg" ]; then
|
|
1471
|
+
wt_write_global_hooks
|
|
1472
|
+
git config --global core.hooksPath "$(wt_global_hooks_dir)"
|
|
1473
|
+
[ "$json" = 0 ] && echo "FIX $repo 全局钩子已重装"
|
|
1474
|
+
fi
|
|
1475
|
+
if [ "$fix" = 1 ] && [ -f "$cfg" ] && [ "$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)" = "global" ]; then
|
|
1476
|
+
:
|
|
1477
|
+
elif [ "$fix" = 1 ] && [ -f "$cfg" ]; then
|
|
1430
1478
|
local fixChain=""
|
|
1431
1479
|
fixChain="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksChain") or "")' "$cfg" 2>/dev/null || true)"
|
|
1432
1480
|
[ -n "$fixChain" ] || fixChain="$common/hooks"
|
|
@@ -1438,22 +1486,29 @@ cmd_doctor() {
|
|
|
1438
1486
|
prev="$hooksdir"
|
|
1439
1487
|
[ "$json" = 0 ] && echo "FIX $repo 钩子已重装(chain=$fixChain)"
|
|
1440
1488
|
fi
|
|
1441
|
-
local bad=0
|
|
1489
|
+
local bad=0 mcfg=""
|
|
1442
1490
|
[ -f "$cfg" ] || { echo "FAIL D01 $repo 未 adopt"; bad=1; }
|
|
1443
|
-
|
|
1491
|
+
[ -f "$cfg" ] && mcfg="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksManager") or "")' "$cfg" 2>/dev/null || true)"
|
|
1492
|
+
local ghd ghp
|
|
1493
|
+
ghd="$(wt_global_hooks_dir)"
|
|
1494
|
+
ghp="$(git config --global --get core.hooksPath 2>/dev/null || true)"
|
|
1495
|
+
# D20 全局配置归属:只认 dia 自己的 hooksPath,被别人占用就报出来(绝不自动覆盖)
|
|
1496
|
+
[ "$ghp" = "$ghd" ] || { echo "FAIL D20 全局 core.hooksPath 未指向 $ghd(当前:${ghp:-未设置})"; bad=1; }
|
|
1444
1497
|
for h in pre-commit pre-push post-checkout; do
|
|
1445
|
-
[ -x "$
|
|
1498
|
+
[ -x "$ghd/$h" ] || { echo "FAIL D02 缺全局钩子 $h"; bad=1; }
|
|
1446
1499
|
done
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$hooksdir/pre-commit" >/dev/null 2>&1 ) \
|
|
1454
|
-
&& { echo "FAIL D03 $repo 守卫未拦截 $def 提交"; bad=1; }
|
|
1500
|
+
# D21 仓库级 hooksPath 外溢:本地设置会覆盖全局(husky/.githooks 等)
|
|
1501
|
+
if [ -n "$prev" ]; then
|
|
1502
|
+
if [ "$prev" = "$hooksdir" ]; then
|
|
1503
|
+
echo "FAIL D21 $repo 旧版本地 hooksPath 残留($prev);跑 dia wt adopt --apply 迁移到全局钩子"; bad=1
|
|
1504
|
+
else
|
|
1505
|
+
echo "WARN D21 $repo 本地 hooksPath=$prev 覆盖全局钩子,本仓守卫不生效"
|
|
1455
1506
|
fi
|
|
1456
1507
|
fi
|
|
1508
|
+
if [ -n "$def" ] && wt_is_main_checkout "$repo" && [ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ]; then
|
|
1509
|
+
( cd "$repo" && DIA_WT_GUARD_PROBE=1 "$ghd/pre-commit" >/dev/null 2>&1 ) \
|
|
1510
|
+
&& { echo "FAIL D03 $repo 守卫探针未拦截 $def 提交"; bad=1; }
|
|
1511
|
+
fi
|
|
1457
1512
|
if wt_is_main_checkout "$repo"; then
|
|
1458
1513
|
[ -z "$(git -C "$repo" status --porcelain 2>/dev/null)" ] || echo "WARN D04 $repo 主 checkout 有未提交改动"
|
|
1459
1514
|
[ "$(git -C "$repo" symbolic-ref --short HEAD 2>/dev/null)" = "$def" ] || echo "WARN D04 $repo 主 checkout 不在 $def"
|
|
@@ -1507,6 +1562,7 @@ case "${1:-}" in
|
|
|
1507
1562
|
status) shift; cmd_status "$@" ;;
|
|
1508
1563
|
check) shift; cmd_check "$@" ;;
|
|
1509
1564
|
adopt) shift; cmd_adopt "$@" ;;
|
|
1565
|
+
guard) shift; cmd_guard "$@" ;;
|
|
1510
1566
|
doctor) shift; cmd_doctor "$@" ;;
|
|
1511
1567
|
gate) shift; cmd_gate "$@" ;;
|
|
1512
1568
|
clean) shift; cmd_clean "$@" ;;
|
|
@@ -1522,6 +1578,7 @@ dia wt —— worktree 目录形式选型 + 创建(唯一实现)
|
|
|
1522
1578
|
dia wt status [repo-path] # ★ 清理前必查:唯一工作副本 + 改动重叠(只读)
|
|
1523
1579
|
dia wt check [<repo>...] [--all] [--deep] [--quiet]
|
|
1524
1580
|
# ★ 依赖体检(R20):软链/逃逸/悬空;--all 扫全部仓
|
|
1581
|
+
dia wt guard <pre-commit|pre-push|post-checkout> # 守卫本体(供 hook manager 调用)
|
|
1525
1582
|
dia wt adopt <repo>|--all [--apply|--check|--revert] [--template T] [--default-branch B] [--json]
|
|
1526
1583
|
# 配置路径模板 + 守卫钩子(默认 dry-run;--all 扫仓根)
|
|
1527
1584
|
# 守卫含推送前敏感扫描(不提供关闭开关;误报修规则)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# adopt / doctor / gate 回归测试(2026-11-
|
|
3
|
-
# 覆盖:dry-run
|
|
2
|
+
# adopt / doctor / gate 回归测试(2026-11-06:本机全局钩子模型)
|
|
3
|
+
# 覆盖:dry-run、apply(全局钩子)、check/doctor、主 checkout 守卫、链式调用仓库钩子、
|
|
4
|
+
# 非 adopt 仓透传、敏感扫描、adopt --all、gate、revert。
|
|
4
5
|
set -uo pipefail
|
|
5
6
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
6
7
|
WT="$HERE/../bin/dia-wt"
|
|
@@ -8,6 +9,13 @@ TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
|
|
|
8
9
|
PASS=0; FAIL=0
|
|
9
10
|
ok(){ echo " ✓ $1"; PASS=$((PASS+1)); }; bad(){ echo " ✗ $1"; FAIL=$((FAIL+1)); }
|
|
10
11
|
|
|
12
|
+
# 隔离:HOME / 全局 git 配置 / 数据根,避免污染真实机器
|
|
13
|
+
export HOME="$TD/home"; mkdir -p "$HOME"
|
|
14
|
+
export GIT_CONFIG_GLOBAL="$TD/gitconfig"; : > "$GIT_CONFIG_GLOBAL"
|
|
15
|
+
export GIT_CONFIG_NOSYSTEM=1
|
|
16
|
+
export DIA_WT_DATA_ROOT="$TD/data"
|
|
17
|
+
GH="$TD/data/hooks"
|
|
18
|
+
|
|
11
19
|
R="$TD/repo"; mkdir -p "$R"; git -C "$R" init -q -b main
|
|
12
20
|
git -C "$R" config user.email t@example.com; git -C "$R" config user.name t
|
|
13
21
|
echo a > "$R/a.txt"; git -C "$R" add -A; git -C "$R" commit -qm init
|
|
@@ -18,16 +26,14 @@ printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "dry-run 识别默
|
|
|
18
26
|
|
|
19
27
|
bash "$WT" adopt "$R" --apply >/dev/null 2>&1 || bad "apply 失败"
|
|
20
28
|
[ -f "$R/.git/dia/config.json" ] && ok "config 已写" || bad "缺 config"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
[ "$md5_before" != "$md5_after" ] && ok "重复 apply 会刷新时间戳(幂等语义:不重复建目录/钩子)" || bad "重复 apply 无变化"
|
|
30
|
-
|
|
29
|
+
[ "$(git config --global --get core.hooksPath)" = "$GH" ] && ok "全局 hooksPath 指向 $GH" || bad "全局 hooksPath 未设置"
|
|
30
|
+
for h in pre-commit pre-push post-checkout; do [ -x "$GH/$h" ] || bad "缺全局钩子 $h"; done
|
|
31
|
+
ok "全局三钩子可执行"
|
|
32
|
+
[ -z "$(git -C "$R" config --local --get core.hooksPath || true)" ] && ok "不再设置仓库本地 hooksPath" || bad "仍设了本地 hooksPath"
|
|
33
|
+
python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksManager")=="global" else 1)' "$R/.git/dia/config.json" \
|
|
34
|
+
&& ok "config 记录 hooksManager=global" || bad "hooksManager 未记录"
|
|
35
|
+
|
|
36
|
+
bash "$WT" adopt "$R" --apply >/dev/null 2>&1 && ok "重复 apply 幂等" || bad "重复 apply 失败"
|
|
31
37
|
bash "$WT" adopt "$R" --check >/dev/null 2>&1 && ok "check 通过" || bad "check 失败"
|
|
32
38
|
bash "$WT" doctor "$R" >/dev/null 2>&1 && ok "doctor 通过" || bad "doctor 失败"
|
|
33
39
|
|
|
@@ -35,119 +41,45 @@ echo b > "$R/b.txt"; git -C "$R" add b.txt
|
|
|
35
41
|
if git -C "$R" commit -qm should-block 2>/dev/null; then bad "主 checkout main 提交未被拦截"; else ok "主 checkout main 提交被拦截"; fi
|
|
36
42
|
git -C "$R" reset -q
|
|
37
43
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
git
|
|
42
|
-
|
|
43
|
-
git -C "$
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
# 误报回归:`$TD/home/kits/...` 这种相对片段不该被判成 /home/<user>
|
|
51
|
-
printf 'path=$TD/home/kits/dia-msg/dia.json\n' > "$TD/wt/vars.txt"
|
|
52
|
-
git -C "$TD/wt" add -A && git -C "$TD/wt" commit -qm vars
|
|
53
|
-
git -C "$TD/wt" push -q origin feat/scan 2>/dev/null && ok "相对片段 home/ 不误报" || bad "相对片段被误判为敏感路径"
|
|
54
|
-
|
|
55
|
-
bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
|
|
56
|
-
[ -z "$(git -C "$R" config --get core.hooksPath || true)" ] && ok "revert 还原 hooksPath" || bad "revert 未还原"
|
|
57
|
-
[ ! -d "$R/.git/dia" ] && ok "revert 删除 dia 目录" || bad "dia 目录残留"
|
|
58
|
-
|
|
59
|
-
# ── 原 hooksPath 保留 + 二次 apply 不改写 hooksPrevious(review P2-7)──────────
|
|
60
|
-
R2="$TD/repo2"; mkdir -p "$R2" "$TD/orighooks"; git -C "$R2" init -q -b main
|
|
44
|
+
# 链式:仓库自己的 .git/hooks/pre-commit 仍要执行(simple-git-hooks 等管理器就写这里)
|
|
45
|
+
printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R/.git/hooks/pre-commit"
|
|
46
|
+
chmod +x "$R/.git/hooks/pre-commit"
|
|
47
|
+
git -C "$R" worktree add -q -b feat/y "$TD/repo-worktree--y" main
|
|
48
|
+
echo c > "$TD/repo-worktree--y/c.txt"; git -C "$TD/repo-worktree--y" add c.txt
|
|
49
|
+
git -C "$TD/repo-worktree--y" commit -qm feat-y >/dev/null 2>&1
|
|
50
|
+
[ -f "$TD/repo-worktree--y/chain.log" ] && ok "仓库自己的钩子被链式执行" || bad "仓库钩子未执行"
|
|
51
|
+
rm -f "$TD/repo-worktree--y/chain.log"
|
|
52
|
+
|
|
53
|
+
# 本地 hooksPath(husky/.githooks)优先于全局 → 守卫不生效,但 adopt 不得破坏它
|
|
54
|
+
R2="$TD/repo2"; mkdir -p "$R2/.githooks"; git -C "$R2" init -q -b main
|
|
61
55
|
git -C "$R2" config user.email t@example.com; git -C "$R2" config user.name t
|
|
62
56
|
echo a > "$R2/a.txt"; git -C "$R2" add -A; git -C "$R2" commit -qm init
|
|
63
|
-
|
|
64
|
-
git -C "$R2" config --local core.hooksPath "$TD/orighooks"
|
|
65
|
-
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1 || bad "R2 apply 失败"
|
|
66
|
-
hp1="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$R2/.git/dia/config.json")"
|
|
67
|
-
[ "$hp1" = "$TD/orighooks" ] && ok "首次 apply 记录 hooksPrevious" || bad "hooksPrevious=$hp1"
|
|
68
|
-
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1
|
|
69
|
-
hp2="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("hooksPrevious") or "")' "$R2/.git/dia/config.json")"
|
|
70
|
-
[ "$hp2" = "$TD/orighooks" ] && ok "二次 apply 不覆盖 hooksPrevious" || bad "hooksPrevious 被改成 $hp2"
|
|
71
|
-
bash "$WT" adopt "$R2" --revert >/dev/null 2>&1 || bad "R2 revert 失败"
|
|
72
|
-
[ "$(git -C "$R2" config --get core.hooksPath)" = "$TD/orighooks" ] && ok "revert 还原原 hooksPath" || bad "revert 未还原原 hooksPath"
|
|
73
|
-
# hooksPath 被第三方改动 → revert 拒绝(不抢配置)
|
|
57
|
+
git -C "$R2" config --local core.hooksPath .githooks
|
|
74
58
|
bash "$WT" adopt "$R2" --apply >/dev/null 2>&1
|
|
75
|
-
git -C "$R2" config --
|
|
76
|
-
|
|
77
|
-
[ -f "$R2/.git/dia/config.json" ] && ok "拒绝 revert 时保留 config 备份" || bad "拒绝 revert 却删了 config"
|
|
78
|
-
git -C "$R2" config --local core.hooksPath "$R2/.git/dia/hooks"; bash "$WT" adopt "$R2" --revert >/dev/null 2>&1
|
|
59
|
+
[ "$(git -C "$R2" config --get core.hooksPath)" = ".githooks" ] && ok "保留仓库本地 hooksPath(husky 等)" || bad "破坏了本地 hooksPath"
|
|
60
|
+
bash "$WT" doctor "$R2" 2>&1 | grep -q "本地 hooksPath" && ok "doctor 提示本地 hooksPath 覆盖全局" || bad "doctor 未提示覆盖"
|
|
79
61
|
|
|
80
|
-
#
|
|
81
|
-
R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b
|
|
62
|
+
# 未 adopt 的仓:全局钩子纯透传(主 checkout 可正常提交)
|
|
63
|
+
R3="$TD/repo3"; mkdir -p "$R3"; git -C "$R3" init -q -b main
|
|
82
64
|
git -C "$R3" config user.email t@example.com; git -C "$R3" config user.name t
|
|
83
|
-
echo a > "$R3/a.txt"; git -C "$R3" add -A
|
|
84
|
-
|
|
85
|
-
bash "$WT" adopt "$R3" --check >/dev/null 2>&1 && ok "自定义默认分支 check 通过" || bad "自定义默认分支 check 失败"
|
|
86
|
-
bash "$WT" doctor "$R3" >/dev/null 2>&1 && ok "自定义默认分支 doctor 通过" || bad "自定义默认分支 doctor 失败"
|
|
87
|
-
echo b > "$R3/b.txt"; git -C "$R3" add b.txt
|
|
88
|
-
if git -C "$R3" commit -qm should-block 2>/dev/null; then bad "trunk 提交未被拦截"; else ok "自定义默认分支 trunk 提交被拦截"; fi
|
|
89
|
-
git -C "$R3" reset -q
|
|
65
|
+
echo a > "$R3/a.txt"; git -C "$R3" add -A
|
|
66
|
+
git -C "$R3" commit -qm init >/dev/null 2>&1 && ok "未 adopt 仓不受守卫影响" || bad "未 adopt 仓被误拦"
|
|
90
67
|
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
git -
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
git -C "$
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
git -C "$R5" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
|
|
107
|
-
bash "$WT" adopt "$R5" --apply >/dev/null 2>&1 || bad "R5 apply 失败"
|
|
108
|
-
# 主 checkout 的 pre-push 拒绝的是「从这里发起的任何 push」(含 feature 分支),不需要脏提交
|
|
109
|
-
git -C "$R5" branch feat/push-probe origin/main
|
|
110
|
-
if git -C "$R5" push origin feat/push-probe >/dev/null 2>&1; then bad "主 checkout push 未被拦截"; else ok "主 checkout push 被拦截"; fi
|
|
111
|
-
git -C "$R5" branch -D feat/push-probe >/dev/null 2>&1 || true
|
|
112
|
-
git -C "$R5" worktree add -q -b feat/x "$TD/repo5-worktree--x" main
|
|
113
|
-
echo b > "$TD/repo5-worktree--x/b.txt"; git -C "$TD/repo5-worktree--x" add b.txt
|
|
114
|
-
git -C "$TD/repo5-worktree--x" commit -qm feat-x 2>/dev/null
|
|
115
|
-
if git -C "$TD/repo5-worktree--x" push -q origin feat/x >/dev/null 2>&1; then ok "worktree push 放行"; else bad "worktree push 被误拦"; fi
|
|
116
|
-
|
|
117
|
-
# ── template 生效(P2-6):adopt 后 create 按 config.template 推导路径 ─────────
|
|
118
|
-
out="$(bash "$WT" create "$R5" t1 --dry-run 2>&1 || true)"
|
|
119
|
-
printf '%s' "$out" | grep -q "repo5-worktree--t1" && ok "默认模板 create 路径" || bad "默认模板 create: $out"
|
|
120
|
-
bash "$WT" adopt "$R5" --apply --template '{{parent}}/{{repo}}.wt.{{feature}}' >/dev/null 2>&1 || bad "template apply 失败"
|
|
121
|
-
out="$(bash "$WT" create "$R5" t1 --dry-run 2>&1 || true)"
|
|
122
|
-
printf '%s' "$out" | grep -q "repo5.wt.t1" && ok "自定义模板 create 路径生效" || bad "自定义模板: $out"
|
|
123
|
-
bash "$WT" adopt "$R5" --apply >/dev/null 2>&1
|
|
124
|
-
|
|
125
|
-
# ── 过期 origin/HEAD(远端分支已删)不误判默认分支 ──────────────────────────
|
|
126
|
-
R6="$TD/repo6"; mkdir -p "$R6"; git -C "$R6" init -q -b main
|
|
127
|
-
git -C "$R6" config user.email t@example.com; git -C "$R6" config user.name t
|
|
128
|
-
echo a > "$R6/a.txt"; git -C "$R6" add -A; git -C "$R6" commit -qm init
|
|
129
|
-
git -C "$R6" symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/gone
|
|
130
|
-
out="$(bash "$WT" adopt "$R6" --json)"
|
|
131
|
-
printf '%s' "$out" | grep -q '"defaultBranch":"main"' && ok "过期 origin/HEAD 回落到 main" || bad "过期 origin/HEAD: $out"
|
|
132
|
-
|
|
133
|
-
# ── 钩子链:原 hooksPath(.githooks / husky)不被吞掉 ─────────────────────────
|
|
134
|
-
R7="$TD/repo7"; mkdir -p "$R7/.githooks"; git -C "$R7" init -q -b main
|
|
135
|
-
git -C "$R7" config user.email t@example.com; git -C "$R7" config user.name t
|
|
136
|
-
printf '#!/bin/sh\necho chained >> "$(git rev-parse --show-toplevel)/chain.log"\n' > "$R7/.githooks/pre-commit"
|
|
137
|
-
chmod +x "$R7/.githooks/pre-commit"
|
|
138
|
-
echo a > "$R7/a.txt"; git -C "$R7" add -A; git -C "$R7" commit -qm init
|
|
139
|
-
git -C "$R7" config --local core.hooksPath .githooks
|
|
140
|
-
bash "$WT" adopt "$R7" --apply >/dev/null 2>&1 || bad "R7 apply 失败"
|
|
141
|
-
python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));sys.exit(0 if d.get("hooksChain")==".githooks" else 1)' "$R7/.git/dia/config.json" \
|
|
142
|
-
&& ok "config 记录 hooksChain" || bad "hooksChain 未记录"
|
|
143
|
-
git -C "$R7" worktree add -q -b feat/y "$TD/repo7-worktree--y" main
|
|
144
|
-
echo b > "$TD/repo7-worktree--y/b.txt"; git -C "$TD/repo7-worktree--y" add b.txt
|
|
145
|
-
git -C "$TD/repo7-worktree--y" commit -qm feat-y >/dev/null 2>&1
|
|
146
|
-
[ -f "$TD/repo7-worktree--y/chain.log" ] && ok "原 pre-commit 被链式执行" || bad "原 pre-commit 未执行(钩子被吞)"
|
|
147
|
-
bash "$WT" adopt "$R7" --revert >/dev/null 2>&1
|
|
148
|
-
[ "$(git -C "$R7" config --get core.hooksPath)" = ".githooks" ] && ok "revert 还原原 hooksPath(相对)" || bad "revert 未还原 .githooks"
|
|
149
|
-
|
|
150
|
-
# ── adopt --all:批量主 checkout,跳过 worktree 目录 ─────────────────────────
|
|
68
|
+
# 敏感扫描:adopted 仓的 worktree 推送带绝对路径 → 阻断;相对片段不误报
|
|
69
|
+
BR="$TD/remote.git"; git init -q --bare "$BR"
|
|
70
|
+
git -C "$R" remote add origin "$BR" 2>/dev/null || true
|
|
71
|
+
git --git-dir="$BR" fetch -q "$R" main:main 2>/dev/null || true
|
|
72
|
+
git -C "$R" fetch -q origin 2>/dev/null || true
|
|
73
|
+
BAD="$(printf '/%s/alice/secret' Users)"; echo "$BAD" > "$TD/repo-worktree--y/bad.txt"
|
|
74
|
+
git -C "$TD/repo-worktree--y" add bad.txt
|
|
75
|
+
git -C "$TD/repo-worktree--y" commit -qm bad >/dev/null 2>&1
|
|
76
|
+
if git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null; then bad "敏感推送未被拦截"; else ok "敏感推送被拦截"; fi
|
|
77
|
+
git -C "$TD/repo-worktree--y" reset -q --hard HEAD~1 # 丢掉含敏感内容的提交
|
|
78
|
+
printf 'path=$TD/home/kits/dia.json\n' > "$TD/repo-worktree--y/vars.txt"
|
|
79
|
+
git -C "$TD/repo-worktree--y" add vars.txt && git -C "$TD/repo-worktree--y" commit -qm vars >/dev/null 2>&1
|
|
80
|
+
git -C "$TD/repo-worktree--y" push -q origin feat/y 2>/dev/null && ok "相对片段 home/ 不误报" || bad "相对片段被误判"
|
|
81
|
+
|
|
82
|
+
# adopt --all:只处理主 checkout,跳过 *-worktree--*
|
|
151
83
|
AR="$TD/allroot"; mkdir -p "$AR/a" "$AR/b"
|
|
152
84
|
for r in a b; do
|
|
153
85
|
git -C "$AR/$r" init -q -b main
|
|
@@ -155,35 +87,27 @@ for r in a b; do
|
|
|
155
87
|
echo x > "$AR/$r/x.txt"; git -C "$AR/$r" add -A; git -C "$AR/$r" commit -qm init
|
|
156
88
|
done
|
|
157
89
|
git -C "$AR/a" worktree add -q -b feat/w "$AR/a-worktree--w" main
|
|
158
|
-
out="$(DIA_WT_REPOS_ROOT="$AR"
|
|
90
|
+
out="$(DIA_WT_REPOS_ROOT="$AR" bash "$WT" adopt --all --apply --json 2>&1)"
|
|
159
91
|
printf '%s' "$out" | grep -q '"repos":2,"ok":2,"fail":0' && ok "adopt --all 覆盖 2 个主 checkout" || bad "adopt --all: $out"
|
|
160
|
-
for r in a b; do
|
|
161
|
-
[ -n "$(git -C "$AR/$r" config --get core.hooksPath || true)" ] && ok "adopt --all: $r 已配 hooksPath" || bad "adopt --all: $r 未配置"
|
|
162
|
-
done
|
|
163
92
|
[ ! -d "$AR/a-worktree--w/.git/dia" ] && ok "adopt --all 跳过 worktree 目录" || bad "adopt --all 误改 worktree"
|
|
164
|
-
out="$(DIA_WT_REPOS_ROOT="$AR"
|
|
165
|
-
printf '%s' "$out" | grep -q "$AR/a" && ok "doctor --all
|
|
166
|
-
DIA_WT_REPOS_ROOT="$TD/empty-root"
|
|
93
|
+
out="$(DIA_WT_REPOS_ROOT="$AR" bash "$WT" doctor --all --json 2>/dev/null)"
|
|
94
|
+
printf '%s' "$out" | grep -q "$AR/a" && ok "doctor --all 覆盖自定义仓根" || bad "doctor --all 未覆盖"
|
|
95
|
+
DIA_WT_REPOS_ROOT="$TD/empty-root" bash "$WT" adopt --all >/dev/null 2>&1
|
|
167
96
|
[ $? = 2 ] && ok "adopt --all 空结果 exit 2" || bad "adopt --all 空结果未 exit 2"
|
|
168
97
|
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if bash "$WT" doctor "$R8" >/dev/null 2>&1; then bad "钩子被覆盖后 doctor 仍通过"; else ok "钩子被覆盖后 doctor 报失败"; fi
|
|
177
|
-
bash "$WT" doctor "$R8" --fix >/dev/null 2>&1 || bad "doctor --fix 失败"
|
|
178
|
-
bash "$WT" doctor "$R8" >/dev/null 2>&1 && ok "doctor --fix 后恢复 ok" || bad "doctor --fix 后仍失败"
|
|
179
|
-
[ -x "$R8/.git/dia/hooks/post-checkout" ] && ok "doctor --fix 补回 post-checkout" || bad "post-checkout 未补回"
|
|
98
|
+
# gate
|
|
99
|
+
rm -f "$TD/repo-worktree--y/chain.log"
|
|
100
|
+
if bash "$WT" gate "$R" y >/tmp/gate.log 2>&1; then ok "gate 成功"; else bad "gate 失败: $(tail -2 /tmp/gate.log | tr "\n" " ")"; fi
|
|
101
|
+
if bash "$WT" gate "$R" yb >/dev/null 2>&1; then bad "gate 子串误匹配"; else ok "gate 精确匹配 feature"; fi
|
|
102
|
+
echo dirty >> "$TD/repo-worktree--y/c.txt"
|
|
103
|
+
if bash "$WT" gate "$R" y >/dev/null 2>&1; then bad "worktree 脏时 gate 通过"; else ok "worktree 脏时 gate 失败"; fi
|
|
104
|
+
git -C "$TD/repo-worktree--y" checkout -q -- c.txt
|
|
180
105
|
|
|
181
|
-
#
|
|
182
|
-
bash "$WT"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
git -C "$TD/repo5-worktree--x" checkout -q -- b.txt
|
|
106
|
+
# revert:撤本仓配置,保留全局钩子
|
|
107
|
+
bash "$WT" adopt "$R" --revert >/dev/null 2>&1 || bad "revert 失败"
|
|
108
|
+
[ ! -f "$R/.git/dia/config.json" ] && ok "revert 删除本仓配置" || bad "配置残留"
|
|
109
|
+
[ -x "$GH/pre-commit" ] && ok "revert 不影响全局钩子" || bad "revert 误删全局钩子"
|
|
110
|
+
git -C "$R" worktree remove --force "$TD/repo-worktree--y" >/dev/null 2>&1 || true
|
|
187
111
|
|
|
188
112
|
echo; echo "── PASS $PASS · FAIL $FAIL ──"
|
|
189
113
|
[ "$FAIL" = 0 ]
|