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
@@ -14,6 +14,7 @@ function coreDeps() {
14
14
  runDiagnostics: require('../lib/core/doctor').runDiagnostics,
15
15
  listBackups: require('../lib/core/backups').listBackups,
16
16
  getBackupFiles: require('../lib/core/backups').getBackupFiles,
17
+ clearAlert: require('../lib/core/anomaly').clearAlert,
17
18
  };
18
19
  }
19
20
 
@@ -196,7 +197,21 @@ function handleApi(pathname, query, registry, res, req) {
196
197
  return json(res, { error: `Project directory not accessible: ${project.pathLabel}` }, 500);
197
198
  }
198
199
 
199
- const { getDashboard, runDiagnostics, listBackups, getBackupFiles } = coreDeps();
200
+ const { getDashboard, runDiagnostics, listBackups, getBackupFiles, clearAlert } = coreDeps();
201
+
202
+ if (pathname === '/api/dismiss-alert') {
203
+ if (req.method !== 'POST') {
204
+ res.writeHead(405);
205
+ return res.end('Method Not Allowed');
206
+ }
207
+ try {
208
+ clearAlert(pp);
209
+ if (_instance?.hub && id) broadcastGuardChanged(_instance.hub, id);
210
+ return json(res, { ok: true });
211
+ } catch (e) {
212
+ return json(res, { ok: false, error: e.message }, 500);
213
+ }
214
+ }
200
215
 
