cursor-guard 4.9.9 → 4.9.15

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 (35) hide show
  1. package/README.md +697 -697
  2. package/README.zh-CN.md +696 -696
  3. package/ROADMAP.md +1775 -1720
  4. package/SKILL.md +631 -629
  5. package/docs/RELEASE.md +197 -196
  6. package/docs/SNAPSHOT-BOOKMARK.md +47 -0
  7. package/package.json +70 -69
  8. package/references/dashboard/public/app.js +2079 -1832
  9. package/references/dashboard/public/style.css +1660 -1573
  10. package/references/dashboard/server.js +197 -4
  11. package/references/lib/core/backups.js +509 -492
  12. package/references/lib/core/core.test.js +1761 -1616
  13. package/references/lib/core/snapshot.js +441 -369
  14. package/references/mcp/mcp.test.js +381 -362
  15. package/references/mcp/server.js +404 -347
  16. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.9.vsix → cursor-guard-ide-4.9.15.vsix} +0 -0
  17. package/references/vscode-extension/dist/dashboard/public/app.js +2079 -1832
  18. package/references/vscode-extension/dist/dashboard/public/style.css +1660 -1573
  19. package/references/vscode-extension/dist/dashboard/server.js +197 -4
  20. package/references/vscode-extension/dist/extension.js +780 -704
  21. package/references/vscode-extension/dist/guard-version.json +1 -1
  22. package/references/vscode-extension/dist/lib/auto-setup.js +201 -192
  23. package/references/vscode-extension/dist/lib/core/backups.js +509 -492
  24. package/references/vscode-extension/dist/lib/core/snapshot.js +441 -369
  25. package/references/vscode-extension/dist/lib/poller.js +161 -21
  26. package/references/vscode-extension/dist/lib/sidebar-webview.js +22 -0
  27. package/references/vscode-extension/dist/mcp/server.js +152 -35
  28. package/references/vscode-extension/dist/package.json +7 -1
  29. package/references/vscode-extension/dist/skill/ROADMAP.md +1775 -1720
  30. package/references/vscode-extension/dist/skill/SKILL.md +631 -629
  31. package/references/vscode-extension/extension.js +780 -704
  32. package/references/vscode-extension/lib/auto-setup.js +201 -192
  33. package/references/vscode-extension/lib/poller.js +161 -21
  34. package/references/vscode-extension/lib/sidebar-webview.js +22 -0
  35. package/references/vscode-extension/package.json +146 -140
