@aiti-ai/aiti-git-trace 0.0.0-stage → 0.2.12

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.
Files changed (41) hide show
  1. package/AGENTS.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +38 -2
  4. package/api.ts +424 -0
  5. package/check-client.mjs +273 -0
  6. package/client/src/common.ts +179 -0
  7. package/client/src/entry.tsx +81 -0
  8. package/client/src/host-shim.ts +118 -0
  9. package/client/src/jsx-runtime.ts +2 -0
  10. package/client/src/panels.tsx +1377 -0
  11. package/client/src/trace-app.tsx +275 -0
  12. package/client/src/trace-files-row.pure.ts +88 -0
  13. package/client/src/trace-files-row.tsx +110 -0
  14. package/client/src/version-mention.ts +90 -0
  15. package/deploy-commit.mjs +52 -0
  16. package/dev-pack.mjs +246 -0
  17. package/dist/aiti-git-trace-client.mjs +4 -0
  18. package/dist/entry-C_le59dp.js +1594 -0
  19. package/dist/panels-BpC4XLni.js +1522 -0
  20. package/ext-icons-deploy.mjs +24 -0
  21. package/icon.svg +1 -0
  22. package/icons/git-branch.svg +1 -0
  23. package/icons/git-commit-horizontal.svg +1 -0
  24. package/index.ts +705 -0
  25. package/package.json +33 -4
  26. package/reconcile-deps.mjs +41 -0
  27. package/reload-instances.mjs +60 -0
  28. package/scripts/aiti-trace.mjs +117 -0
  29. package/skills/index.json +1 -0
  30. package/skills/rollback-session/SKILL.md +34 -0
  31. package/skills/trace-ops/SKILL.md +31 -0
  32. package/skills/watch-path/SKILL.md +34 -0
  33. package/test/fixtures/ops-sample.jsonl +3 -0
  34. package/tools/deploy.mjs +103 -0
  35. package/tools/trace-compact.mjs +58 -0
  36. package/trace-core.test.mjs +1120 -0
  37. package/trace-core.ts +1346 -0
  38. package/trace-files-row.test.mjs +66 -0
  39. package/tsconfig.check.json +21 -0
  40. package/version-mention.test.mjs +76 -0
  41. package/vite.config.ts +43 -0
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@aiti-ai/aiti-git-trace",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.12",
4
+ "type": "module",
5
+ "description": "编外 AI 变更追踪扩展(.git/aiti 追踪仓 + 关注路径 + 回撤/部分回滚)—— JSX + vite 构建 client bundle",
6
+ "scripts": {
7
+ "build:client": "vite build --config vite.config.ts",
8
+ "watch": "vite build --watch --config vite.config.ts",
9
+ "deploy": "node tools/deploy.mjs",
10
+ "test": "node --test trace-core.test.mjs"
11
+ },
12
+ "devDependencies": {
13
+ "vite": "^6.4.3"
14
+ },
15
+ "aitiConfigs": {
16
+ "configFiles": [
17
+ "config.json"
18
+ ]
19
+ },
20
+ "aitiExtProtocol": "1.0",
21
+ "dependencies": {
22
+ "@aiti-ai/ext-shared": "^0.1.3"
23
+ },
24
+ "license": "MIT",
25
+ "contentHash": "fb74b3285b6a2695",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/aiti-ai/aiti-ai.git",
29
+ "directory": "extensions/aiti-git-trace"
30
+ },
31
+ "homepage": "https://github.com/aiti-ai/aiti-ai#readme",
32
+ "bugs": {
33
+ "url": "https://github.com/aiti-ai/aiti-ai/issues"
34
+ }
35
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * _shared/reconcile-deps.mjs · 通知 pi-web 双实例对齐扩展共享依赖(各扩展 deploy.mjs 共用)
3
+ *
4
+ * 背景:扩展运行时依赖统一装进共享根 <webRoot>/node_modules(jiti 从扩展目录向上解析
5
+ * 自动命中;机制仿 pi 本体 npm: 包安装)。共享根的唯一写入方是 aiti-extman 的
6
+ * shared-deps reconcile——deploy.mjs 拷完文件后 POST 本通知即可,不在部署脚本里跑 npm。
7
+ *
8
+ * 用法(与 reload-instances 同款:允许失败,永不 throw、不影响 deploy 退出码):
9
+ * import { notifyReconcile } from "../../_shared/reconcile-deps.mjs";
10
+ * await notifyReconcile(); // 默认 "[deploy] " 前缀;建议在 reloadInstances() 之前调
11
+ *
12
+ * 失败无害:extman 在下一次 install/remove/reconcile-deps 时会自愈对齐;双实例各自
13
+ * reconcile 各自的 webRoot(~/.pi/web 与 ~/.pi/web-dev),互不相干。
14
+ */
15
+ import { buildInstances } from "./reload-instances.mjs";
16
+
17
+ export async function notifyReconcile(prefix = "[deploy] ") {
18
+ for (const inst of buildInstances()) {
19
+ if (inst.base === null) {
20
+ console.log(`${prefix}reconcile ${inst.name} 跳过:basePath 非法`);
21
+ continue;
22
+ }
23
+ try {
24
+ const r = await fetch(inst.base + "/api/extensions/aiti-extman/ops", {
25
+ method: "POST",
26
+ headers: { "content-type": "application/json" },
27
+ body: JSON.stringify({ action: "reconcile-deps", source: "deploy" }),
28
+ });
29
+ const body = await r.json().catch(() => null);
30
+ const err = body && body.deps && body.deps.error;
31
+ if (r.ok && body && body.ok) {
32
+ console.log(`${prefix}reconcile ${inst.name} → ✓(${Object.keys(body.deps?.deps || {}).length} 个共享依赖)`);
33
+ } else {
34
+ console.log(`${prefix}reconcile ${inst.name} → HTTP ${r.status} ⚠${err ? " " + err : "(extman 未就绪?下次安装时自动对齐)"}`);
35
+ }
36
+ } catch (e) {
37
+ const cause = e && e.cause ? String(e.cause.code || e.cause.message || e.cause) : String((e && e.message) || e);
38
+ console.log(`${prefix}reconcile ${inst.name} 不可达(${cause})— 下次安装时自动对齐`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * _shared/reload-instances.mjs · pi-web 双实例热重载(各扩展 deploy.mjs 共用)
3
+ *
4
+ * dev 3001 / 生产 3002;端口可经 AITI_DEV_PORT / AITI_PROD_PORT 覆盖;
5
+ * basePath 双 env(AITI_DEV_BASE_PATH / AITI_PROD_BASE_PATH)按机器设值,
6
+ * 推荐值不带斜杠(48:dev 与 空;155:系统级 wdev 与 win)——git-bash 启动
7
+ * 原生程序时会把 / 开头的 env 值做 MSYS 路径转换(/wdev → D:/apps/Git/wdev)
8
+ * 致 URL 非法;不带斜杠则不触发转换。此处自动补 /(旧 /xxx 格式仍兼容),
9
+ * 兼容不了时提示 MSYS_NO_PATHCONV=1。
10
+ * 避免「server 端代码改了、实例没 reload 导致跑旧逻辑」。
11
+ *
12
+ * 用法(静态 import 或顶层 await import 均可):
13
+ * import { reloadInstances } from "../../_shared/reload-instances.mjs";
14
+ * await reloadInstances(); // 默认 "[deploy] " 前缀
15
+ */
16
+ const BASE_PATH_RE = /^\/[A-Za-z0-9._-]*$/;
17
+
18
+ function buildInstance(name, portEnv, portDefault, pathEnv, pathDefault) {
19
+ const origin = `http://127.0.0.1:${process.env[portEnv] || portDefault}`;
20
+ const raw = process.env[pathEnv];
21
+ if (raw === undefined || raw === null || String(raw).trim() === "") return { name, base: origin + pathDefault };
22
+ // 自动补 /:wdev → /wdev(推荐无斜杠格式,MSYS 不转换);/wdev → /wdev(旧格式兼容)
23
+ const p = "/" + String(raw).trim().replace(/^\/+|\/+$/g, "");
24
+ if (BASE_PATH_RE.test(p)) return { name, base: origin + p };
25
+ return { name, base: null, raw };
26
+ }
27
+
28
+ /** 双实例清单(dev/prod 各 {name, base});reload-instances 与 reconcile-deps 等共用。 */
29
+ export function buildInstances() {
30
+ return [
31
+ buildInstance("dev", "AITI_DEV_PORT", 3001, "AITI_DEV_BASE_PATH", "/dev"),
32
+ buildInstance("prod", "AITI_PROD_PORT", 3002, "AITI_PROD_BASE_PATH", ""),
33
+ ];
34
+ }
35
+
36
+ export async function reloadInstances(prefix = "[deploy] ") {
37
+ // 批次合并(deploy-all 场景):子条目部署脚本被注入 PI_WEB_DEFER_RELOAD=1 时不各自 reload,
38
+ // 由 deploy-all 在全部条目完成后统一执行一轮(N 条目 × 2 实例 → 2 次 POST)。
39
+ // 避免批量部署时的 reload 风暴(2026-09-15 实测:65 条目逐个 reload 把 dev 实例打满)。
40
+ // 单独跑某个扩展 deploy.mjs 不带该 env → 行为不变(部署完立即 reload)。
41
+ if (process.env.PI_WEB_DEFER_RELOAD === "1") {
42
+ console.log(`${prefix}reload 已 defer(PI_WEB_DEFER_RELOAD=1,由 deploy-all 收尾统一执行)`);
43
+ return;
44
+ }
45
+ // 允许失败:网络异常/非 2xx/服务未启动/env 非法一律只打提示,永不 throw——不影响 deploy 退出码
46
+ for (const inst of buildInstances()) {
47
+ if (inst.base === null) {
48
+ const msysHint = /[\\:]/.test(inst.raw) ? "——疑似 git-bash MSYS 路径转换,用 MSYS_NO_PATHCONV=1 前缀重跑" : "";
49
+ console.log(`${prefix}reload ${inst.name} 跳过:AITI_${inst.name.toUpperCase()}_BASE_PATH="${inst.raw}" 非法(须形如 /xxx 或留空)${msysHint}`);
50
+ continue;
51
+ }
52
+ try {
53
+ const r = await fetch(inst.base + "/api/extensions/reload", { method: "POST" });
54
+ console.log(`${prefix}reload ${inst.name} → HTTP ${r.status} ${r.ok ? "✓" : "⚠ 失败(请手动 POST /api/extensions/reload)"}`);
55
+ } catch (e) {
56
+ const cause = e && e.cause ? String(e.cause.code || e.cause.message || e.cause) : String((e && e.message) || e);
57
+ console.log(`${prefix}reload ${inst.name} 不可达(${cause})— 需手动 POST /api/extensions/reload`);
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aiti-trace CLI(M4-2):编外追踪仓的瘦 身 / 报告 / 会话清单。
4
+ *
5
+ * 用法:
6
+ * node aiti-trace.mjs sessions <cwd> 列出 cwd 仓被追踪的全部会话(分支 + op 数 + 末次时间)
7
+ * node aiti-trace.mjs report <cwd> [sessionId] 打印某会话(缺省全部)台账摘要
8
+ * node aiti-trace.mjs gc <cwd> --days 30 [--dry-run]
9
+ * 删除末次提交早于 N 天的会话分支(追加式历史瘦身;
10
+ * --dry-run 只列出将删除的分支)
11
+ *
12
+ * 说明:
13
+ * - 追踪仓 = <repoRoot>/.aiti-trace(§13.2 顶层);一切调用显式 --git-dir/--work-tree(hermetic 红线)
14
+ * - 台账(ops/*.jsonl)不删(体积小、可审计);分支删除后历史 commit 成为孤儿,待 git gc 自然回收
15
+ * - 安全分支永不删:refs/heads/aiti/safety/*(safety 存档无时间语义,宁多勿删)
16
+ */
17
+ import { execFileSync } from "node:child_process";
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import os from "node:os";
21
+
22
+ const args = process.argv.slice(2);
23
+ const cmd = args[0];
24
+ const cwd = path.resolve(args[1] && !args[1].startsWith("--") ? args[1] : process.cwd());
25
+ const flag = (name, dft) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : dft; };
26
+ const dryRun = args.includes("--dry-run");
27
+ const days = Number(flag("--days", 30));
28
+
29
+ const configDir = process.env.PI_WEB_DATA_DIR
30
+ ? path.join(path.resolve(process.env.PI_WEB_DATA_DIR), "aiti-git-trace", "config")
31
+ : path.join(os.homedir(), ".pi", "web", "data", "aiti-git-trace", "config");
32
+
33
+ function git(gitDir, workTree, gitArgs) {
34
+ return execFileSync("git", [
35
+ "--git-dir", gitDir, "--work-tree", workTree,
36
+ "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false",
37
+ ...gitArgs,
38
+ ], { cwd: workTree, encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
39
+ env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" } });
40
+ }
41
+
42
+ function resolveRepoRoot(start) {
43
+ let dir = path.resolve(start);
44
+ for (;;) {
45
+ const g = path.join(dir, ".git");
46
+ try { if (fs.statSync(g).isDirectory() && fs.existsSync(path.join(g, "HEAD"))) return dir; } catch {}
47
+ const parent = path.dirname(dir);
48
+ if (parent === dir) return null;
49
+ dir = parent;
50
+ }
51
+ }
52
+
53
+ const repoRoot = resolveRepoRoot(cwd);
54
+ if (!repoRoot) { console.error("该目录无外层 git 仓:" + cwd); process.exit(1); }
55
+ const gitDir = path.join(repoRoot, ".aiti-trace");
56
+ if (!fs.existsSync(gitDir)) { console.log("该仓尚无追踪史(.aiti-trace 不存在)"); process.exit(0); }
57
+
58
+ function branches() {
59
+ const out = git(gitDir, repoRoot, ["for-each-ref", "--format=%(refname)%09%(committerdate:iso-strict)%09%(committerdate:unix)", "refs/heads/aiti/"]);
60
+ return out.trim().split("\n").filter(Boolean).map((l) => {
61
+ const [ref, iso, unix] = l.split("\t");
62
+ return { ref, iso, unix: Number(unix) * 1000, session: path.basename(ref) };
63
+ });
64
+ }
65
+
66
+ function ledgerFor(session) {
67
+ // 分支名是 16 位短 id;台账文件用完整会话 id → state/ops 目录按前缀匹配(数据分层:台账在 state/ops)
68
+ // 数据分层:configDir = <extRoot>/config,台账在 <extRoot>/state/ops
69
+ const dir = path.join(path.dirname(configDir), "state", "ops");
70
+ let f = path.join(dir, session + ".jsonl");
71
+ if (!fs.existsSync(f)) {
72
+ try {
73
+ const hit = fs.readdirSync(dir).find((n) => n.startsWith(session));
74
+ if (hit) f = path.join(dir, hit);
75
+ } catch { /* ops 目录缺失 */ }
76
+ }
77
+ try { return fs.readFileSync(f, "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l)); }
78
+ catch { return []; }
79
+ }
80
+
81
+ if (cmd === "sessions") {
82
+ for (const b of branches()) {
83
+ const rows = ledgerFor(b.session);
84
+ const files = new Set();
85
+ for (const r of rows) for (const st of r.stores || []) for (const p of st.paths || []) files.add(p);
86
+ console.log(`${b.session.slice(0, 16)} ops=${rows.length} files=${files.size} last=${b.iso}`);
87
+ }
88
+ } else if (cmd === "report") {
89
+ const sid = args[2] && !args[2].startsWith("--") ? args[2] : null;
90
+ for (const b of branches().filter((x) => !sid || x.session.startsWith(sid))) {
91
+ const rows = ledgerFor(b.session);
92
+ console.log("== " + b.session + " ops=" + rows.length + " last=" + b.iso);
93
+ for (const r of rows) {
94
+ const paths = (r.stores || []).flatMap((s) => s.paths || []);
95
+ console.log(` op#${r.op} ${r.tool} ${String(r.summary).slice(0, 60)} [${paths.slice(0, 5).join(", ")}${paths.length > 5 ? "…" : ""}]`);
96
+ }
97
+ }
98
+ } else if (cmd === "gc") {
99
+ const cutoff = Date.now() - days * 86400_000;
100
+ const victims = branches().filter((b) => b.unix < cutoff && !/\/safety\//.test(b.ref));
101
+ if (!victims.length) { console.log("无可回收分支(" + days + " 天内均有活动)"); process.exit(0); }
102
+ const before = git(gitDir, repoRoot, ["count-objects", "-v"]);
103
+ const sz = (s) => Number((s.match(/size-pack: (\d+)/) || [])[1] || 0);
104
+ for (const v of victims) {
105
+ console.log((dryRun ? "[dry-run] 将删除 " : "删除 ") + v.ref + " last=" + v.iso);
106
+ if (!dryRun) git(gitDir, repoRoot, ["update-ref", "-d", v.ref]);
107
+ }
108
+ if (!dryRun) {
109
+ try { git(gitDir, repoRoot, ["reflog", "expire", "--expire=now", "--all"]); git(gitDir, repoRoot, ["gc", "--prune=now", "--quiet"]); } catch {}
110
+ const after = git(gitDir, repoRoot, ["count-objects", "-v"]);
111
+ console.log("size-pack KB: " + sz(before) + " → " + sz(after));
112
+ } else {
113
+ console.log("[dry-run] 未执行删除;去掉 --dry-run 生效");
114
+ }
115
+ } else {
116
+ console.log("用法:aiti-trace.mjs sessions|report|gc <cwd> [--days N] [--dry-run]");
117
+ }
@@ -0,0 +1 @@
1
+ ["trace-ops", "rollback-session", "watch-path"]
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: rollback-session
3
+ title: 回滚会话的文件变更(恢复到指定提交)
4
+ placeholder: 描述回滚需求,例如:把 pi-work 会话 7942… 的文件恢复到 op1 之前
5
+ summary: 用 aiti-git-trace 的安全机制把工作区文件恢复到追踪历史中的某个提交(先自动打 safety 存档,可再回滚)。涉及 storeRoot/sessionId/targetCommit 三个参数。
6
+ outputs:
7
+ format: json-obj
8
+ schema:
9
+ rolledBack: 是否成功
10
+ storeRoot: 恢复的仓根
11
+ commit: 恢复到的提交
12
+ action:
13
+ type: api
14
+ method: POST
15
+ path: /api/extensions/aiti-git-trace/rollback
16
+ mapping:
17
+ storeRoot: storeRoot
18
+ sessionId: sessionId
19
+ targetCommit: targetCommit
20
+ ---
21
+
22
+ # rollback-session · 回滚会话文件变更
23
+
24
+ 你是 aiti-git-trace 的回滚助手。用户想把工作区文件恢复到追踪历史中的某个时点。
25
+
26
+ ## 步骤
27
+ 1. 先 GET `{basePath}/api/extensions/aiti-git-trace/ops?session=<sessionId>` 查台账,和用户确认要恢复到哪个 op(用其 before 提交;partial 仓最早可恢复点=首 commit)
28
+ 2. 确定参数:storeRoot=仓根(台账 stores[].root)、sessionId=会话 ID、targetCommit=目标提交 sha
29
+ 3. 严格按 outputs 字段输出;client 预览后由用户确认执行
30
+
31
+ ## 注意
32
+ - 回滚前 API 会自动打 safety 存档(phase=safety),回滚本身也可再回滚
33
+ - 只能恢复被追踪仓(.git/aiti 存在)内的文件;ignored/未跟踪文件不受影响
34
+ - 用户犹豫时用 aiti_user_choose 列出候选 op 让其选择,不要擅自回滚
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: trace-ops
3
+ title: 查询会话的 AI 文件追踪操作
4
+ placeholder: 想查哪个会话的操作?给我会话 ID(不填就查当前会话)
5
+ summary: 读取 aiti-git-trace 台账,列出某会话被追踪的全部 AI 文件操作(op 编号/工具/变更文件),可用于「这个文件是谁改的」「这轮对话动了哪些文件」类问题。
6
+ outputs:
7
+ format: json-obj
8
+ schema:
9
+ session: 会话 ID
10
+ ops_count: 操作条数
11
+ ops: 操作摘要数组(每项含 op/tool/paths/时间)
12
+ action:
13
+ type: api
14
+ method: GET
15
+ path: /api/extensions/aiti-git-trace/ops?session={session}
16
+ ---
17
+
18
+ # trace-ops · 查询会话追踪操作
19
+
20
+ 你是 aiti-git-trace 的查询助手。用户想了解某会话里 AI 对文件做了哪些改动。
21
+
22
+ ## 步骤
23
+ 1. 确定 session ID:用户给了就用;没给就查当前会话(发起上下文里的「来源会话 ID」)
24
+ 2. GET `{basePath}/api/extensions/aiti-git-trace/ops?session=<session>` 拿台账
25
+ 3. 按用户关注点整理:按 op 编号/工具/文件路径归纳;文件级追溯可指出 op 与 paths 的对应
26
+
27
+ ## 注意
28
+ - `stores[]` 里 `paths` 是该 op 实际变更的文件(来自 before..after diff,上限 50 条)
29
+ - `partial:true` 表示该仓首次被追踪时缺 before(最早可恢复点=首 commit)
30
+ - `leaks[]` 是 cwd 之外、无 git 仓归属的改动(仅列示,不入库)
31
+ - 台账为空说明该会话未产生被追踪的文件变更(纯读/未开启)
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: watch-path
3
+ title: 管理关注路径(cwd 之外的追踪范围)
4
+ placeholder: 例如:把 ~/workspace/pi-web 加入关注;或查看当前关注列表
5
+ summary: 查看/添加/删除 aiti-git-trace 的 workspace 级关注路径。关注路径中已识别的 git 仓会被副 store 正式追踪(cwd 之外的多仓场景)。
6
+ outputs:
7
+ format: json-obj
8
+ schema:
9
+ done: 是否完成
10
+ key: workspace key
11
+ config: 当前关注路径配置
12
+ action:
13
+ type: api
14
+ method: POST
15
+ path: /api/extensions/aiti-git-trace/workspace
16
+ mapping:
17
+ key: key
18
+ config: config
19
+ ---
20
+
21
+ # watch-path · 管理关注路径
22
+
23
+ 你是 aiti-git-trace 的关注路径助手。用户想查看或修改「cwd 之外哪些仓要被追踪」。
24
+
25
+ ## 步骤
26
+ 1. 确定 workspace:用户指定 cwd 就用;否则用发起上下文的「工作目录」
27
+ 2. 查列表:POST `{basePath}/api/extensions/aiti-git-trace/workspace/resolve` body {"cwd": "<cwd>"} 得到 key 与现有配置
28
+ 3. 添加:按用户说的路径构造 config({autoDiscover, paths:[{path, origin:"user", repoRoot, addedAt}]}),POST `{basePath}/api/extensions/aiti-git-trace/workspace` body {key, ...config}
29
+ 4. 删除:DELETE `{basePath}/api/extensions/aiti-git-trace/workspace?key=<key>`(整份删除;改单条走「读-改-POST」)
30
+
31
+ ## 注意
32
+ - 只有路径归属的 git 仓会被追踪(向上走查 .git;目录会向下扫一层找嵌套仓)
33
+ - key = 主仓根(无 git 则 cwd)规范路径的 sha256 前 16 位;不确定就让 resolve 算
34
+ - 修改后对新操作即时生效(pi 扩展每个 op 边界重读配置)
@@ -0,0 +1,3 @@
1
+ {"op":1,"entryId":"e_a1","msgSeq":3,"tool":"bash","summary":"npm test","t0":1726000000000,"t1":1726000001000,"cwd":"/home/pi/workspace/x","userHead":"2e519b1f","parent":null,"stores":[{"root":"/home/pi/workspace/x","before":"aaa111","after":"bbb222","files":{"M":1,"A":0,"D":0,"skipped":0},"paths":["src/a.ts"],"partial":false}],"leaks":[]}
2
+ {"op":2,"entryId":"e_a2","msgSeq":3,"tool":"edit","summary":"edit /home/pi/workspace/pi-web/src/b.ts","t0":1726000002000,"t1":1726000003000,"cwd":"/home/pi/workspace/x","userHead":"2e519b1f","parent":null,"stores":[{"root":"/home/pi/workspace/x","before":"bbb222","after":"ccc333","files":{"M":0,"A":1,"D":0,"skipped":0},"paths":["x.md"],"partial":false},{"root":"/home/pi/workspace/pi-web","before":null,"after":"9aa8fed","files":{"M":2},"paths":["src/b.ts"],"partial":true}],"leaks":[]}
3
+ {"op":3,"entryId":"e_a2","msgSeq":4,"tool":"bash","summary":"echo x > /tmp/leak.log","t0":1726000004000,"t1":1726000005000,"cwd":"/home/pi/workspace/x","userHead":"2e519b1f","parent":null,"stores":[{"root":"/home/pi/workspace/x","before":"ccc333","after":"ddd444","files":{"M":0,"A":1,"D":0},"paths":["out.txt"],"partial":false}],"leaks":[{"path":"/tmp/leak.log","size":12,"kind":"create"}]}
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aiti-git-trace · 部署到 ~/.pi/web/extensions/ 与 ~/.pi/web-dev/extensions/(pi-web 双实例)
4
+ *
5
+ * 步骤:
6
+ * 0) 构建 JSX client bundle(client/src/*.tsx → client/aiti-git-trace-client.mjs)
7
+ * 1) 拷贝服务端源码 index.ts → 两个扩展目录
8
+ * 2) 拷贝 client 产物 → 两个扩展目录 client/
9
+ * 3) 提交部署状态到扩展 git 仓库(~/.pi/web/extensions/)(失败不影响部署)
10
+ *
11
+ * 用法:node tools/deploy.mjs
12
+ * 生效:POST /api/extensions/reload(或重启 pi-web);client no-store 刷新页面即新
13
+ */
14
+ import { copyFileSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { homedir } from "node:os";
18
+ import { execSync } from "node:child_process";
19
+ import { deployCommit } from "../../_shared/deploy-commit.mjs";
20
+ import { copyIcons } from "../../_shared/ext-icons-deploy.mjs";
21
+ import { notifyReconcile } from "../../_shared/reconcile-deps.mjs";
22
+
23
+ // client TS 残留检查(.mjs 里是合法 JS 语法检查不报、运行时必炸;FAIL 即部署失败)
24
+ {
25
+ const { checkClientDir } = await import("../../_shared/check-client.mjs");
26
+ checkClientDir(fileURLToPath(new URL("../client", import.meta.url)), { log: (m) => console.log("[deploy] " + m) });
27
+ }
28
+
29
+ const root = resolve(import.meta.dirname, ".."); // pi-web-ext/aiti-git-trace
30
+ const targets = [
31
+ resolve(homedir(), ".pi", "web", "extensions", "aiti-git-trace"),
32
+ resolve(homedir(), ".pi", "web-dev", "extensions", "aiti-git-trace"),
33
+ ];
34
+ const extRepo = resolve(homedir(), ".pi", "web", "extensions");
35
+
36
+ // 0) 客户端 bundle 构建(client/src/*.tsx → client/aiti-git-trace-client.mjs)
37
+ console.log("[deploy] 构建 JSX client bundle…");
38
+ try {
39
+ execSync("npm run build:client", { cwd: root, stdio: "inherit" });
40
+ } catch (e) {
41
+ console.error("[deploy] 构建失败,中止部署:", e.message);
42
+ process.exit(1);
43
+ }
44
+
45
+ for (const extDir of targets) {
46
+ mkdirSync(extDir, { recursive: true });
47
+ for (const f of ["package.json", "index.ts", "icon.svg", "api.ts", "trace-core.ts"]) {
48
+ copyFileSync(resolve(root, f), resolve(extDir, f));
49
+ console.log("[deploy] 源码 → " + resolve(extDir, f));
50
+ }
51
+ // trace-compact CLI(影子仓瘦身;schedule/startup 子进程调用 + 手动 CLI)
52
+ try {
53
+ mkdirSync(resolve(extDir, "tools"), { recursive: true });
54
+ copyFileSync(resolve(root, "tools", "trace-compact.mjs"), resolve(extDir, "tools", "trace-compact.mjs"));
55
+ console.log("[deploy] CLI → " + resolve(extDir, "tools", "trace-compact.mjs"));
56
+ } catch (e) { console.warn("[deploy] trace-compact.mjs 缺失"); }
57
+ // skills/(aiti-helper 规范:整目录递归拷贝)
58
+ try {
59
+ const { cpSync } = await import("node:fs");
60
+ cpSync(resolve(root, "skills"), resolve(extDir, "skills"), { recursive: true });
61
+ console.log("[deploy] skills → " + resolve(extDir, "skills"));
62
+ } catch (e) { console.warn("[deploy] skills 缺失"); }
63
+
64
+ // client(薄壳拆分 2026-09-13:主 bundle + 全部 hash 分块 panels-*.js/entry-*.js,清理陈旧分块)
65
+ const clientDir = resolve(extDir, "dist");
66
+ mkdirSync(clientDir, { recursive: true });
67
+ try {
68
+ const srcClient = resolve(root, "dist");
69
+ const srcFiles = readdirSync(srcClient).filter((f) => f.endsWith(".mjs") || f.endsWith(".js"));
70
+ for (const f of srcFiles) copyFileSync(resolve(srcClient, f), resolve(clientDir, f));
71
+ for (const f of readdirSync(clientDir)) {
72
+ if (f.endsWith(".js") && !srcFiles.includes(f)) { try { unlinkSync(resolve(clientDir, f)); } catch { /* noop */ } }
73
+ }
74
+ console.log("[deploy] client bundle + chunks(" + srcFiles.length + " 文件) → " + clientDir);
75
+ } catch (e) { console.warn("[deploy] client 缺失:", e.message); }
76
+ }
77
+ console.log("[deploy] 完成。生效:POST /api/extensions/reload(或重启 pi-web);client 刷新页面即新");
78
+
79
+ // 提交部署状态到扩展 git 仓库(生产目录是 git 仓库)
80
+
81
+
82
+ // 图标文件拷贝(icon: "icons/<name>.svg" → 宿主加载时内嵌;库在 _shared/icons/)
83
+ for (const t of targets) copyIcons(resolve(t, "icons"), ["git-branch", "git-commit-horizontal"], { log: (m2) => console.log(m2) });
84
+ deployCommit({ repo: extRepo, name: "aiti-git-trace", paths: ["aiti-git-trace"], log: (m) => console.log("[deploy] " + m) });
85
+
86
+ // ── 共享依赖 reconcile 通知(依赖进 <webRoot>/node_modules;允许失败,永不 throw)──
87
+ await notifyReconcile();
88
+
89
+ // ── 双实例热重载(basePath 双 env 按机器设值)——允许失败:服务未启动/共享模块缺失仅提示,不影响 deploy 退出码 ──
90
+ try {
91
+ const { reloadInstances } = await import("../../_shared/reload-instances.mjs");
92
+ await reloadInstances();
93
+ } catch (e) {
94
+ console.log("[deploy] 自动 reload 未执行(" + String((e && e.message) || e) + ")— 需手动 POST /api/extensions/reload");
95
+ }
96
+
97
+ // ── dev-pack(方案 §3.2:源码+构建工具+AGENTS.md 直达部署目录,AI 可现场直改)——允许失败,不影响 deploy 退出码 ──
98
+ try {
99
+ const { packDevPack } = await import("../../_shared/dev-pack.mjs");
100
+ packDevPack({ extRoot: resolve(import.meta.dirname, ".."), log: (m) => console.log("[deploy] " + m) });
101
+ } catch (e) {
102
+ console.log("[deploy] dev-pack 未执行(" + String((e && e.message) || e) + ")");
103
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * aiti-git-trace · 影子仓按时间瘦身 CLI(方案四 §5.2 / M6)
4
+ *
5
+ * 用法:
6
+ * node tools/trace-compact.mjs <repoRoot>... # 实跑:删过期会话分支 + gc 回收 + 同批清台账/state
7
+ * node tools/trace-compact.mjs <repoRoot> --dry-run # 默认 dry-run 出报告(不删任何数据)
8
+ * node tools/trace-compact.mjs --all [--ext-config-dir <dir>] # 遍历已知仓(state cwd + workspace paths)
9
+ * 参数:--days 30(保留窗)· --keep-active-hours 24(活跃保护)· --max-objects-mb 300(水位)
10
+ *
11
+ * 不可逆提示:裁剪丢的是 N 天前的 AI 操作追溯(时间线/回滚能力);默认 30 天审计窗。
12
+ * 并发安全:compact 全程持 compact.lock(O_EXCL);trace 写入侧 commit 前探测等待(防撕裂窗口)。
13
+ */
14
+ import { resolve, dirname, join } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { homedir } from "node:os";
17
+ import { pathToFileURL } from "node:url";
18
+
19
+ const args = process.argv.slice(2);
20
+ const opt = (name, dflt) => {
21
+ const i = args.indexOf("--" + name);
22
+ return i >= 0 ? args[i + 1] : dflt;
23
+ };
24
+ const has = (name) => args.includes("--" + name);
25
+ const DRY = has("dry-run");
26
+ const DAYS = Number(opt("days", "30")) || 30;
27
+ const KEEP_ACTIVE = Number(opt("keep-active-hours", "24")) || 24;
28
+ const MAX_MB = Number(opt("max-objects-mb", "300")) || 300;
29
+
30
+ const { compact, collectTraceRepos, storeOk } = await import(pathToFileURL(resolve(dirname(fileURLToPath(import.meta.url)), "..", "trace-core.ts")).href); // Windows 盘符路径必须转 file:// URL
31
+
32
+ // 仓根列表:位置参数优先;--all = 从扩展 configDir 收集已知仓
33
+ const extConfigDir = opt("ext-config-dir", join(homedir(), ".pi", "web", "data", "aiti-git-trace", "config"));
34
+ let roots = args.filter((a) => !a.startsWith("--") && !/^\d+$/.test(a)).map((r) => resolve(r));
35
+ if (!roots.length && (has("all") || !roots.length)) {
36
+ roots = collectTraceRepos(extConfigDir);
37
+ console.log(`[trace-compact] 收集到 ${roots.length} 个已知追踪仓(来源:state cwd + workspace paths)`);
38
+ }
39
+ if (!roots.length) {
40
+ console.log("未指定仓根且未发现已知仓。用法:node tools/trace-compact.mjs <repoRoot> --dry-run [--days 30]");
41
+ process.exit(0);
42
+ }
43
+
44
+ let totalReclaimed = 0, totalRemoved = 0, failed = 0;
45
+ for (const repo of roots) {
46
+ if (!storeOk(repo)) { console.log(`[skip] ${repo}(无 .aiti-trace)`); continue; }
47
+ const r = await compact(repo, { days: DAYS, keepActiveHours: KEEP_ACTIVE, maxObjectsMB: MAX_MB, dryRun: DRY, extConfigDir, log: console.log });
48
+ if (r.error) { console.log(`[✗] ${repo} → ${r.error}`); failed++; continue; }
49
+ if (DRY) {
50
+ console.log(`[dry-run] ${repo}\n 候删 ${r.branchesCandidate ?? 0} 分支(含 ${r.tagsCandidate ?? 0} tag / ${r.indexesCandidate ?? 0} index):${(r.removedBranches || []).slice(0, 10).join(" ")}${(r.removedBranches || []).length > 10 ? " …" : ""}\n objects 现 ${((r.objectsMBBefore ?? 0) || 0).toFixed(0)}MB · 无主台账/state 可清 ${r.opsStateCleaned ?? 0} 个`);
51
+ } else {
52
+ totalReclaimed += r.reclaimedMB || 0;
53
+ totalRemoved += r.branchesRemoved || 0;
54
+ console.log(`[✓] ${repo} → 删 ${r.branchesRemoved ?? 0} 分支 · ${r.tagsRemoved ?? 0} tag · ${r.indexesRemoved ?? 0} index · 回收 ${(r.reclaimedMB ?? 0).toFixed(0)}MB(objects ${((r.objectsMBBefore ?? 0) || 0).toFixed(0)}→${((r.objectsMBAfter ?? 0) || 0).toFixed(0)}MB · 台账/state 清 ${r.opsStateCleaned ?? 0})`);
55
+ }
56
+ }
57
+ if (!DRY) console.log(`\n完成:共删 ${totalRemoved} 分支 · 回收 ${totalReclaimed.toFixed(0)}MB${failed ? ` · 失败 ${failed} 仓` : ""}`);
58
+ else console.log("\n(dry-run 未动数据;确认后去掉 --dry-run 实跑)");