cursor-guard 4.9.8 → 4.9.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 (32) hide show
  1. package/README.md +10 -1
  2. package/README.zh-CN.md +10 -1
  3. package/ROADMAP.md +50 -5
  4. package/docs/RELEASE.md +196 -0
  5. package/package.json +69 -68
  6. package/references/dashboard/public/app.js +313 -95
  7. package/references/dashboard/public/style.css +320 -160
  8. package/references/dashboard/server.js +197 -4
  9. package/references/lib/core/backups.js +36 -21
  10. package/references/lib/core/core.test.js +1629 -1484
  11. package/references/lib/core/snapshot.js +59 -8
  12. package/references/mcp/server.js +73 -72
  13. package/references/vscode-extension/{dist/cursor-guard-ide-4.9.8.vsix → cursor-guard-ide-4.9.12.vsix} +0 -0
  14. package/references/vscode-extension/dist/cursor-guard-ide-4.9.12.vsix +0 -0
  15. package/references/vscode-extension/dist/dashboard/public/app.js +313 -95
  16. package/references/vscode-extension/dist/dashboard/public/style.css +320 -160
  17. package/references/vscode-extension/dist/dashboard/server.js +197 -4
  18. package/references/vscode-extension/dist/extension.js +9 -2
  19. package/references/vscode-extension/dist/guard-version.json +1 -1
  20. package/references/vscode-extension/dist/lib/core/backups.js +36 -21
  21. package/references/vscode-extension/dist/lib/core/snapshot.js +59 -8
  22. package/references/vscode-extension/dist/lib/dashboard-manager.js +110 -103
  23. package/references/vscode-extension/dist/lib/poller.js +161 -21
  24. package/references/vscode-extension/dist/lib/sidebar-webview.js +469 -156
  25. package/references/vscode-extension/dist/mcp/server.js +85 -31
  26. package/references/vscode-extension/dist/package.json +1 -1
  27. package/references/vscode-extension/dist/skill/ROADMAP.md +50 -5
  28. package/references/vscode-extension/extension.js +9 -2
  29. package/references/vscode-extension/lib/dashboard-manager.js +110 -103
  30. package/references/vscode-extension/lib/poller.js +161 -21
  31. package/references/vscode-extension/lib/sidebar-webview.js +469 -156
  32. package/references/vscode-extension/package.json +140 -140