@@ -1,192 +1,201 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function detectIdeDir(vscode) {
7
- const appName = (vscode.env.appName || '').toLowerCase();
8
- const home = process.env.USERPROFILE || process.env.HOME || '';
9
-
10
- const ideDirs = [];
11
-
12
- if (appName.includes('cursor')) {
13
- ideDirs.push('.cursor');
14
- } else if (appName.includes('windsurf')) {
15
- ideDirs.push('.windsurf');
16
- } else if (appName.includes('trae')) {
17
- ideDirs.push('.trae');
18
- }
19
-
20
- ideDirs.push('.cursor', '.windsurf', '.vscode');
21
-
22
- const seen = new Set();
23
- for (const dir of ideDirs) {
24
- if (seen.has(dir)) continue;
25
- seen.add(dir);
26
- const full = path.join(home, dir);
27
- if (fs.existsSync(full)) return { dirName: dir, homePath: full };
28
- }
29
-
30
- return { dirName: ideDirs[0], homePath: path.join(home, ideDirs[0]) };
31
- }
32
-
33
- function getExtensionRoot(context) {
34
- return context.extensionPath;
35
- }
36
-
37
- function findBundledSkill(extRoot) {
38
- const skillDir = path.join(extRoot, 'skill');
39
- if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) return skillDir;
40
- const fromRefs = path.resolve(extRoot, '..', '..', 'SKILL.md');
41
- if (fs.existsSync(fromRefs)) return path.resolve(extRoot, '..', '..');
42
- return null;
43
- }
44
-
45
- async function autoSetup(context, vscode) {
46
- const extRoot = getExtensionRoot(context);
47
- const { dirName, homePath } = detectIdeDir(vscode);
48
- const workspaceFolders = vscode.workspace.workspaceFolders;
49
- const wsRoot = workspaceFolders?.[0]?.uri.fsPath;
50
-
51
- const actions = [];
52
-
53
- try { actions.push(...autoInstallSkill(extRoot, homePath, dirName)); } catch { /* non-critical */ }
54
- try { actions.push(...autoRegisterMcp(extRoot, homePath, wsRoot)); } catch { /* non-critical */ }
55
- if (wsRoot) {
56
- try { actions.push(...autoCreateConfig(extRoot, wsRoot)); } catch { /* non-critical */ }
57
- }
58
-
59
- if (actions.length > 0) {
60
- vscode.window.showInformationMessage(`Cursor Guard: auto-setup complete — ${actions.join(', ')}`);
61
- }
62
- }
63
-
64
- function autoInstallSkill(extRoot, homePath, dirName) {
65
- const actions = [];
66
- const skillSrc = findBundledSkill(extRoot);
67
- if (!skillSrc) return actions;
68
-
69
- const skillTarget = path.join(homePath, 'skills', 'cursor-guard');
70
- fs.mkdirSync(skillTarget, { recursive: true });
71
-
72
- // ── Install/update SKILL.md and ROADMAP.md ──
73
- const skillMdSrc = path.join(skillSrc, 'SKILL.md');
74
- const skillMdTarget = path.join(skillTarget, 'SKILL.md');
75
- if (fs.existsSync(skillMdSrc) && !fs.existsSync(skillMdTarget)) {
76
- fs.copyFileSync(skillMdSrc, skillMdTarget);
77
- actions.push('SKILL.md installed');
78
- }
79
-
80
- const roadmapSrc = path.join(skillSrc, 'ROADMAP.md');
81
- const roadmapDst = path.join(skillTarget, 'ROADMAP.md');
82
- if (fs.existsSync(roadmapSrc) && !fs.existsSync(roadmapDst)) {
83
- fs.copyFileSync(roadmapSrc, roadmapDst);
84
- }
85
-
86
- // ── Ensure references/ junction exists (runs even for existing installations) ──
87
- const refsTarget = path.join(skillTarget, 'references');
88
- const refsIsJunction = _isSymlinkOrJunction(refsTarget);
89
- const refsIsPlainDir = !refsIsJunction && fs.existsSync(refsTarget);
90
- const refsMissingRuntime = refsIsPlainDir && !fs.existsSync(path.join(refsTarget, 'mcp'));
91
-
92
- if (!fs.existsSync(refsTarget) || refsMissingRuntime) {
93
- // Remove old plain directory if it only has docs (no runtime)
94
- if (refsMissingRuntime) {
95
- try { fs.rmSync(refsTarget, { recursive: true, force: true }); } catch { /* ok */ }
96
- }
97
- try {
98
- fs.symlinkSync(extRoot, refsTarget, 'junction');
99
- actions.push('references/ linked');
100
- } catch {
101
- fs.mkdirSync(refsTarget, { recursive: true });
102
- _copyDocFiles(skillSrc, refsTarget);
103
- }
104
- }
105
-
106
- // ── Ensure package.json exists ──
107
- const pkgDst = path.join(skillTarget, 'package.json');
108
- if (!fs.existsSync(pkgDst)) {
109
- const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
110
- const guardVer = path.join(extRoot, 'guard-version.json');
111
- if (fs.existsSync(pkgSrc)) {
112
- fs.copyFileSync(pkgSrc, pkgDst);
113
- } else if (fs.existsSync(guardVer)) {
114
- fs.copyFileSync(guardVer, pkgDst);
115
- }
116
- }
117
-
118
- return actions;
119
- }
120
-
121
- function _isSymlinkOrJunction(p) {
122
- try {
123
- const stat = fs.lstatSync(p);
124
- return stat.isSymbolicLink();
125
- } catch { return false; }
126
- }
127
-
128
- function _copyDocFiles(skillSrc, refsTarget) {
129
- const docs = [
130
- 'config-reference.md', 'config-reference.zh-CN.md',
131
- 'recovery.md', 'cursor-guard.schema.json', 'cursor-guard.example.json',
132
- ];
133
- for (const f of docs) {
134
- const src = path.join(skillSrc, f);
135
- if (fs.existsSync(src)) fs.copyFileSync(src, path.join(refsTarget, f));
136
- }
137
- }
138
-
139
- function autoRegisterMcp(extRoot, homePath, wsRoot) {
140
- const actions = [];
141
- const mcpServerPath = path.join(extRoot, 'mcp', 'server.js');
142
- if (!fs.existsSync(mcpServerPath)) return actions;
143
-
144
- const mcpJsonPaths = [
145
- wsRoot ? path.join(wsRoot, '.cursor', 'mcp.json') : null,
146
- wsRoot ? path.join(wsRoot, '.windsurf', 'mcp.json') : null,
147
- path.join(homePath, 'mcp.json'),
148
- ].filter(Boolean);
149
-
150
- for (const mcpJsonPath of mcpJsonPaths) {
151
- const dir = path.dirname(mcpJsonPath);
152
- if (!fs.existsSync(dir)) continue;
153
-
154
- let mcpConfig = { mcpServers: {} };
155
- if (fs.existsSync(mcpJsonPath)) {
156
- try { mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')); } catch { continue; }
157
- }
158
-
159
- if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
160
- if (mcpConfig.mcpServers['cursor-guard']) return actions;
161
-
162
- mcpConfig.mcpServers['cursor-guard'] = {
163
- command: 'node',
164
- args: [mcpServerPath.replace(/\\/g, '/')],
165
- };
166
-
167
- fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
168
- actions.push('MCP registered');
169
- return actions;
170
- }
171
-
172
- return actions;
173
- }
174
-
175
- function autoCreateConfig(extRoot, wsRoot) {
176
- const actions = [];
177
- const configTarget = path.join(wsRoot, '.cursor-guard.json');
178
- if (fs.existsSync(configTarget)) return actions;
179
-
180
- const exampleSrc = path.join(extRoot, 'skill', 'cursor-guard.example.json');
181
- const exampleFromRefs = path.join(extRoot, '..', 'cursor-guard.example.json');
182
- const src = fs.existsSync(exampleSrc) ? exampleSrc : fs.existsSync(exampleFromRefs) ? exampleFromRefs : null;
183
-
184
- if (src) {
185
- fs.copyFileSync(src, configTarget);
186
- actions.push('.cursor-guard.json created');
187
- }
188
-
189
- return actions;
190
- }
191
-
192
- module.exports = { autoSetup, detectIdeDir };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function detectIdeDir(vscode) {
7
+ const appName = (vscode.env.appName || '').toLowerCase();
8
+ const home = process.env.USERPROFILE || process.env.HOME || '';
9
+
10
+ const ideDirs = [];
11
+
12
+ if (appName.includes('cursor')) {
13
+ ideDirs.push('.cursor');
14
+ } else if (appName.includes('windsurf')) {
15
+ ideDirs.push('.windsurf');
16
+ } else if (appName.includes('trae')) {
17
+ ideDirs.push('.trae');
18
+ }
19
+
20
+ ideDirs.push('.cursor', '.windsurf', '.vscode');
21
+
22
+ const seen = new Set();
23
+ for (const dir of ideDirs) {
24
+ if (seen.has(dir)) continue;
25
+ seen.add(dir);
26
+ const full = path.join(home, dir);
27
+ if (fs.existsSync(full)) return { dirName: dir, homePath: full };
28
+ }
29
+
30
+ return { dirName: ideDirs[0], homePath: path.join(home, ideDirs[0]) };
31
+ }
32
+
33
+ function getExtensionRoot(context) {
34
+ return context.extensionPath;
35
+ }
36
+
37
+ function findBundledSkill(extRoot) {
38
+ const skillDir = path.join(extRoot, 'skill');
39
+ if (fs.existsSync(path.join(skillDir, 'SKILL.md'))) return skillDir;
40
+ const fromRefs = path.resolve(extRoot, '..', '..', 'SKILL.md');
41
+ if (fs.existsSync(fromRefs)) return path.resolve(extRoot, '..', '..');
42
+ return null;
43
+ }
44
+
45
+ async function autoSetup(context, vscode) {
46
+ const extRoot = getExtensionRoot(context);
47
+ const { dirName, homePath } = detectIdeDir(vscode);
48
+ const workspaceFolders = vscode.workspace.workspaceFolders;
49
+ const wsRoot = workspaceFolders?.[0]?.uri.fsPath;
50
+
51
+ const actions = [];
52
+ let skillPath = null;
53
+
54
+ try {
55
+ const skillResult = installAgentSkill(extRoot, homePath, dirName);
56
+ actions.push(...skillResult.actions);
57
+ skillPath = skillResult.skillPath;
58
+ } catch { /* non-critical */ }
59
+ try { actions.push(...autoRegisterMcp(extRoot, homePath, wsRoot)); } catch { /* non-critical */ }
60
+ if (wsRoot) {
61
+ try { actions.push(...autoCreateConfig(extRoot, wsRoot)); } catch { /* non-critical */ }
62
+ }
63
+
64
+ return { actions, skillPath };
65
+ }
66
+
67
+ /** Copy / link bundled Agent Skill into ~/.cursor/skills/cursor-guard (or IDE-specific home). */
68
+ function installAgentSkill(extRoot, homePath, dirName) {
69
+ const actions = [];
70
+ const skillSrc = findBundledSkill(extRoot);
71
+ if (!skillSrc) return { actions, skillPath: null };
72
+
73
+ const skillTarget = path.join(homePath, 'skills', 'cursor-guard');
74
+ fs.mkdirSync(skillTarget, { recursive: true });
75
+
76
+ // ── Install/update SKILL.md and ROADMAP.md ──
77
+ const skillMdSrc = path.join(skillSrc, 'SKILL.md');
78
+ const skillMdTarget = path.join(skillTarget, 'SKILL.md');
79
+ if (fs.existsSync(skillMdSrc) && !fs.existsSync(skillMdTarget)) {
80
+ fs.copyFileSync(skillMdSrc, skillMdTarget);
81
+ actions.push('SKILL.md installed');
82
+ }
83
+
84
+ const roadmapSrc = path.join(skillSrc, 'ROADMAP.md');
85
+ const roadmapDst = path.join(skillTarget, 'ROADMAP.md');
86
+ if (fs.existsSync(roadmapSrc) && !fs.existsSync(roadmapDst)) {
87
+ fs.copyFileSync(roadmapSrc, roadmapDst);
88
+ }
89
+
90
+ // ── Ensure references/ junction exists (runs even for existing installations) ──
91
+ const refsTarget = path.join(skillTarget, 'references');
92
+ const refsIsJunction = _isSymlinkOrJunction(refsTarget);
93
+ const refsIsPlainDir = !refsIsJunction && fs.existsSync(refsTarget);
94
+ const refsMissingRuntime = refsIsPlainDir && !fs.existsSync(path.join(refsTarget, 'mcp'));
95
+
96
+ if (!fs.existsSync(refsTarget) || refsMissingRuntime) {
97
+ // Remove old plain directory if it only has docs (no runtime)
98
+ if (refsMissingRuntime) {
99
+ try { fs.rmSync(refsTarget, { recursive: true, force: true }); } catch { /* ok */ }
100
+ }
101
+ try {
102
+ fs.symlinkSync(extRoot, refsTarget, 'junction');
103
+ actions.push('references/ linked');
104
+ } catch {
105
+ fs.mkdirSync(refsTarget, { recursive: true });
106
+ _copyDocFiles(skillSrc, refsTarget);
107
+ }
108
+ }
109
+
110
+ // ── Ensure package.json exists ──
111
+ const pkgDst = path.join(skillTarget, 'package.json');
112
+ if (!fs.existsSync(pkgDst)) {
113
+ const pkgSrc = path.join(extRoot, '..', '..', 'package.json');
114
+ const guardVer = path.join(extRoot, 'guard-version.json');
115
+ if (fs.existsSync(pkgSrc)) {
116
+ fs.copyFileSync(pkgSrc, pkgDst);
117
+ } else if (fs.existsSync(guardVer)) {
118
+ fs.copyFileSync(guardVer, pkgDst);
119
+ }
120
+ }
121
+
122
+ return { actions, skillPath: skillTarget };
123
+ }
124
+
125
+ function _isSymlinkOrJunction(p) {
126
+ try {
127
+ const stat = fs.lstatSync(p);
128
+ return stat.isSymbolicLink();
129
+ } catch { return false; }
130
+ }
131
+
132
+ function _copyDocFiles(skillSrc, refsTarget) {
133
+ const docs = [
134
+ 'config-reference.md', 'config-reference.zh-CN.md',
135
+ 'recovery.md', 'cursor-guard.schema.json', 'cursor-guard.example.json',
136
+ ];
137
+ for (const f of docs) {
138
+ const src = path.join(skillSrc, f);
139
+ if (fs.existsSync(src)) fs.copyFileSync(src, path.join(refsTarget, f));
140
+ }
141
+ }
142
+
143
+ function autoRegisterMcp(extRoot, homePath, wsRoot) {
144
+ const actions = [];
145
+ const mcpServerPath = path.join(extRoot, 'mcp', 'server.js');
146
+ if (!fs.existsSync(mcpServerPath)) return actions;
147
+
148
+ const mcpJsonPaths = [
149
+ wsRoot ? path.join(wsRoot, '.cursor', 'mcp.json') : null,
150
+ wsRoot ? path.join(wsRoot, '.windsurf', 'mcp.json') : null,
151
+ path.join(homePath, 'mcp.json'),
152
+ ].filter(Boolean);
153
+
154
+ for (const mcpJsonPath of mcpJsonPaths) {
155
+ const dir = path.dirname(mcpJsonPath);
156
+ if (!fs.existsSync(dir)) continue;
157
+
158
+ let mcpConfig = { mcpServers: {} };
159
+ if (fs.existsSync(mcpJsonPath)) {
160
+ try { mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf-8')); } catch { continue; }
161
+ }
162
+
163
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
164
+ if (mcpConfig.mcpServers['cursor-guard']) return actions;
165
+
166
+ mcpConfig.mcpServers['cursor-guard'] = {
167
+ command: 'node',
168
+ args: [mcpServerPath.replace(/\\/g, '/')],
169
+ };
170
+
171
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
172
+ actions.push('MCP registered');
173
+ return actions;
174
+ }
175
+
176
+ return actions;
177
+ }
178
+
179
+ function autoCreateConfig(extRoot, wsRoot) {
180
+ const actions = [];
181
+ const configTarget = path.join(wsRoot, '.cursor-guard.json');
182
+ if (fs.existsSync(configTarget)) return actions;
183
+
184
+ const exampleSrc = path.join(extRoot, 'skill', 'cursor-guard.example.json');
185
+ const exampleFromRefs = path.join(extRoot, '..', 'cursor-guard.example.json');
186
+ const src = fs.existsSync(exampleSrc) ? exampleSrc : fs.existsSync(exampleFromRefs) ? exampleFromRefs : null;
187
+
188
+ if (src) {
189
+ fs.copyFileSync(src, configTarget);
190
+ actions.push('.cursor-guard.json created');
191
+ }
192
+
193
+ return actions;
194
+ }
195
+
196
+ module.exports = {
197
+ autoSetup,
198
+ detectIdeDir,
199
+ installAgentSkill,
200
+ getExtensionRoot,
201
+ };
@@ -1,8 +1,23 @@
1
1
  'use strict';
2
2
 
3
- const vscode = require('vscode');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { guardPath } = require('./paths');
4
6
 
5
- const HEARTBEAT_INTERVAL = 30000;
7
+ /** Safety net if fs.watch misses (platform limits); primary updates are filesystem events. */
8
+ const FALLBACK_POLL_MS = 180000;
9
+
10
+ function tryWatchDirRecursive(dir, cb) {
11
+ try {
12
+ return fs.watch(dir, { recursive: true }, cb);
13
+ } catch {
14
+ try {
15
+ return fs.watch(dir, cb);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ }
6
21
 
7
22
  class Poller {
8
23
  constructor(dashMgr) {
@@ -11,6 +26,12 @@ class Poller {
11
26
  this._listeners = [];
12
27
  this._data = new Map();
13
28
  this._pollRunning = false;
29
+ this._pollAgainAfter = false;
30
+ this._pollWaiters = [];
31
+ this._fsWatchers = [];
32
+ this._watchedRegistryKey = '';
33
+ this._fsDebounceTimer = null;
34
+ this._reattachTimer = null;
14
35
  }
15
36
 
16
37
  get data() { return this._data; }
@@ -26,10 +47,107 @@ class Poller {
26
47
  }
27
48
  }
28
49
 
50
+ _teardownFsWatchers() {
51
+ if (this._reattachTimer) {
52
+ clearTimeout(this._reattachTimer);
53
+ this._reattachTimer = null;
54
+ }
55
+ if (this._fsDebounceTimer) {
56
+ clearTimeout(this._fsDebounceTimer);
57
+ this._fsDebounceTimer = null;
58
+ }
59
+ for (const w of this._fsWatchers) {
60
+ try { w.close(); } catch { /* ignore */ }
61
+ }
62
+ this._fsWatchers = [];
63
+ }
64
+
65
+ _scheduleFsRefresh() {
66
+ if (this._fsDebounceTimer) clearTimeout(this._fsDebounceTimer);
67
+ this._fsDebounceTimer = setTimeout(() => {
68
+ this._fsDebounceTimer = null;
69
+ void this.forceRefresh();
70
+ }, 400);
71
+ }
72
+
73
+ _syncFileWatchers() {
74
+ const reg = this._dashMgr?.registry;
75
+ if (!reg) return;
76
+ const key = [...reg.entries()].map(([k, v]) => `${k}:${v._path}`).sort().join('|');
77
+ if (key === this._watchedRegistryKey) return;
78
+ this._watchedRegistryKey = key;
79
+
80
+ this._teardownFsWatchers();
81
+
82
+ let utils;
83
+ try {
84
+ utils = require(guardPath('lib', 'utils'));
85
+ } catch {
86
+ return;
87
+ }
88
+ const { isGitRepo, gitDir: getGitDir } = utils;
89
+
90
+ for (const proj of reg.values()) {
91
+ const pp = proj._path;
92
+ const pid = proj.id;
93
+
94
+ if (isGitRepo(pp)) {
95
+ const gDir = getGitDir(pp);
96
+ if (gDir && fs.existsSync(gDir)) {
97
+ try {
98
+ const w = fs.watch(gDir, (ev, fname) => {
99
+ if (fname === 'cursor-guard-alert.json' || fname === 'cursor-guard.lock') {
100
+ this._scheduleFsRefresh();
101
+ }
102
+ });
103
+ this._fsWatchers.push(w);
104
+ } catch { /* ignore */ }
105
+
106
+ const refsDir = path.join(gDir, 'refs');
107
+ if (fs.existsSync(refsDir)) {
108
+ try {
109
+ const w = fs.watch(refsDir, (ev, fname) => {
110
+ if (!fname) return;
111
+ if (fname === 'guard' || fname.startsWith('guard')) {
112
+ this._scheduleFsRefresh();
113
+ if (!this._reattachTimer) {
114
+ this._reattachTimer = setTimeout(() => {
115
+ this._reattachTimer = null;
116
+ this._watchedRegistryKey = '';
117
+ this._syncFileWatchers();
118
+ }, 600);
119
+ }
120
+ }
121
+ });
122
+ this._fsWatchers.push(w);
123
+ } catch { /* ignore */ }
124
+ }
125
+
126
+ const guardDir = path.join(gDir, 'refs', 'guard');
127
+ if (fs.existsSync(guardDir)) {
128
+ const w = tryWatchDirRecursive(guardDir, () => this._scheduleFsRefresh());
129
+ if (w) this._fsWatchers.push(w);
130
+ const preRestoreDir = path.join(guardDir, 'pre-restore');
131
+ if (fs.existsSync(preRestoreDir)) {
132
+ const w2 = tryWatchDirRecursive(preRestoreDir, () => this._scheduleFsRefresh());
133
+ if (w2) this._fsWatchers.push(w2);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ const backupDir = path.join(pp, '.cursor-guard-backup');
140
+ if (fs.existsSync(backupDir)) {
141
+ const w = tryWatchDirRecursive(backupDir, () => this._scheduleFsRefresh());
142
+ if (w) this._fsWatchers.push(w);
143
+ }
144
+ }
145
+ }
146
+
29
147
  start() {
30
148
  if (this._timer) return;
31
- this._poll();
32
- this._timer = setInterval(() => this._poll(), HEARTBEAT_INTERVAL);
149
+ void this._poll();
150
+ this._timer = setInterval(() => void this._poll(), FALLBACK_POLL_MS);
33
151
  }
34
152
 
35
153
  stop() {
@@ -37,33 +155,55 @@ class Poller {
37
155
  }
38
156
 
39
157
  async _poll() {
40
- if (this._pollRunning) return;
158
+ if (this._pollRunning) {
159
+ this._pollAgainAfter = true;
160
+ return;
161
+ }
41
162
  this._pollRunning = true;
42
163
  try {
43
- if (!this._dashMgr.running) return;
44
- const projects = await this._dashMgr.getProjects();
45
- if (!Array.isArray(projects)) return;
46
- for (const p of projects) {
47
- const fullData = await this._dashMgr.getFullPageData(p.id);
48
- this._data.set(p.id, {
49
- ...p,
50
- dashboard: fullData?.dashboard || null,
51
- backups: fullData?.backups || [],
52
- scope: fullData?.scope || null,
53
- doctor: fullData?.doctor || null,
54
- });
55
- }
56
- this._emit();
164
+ do {
165
+ this._pollAgainAfter = false;
166
+ if (!this._dashMgr.running) break;
167
+ const projects = await this._dashMgr.getProjects();
168
+ if (!Array.isArray(projects)) break;
169
+ for (const p of projects) {
170
+ const fullData = await this._dashMgr.getFullPageData(p.id);
171
+ this._data.set(p.id, {
172
+ ...p,
173
+ dashboard: fullData?.dashboard || null,
174
+ backups: fullData?.backups || [],
175
+ scope: fullData?.scope || null,
176
+ doctor: fullData?.doctor || null,
177
+ });
178
+ }
179
+ this._emit();
180
+ } while (this._pollAgainAfter);
57
181
  } catch { /* non-critical */ }
58
- finally { this._pollRunning = false; }
182
+ finally {
183
+ this._pollRunning = false;
184
+ const waiters = this._pollWaiters.splice(0);
185
+ for (const fn of waiters) {
186
+ try { fn(); } catch { /* ignore */ }
187
+ }
188
+ try {
189
+ this._syncFileWatchers();
190
+ } catch { /* ignore */ }
191
+ }
59
192
  }
60
193
 
61
194
  async forceRefresh() {
62
- await this._poll();
195
+ return new Promise(resolve => {
196
+ this._pollWaiters.push(resolve);
197
+ this._pollAgainAfter = true;
198
+ if (!this._pollRunning) {
199
+ void this._poll();
200
+ }
201
+ });
63
202
  }
64
203
 
65
204
  dispose() {
66
205
  this.stop();
206
+ this._teardownFsWatchers();
67
207
  this._listeners = [];
68
208
  }
69
209
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const vscode = require('vscode');
4
4
  const { getLocale, setLocale } = require('./locale');
5
+ const { guardPath } = require('./paths');
5
6
 
6
7
  class SidebarDashboardProvider {
7
8
  constructor(poller, context) {
@@ -41,6 +42,19 @@ class SidebarDashboardProvider {
41
42
  this._postLocale();
42
43
  }
43
44
  if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
45
+ if (msg.cmd === 'clearAlert' && msg.projectId) {
46
+ (async () => {
47
+ try {
48
+ const reg = this._poller._dashMgr?.registry;
49
+ const entry = reg?.get(msg.projectId);
50
+ if (entry?._path) {
51
+ const { clearAlert } = require(guardPath('lib', 'core', 'anomaly'));
52
+ clearAlert(entry._path);
53
+ await this._poller.forceRefresh();
54
+ }
55
+ } catch { /* ignore */ }
56
+ })();
57
+ }
44
58
  });
45
59
 
46
60
  webviewView.onDidChangeVisibility(() => {
@@ -1058,6 +1072,7 @@ const I18N = {
1058
1072
  'actions.openDashboard': '打开看板',
1059
1073
  'actions.restore': '恢复',
1060
1074
  'actions.viewDetails': '查看详情',
1075
+ 'actions.dismissAlert': '忽略告警',
1061
1076
  'actions.snapshot': '立即快照',
1062
1077
  'actions.watcherOn': '停止 Watcher',
1063
1078
  'actions.watcherOff': '启动 Watcher',
@@ -1290,6 +1305,13 @@ function render(projects) {
1290
1305
  vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd });
1291
1306
  });
1292
1307
  });
1308
+
1309
+ root.querySelectorAll('[data-dismiss-alert]').forEach(btn => {
1310
+ btn.addEventListener('click', () => {
1311
+ const pid = btn.getAttribute('data-dismiss-alert');
1312
+ if (pid) vscode.postMessage({ cmd: 'clearAlert', projectId: pid });
1313
+ });
1314
+ });
1293
1315
  }
1294
1316
 
1295
1317
  function renderProject(dashboard, projectId) {