201
216
  if (pathname === '/api/page-data') {
202
217
  const scope = query.get('scope');
@@ -245,6 +260,164 @@ function handleApi(pathname, query, registry, res, req) {
245
260
  return notFound(res);
246
261
  }
247
262
 
263
+ /* ── Live push (SSE) + fs.watch on Guard refs / backups ─────── */
264
+
265
+ function broadcastGuardChanged(hub, projectId) {
266
+ if (!hub?.sseClients?.size) return;
267
+ const line = `data: ${JSON.stringify({ type: 'guard-changed', projectId })}\n\n`;
268
+ for (const c of hub.sseClients) {
269
+ try {
270
+ if (c.projectId && c.projectId !== projectId) continue;
271
+ c.res.write(line);
272
+ } catch {
273
+ hub.sseClients.delete(c);
274
+ }
275
+ }
276
+ }
277
+
278
+ function teardownGuardWatchersOnly(hub) {
279
+ if (hub._reattachTimer) {
280
+ clearTimeout(hub._reattachTimer);
281
+ hub._reattachTimer = null;
282
+ }
283
+ if (hub.guardDebounce) {
284
+ for (const t of hub.guardDebounce.values()) clearTimeout(t);
285
+ hub.guardDebounce.clear();
286
+ }
287
+ for (const w of hub.guardWatchers) {
288
+ try { w.close(); } catch { /* ignore */ }
289
+ }
290
+ hub.guardWatchers = [];
291
+ }
292
+
293
+ function tryWatchDirRecursive(dir, cb) {
294
+ try {
295
+ return fs.watch(dir, { recursive: true }, cb);
296
+ } catch {
297
+ try {
298
+ return fs.watch(dir, cb);
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+ }
304
+
305
+ /**
306
+ * When Git refs under refs/guard/, shadow backup dir, or alert/lock change, notify SSE clients.
307
+ */
308
+ function attachGuardWatchers(hub) {
309
+ const { isGitRepo, gitDir: getGitDir } = require('../lib/utils');
310
+ teardownGuardWatchersOnly(hub);
311
+ if (!hub.guardDebounce) hub.guardDebounce = new Map();
312
+
313
+ function schedule(projectId) {
314
+ const prev = hub.guardDebounce.get(projectId);
315
+ if (prev) clearTimeout(prev);
316
+ hub.guardDebounce.set(projectId, setTimeout(() => {
317
+ hub.guardDebounce.delete(projectId);
318
+ broadcastGuardChanged(hub, projectId);
319
+ }, 400));
320
+ }
321
+
322
+ for (const proj of hub.registry.values()) {
323
+ const pp = proj._path;
324
+ const pid = proj.id;
325
+
326
+ if (isGitRepo(pp)) {
327
+ const gDir = getGitDir(pp);
328
+ if (gDir && fs.existsSync(gDir)) {
329
+ try {
330
+ const w = fs.watch(gDir, (ev, fname) => {
331
+ if (fname === 'cursor-guard-alert.json' || fname === 'cursor-guard.lock') schedule(pid);
332
+ });
333
+ hub.guardWatchers.push(w);
334
+ } catch { /* ignore */ }
335
+
336
+ const refsDir = path.join(gDir, 'refs');
337
+ if (fs.existsSync(refsDir)) {
338
+ try {
339
+ const w = fs.watch(refsDir, (ev, fname) => {
340
+ if (!fname) return;
341
+ if (fname === 'guard' || fname.startsWith('guard')) {
342
+ schedule(pid);
343
+ if (!hub._reattachTimer) {
344
+ hub._reattachTimer = setTimeout(() => {
345
+ hub._reattachTimer = null;
346
+ attachGuardWatchers(hub);
347
+ }, 600);
348
+ }
349
+ }
350
+ });
351
+ hub.guardWatchers.push(w);
352
+ } catch { /* ignore */ }
353
+ }
354
+
355
+ const guardDir = path.join(gDir, 'refs', 'guard');
356
+ if (fs.existsSync(guardDir)) {
357
+ const w = tryWatchDirRecursive(guardDir, () => schedule(pid));
358
+ if (w) hub.guardWatchers.push(w);
359
+ const preRestoreDir = path.join(guardDir, 'pre-restore');
360
+ if (fs.existsSync(preRestoreDir)) {
361
+ const w2 = tryWatchDirRecursive(preRestoreDir, () => schedule(pid));
362
+ if (w2) hub.guardWatchers.push(w2);
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ const backupDir = path.join(pp, '.cursor-guard-backup');
369
+ if (fs.existsSync(backupDir)) {
370
+ const w = tryWatchDirRecursive(backupDir, () => schedule(pid));
371
+ if (w) hub.guardWatchers.push(w);
372
+ }
373
+ }
374
+ }
375
+
376
+ function teardownLivePush(hub) {
377
+ teardownGuardWatchersOnly(hub);
378
+ for (const c of hub.sseClients) {
379
+ try { c.res.end(); } catch { /* ignore */ }
380
+ }
381
+ hub.sseClients.clear();
382
+ if (hub.ssePingTimer) {
383
+ clearInterval(hub.ssePingTimer);
384
+ hub.ssePingTimer = null;
385
+ }
386
+ }
387
+
388
+ function openSseStream(query, hub, res, req) {
389
+ const subscribeId = query.get('id') || null;
390
+ res.writeHead(200, {
391
+ 'Content-Type': 'text/event-stream; charset=utf-8',
392
+ 'Cache-Control': 'no-store, no-cache',
393
+ 'Connection': 'keep-alive',
394
+ 'X-Accel-Buffering': 'no',
395
+ 'Access-Control-Allow-Origin': '*',
396
+ });
397
+ res.write(': connected\n\n');
398
+ const client = { res, projectId: subscribeId };
399
+ hub.sseClients.add(client);
400
+ req.on('close', () => {
401
+ hub.sseClients.delete(client);
402
+ if (hub.sseClients.size === 0 && hub.ssePingTimer) {
403
+ clearInterval(hub.ssePingTimer);
404
+ hub.ssePingTimer = null;
405
+ }
406
+ });
407
+ if (!hub.ssePingTimer) {
408
+ hub.ssePingTimer = setInterval(() => {
409
+ if (hub.sseClients.size === 0) return;
410
+ for (const c of hub.sseClients) {
411
+ try {
412
+ c.res.write(': ping\n\n');
413
+ } catch {
414
+ hub.sseClients.delete(c);
415
+ }
416
+ }
417
+ }, 25000);
418
+ }
419
+ }
420
+
248
421
  /* ── Server (singleton) ─────────────────────────────────────── */
249
422
 
250
423
  let _instance = null;
@@ -280,8 +453,11 @@ function _mergeProjects(registry, paths) {
280
453
  function startDashboardServer(paths, opts = {}) {
281
454
  if (_instance) {
282
455
  const added = _mergeProjects(_instance.registry, paths);
283
- if (added > 0 && !opts.silent) {
284
- console.log(` [dashboard] Hot-added ${added} project(s) — total: ${_instance.registry.size} on port ${_instance.port}`);
456
+ if (added > 0) {
457
+ attachGuardWatchers(_instance.hub);
458
+ if (!opts.silent) {
459
+ console.log(` [dashboard] Hot-added ${added} project(s) — total: ${_instance.registry.size} on port ${_instance.port}`);
460
+ }
285
461
  }
286
462
  return Promise.resolve(_instance);
287
463
  }
@@ -290,6 +466,14 @@ function startDashboardServer(paths, opts = {}) {
290
466
  const silent = opts.silent || false;
291
467
  const registry = buildRegistry(paths);
292
468
  const token = crypto.randomBytes(16).toString('hex');
469
+ const hub = {
470
+ registry,
471
+ token,
472
+ sseClients: new Set(),
473
+ guardWatchers: [],
474
+ guardDebounce: new Map(),
475
+ ssePingTimer: null,
476
+ };
293
477
 
294
478
  return new Promise((resolve, reject) => {
295
479
  let currentPort = port;
@@ -326,6 +510,13 @@ function startDashboardServer(paths, opts = {}) {
326
510
  res.writeHead(403);
327
511
  return res.end('Forbidden: invalid token');
328
512
  }
513
+ if (parsed.pathname === '/api/events') {
514
+ if (req.method !== 'GET') {
515
+ res.writeHead(405);
516
+ return res.end('Method Not Allowed');
517
+ }
518
+ return openSseStream(parsed.searchParams, hub, res, req);
519
+ }
329
520
  handleApi(parsed.pathname, parsed.searchParams, registry, res, req);
330
521
  } else {
331
522
  if (req.method !== 'GET') { res.writeHead(405); return res.end('Method Not Allowed'); }
@@ -345,7 +536,8 @@ function startDashboardServer(paths, opts = {}) {
345
536
 
346
537
  server.on('listening', () => {
347
538
  const addr = server.address();
348
- _instance = { server, port: addr.port, registry, token };
539
+ _instance = { server, port: addr.port, registry, token, hub };
540
+ attachGuardWatchers(hub);
349
541
  if (!silent) {
350
542
  console.log('');
351
543
  console.log(' Cursor Guard Dashboard');
@@ -371,6 +563,7 @@ async function restartDashboard() {
371
563
  const paths = [..._instance.registry.values()].map(p => p._path);
372
564
  const port = _instance.port;
373
565
 
566
+ if (_instance.hub) teardownLivePush(_instance.hub);
374
567
  _instance.server.close();
375
568
  _instance = null;
376
569