@@ -35594,7 +35594,7 @@ var require_package = __commonJS({
35594
35594
  "package.json"(exports2, module2) {
35595
35595
  module2.exports = {
35596
35596
  name: "cursor-guard",
35597
- version: "4.9.8",
35597
+ version: "4.9.12",
35598
35598
  description: "Protects code from accidental AI overwrite or deletion in Cursor IDE \u2014 mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | \u4FDD\u62A4\u4EE3\u7801\u514D\u53D7 Cursor AI \u4EE3\u7406\u610F\u5916\u8986\u5199\u6216\u5220\u9664\u2014\u2014\u5F3A\u5236\u5199\u524D\u5FEB\u7167\u3001\u9884\u89C8\u518D\u6267\u884C\u3001\u672C\u5730 Git \u5B89\u5168\u7F51\u3001\u786E\u5B9A\u6027\u6062\u590D\u3002",
35599
35599
  keywords: [
35600
35600
  "cursor",
@@ -35632,6 +35632,7 @@ var require_package = __commonJS({
35632
35632
  "SKILL.md",
35633
35633
  "README.md",
35634
35634
  "README.zh-CN.md",
35635
+ "docs/RELEASE.md",
35635
35636
  "ROADMAP.md",
35636
35637
  "LICENSE",
35637
35638
  "references/auto-backup.ps1",
@@ -36013,6 +36014,25 @@ var require_snapshot = __commonJS({
36013
36014
  const pad = (n) => String(n).padStart(2, "0");
36014
36015
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
36015
36016
  }
36017
+ var REF_GUARD_AUTO_BACKUP = "refs/guard/auto-backup";
36018
+ var REF_GUARD_SNAPSHOT = "refs/guard/snapshot";
36019
+ function resolveGuardParentHash(cwd, branchRef) {
36020
+ if (branchRef !== REF_GUARD_AUTO_BACKUP && branchRef !== REF_GUARD_SNAPSHOT) {
36021
+ return git(["rev-parse", "--verify", branchRef], { cwd, allowFail: true });
36022
+ }
36023
+ const autoH = git(["rev-parse", "--verify", REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
36024
+ const snapH = git(["rev-parse", "--verify", REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
36025
+ if (!autoH && !snapH) return null;
36026
+ if (!autoH) return snapH;
36027
+ if (!snapH) return autoH;
36028
+ const commitUnix = (h) => {
36029
+ const s = git(["log", "-1", "--format=%ct", h], { cwd, allowFail: true });
36030
+ return s ? parseInt(String(s).trim(), 10) : 0;
36031
+ };
36032
+ const tAuto = commitUnix(autoH);
36033
+ const tSnap = commitUnix(snapH);
36034
+ return tSnap > tAuto ? snapH : autoH;
36035
+ }
36016
36036
  function listIndexFiles(cwd, env) {
36017
36037
  try {
36018
36038
  const out = execFileSync("git", ["ls-files", "--cached"], {
@@ -36078,6 +36098,7 @@ var require_snapshot = __commonJS({
36078
36098
  const cwd = projectDir;
36079
36099
  const gDir = getGitDir2(projectDir);
36080
36100
  if (!gDir) return { status: "error", error: "not a git repository" };
36101
+ const narrowProtect = cfg.protect.length > 0 && !opts.fullWorkspaceSnapshot;
36081
36102
  const guardIndex = path2.join(gDir, "cursor-guard-index");
36082
36103
  const guardIndexLock = guardIndex + ".lock";
36083
36104
  const env = { ...process.env, GIT_INDEX_FILE: guardIndex };
@@ -36090,21 +36111,21 @@ var require_snapshot = __commonJS({
36090
36111
  } catch {
36091
36112
  }
36092
36113
  try {
36093
- const parentHash = git(["rev-parse", "--verify", branchRef], { cwd, allowFail: true });
36094
- if (cfg.protect.length > 0) {
36114
+ const parentHash = resolveGuardParentHash(cwd, branchRef);
36115
+ if (narrowProtect) {
36095
36116
  execFileSync("git", ["add", "-A"], { cwd, env, stdio: "pipe" });
36096
36117
  pruneIndexFiles(cwd, env, (f) => !matchesAny(cfg.protect, f, { strict: true }));
36097
36118
  } else {
36098
36119
  if (parentHash) {
36099
- execFileSync("git", ["read-tree", branchRef], { cwd, env, stdio: "pipe" });
36120
+ execFileSync("git", ["read-tree", parentHash], { cwd, env, stdio: "pipe" });
36100
36121
  }
36101
36122
  execFileSync("git", ["add", "-A"], { cwd, env, stdio: "pipe" });
36102
36123
  }
36103
36124
  pruneIndexFiles(cwd, env, (f) => matchesAny(cfg.ignore, f));
36104
36125
  const secretsExcluded = removeSecretsFromIndex(cfg.secrets_patterns, cwd, env);
36105
36126
  const newTree = execFileSync("git", ["write-tree"], { cwd, env, stdio: "pipe", encoding: "utf-8" }).trim();
36106
- const parentTree = parentHash ? git(["rev-parse", `${branchRef}^{tree}`], { cwd, allowFail: true }) : null;
36107
- if (newTree === parentTree) {
36127
+ const parentTree = parentHash ? git(["rev-parse", `${parentHash}^{tree}`], { cwd, allowFail: true }) : null;
36128
+ if (newTree === parentTree && !opts.allowEmptyTree) {
36108
36129
  return { status: "skipped", reason: "tree unchanged" };
36109
36130
  }
36110
36131
  let changedCount;
@@ -36129,7 +36150,7 @@ var require_snapshot = __commonJS({
36129
36150
  const key = code.startsWith("R") ? "R" : code === "D" ? "D" : code === "A" ? "A" : "M";
36130
36151
  const fileName = filePart.split(" ").pop();
36131
36152
  if (matchesAny(cfg.ignore, fileName) || matchesAny(cfg.ignore, path2.basename(fileName))) continue;
36132
- if (cfg.protect.length > 0 && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
36153
+ if (narrowProtect && !matchesAny(cfg.protect, fileName, { strict: true })) continue;
36133
36154
  groups[key].push(fileName);
36134
36155
  }
36135
36156
  changedCount = Object.values(groups).reduce((sum, arr) => sum + arr.length, 0);
@@ -36168,7 +36189,7 @@ var require_snapshot = __commonJS({
36168
36189
  return s ? `${f} (+${s.added} -${s.deleted})` : f;
36169
36190
  }).join(", ");
36170
36191
  };
36171
- const files = lsInitial.split("\n").filter(Boolean).filter((f) => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path2.basename(f))).filter((f) => cfg.protect.length === 0 || matchesAny(cfg.protect, f, { strict: true }));
36192
+ const files = lsInitial.split("\n").filter(Boolean).filter((f) => !matchesAny(cfg.ignore, f) && !matchesAny(cfg.ignore, path2.basename(f))).filter((f) => !narrowProtect || matchesAny(cfg.protect, f, { strict: true }));
36172
36193
  changedCount = files.length;
36173
36194
  const sample = files.slice(0, 5).join(", ");
36174
36195
  const numstatInit = git(["diff-tree", "--no-commit-id", "--numstat", "-r", EMPTY_TREE, newTree], { cwd, allowFail: true });
@@ -36196,7 +36217,24 @@ var require_snapshot = __commonJS({
36196
36217
  opts.context.changedFileCount = changedCount;
36197
36218
  }
36198
36219
  const ts = formatTimestamp(/* @__PURE__ */ new Date());
36199
- const msg = buildCommitMessage(ts, opts);
36220
+ let msg = buildCommitMessage(ts, opts);
36221
+ const autoTip = git(["rev-parse", "--verify", REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
36222
+ const snapTip = git(["rev-parse", "--verify", REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
36223
+ const autoTipTrim = autoTip ? String(autoTip).trim() : "";
36224
+ const snapTipTrim = snapTip ? String(snapTip).trim() : "";
36225
+ let diffBaseLabel = "initial";
36226
+ if (parentHash) {
36227
+ if (parentHash === autoTipTrim) diffBaseLabel = "auto-backup";
36228
+ else if (parentHash === snapTipTrim) diffBaseLabel = "snapshot";
36229
+ else diffBaseLabel = "other";
36230
+ }
36231
+ const scopeTrailer = narrowProtect ? "narrow" : "full";
36232
+ const guardBlock = `Guard-Diff-Base: ${diffBaseLabel}
36233
+ Guard-Scope: ${scopeTrailer}`;
36234
+ msg = msg.includes("\n\n") ? `${msg}
36235
+ ${guardBlock}` : `${msg}
36236
+
36237
+ ${guardBlock}`;
36200
36238
  const commitArgs = parentHash ? ["commit-tree", newTree, "-p", parentHash, "-m", msg] : ["commit-tree", newTree, "-m", msg];
36201
36239
  const commitHash = execFileSync("git", commitArgs, { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
36202
36240
  if (!commitHash) {
@@ -36351,7 +36389,9 @@ var require_backups = __commonJS({
36351
36389
  "Session": { key: "session" },
36352
36390
  "From": { key: "from" },
36353
36391
  "Restore-To": { key: "restoreTo" },
36354
- "File": { key: "restoreFile" }
36392
+ "File": { key: "restoreFile" },
36393
+ "Guard-Diff-Base": { key: "guardDiffBase" },
36394
+ "Guard-Scope": { key: "guardScope" }
36355
36395
  };
36356
36396
  function parseCommitTrailers(body) {
36357
36397
  if (!body) return {};
@@ -36361,7 +36401,9 @@ var require_backups = __commonJS({
36361
36401
  const m = line.match(pattern);
36362
36402
  if (m) {
36363
36403
  const def = TRAILER_MAP[m[1]];
36364
- result[def.key] = def.parse ? def.parse(m[2]) : m[2];
36404
+ const raw = m[2].replace(/\r/g, "");
36405
+ const val = def.parse ? def.parse(raw) : raw.trim();
36406
+ result[def.key] = typeof val === "string" ? val.trim() : val;
36365
36407
  }
36366
36408
  }
36367
36409
  return result;
@@ -36436,25 +36478,36 @@ var require_backups = __commonJS({
36436
36478
  sources.push(entry);
36437
36479
  }
36438
36480
  }
36439
- const snapshotHash = git(["rev-parse", "--verify", "refs/guard/snapshot"], { cwd: projectDir, allowFail: true });
36440
- if (snapshotHash) {
36441
- const snapLog = git(["log", "-1", "--format=%aI%B", "refs/guard/snapshot"], { cwd: projectDir, allowFail: true });
36442
- const snapParts = snapLog ? snapLog.split("") : [];
36443
- const ts = snapParts[0] || null;
36444
- const snapBody = snapParts[1] || "";
36445
- const snapTrailers = parseCommitTrailers(snapBody);
36446
- const snapSubject = snapBody.split("\n")[0] || "";
36447
- const include = !beforeDate || ts && Date.parse(ts) <= beforeDate.getTime();
36448
- if (include) {
36449
- sources.push({
36450
- type: "git-snapshot",
36451
- ref: "refs/guard/snapshot",
36452
- commitHash: snapshotHash,
36453
- shortHash: snapshotHash.substring(0, 7),
36454
- timestamp: ts,
36455
- message: snapSubject || void 0,
36456
- ...snapTrailers
36457
- });
36481
+ const snapRef = "refs/guard/snapshot";
36482
+ const snapshotExists = git(["rev-parse", "--verify", snapRef], { cwd: projectDir, allowFail: true });
36483
+ if (snapshotExists) {
36484
+ const snapLogArgs = ["log", snapRef, "--format=%H%aI%B", `-${limit}`];
36485
+ if (opts.before) snapLogArgs.push(`--before=${opts.before}`);
36486
+ if (opts.file) snapLogArgs.push("--", opts.file);
36487
+ const snapOut = git(snapLogArgs, { cwd: projectDir, allowFail: true });
36488
+ if (snapOut) {
36489
+ for (const record of snapOut.split("").filter((r) => r.trim())) {
36490
+ const parts = record.split("");
36491
+ if (parts.length < 3) continue;
36492
+ const hash = parts[0].trim();
36493
+ const timestamp = parts[1];
36494
+ const body = parts[2];
36495
+ const subject = body.split("\n")[0];
36496
+ const trailers = parseCommitTrailers(body);
36497
+ if (beforeDate && timestamp) {
36498
+ const ms = Date.parse(timestamp);
36499
+ if (!isNaN(ms) && ms > beforeDate.getTime()) continue;
36500
+ }
36501
+ sources.push({
36502
+ type: "git-snapshot",
36503
+ ref: snapRef,
36504
+ commitHash: hash,
36505
+ shortHash: hash.substring(0, 7),
36506
+ timestamp,
36507
+ message: subject || void 0,
36508
+ ...trailers
36509
+ });
36510
+ }
36458
36511
  }
36459
36512
  }
36460
36513
  }
@@ -38198,7 +38251,8 @@ server.tool(
38198
38251
  results.git = createGitSnapshot(resolved, cfg, {
38199
38252
  branchRef: "refs/guard/snapshot",
38200
38253
  message: message || `guard: manual snapshot ${(/* @__PURE__ */ new Date()).toISOString()}`,
38201
- context
38254
+ context,
38255
+ allowEmptyTree: true
38202
38256
  });
38203
38257
  }
38204
38258
  if (effectiveStrategy === "shadow" || effectiveStrategy === "both") {
@@ -2,7 +2,7 @@
2
2
  "name": "cursor-guard-ide",
3
3
  "displayName": "Cursor Guard",
4
4
  "description": "AI code protection dashboard embedded in your IDE — real-time alerts, backup history, one-click snapshots",
5
- "version": "4.9.8",
5
+ "version": "4.9.12",
6
6
  "publisher": "zhangqiang8vipp",
7
7
  "license": "BUSL-1.1",
8
8
  "engines": {
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.9.8`
7
- > **文档状态**:`V2` ~ `V4.9.8` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.9.12`
7
+ > **文档状态**:`V2` ~ `V4.9.12` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,6 +734,34 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
+ ### V4.9.9:独立发版指南(人 + AI)✅
738
+ | 能力 | 说明 |
739
+ |------|------|
740
+ | **docs/RELEASE.md** | 中英双语完整发版流程:版本源、VSIX、Git、GitHub Release、npm OTP;**Windows 下 `gh` 须用 UTF-8 文件 + `--notes-file`** 避免 Release 正文乱码;专设「给其他 AI 助手」摘要 |
741
+ | **入口** | README / README.zh-CN 顶部与发版小节链接;`print-release-checklist.js` 末尾提示;`package.json` `files` 纳入该文档以便 npm tarball 携带 |
742
+
743
+ ### V4.9.11:仪表盘推送同步 + 备份元数据/交互收口 ✅
744
+
745
+ | 能力 | 说明 |
746
+ |------|------|
747
+ | **推送优先于轮询** | Dashboard HTTP:`GET /api/events`(SSE)+ `fs.watch` 监听 `refs/guard/*`、`.cursor-guard-backup/`、`.git` 下 `cursor-guard-alert.json` / `cursor-guard.lock`;`refs` 下首次出现 `guard` 时自动重挂 watcher。前端 `EventSource` 收 `guard-changed` 即刷新;仅保留 3min(前台)/ 10min(后台标签)兜底拉取 |
748
+ | **扩展 Poller** | 以 `fs.watch` 同上路径触发 `forceRefresh`,HTTP 心跳改为约 **3min** 兜底(不再高频轮询) |
749
+ | **活跃告警忽略** | `POST /api/dismiss-alert` + 仪表盘/侧边栏「忽略」;dismiss 后主动 SSE 广播 |
750
+ | **备份列表可读性** | Git 提交 `Guard-Diff-Base` / `Guard-Scope` trailer;`listBackups` 解析 + 仪表盘「范围/基线」列;`refs/guard/snapshot` 完整历史;`Summary` trailer 去 `\r` + 列表行合计 `+/-` 行数 |
751
+ | **5.x 后续(已记入下方 V5 规划)** | watcher 进程与仪表盘 **进程内/管道事件**、MCP `notifications`、统一事件总线——见「V5.x 深化:仪表盘与 watcher 联动」 |
752
+
753
+ ### V4.9.12:仪表盘 Intent 展示修复 ✅
754
+
755
+ | 能力 | 说明 |
756
+ |------|------|
757
+ | **变更列 Intent** | 仅当备份类型为 **`git-pre-restore` / `shadow-pre-restore`** 时使用 `From` + `Restore-To` 琥珀色恢复条;自动备份与手动快照不再被误解析的 `from`/`restoreTo` 抢占,**Intent** 蓝条正常显示 |
758
+
759
+ ### V4.9.10:扩展版本线对齐 ✅
760
+
761
+ | 说明 |
762
+ |------|
763
+ | IDE 扩展与根目录 `package.json` 版本对齐发版(VSIX 构建链 `build-vsix.js` 以根版本为准)。 |
764
+
737
765
  ### V4.9.8:发版流程文档化 + 侧边栏品牌占位 ✅
738
766
  | 能力 | 说明 |
739
767
  |------|------|
@@ -1137,6 +1165,19 @@ Phase 2 (V5.x):
1137
1165
  └── 完整审计链闭环,支持 restore_from_event
1138
1166
  ```
1139
1167
 
1168
+ ### V5.x 深化:仪表盘与 watcher 联动(建议纳入 5.x 更新)
1169
+
1170
+ > V4.9.11 已用 **文件系统监听 + SSE** 实现「备份/告警变更 → 仪表盘几乎实时更新」,无需依赖高频 HTTP 轮询。以下能力适合在 **V5.x** 与 embedded watcher / 审计事件链一并演进:
1171
+
1172
+ | 方向 | 说明 |
1173
+ |------|------|
1174
+ | **跨进程显式通知** | 独立 `auto-backup` 子进程在 snapshot 成功后,通过 **本地 socket / 命名管道 / 向 dashboard 进程发 UDP** 等方式推送 `backup_completed`(含 project root、shortHash、timestamp),补全 `fs.watch` 在少数 OS/网络盘上的漏事件 |
1175
+ | **MCP / LSP 式通知** | 若 MCP 宿主支持 `notifications`,由 server 在备份/告警状态变化时向客户端推送,IDE 与 Web 共用同一语义字段 |
1176
+ | **统一事件模型** | 将 `guard-changed`、intent 事件、审计 JSONL 写入映射到同一 **event type + payload schema**,便于 dashboard 时间线与 `restore_from_event` 对齐 |
1177
+ | **可观测性** | 可选:推送失败降级策略、SSE 连接数上限、watch 句柄泄漏检测(长会话桌面环境) |
1178
+
1179
+ **原则**:V4.9.x 保持「零新常驻依赖 + 本地回环」;V5.x 可在确认架构(embedded watcher)后引入更重的 IPC,且不破坏现有 SSE/Watch 降级路径。
1180
+
1140
1181
  #### 与"意图队列"方案的本质区别
1141
1182
 
1142
1183
  | | 意图队列(已否决) | embedded watcher + begin_edit |
@@ -1620,7 +1661,11 @@ V4.3.5 ───── ✅ Summary 增量 diff-tree 修复 + 变更列堆叠布
1620
1661
  V4.4.0 ───── ✅ V4 收官:首次快照 summary + doctor 完整性/retention 检查 + init 升级检测
1621
1662
  V4.9.0 ───── ✅ 事件驱动 watcher + 实时侧边栏计时
1622
1663
  V4.9.5 ───── ✅ 修复 `.git` 写入导致的疯狂自触发备份
1623
- V4.9.8 ─────发版清单(README 双语 + checklist 脚本)+ 侧边栏品牌图 ← 当前版本
1664
+ V4.9.12 ────仪表盘变更列:Intent 仅被真正的恢复前快照类型让位给 From→Restore-To;常规备份正确显示 Intent ← 当前版本
1665
+ V4.9.11 ──── ✅ 仪表盘 SSE + fs.watch 推送;Poller 事件驱动;告警忽略;Guard trailer 列;Summary/列表行数;snapshot 历史列表
1666
+ V4.9.10 ──── ✅ 扩展版本线与根包对齐(VSIX 发版链)
1667
+ V4.9.9 ───── ✅ docs/RELEASE.md 发版指南(人 + AI)+ gh UTF-8 说明
1668
+ V4.9.8 ───── ✅ 发版清单(README 双语 + checklist 脚本)+ 侧边栏品牌图
1624
1669
  V4.9.7 ───── ✅ 预警体验打磨 + 语言同步 + watcher 单例保护
1625
1670
 
1626
1671
  │ 前提:MVP 已跑通,需要把提示从“能用”打磨到“适合常开”
@@ -1709,5 +1754,5 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
1709
1754
 
1710
1755
  ---
1711
1756
 
1712
- *最后更新:2026-03-22*
1713
- *版本:v1.8(V4.9.8,在 V4.9.7 基础上增加发版流程文档与侧边栏品牌资源;历史能力含事件驱动 watcher、自触发备份反馈环修复、`pre_warning` 事先预警 MVP)*
1757
+ *最后更新:2026-03-23*
1758
+ *版本:v1.11(V4.9.12:仪表盘 Intent 列修复;V4.9.11 SSE/fs.watch、Poller 事件驱动等;历史含 V4.9.9 发版指南、V4.9.0 事件驱动 watcher、`pre_warning` 等)*
@@ -414,9 +414,16 @@ async function activate(context) {
414
414
  const projectPath = folders[0].uri.fsPath;
415
415
  const result = await dashMgr.snapshotNow(projectPath);
416
416
  if (result?.status === 'created') {
417
- vscode.window.showInformationMessage(`Cursor Guard: snapshot created (${result.changedCount || 0} changes)`);
417
+ const n = result.changedCount ?? 0;
418
+ const msg =
419
+ n > 0
420
+ ? `Cursor Guard: snapshot created (${n} file change(s))`
421
+ : 'Cursor Guard: snapshot created (restore point saved; no file changes since last snapshot)';
422
+ vscode.window.showInformationMessage(msg);
418
423
  } else if (result?.status === 'unchanged' || result?.status === 'skipped') {
419
- vscode.window.showInformationMessage('Cursor Guard: no changes to snapshot');
424
+ vscode.window.showInformationMessage(
425
+ `Cursor Guard: no snapshot created (${result.reason || 'unchanged'})`
426
+ );
420
427
  } else if (result?.status === 'error') {
421
428
  vscode.window.showWarningMessage(`Cursor Guard: ${result.error}`);
422
429
  } else {
@@ -3,18 +3,18 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const http = require('http');
6
- const { spawn } = require('child_process');
7
- const { guardPath } = require('./paths');
8
-
9
- const CONFIG_FILE = '.cursor-guard.json';
10
- const WATCHER_START_GRACE_MS = 8000;
11
-
12
- class DashboardManager {
13
- constructor() {
14
- this._instance = null;
15
- this._serverModule = null;
16
- this._startingWatchers = new Map();
17
- }
6
+ const { spawn } = require('child_process');
7
+ const { guardPath } = require('./paths');
8
+
9
+ const CONFIG_FILE = '.cursor-guard.json';
10
+ const WATCHER_START_GRACE_MS = 8000;
11
+
12
+ class DashboardManager {
13
+ constructor() {
14
+ this._instance = null;
15
+ this._serverModule = null;
16
+ this._startingWatchers = new Map();
17
+ }
18
18
 
19
19
  get running() { return !!this._instance; }
20
20
  get port() { return this._instance?.port; }
@@ -85,108 +85,115 @@ class DashboardManager {
85
85
  return this.fetchApi(`/api/backup-files?id=${projectId}&hash=${hash}`);
86
86
  }
87
87
 
88
- async snapshotNow(projectPath) {
88
+ async snapshotNow(projectPath) {
89
89
  if (!projectPath) return;
90
90
  try {
91
91
  const { createGitSnapshot } = require(guardPath('lib', 'core', 'snapshot'));
92
92
  const { loadConfig } = require(guardPath('lib', 'utils'));
93
93
  const { cfg } = loadConfig(projectPath);
94
- return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
94
+ return createGitSnapshot(projectPath, cfg, {
95
+ branchRef: 'refs/guard/snapshot',
96
+ message: `guard: manual snapshot via IDE (${new Date().toISOString()})`,
97
+ context: { trigger: 'manual' },
98
+ allowEmptyTree: true,
99
+ // Match user expectation: "Snapshot now" captures the whole repo (except ignore/secrets), not only protect globs
100
+ fullWorkspaceSnapshot: true,
101
+ });
95
102
  } catch (e) {
96
103
  return { status: 'error', error: e.message };
97
104
  }
98
- }
99
-
100
- _getWatcherLockPath(projectPath) {
101
- try {
102
- const { gitAvailable, isGitRepo, gitDir: getGitDir } = require(guardPath('lib', 'utils'));
103
- const repo = gitAvailable() && isGitRepo(projectPath);
104
- if (repo) {
105
- const gDir = getGitDir(projectPath);
106
- if (gDir) return path.join(gDir, 'cursor-guard.lock');
107
- }
108
- } catch { /* ignore */ }
109
- return path.join(projectPath, '.cursor-guard-backup', 'cursor-guard.lock');
110
- }
111
-
112
- _getPendingWatcherPid(projectPath) {
113
- const pending = this._startingWatchers.get(projectPath);
114
- if (!pending) return null;
115
- try {
116
- process.kill(pending.pid, 0);
117
- return pending.pid;
118
- } catch {
119
- this._startingWatchers.delete(projectPath);
120
- return null;
121
- }
122
- }
123
-
124
- _clearPendingWatcher(projectPath, pid) {
125
- const pending = this._startingWatchers.get(projectPath);
126
- if (!pending) return;
127
- if (pid == null || pending.pid === pid) {
128
- this._startingWatchers.delete(projectPath);
129
- }
130
- }
131
-
132
- startWatcher(projectPath) {
133
- if (!projectPath) return null;
134
- const existingPid = this.getWatcherPid(projectPath);
135
- if (existingPid) return existingPid;
136
- const cliScript = guardPath('bin', 'cursor-guard-backup.js');
105
+ }
106
+
107
+ _getWatcherLockPath(projectPath) {
108
+ try {
109
+ const { gitAvailable, isGitRepo, gitDir: getGitDir } = require(guardPath('lib', 'utils'));
110
+ const repo = gitAvailable() && isGitRepo(projectPath);
111
+ if (repo) {
112
+ const gDir = getGitDir(projectPath);
113
+ if (gDir) return path.join(gDir, 'cursor-guard.lock');
114
+ }
115
+ } catch { /* ignore */ }
116
+ return path.join(projectPath, '.cursor-guard-backup', 'cursor-guard.lock');
117
+ }
118
+
119
+ _getPendingWatcherPid(projectPath) {
120
+ const pending = this._startingWatchers.get(projectPath);
121
+ if (!pending) return null;
122
+ try {
123
+ process.kill(pending.pid, 0);
124
+ return pending.pid;
125
+ } catch {
126
+ this._startingWatchers.delete(projectPath);
127
+ return null;
128
+ }
129
+ }
130
+
131
+ _clearPendingWatcher(projectPath, pid) {
132
+ const pending = this._startingWatchers.get(projectPath);
133
+ if (!pending) return;
134
+ if (pid == null || pending.pid === pid) {
135
+ this._startingWatchers.delete(projectPath);
136
+ }
137
+ }
138
+
139
+ startWatcher(projectPath) {
140
+ if (!projectPath) return null;
141
+ const existingPid = this.getWatcherPid(projectPath);
142
+ if (existingPid) return existingPid;
143
+ const cliScript = guardPath('bin', 'cursor-guard-backup.js');
137
144
  const child = spawn(process.execPath, [cliScript, '--path', projectPath], {
138
145
  cwd: projectPath,
139
146
  stdio: 'ignore',
140
- detached: true,
141
- env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
142
- });
143
- this._startingWatchers.set(projectPath, { pid: child.pid, startedAt: Date.now() });
144
- const clearPending = () => this._clearPendingWatcher(projectPath, child.pid);
145
- child.once('exit', clearPending);
146
- child.once('error', clearPending);
147
- setTimeout(clearPending, WATCHER_START_GRACE_MS);
148
- child.unref();
149
- return child.pid;
150
- }
151
-
152
- stopWatcher(projectPath) {
153
- if (!projectPath) return false;
154
- const pendingPid = this._getPendingWatcherPid(projectPath);
155
- if (pendingPid) {
156
- try { process.kill(pendingPid, 'SIGTERM'); } catch { /* ignore */ }
157
- this._clearPendingWatcher(projectPath, pendingPid);
158
- }
159
- try {
160
- const lockPath = this._getWatcherLockPath(projectPath);
161
- if (!fs.existsSync(lockPath)) return false;
162
- const content = fs.readFileSync(lockPath, 'utf-8');
163
- const pidMatch = content.match(/pid=(\d+)/);
164
- if (pidMatch) {
165
- process.kill(parseInt(pidMatch[1], 10), 'SIGTERM');
166
- }
167
- try { fs.unlinkSync(lockPath); } catch { /* ok */ }
168
- return true;
169
- } catch { /* ok */ }
170
- return !!pendingPid;
171
- }
172
-
173
- getWatcherPid(projectPath) {
174
- const pendingPid = this._getPendingWatcherPid(projectPath);
175
- if (pendingPid) return pendingPid;
176
- try {
177
- const lockPath = this._getWatcherLockPath(projectPath);
178
- if (!fs.existsSync(lockPath)) return null;
179
- const content = fs.readFileSync(lockPath, 'utf-8');
180
- const pidMatch = content.match(/pid=(\d+)/);
181
- if (pidMatch) {
182
- const pid = parseInt(pidMatch[1], 10);
183
- process.kill(pid, 0);
184
- this._clearPendingWatcher(projectPath, pid);
185
- return pid;
186
- }
187
- } catch { /* not running */ }
188
- return null;
189
- }
147
+ detached: true,
148
+ env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
149
+ });
150
+ this._startingWatchers.set(projectPath, { pid: child.pid, startedAt: Date.now() });
151
+ const clearPending = () => this._clearPendingWatcher(projectPath, child.pid);
152
+ child.once('exit', clearPending);
153
+ child.once('error', clearPending);
154
+ setTimeout(clearPending, WATCHER_START_GRACE_MS);
155
+ child.unref();
156
+ return child.pid;
157
+ }
158
+
159
+ stopWatcher(projectPath) {
160
+ if (!projectPath) return false;
161
+ const pendingPid = this._getPendingWatcherPid(projectPath);
162
+ if (pendingPid) {
163
+ try { process.kill(pendingPid, 'SIGTERM'); } catch { /* ignore */ }
164
+ this._clearPendingWatcher(projectPath, pendingPid);
165
+ }
166
+ try {
167
+ const lockPath = this._getWatcherLockPath(projectPath);
168
+ if (!fs.existsSync(lockPath)) return false;
169
+ const content = fs.readFileSync(lockPath, 'utf-8');
170
+ const pidMatch = content.match(/pid=(\d+)/);
171
+ if (pidMatch) {
172
+ process.kill(parseInt(pidMatch[1], 10), 'SIGTERM');
173
+ }
174
+ try { fs.unlinkSync(lockPath); } catch { /* ok */ }
175
+ return true;
176
+ } catch { /* ok */ }
177
+ return !!pendingPid;
178
+ }
179
+
180
+ getWatcherPid(projectPath) {
181
+ const pendingPid = this._getPendingWatcherPid(projectPath);
182
+ if (pendingPid) return pendingPid;
183
+ try {
184
+ const lockPath = this._getWatcherLockPath(projectPath);
185
+ if (!fs.existsSync(lockPath)) return null;
186
+ const content = fs.readFileSync(lockPath, 'utf-8');
187
+ const pidMatch = content.match(/pid=(\d+)/);
188
+ if (pidMatch) {
189
+ const pid = parseInt(pidMatch[1], 10);
190
+ process.kill(pid, 0);
191
+ this._clearPendingWatcher(projectPath, pid);
192
+ return pid;
193
+ }
194
+ } catch { /* not running */ }
195
+ return null;
196
+ }
190
197
 
191
198
  dispose() {
192
199
  this._instance = null;