cursor-guard 4.8.5 → 4.9.0
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/ROADMAP.md +22 -2
- package/package.json +1 -1
- package/references/lib/auto-backup.js +129 -60
- package/references/vscode-extension/dist/{cursor-guard-ide-4.8.5.vsix → cursor-guard-ide-4.9.0.vsix} +0 -0
- package/references/vscode-extension/dist/extension.js +12 -0
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-backup.js +129 -60
- package/references/vscode-extension/dist/lib/poller.js +7 -3
- package/references/vscode-extension/dist/mcp/server.js +1 -1
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +22 -2
- package/references/vscode-extension/extension.js +12 -0
- package/references/vscode-extension/lib/poller.js +7 -3
- package/references/vscode-extension/package.json +1 -1
package/ROADMAP.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
|
|
4
4
|
> 每一代向下兼容,低版本功能永远不废弃。
|
|
5
5
|
>
|
|
6
|
-
> **当前版本**:`V4.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
6
|
+
> **当前版本**:`V4.9.0`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.9.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -734,6 +734,26 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
734
734
|
}
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
+
### V4.9.0:事件驱动架构 — 从轮询到实时响应 ✅
|
|
738
|
+
|
|
739
|
+
| 改造 | 说明 |
|
|
740
|
+
|------|------|
|
|
741
|
+
| **Watcher 事件驱动** | `auto-backup.js` 从 `while+sleep(N秒)` 盲轮询重构为 `fs.watch` 事件驱动。文件变化时 500ms 防抖后立即触发备份;无变化时零消耗。30s 心跳兜底负责配置热加载、retention 清理、告警过期 |
|
|
742
|
+
| **轮询降级** | 若 `fs.watch` 不可用(如 Linux 旧内核),自动回退到原始轮询模式,保持向后兼容 |
|
|
743
|
+
| **配置即时响应** | `.cursor-guard.json` 变化时 `fs.watch` 直接触发配置热加载,不再等待 10 个轮询周期 |
|
|
744
|
+
| **IDE 侧 FileSystemWatcher** | 扩展使用 VSCode 内置 `createFileSystemWatcher` 监听文件变化,1.5s 防抖后触发 Poller 即时刷新 |
|
|
745
|
+
| **Poller 心跳模式** | 从 5s 固定轮询改为 30s 心跳。UI 更新由 FileSystemWatcher 事件驱动,心跳仅做兜底健康同步 |
|
|
746
|
+
| **UI 按需刷新** | Sidebar、StatusBar、TreeView 通过 `poller.onChange()` 订阅,只在有数据变化时才重绘,无变化时零消耗 |
|
|
747
|
+
|
|
748
|
+
**性能对比**:
|
|
749
|
+
|
|
750
|
+
| 指标 | v4.8.x (轮询) | v4.9.0 (事件驱动) |
|
|
751
|
+
|------|---------------|------------------|
|
|
752
|
+
| 备份响应延迟 | 最差 10s | < 500ms |
|
|
753
|
+
| 无变化时 CPU | 每 10s 执行 git status | 零(idle) |
|
|
754
|
+
| UI 刷新频率 | 每 5s 盲刷新 | 事件触发 + 30s 心跳 |
|
|
755
|
+
| 新增依赖 | — | 零(fs.watch + VSCode 内置) |
|
|
756
|
+
|
|
737
757
|
### V4.8.5:修复变更摘要中 protect 范围外文件误标为"删除"的 Bug ✅
|
|
738
758
|
|
|
739
759
|
| 修复 | 说明 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -171,48 +171,24 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
171
171
|
console.log(color.cyan(`[guard] Proactive alert: ON (threshold: ${cfg.alert_thresholds.files_per_window} files / ${cfg.alert_thresholds.window_seconds}s)`));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
//
|
|
175
|
-
console.log('');
|
|
176
|
-
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
177
|
-
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
178
|
-
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
174
|
+
// ── Extracted cycle functions ──────────────────────────────────
|
|
179
175
|
|
|
180
|
-
|
|
181
|
-
if (opts.dashboardPort) {
|
|
176
|
+
async function hotReloadConfig() {
|
|
182
177
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
console.log('');
|
|
192
|
-
|
|
193
|
-
// Main loop
|
|
194
|
-
let cycle = 0;
|
|
195
|
-
while (true) {
|
|
196
|
-
await sleep(interval * 1000);
|
|
197
|
-
cycle++;
|
|
198
|
-
|
|
199
|
-
// Hot-reload config every 10 cycles
|
|
200
|
-
if (cycle % 10 === 0) {
|
|
201
|
-
try {
|
|
202
|
-
const newMtime = fs.statSync(cfgPath).mtimeMs;
|
|
203
|
-
if (newMtime !== cfgMtime) {
|
|
204
|
-
const reload = loadConfig(projectDir);
|
|
205
|
-
if (reload.loaded && !reload.error) {
|
|
206
|
-
cfg = reload.cfg;
|
|
207
|
-
cfgMtime = newMtime;
|
|
208
|
-
tracker = createChangeTracker(cfg);
|
|
209
|
-
logger.info('Config reloaded (file changed)');
|
|
210
|
-
}
|
|
178
|
+
const newMtime = fs.statSync(cfgPath).mtimeMs;
|
|
179
|
+
if (newMtime !== cfgMtime) {
|
|
180
|
+
const reload = loadConfig(projectDir);
|
|
181
|
+
if (reload.loaded && !reload.error) {
|
|
182
|
+
cfg = reload.cfg;
|
|
183
|
+
cfgMtime = newMtime;
|
|
184
|
+
tracker = createChangeTracker(cfg);
|
|
185
|
+
logger.info('Config reloaded (file changed)');
|
|
211
186
|
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
187
|
+
}
|
|
188
|
+
} catch { /* no config file or read error, keep current */ }
|
|
189
|
+
}
|
|
214
190
|
|
|
215
|
-
|
|
191
|
+
async function backupCycle() {
|
|
216
192
|
let hasChanges = false;
|
|
217
193
|
let pendingManifest = null;
|
|
218
194
|
let lastManifest = null;
|
|
@@ -230,11 +206,10 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
230
206
|
}
|
|
231
207
|
} catch (e) {
|
|
232
208
|
logger.error(`Change detection failed: ${e.message}`);
|
|
233
|
-
|
|
209
|
+
return;
|
|
234
210
|
}
|
|
235
|
-
if (!hasChanges)
|
|
211
|
+
if (!hasChanges) return;
|
|
236
212
|
|
|
237
|
-
// Shadow: pre-compute changed file count from manifest diff (already accurate)
|
|
238
213
|
let changedFileCount = 0;
|
|
239
214
|
if (!repo && pendingManifest) {
|
|
240
215
|
if (!lastManifest) {
|
|
@@ -253,7 +228,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
253
228
|
}
|
|
254
229
|
}
|
|
255
230
|
|
|
256
|
-
// Git snapshot via Core — changedFileCount comes from diff-tree (accurate incremental)
|
|
257
231
|
let changedFiles;
|
|
258
232
|
if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
|
|
259
233
|
const context = { trigger: 'auto' };
|
|
@@ -273,7 +247,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
273
247
|
}
|
|
274
248
|
}
|
|
275
249
|
|
|
276
|
-
// V4: Record change event and check for anomalies (after snapshot, using accurate count)
|
|
277
250
|
recordChange(tracker, changedFileCount, changedFiles);
|
|
278
251
|
const anomalyResult = checkAnomaly(tracker);
|
|
279
252
|
if (anomalyResult.anomaly && anomalyResult.alert && !anomalyResult.suppressed) {
|
|
@@ -281,7 +254,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
281
254
|
logger.warn(`ALERT: ${anomalyResult.alert.fileCount} files changed in ${anomalyResult.alert.windowSeconds}s (threshold: ${anomalyResult.alert.threshold})`);
|
|
282
255
|
}
|
|
283
256
|
|
|
284
|
-
// Shadow copy via Core
|
|
285
257
|
if (cfg.backup_strategy === 'shadow' || cfg.backup_strategy === 'both') {
|
|
286
258
|
const shadowResult = createShadowCopy(projectDir, cfg, { backupDir });
|
|
287
259
|
if (shadowResult.status === 'created') {
|
|
@@ -295,28 +267,125 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
295
267
|
logger.error(`Shadow copy failed: ${shadowResult.error}`);
|
|
296
268
|
}
|
|
297
269
|
}
|
|
270
|
+
}
|
|
298
271
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
272
|
+
async function maintenanceCycle() {
|
|
273
|
+
const retResult = cleanShadowRetention(backupDir, cfg);
|
|
274
|
+
if (retResult.removed > 0) {
|
|
275
|
+
logger.log(`Retention (${retResult.mode}): cleaned ${retResult.removed} old snapshot(s)`, 'gray');
|
|
276
|
+
}
|
|
277
|
+
if (retResult.diskWarning === 'critically low') {
|
|
278
|
+
logger.error(`WARNING: disk critically low — ${retResult.diskFreeGB} GB free`);
|
|
279
|
+
} else if (retResult.diskWarning === 'low') {
|
|
280
|
+
logger.warn(`Disk note: ${retResult.diskFreeGB} GB free`);
|
|
281
|
+
}
|
|
310
282
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
283
|
+
if (repo) {
|
|
284
|
+
const gitRetResult = cleanGitRetention(branchRef, gDir, cfg, projectDir);
|
|
285
|
+
if (gitRetResult.rebuilt) {
|
|
286
|
+
logger.log(`Git retention (${gitRetResult.mode}): rebuilt branch with ${gitRetResult.kept} newest snapshots, pruned ${gitRetResult.pruned}. Run 'git gc' to reclaim space.`, 'gray');
|
|
316
287
|
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
clearExpiredAlert(projectDir);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Optional embedded dashboard ────────────────────────────────
|
|
294
|
+
|
|
295
|
+
if (opts.dashboardPort) {
|
|
296
|
+
try {
|
|
297
|
+
const { startDashboardServer } = require('../dashboard/server');
|
|
298
|
+
const { port } = await startDashboardServer([projectDir], { port: opts.dashboardPort, silent: true });
|
|
299
|
+
console.log(color.cyan(`[guard] Dashboard: http://127.0.0.1:${port}`));
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.log(color.yellow(`[guard] Dashboard failed to start: ${e.message}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Try event-driven mode (fs.watch) ───────────────────────────
|
|
306
|
+
|
|
307
|
+
let eventDriven = false;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const watcher = fs.watch(projectDir, { recursive: true });
|
|
311
|
+
let debounceTimer = null;
|
|
312
|
+
let backupRunning = false;
|
|
313
|
+
|
|
314
|
+
function scheduleBackup() {
|
|
315
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
316
|
+
debounceTimer = setTimeout(async () => {
|
|
317
|
+
if (backupRunning) return;
|
|
318
|
+
backupRunning = true;
|
|
319
|
+
try { await backupCycle(); } catch (e) { logger.error(`Backup cycle error: ${e.message}`); }
|
|
320
|
+
backupRunning = false;
|
|
321
|
+
}, 500);
|
|
322
|
+
}
|
|
317
323
|
|
|
324
|
+
watcher.on('change', (_eventType, filename) => {
|
|
325
|
+
if (!filename) return;
|
|
326
|
+
const f = filename.replace(/\\/g, '/');
|
|
327
|
+
if (f.startsWith('.git/') || f.startsWith('.git\\')) return;
|
|
328
|
+
if (f.startsWith('.cursor-guard-backup')) return;
|
|
329
|
+
if (f === '.cursor-guard.json') {
|
|
330
|
+
hotReloadConfig();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
scheduleBackup();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
watcher.on('error', (e) => {
|
|
337
|
+
logger.error(`fs.watch error: ${e.message}`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const origCleanup = cleanup;
|
|
341
|
+
cleanup = function() { try { watcher.close(); } catch {} origCleanup(); };
|
|
342
|
+
|
|
343
|
+
eventDriven = true;
|
|
344
|
+
|
|
345
|
+
console.log('');
|
|
346
|
+
console.log(color.cyan(`[guard] Watching '${projectDir}' — event-driven (fs.watch + 30s heartbeat)`));
|
|
347
|
+
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
348
|
+
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
349
|
+
console.log('');
|
|
350
|
+
|
|
351
|
+
let hbCycle = 0;
|
|
352
|
+
while (true) {
|
|
353
|
+
await sleep(30000);
|
|
354
|
+
hbCycle++;
|
|
355
|
+
if (hbCycle % 2 === 0) await hotReloadConfig();
|
|
356
|
+
if (hbCycle % 4 === 0) {
|
|
357
|
+
try { await maintenanceCycle(); } catch (e) { logger.error(`Maintenance error: ${e.message}`); }
|
|
358
|
+
}
|
|
318
359
|
clearExpiredAlert(projectDir);
|
|
319
360
|
}
|
|
361
|
+
} catch (watchErr) {
|
|
362
|
+
if (!eventDriven) {
|
|
363
|
+
console.log(color.yellow(`[guard] fs.watch not available (${watchErr.message}), using ${interval}s polling fallback`));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Polling fallback ───────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
if (!eventDriven) {
|
|
370
|
+
console.log('');
|
|
371
|
+
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
372
|
+
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
373
|
+
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
374
|
+
console.log('');
|
|
375
|
+
|
|
376
|
+
let cycle = 0;
|
|
377
|
+
while (true) {
|
|
378
|
+
await sleep(interval * 1000);
|
|
379
|
+
cycle++;
|
|
380
|
+
|
|
381
|
+
if (cycle % 10 === 0) await hotReloadConfig();
|
|
382
|
+
|
|
383
|
+
try { await backupCycle(); } catch (e) { logger.error(`Backup cycle error: ${e.message}`); }
|
|
384
|
+
|
|
385
|
+
if (cycle % 10 === 0) {
|
|
386
|
+
try { await maintenanceCycle(); } catch (e) { logger.error(`Maintenance error: ${e.message}`); }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
320
389
|
}
|
|
321
390
|
}
|
|
322
391
|
|
package/references/vscode-extension/dist/{cursor-guard-ide-4.8.5.vsix → cursor-guard-ide-4.9.0.vsix}
RENAMED
|
Binary file
|
|
@@ -189,6 +189,18 @@ async function activate(context) {
|
|
|
189
189
|
vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Event-driven UI refresh: FileSystemWatcher triggers immediate poller refresh
|
|
193
|
+
let _fsRefreshTimer = null;
|
|
194
|
+
const _scheduleRefresh = () => {
|
|
195
|
+
if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
|
|
196
|
+
_fsRefreshTimer = setTimeout(() => poller.forceRefresh(), 1500);
|
|
197
|
+
};
|
|
198
|
+
const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, false, false);
|
|
199
|
+
fileWatcher.onDidChange(_scheduleRefresh);
|
|
200
|
+
fileWatcher.onDidCreate(_scheduleRefresh);
|
|
201
|
+
fileWatcher.onDidDelete(_scheduleRefresh);
|
|
202
|
+
context.subscriptions.push(fileWatcher);
|
|
203
|
+
|
|
192
204
|
context.subscriptions.push(
|
|
193
205
|
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
194
206
|
const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.
|
|
1
|
+
{"version":"4.9.0"}
|
|
@@ -171,48 +171,24 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
171
171
|
console.log(color.cyan(`[guard] Proactive alert: ON (threshold: ${cfg.alert_thresholds.files_per_window} files / ${cfg.alert_thresholds.window_seconds}s)`));
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
//
|
|
175
|
-
console.log('');
|
|
176
|
-
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
177
|
-
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
178
|
-
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
174
|
+
// ── Extracted cycle functions ──────────────────────────────────
|
|
179
175
|
|
|
180
|
-
|
|
181
|
-
if (opts.dashboardPort) {
|
|
176
|
+
async function hotReloadConfig() {
|
|
182
177
|
try {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
console.log('');
|
|
192
|
-
|
|
193
|
-
// Main loop
|
|
194
|
-
let cycle = 0;
|
|
195
|
-
while (true) {
|
|
196
|
-
await sleep(interval * 1000);
|
|
197
|
-
cycle++;
|
|
198
|
-
|
|
199
|
-
// Hot-reload config every 10 cycles
|
|
200
|
-
if (cycle % 10 === 0) {
|
|
201
|
-
try {
|
|
202
|
-
const newMtime = fs.statSync(cfgPath).mtimeMs;
|
|
203
|
-
if (newMtime !== cfgMtime) {
|
|
204
|
-
const reload = loadConfig(projectDir);
|
|
205
|
-
if (reload.loaded && !reload.error) {
|
|
206
|
-
cfg = reload.cfg;
|
|
207
|
-
cfgMtime = newMtime;
|
|
208
|
-
tracker = createChangeTracker(cfg);
|
|
209
|
-
logger.info('Config reloaded (file changed)');
|
|
210
|
-
}
|
|
178
|
+
const newMtime = fs.statSync(cfgPath).mtimeMs;
|
|
179
|
+
if (newMtime !== cfgMtime) {
|
|
180
|
+
const reload = loadConfig(projectDir);
|
|
181
|
+
if (reload.loaded && !reload.error) {
|
|
182
|
+
cfg = reload.cfg;
|
|
183
|
+
cfgMtime = newMtime;
|
|
184
|
+
tracker = createChangeTracker(cfg);
|
|
185
|
+
logger.info('Config reloaded (file changed)');
|
|
211
186
|
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
187
|
+
}
|
|
188
|
+
} catch { /* no config file or read error, keep current */ }
|
|
189
|
+
}
|
|
214
190
|
|
|
215
|
-
|
|
191
|
+
async function backupCycle() {
|
|
216
192
|
let hasChanges = false;
|
|
217
193
|
let pendingManifest = null;
|
|
218
194
|
let lastManifest = null;
|
|
@@ -230,11 +206,10 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
230
206
|
}
|
|
231
207
|
} catch (e) {
|
|
232
208
|
logger.error(`Change detection failed: ${e.message}`);
|
|
233
|
-
|
|
209
|
+
return;
|
|
234
210
|
}
|
|
235
|
-
if (!hasChanges)
|
|
211
|
+
if (!hasChanges) return;
|
|
236
212
|
|
|
237
|
-
// Shadow: pre-compute changed file count from manifest diff (already accurate)
|
|
238
213
|
let changedFileCount = 0;
|
|
239
214
|
if (!repo && pendingManifest) {
|
|
240
215
|
if (!lastManifest) {
|
|
@@ -253,7 +228,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
253
228
|
}
|
|
254
229
|
}
|
|
255
230
|
|
|
256
|
-
// Git snapshot via Core — changedFileCount comes from diff-tree (accurate incremental)
|
|
257
231
|
let changedFiles;
|
|
258
232
|
if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
|
|
259
233
|
const context = { trigger: 'auto' };
|
|
@@ -273,7 +247,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
273
247
|
}
|
|
274
248
|
}
|
|
275
249
|
|
|
276
|
-
// V4: Record change event and check for anomalies (after snapshot, using accurate count)
|
|
277
250
|
recordChange(tracker, changedFileCount, changedFiles);
|
|
278
251
|
const anomalyResult = checkAnomaly(tracker);
|
|
279
252
|
if (anomalyResult.anomaly && anomalyResult.alert && !anomalyResult.suppressed) {
|
|
@@ -281,7 +254,6 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
281
254
|
logger.warn(`ALERT: ${anomalyResult.alert.fileCount} files changed in ${anomalyResult.alert.windowSeconds}s (threshold: ${anomalyResult.alert.threshold})`);
|
|
282
255
|
}
|
|
283
256
|
|
|
284
|
-
// Shadow copy via Core
|
|
285
257
|
if (cfg.backup_strategy === 'shadow' || cfg.backup_strategy === 'both') {
|
|
286
258
|
const shadowResult = createShadowCopy(projectDir, cfg, { backupDir });
|
|
287
259
|
if (shadowResult.status === 'created') {
|
|
@@ -295,28 +267,125 @@ async function runBackup(projectDir, intervalOverride, opts = {}) {
|
|
|
295
267
|
logger.error(`Shadow copy failed: ${shadowResult.error}`);
|
|
296
268
|
}
|
|
297
269
|
}
|
|
270
|
+
}
|
|
298
271
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
272
|
+
async function maintenanceCycle() {
|
|
273
|
+
const retResult = cleanShadowRetention(backupDir, cfg);
|
|
274
|
+
if (retResult.removed > 0) {
|
|
275
|
+
logger.log(`Retention (${retResult.mode}): cleaned ${retResult.removed} old snapshot(s)`, 'gray');
|
|
276
|
+
}
|
|
277
|
+
if (retResult.diskWarning === 'critically low') {
|
|
278
|
+
logger.error(`WARNING: disk critically low — ${retResult.diskFreeGB} GB free`);
|
|
279
|
+
} else if (retResult.diskWarning === 'low') {
|
|
280
|
+
logger.warn(`Disk note: ${retResult.diskFreeGB} GB free`);
|
|
281
|
+
}
|
|
310
282
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
283
|
+
if (repo) {
|
|
284
|
+
const gitRetResult = cleanGitRetention(branchRef, gDir, cfg, projectDir);
|
|
285
|
+
if (gitRetResult.rebuilt) {
|
|
286
|
+
logger.log(`Git retention (${gitRetResult.mode}): rebuilt branch with ${gitRetResult.kept} newest snapshots, pruned ${gitRetResult.pruned}. Run 'git gc' to reclaim space.`, 'gray');
|
|
316
287
|
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
clearExpiredAlert(projectDir);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Optional embedded dashboard ────────────────────────────────
|
|
294
|
+
|
|
295
|
+
if (opts.dashboardPort) {
|
|
296
|
+
try {
|
|
297
|
+
const { startDashboardServer } = require('../dashboard/server');
|
|
298
|
+
const { port } = await startDashboardServer([projectDir], { port: opts.dashboardPort, silent: true });
|
|
299
|
+
console.log(color.cyan(`[guard] Dashboard: http://127.0.0.1:${port}`));
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.log(color.yellow(`[guard] Dashboard failed to start: ${e.message}`));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Try event-driven mode (fs.watch) ───────────────────────────
|
|
306
|
+
|
|
307
|
+
let eventDriven = false;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const watcher = fs.watch(projectDir, { recursive: true });
|
|
311
|
+
let debounceTimer = null;
|
|
312
|
+
let backupRunning = false;
|
|
313
|
+
|
|
314
|
+
function scheduleBackup() {
|
|
315
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
316
|
+
debounceTimer = setTimeout(async () => {
|
|
317
|
+
if (backupRunning) return;
|
|
318
|
+
backupRunning = true;
|
|
319
|
+
try { await backupCycle(); } catch (e) { logger.error(`Backup cycle error: ${e.message}`); }
|
|
320
|
+
backupRunning = false;
|
|
321
|
+
}, 500);
|
|
322
|
+
}
|
|
317
323
|
|
|
324
|
+
watcher.on('change', (_eventType, filename) => {
|
|
325
|
+
if (!filename) return;
|
|
326
|
+
const f = filename.replace(/\\/g, '/');
|
|
327
|
+
if (f.startsWith('.git/') || f.startsWith('.git\\')) return;
|
|
328
|
+
if (f.startsWith('.cursor-guard-backup')) return;
|
|
329
|
+
if (f === '.cursor-guard.json') {
|
|
330
|
+
hotReloadConfig();
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
scheduleBackup();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
watcher.on('error', (e) => {
|
|
337
|
+
logger.error(`fs.watch error: ${e.message}`);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const origCleanup = cleanup;
|
|
341
|
+
cleanup = function() { try { watcher.close(); } catch {} origCleanup(); };
|
|
342
|
+
|
|
343
|
+
eventDriven = true;
|
|
344
|
+
|
|
345
|
+
console.log('');
|
|
346
|
+
console.log(color.cyan(`[guard] Watching '${projectDir}' — event-driven (fs.watch + 30s heartbeat)`));
|
|
347
|
+
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
348
|
+
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
349
|
+
console.log('');
|
|
350
|
+
|
|
351
|
+
let hbCycle = 0;
|
|
352
|
+
while (true) {
|
|
353
|
+
await sleep(30000);
|
|
354
|
+
hbCycle++;
|
|
355
|
+
if (hbCycle % 2 === 0) await hotReloadConfig();
|
|
356
|
+
if (hbCycle % 4 === 0) {
|
|
357
|
+
try { await maintenanceCycle(); } catch (e) { logger.error(`Maintenance error: ${e.message}`); }
|
|
358
|
+
}
|
|
318
359
|
clearExpiredAlert(projectDir);
|
|
319
360
|
}
|
|
361
|
+
} catch (watchErr) {
|
|
362
|
+
if (!eventDriven) {
|
|
363
|
+
console.log(color.yellow(`[guard] fs.watch not available (${watchErr.message}), using ${interval}s polling fallback`));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ── Polling fallback ───────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
if (!eventDriven) {
|
|
370
|
+
console.log('');
|
|
371
|
+
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
372
|
+
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
373
|
+
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
374
|
+
console.log('');
|
|
375
|
+
|
|
376
|
+
let cycle = 0;
|
|
377
|
+
while (true) {
|
|
378
|
+
await sleep(interval * 1000);
|
|
379
|
+
cycle++;
|
|
380
|
+
|
|
381
|
+
if (cycle % 10 === 0) await hotReloadConfig();
|
|
382
|
+
|
|
383
|
+
try { await backupCycle(); } catch (e) { logger.error(`Backup cycle error: ${e.message}`); }
|
|
384
|
+
|
|
385
|
+
if (cycle % 10 === 0) {
|
|
386
|
+
try { await maintenanceCycle(); } catch (e) { logger.error(`Maintenance error: ${e.message}`); }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
320
389
|
}
|
|
321
390
|
}
|
|
322
391
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
6
6
|
|
|
7
7
|
class Poller {
|
|
8
8
|
constructor(dashMgr) {
|
|
@@ -10,6 +10,7 @@ class Poller {
|
|
|
10
10
|
this._timer = null;
|
|
11
11
|
this._listeners = [];
|
|
12
12
|
this._data = new Map();
|
|
13
|
+
this._pollRunning = false;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
get data() { return this._data; }
|
|
@@ -28,7 +29,7 @@ class Poller {
|
|
|
28
29
|
start() {
|
|
29
30
|
if (this._timer) return;
|
|
30
31
|
this._poll();
|
|
31
|
-
this._timer = setInterval(() => this._poll(),
|
|
32
|
+
this._timer = setInterval(() => this._poll(), HEARTBEAT_INTERVAL);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
stop() {
|
|
@@ -36,8 +37,10 @@ class Poller {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
async _poll() {
|
|
39
|
-
if (
|
|
40
|
+
if (this._pollRunning) return;
|
|
41
|
+
this._pollRunning = true;
|
|
40
42
|
try {
|
|
43
|
+
if (!this._dashMgr.running) return;
|
|
41
44
|
const projects = await this._dashMgr.getProjects();
|
|
42
45
|
if (!Array.isArray(projects)) return;
|
|
43
46
|
for (const p of projects) {
|
|
@@ -52,6 +55,7 @@ class Poller {
|
|
|
52
55
|
}
|
|
53
56
|
this._emit();
|
|
54
57
|
} catch { /* non-critical */ }
|
|
58
|
+
finally { this._pollRunning = false; }
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
async forceRefresh() {
|
|
@@ -35568,7 +35568,7 @@ var require_package = __commonJS({
|
|
|
35568
35568
|
"package.json"(exports2, module2) {
|
|
35569
35569
|
module2.exports = {
|
|
35570
35570
|
name: "cursor-guard",
|
|
35571
|
-
version: "4.
|
|
35571
|
+
version: "4.9.0",
|
|
35572
35572
|
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",
|
|
35573
35573
|
keywords: [
|
|
35574
35574
|
"cursor",
|
|
@@ -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.
|
|
5
|
+
"version": "4.9.0",
|
|
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.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
6
|
+
> **当前版本**:`V4.9.0`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.9.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -734,6 +734,26 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
734
734
|
}
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
+
### V4.9.0:事件驱动架构 — 从轮询到实时响应 ✅
|
|
738
|
+
|
|
739
|
+
| 改造 | 说明 |
|
|
740
|
+
|------|------|
|
|
741
|
+
| **Watcher 事件驱动** | `auto-backup.js` 从 `while+sleep(N秒)` 盲轮询重构为 `fs.watch` 事件驱动。文件变化时 500ms 防抖后立即触发备份;无变化时零消耗。30s 心跳兜底负责配置热加载、retention 清理、告警过期 |
|
|
742
|
+
| **轮询降级** | 若 `fs.watch` 不可用(如 Linux 旧内核),自动回退到原始轮询模式,保持向后兼容 |
|
|
743
|
+
| **配置即时响应** | `.cursor-guard.json` 变化时 `fs.watch` 直接触发配置热加载,不再等待 10 个轮询周期 |
|
|
744
|
+
| **IDE 侧 FileSystemWatcher** | 扩展使用 VSCode 内置 `createFileSystemWatcher` 监听文件变化,1.5s 防抖后触发 Poller 即时刷新 |
|
|
745
|
+
| **Poller 心跳模式** | 从 5s 固定轮询改为 30s 心跳。UI 更新由 FileSystemWatcher 事件驱动,心跳仅做兜底健康同步 |
|
|
746
|
+
| **UI 按需刷新** | Sidebar、StatusBar、TreeView 通过 `poller.onChange()` 订阅,只在有数据变化时才重绘,无变化时零消耗 |
|
|
747
|
+
|
|
748
|
+
**性能对比**:
|
|
749
|
+
|
|
750
|
+
| 指标 | v4.8.x (轮询) | v4.9.0 (事件驱动) |
|
|
751
|
+
|------|---------------|------------------|
|
|
752
|
+
| 备份响应延迟 | 最差 10s | < 500ms |
|
|
753
|
+
| 无变化时 CPU | 每 10s 执行 git status | 零(idle) |
|
|
754
|
+
| UI 刷新频率 | 每 5s 盲刷新 | 事件触发 + 30s 心跳 |
|
|
755
|
+
| 新增依赖 | — | 零(fs.watch + VSCode 内置) |
|
|
756
|
+
|
|
737
757
|
### V4.8.5:修复变更摘要中 protect 范围外文件误标为"删除"的 Bug ✅
|
|
738
758
|
|
|
739
759
|
| 修复 | 说明 |
|
|
@@ -189,6 +189,18 @@ async function activate(context) {
|
|
|
189
189
|
vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Event-driven UI refresh: FileSystemWatcher triggers immediate poller refresh
|
|
193
|
+
let _fsRefreshTimer = null;
|
|
194
|
+
const _scheduleRefresh = () => {
|
|
195
|
+
if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
|
|
196
|
+
_fsRefreshTimer = setTimeout(() => poller.forceRefresh(), 1500);
|
|
197
|
+
};
|
|
198
|
+
const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, false, false);
|
|
199
|
+
fileWatcher.onDidChange(_scheduleRefresh);
|
|
200
|
+
fileWatcher.onDidCreate(_scheduleRefresh);
|
|
201
|
+
fileWatcher.onDidDelete(_scheduleRefresh);
|
|
202
|
+
context.subscriptions.push(fileWatcher);
|
|
203
|
+
|
|
192
204
|
context.subscriptions.push(
|
|
193
205
|
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
194
206
|
const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const HEARTBEAT_INTERVAL = 30000;
|
|
6
6
|
|
|
7
7
|
class Poller {
|
|
8
8
|
constructor(dashMgr) {
|
|
@@ -10,6 +10,7 @@ class Poller {
|
|
|
10
10
|
this._timer = null;
|
|
11
11
|
this._listeners = [];
|
|
12
12
|
this._data = new Map();
|
|
13
|
+
this._pollRunning = false;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
get data() { return this._data; }
|
|
@@ -28,7 +29,7 @@ class Poller {
|
|
|
28
29
|
start() {
|
|
29
30
|
if (this._timer) return;
|
|
30
31
|
this._poll();
|
|
31
|
-
this._timer = setInterval(() => this._poll(),
|
|
32
|
+
this._timer = setInterval(() => this._poll(), HEARTBEAT_INTERVAL);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
stop() {
|
|
@@ -36,8 +37,10 @@ class Poller {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
async _poll() {
|
|
39
|
-
if (
|
|
40
|
+
if (this._pollRunning) return;
|
|
41
|
+
this._pollRunning = true;
|
|
40
42
|
try {
|
|
43
|
+
if (!this._dashMgr.running) return;
|
|
41
44
|
const projects = await this._dashMgr.getProjects();
|
|
42
45
|
if (!Array.isArray(projects)) return;
|
|
43
46
|
for (const p of projects) {
|
|
@@ -52,6 +55,7 @@ class Poller {
|
|
|
52
55
|
}
|
|
53
56
|
this._emit();
|
|
54
57
|
} catch { /* non-critical */ }
|
|
58
|
+
finally { this._pollRunning = false; }
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
async forceRefresh() {
|
|
@@ -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.
|
|
5
|
+
"version": "4.9.0",
|
|
6
6
|
"publisher": "zhangqiang8vipp",
|
|
7
7
|
"license": "BUSL-1.1",
|
|
8
8
|
"engines": {
|