cursor-guard 4.6.2 → 4.7.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/README.md CHANGED
@@ -274,7 +274,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
274
274
  node references\dashboard\server.js --path "D:\MyProject"
275
275
  ```
276
276
 
277
- Then open `http://127.0.0.1:3120` in your browser.
277
+ Then open `http://127.0.0.1:3120` in your browser. Or use the **IDE Extension** (see below) to embed the dashboard directly in your editor.
278
278
 
279
279
  Features:
280
280
 
@@ -287,6 +287,29 @@ Features:
287
287
  - **Security** — binds to `127.0.0.1` only (not exposed to LAN), API uses project IDs instead of raw file paths, static file serving restricted to `public/` directory
288
288
  - **Zero extra dependencies** — uses Node.js built-in `http` module + existing cursor-guard core modules
289
289
 
290
+ ### IDE Extension (VSCode / Cursor)
291
+
292
+ Embed the full dashboard directly inside your IDE — no browser needed.
293
+
294
+ The extension is located at `references/vscode-extension/`. To install:
295
+
296
+ ```bash
297
+ # From the cursor-guard skill directory
298
+ cd references/vscode-extension
299
+ # Install as a development extension in your IDE
300
+ code --install-extension .
301
+ ```
302
+
303
+ Features:
304
+
305
+ - **WebView Dashboard** — full dashboard embedded as an editor tab, identical to the browser version
306
+ - **Status Bar Indicator** — shows `Guard: OK` (green) or `Guard: 22 files!` (yellow) in real-time
307
+ - **Sidebar TreeView** — activity bar icon with project list, watcher status, backup stats, alerts, health
308
+ - **Command Palette** — `Cursor Guard: Open Dashboard`, `Snapshot Now`, `Start Watcher`, `Refresh`
309
+ - **Auto-activation** — detects `.cursor-guard.json` in workspace, starts dashboard server automatically
310
+ - **Multi-project** — hot-loads all workspace folders with `.cursor-guard.json`
311
+ - **Compatible** — works with VSCode ^1.74.0, Cursor, Windsurf, and all VSCode-based IDEs
312
+
290
313
  ---
291
314
 
292
315
  ## Recovery
package/README.zh-CN.md CHANGED
@@ -274,7 +274,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
274
274
  node references\dashboard\server.js --path "D:\MyProject"
275
275
  ```
276
276
 
277
- 然后在浏览器打开 `http://127.0.0.1:3120`。
277
+ 然后在浏览器打开 `http://127.0.0.1:3120`。也可以使用 **IDE 扩展**(见下方)将仪表盘直接嵌入编辑器。
278
278
 
279
279
  特性:
280
280
 
@@ -287,6 +287,29 @@ node references\dashboard\server.js --path "D:\MyProject"
287
287
  - **安全性** — 仅绑定 `127.0.0.1`(不暴露到局域网)、API 使用项目 ID 而非原始路径、静态文件服务严格限制在 `public/` 目录
288
288
  - **零额外依赖** — 使用 Node.js 内置 `http` 模块 + cursor-guard 已有核心模块
289
289
 
290
+ ### IDE 扩展(VSCode / Cursor)
291
+
292
+ 将完整仪表盘直接嵌入 IDE 内部,无需打开浏览器。
293
+
294
+ 扩展位于 `references/vscode-extension/`。安装方式:
295
+
296
+ ```bash
297
+ # 从 cursor-guard skill 目录
298
+ cd references/vscode-extension
299
+ # 作为开发扩展安装到 IDE
300
+ code --install-extension .
301
+ ```
302
+
303
+ 功能:
304
+
305
+ - **WebView 仪表盘** — 完整仪表盘作为编辑器标签页嵌入,与浏览器版本完全一致
306
+ - **状态栏指示器** — 实时显示 `Guard: OK`(绿色)或 `Guard: 22 files!`(黄色告警)
307
+ - **侧边栏 TreeView** — Activity Bar 图标,树形展示项目列表、Watcher 状态、备份统计、告警、健康评估
308
+ - **命令面板** — `Cursor Guard: Open Dashboard`、`Snapshot Now`、`Start Watcher`、`Refresh`
309
+ - **自动激活** — 检测到工作区有 `.cursor-guard.json` 时自动启动 Dashboard 服务
310
+ - **多项目** — 热加载所有包含 `.cursor-guard.json` 的工作区文件夹
311
+ - **兼容性** — 支持 VSCode ^1.74.0、Cursor、Windsurf 及所有基于 VSCode 的 IDE
312
+
290
313
  ---
291
314
 
292
315
  ## 恢复
package/ROADMAP.md CHANGED
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.6.1`
7
- > **文档状态**:`V2` ~ `V4.6.1` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.7.0`
7
+ > **文档状态**:`V2` ~ `V4.7.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,14 +734,19 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
- ### V4.7 规划:IDE 集成(VSCode/Cursor Extension)
737
+ ### V4.7.0:IDE 集成(VSCode/Cursor Extension)
738
738
 
739
- | 项目 | 说明 |
739
+ | 组件 | 说明 |
740
740
  |------|------|
741
- | 定位 | Dashboard 从浏览器迁移到 IDE 内置 WebView,减少上下文切换 |
742
- | 实现方式 | VSCode Extension + WebView Panel,复用现有 `dashboard/public/` 前端代码 |
743
- | 核心能力 | IDE 侧边栏入口、状态栏告警指示器、WebView 内嵌 Dashboard、一键快照按钮 |
744
- | 状态 | 🔮 规划中 |
741
+ | `extension.js` | 扩展入口。自动检测 `.cursor-guard.json` 激活,启动内嵌 Dashboard Server,注册所有命令和视图 |
742
+ | `dashboard-manager.js` | 复用现有 `dashboard/server.js` 单例模式,在扩展宿主进程内直接 require,零额外开销。支持多 workspace folder 热加载 |
743
+ | `webview-provider.js` | WebView Panel 管理。加载 `dashboard/public/` 前端,通过 `asWebviewUri()` 转换资源路径,注入 `__GUARD_TOKEN__` + `__GUARD_BASE_URL__` + `__IN_VSCODE__` |
744
+ | `status-bar.js` | 状态栏告警指示器。正常时显示 `$(shield) Guard: OK`,告警时黄色背景 `$(warning) Guard: 22 files!`,点击打开 Dashboard |
745
+ | `tree-view.js` | Activity Bar 侧边栏。树形展示项目列表,每个项目下显示 Watcher 状态、最近备份时间、备份统计、活跃告警、健康评估 |
746
+ | `poller.js` | 后台每 5 秒轮询 `/api/page-data`,驱动状态栏和 TreeView 实时更新 |
747
+ | `app.js` 适配 | `fetchJson` 支持 `window.__GUARD_BASE_URL__` 前缀(兼容浏览器和 WebView);`copyText` 在 VSCode 中通过 `postMessage` 桥接到 `vscode.env.clipboard` |
748
+ | 命令面板 | `Open Dashboard` / `Snapshot Now` / `Start/Stop Watcher` / `Refresh` |
749
+ | 兼容性 | VSCode ^1.74.0,覆盖 Cursor、Windsurf 等所有 VSCode 衍生 IDE |
745
750
 
746
751
  ### V4 不做的事
747
752
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.6.2",
3
+ "version": "4.7.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",
@@ -50,6 +50,7 @@
50
50
  "references/lib/core/",
51
51
  "references/mcp/",
52
52
  "references/dashboard/",
53
+ "references/vscode-extension/",
53
54
  "references/config-reference.md",
54
55
  "references/config-reference.zh-CN.md",
55
56
  "references/cursor-guard.example.json",
@@ -674,9 +674,10 @@ function relativeTime(ts) {
674
674
  /* ── Data fetching ────────────────────────────────────────── */
675
675
 
676
676
  async function fetchJson(url) {
677
+ const base = window.__GUARD_BASE_URL__ || '';
677
678
  const sep = url.includes('?') ? '&' : '?';
678
679
  const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
679
- const r = await fetch(url + tokenParam);
680
+ const r = await fetch(base + url + tokenParam);
680
681
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
681
682
  return r.json();
682
683
  }
@@ -1526,16 +1527,21 @@ function openDoctorDrawer() {
1526
1527
  /* ── Copy to clipboard ────────────────────────────────────── */
1527
1528
 
1528
1529
  async function copyText(text) {
1529
- try {
1530
- await navigator.clipboard.writeText(text);
1531
- } catch {
1532
- const ta = document.createElement('textarea');
1533
- ta.value = text;
1534
- ta.style.cssText = 'position:fixed;left:-9999px';
1535
- document.body.appendChild(ta);
1536
- ta.select();
1537
- document.execCommand('copy');
1538
- document.body.removeChild(ta);
1530
+ if (window.__IN_VSCODE__ && window.acquireVsCodeApi) {
1531
+ try { window.__vscodeApi = window.__vscodeApi || acquireVsCodeApi(); } catch { /* already acquired */ }
1532
+ window.__vscodeApi?.postMessage({ type: 'copy', text });
1533
+ } else {
1534
+ try {
1535
+ await navigator.clipboard.writeText(text);
1536
+ } catch {
1537
+ const ta = document.createElement('textarea');
1538
+ ta.value = text;
1539
+ ta.style.cssText = 'position:fixed;left:-9999px';
1540
+ document.body.appendChild(ta);
1541
+ ta.select();
1542
+ document.execCommand('copy');
1543
+ document.body.removeChild(ta);
1544
+ }
1539
1545
  }
1540
1546
  showToast(t('drawer.copied'));
1541
1547
  }
@@ -1704,9 +1710,10 @@ async function restartServer(banner) {
1704
1710
  banner.querySelector('.upgrade-banner-close').style.display = 'none';
1705
1711
 
1706
1712
  try {
1713
+ const base = window.__GUARD_BASE_URL__ || '';
1707
1714
  const sep = '/api/restart'.includes('?') ? '&' : '?';
1708
1715
  const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
1709
- await fetch('/api/restart' + tokenParam, { method: 'POST' });
1716
+ await fetch(base + '/api/restart' + tokenParam, { method: 'POST' });
1710
1717
  } catch { /* server may close connection */ }
1711
1718
 
1712
1719
  btn.textContent = t('upgrade.waiting');
@@ -1714,7 +1721,7 @@ async function restartServer(banner) {
1714
1721
  for (let i = 0; i < 20; i++) {
1715
1722
  await new Promise(r => setTimeout(r, 500));
1716
1723
  try {
1717
- const r = await fetch('/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
1724
+ const r = await fetch((window.__GUARD_BASE_URL__ || '') + '/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
1718
1725
  if (r.ok) { ready = true; break; }
1719
1726
  } catch { /* still restarting */ }
1720
1727
  }
@@ -0,0 +1,4 @@
1
+ .vscode/**
2
+ node_modules/**
3
+ *.md
4
+ .gitignore
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const { DashboardManager } = require('./lib/dashboard-manager');
5
+ const { WebViewProvider } = require('./lib/webview-provider');
6
+ const { StatusBarController } = require('./lib/status-bar');
7
+ const { GuardTreeView } = require('./lib/tree-view');
8
+ const { Poller } = require('./lib/poller');
9
+
10
+ let dashMgr, poller, statusBar, treeView, webviewProvider;
11
+
12
+ async function activate(context) {
13
+ dashMgr = new DashboardManager();
14
+ poller = new Poller(dashMgr);
15
+ statusBar = new StatusBarController(poller);
16
+ treeView = new GuardTreeView(poller, dashMgr);
17
+ webviewProvider = new WebViewProvider(context, dashMgr);
18
+
19
+ context.subscriptions.push(
20
+ vscode.commands.registerCommand('cursorGuard.openDashboard', () => {
21
+ if (!dashMgr.running) {
22
+ vscode.window.showWarningMessage('Cursor Guard: no projects detected. Add .cursor-guard.json to your workspace.');
23
+ return;
24
+ }
25
+ webviewProvider.show();
26
+ }),
27
+
28
+ vscode.commands.registerCommand('cursorGuard.snapshotNow', async () => {
29
+ const folders = vscode.workspace.workspaceFolders;
30
+ if (!folders || folders.length === 0) return;
31
+ const projectPath = folders[0].uri.fsPath;
32
+ const result = await dashMgr.snapshotNow(projectPath);
33
+ if (result?.status === 'created') {
34
+ vscode.window.showInformationMessage(`Cursor Guard: snapshot created (${result.changedCount || 0} changes)`);
35
+ } else if (result?.status === 'unchanged') {
36
+ vscode.window.showInformationMessage('Cursor Guard: no changes to snapshot');
37
+ } else {
38
+ vscode.window.showWarningMessage(`Cursor Guard: ${result?.error || 'snapshot failed'}`);
39
+ }
40
+ poller.forceRefresh();
41
+ }),
42
+
43
+ vscode.commands.registerCommand('cursorGuard.startWatcher', () => {
44
+ vscode.window.showInformationMessage(
45
+ 'Cursor Guard: run `cursor-guard-backup --path <dir> --dashboard` in terminal to start the watcher.'
46
+ );
47
+ }),
48
+
49
+ vscode.commands.registerCommand('cursorGuard.stopWatcher', () => {
50
+ vscode.window.showInformationMessage(
51
+ 'Cursor Guard: stop the watcher by terminating its terminal process (Ctrl+C).'
52
+ );
53
+ }),
54
+
55
+ vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
56
+ poller.forceRefresh();
57
+ treeView.refresh();
58
+ }),
59
+
60
+ statusBar,
61
+ poller,
62
+ treeView,
63
+ webviewProvider,
64
+ );
65
+
66
+ const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
67
+ if (started) {
68
+ poller.start();
69
+ vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
70
+ }
71
+
72
+ context.subscriptions.push(
73
+ vscode.workspace.onDidChangeWorkspaceFolders(async () => {
74
+ const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
75
+ if (restarted && !poller._timer) poller.start();
76
+ poller.forceRefresh();
77
+ })
78
+ );
79
+ }
80
+
81
+ function deactivate() {
82
+ if (poller) poller.dispose();
83
+ if (statusBar) statusBar.dispose();
84
+ if (treeView) treeView.dispose();
85
+ if (webviewProvider) webviewProvider.dispose();
86
+ if (dashMgr) dashMgr.dispose();
87
+ }
88
+
89
+ module.exports = { activate, deactivate };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const http = require('http');
6
+
7
+ const CONFIG_FILE = '.cursor-guard.json';
8
+
9
+ class DashboardManager {
10
+ constructor() {
11
+ this._instance = null;
12
+ this._serverModule = null;
13
+ }
14
+
15
+ get running() { return !!this._instance; }
16
+ get port() { return this._instance?.port; }
17
+ get token() { return this._instance?.token; }
18
+ get baseUrl() { return this._instance ? `http://127.0.0.1:${this._instance.port}` : null; }
19
+ get registry() { return this._instance?.registry; }
20
+
21
+ async autoStart(workspaceFolders) {
22
+ if (!workspaceFolders || workspaceFolders.length === 0) return false;
23
+ const paths = workspaceFolders
24
+ .map(f => f.uri.fsPath)
25
+ .filter(p => fs.existsSync(path.join(p, CONFIG_FILE)));
26
+ if (paths.length === 0) return false;
27
+ return this.start(paths);
28
+ }
29
+
30
+ async start(paths) {
31
+ if (!this._serverModule) {
32
+ this._serverModule = require('../../dashboard/server');
33
+ }
34
+ const { startDashboardServer, getInstance } = this._serverModule;
35
+ const existing = getInstance();
36
+ if (existing) {
37
+ await startDashboardServer(paths, { silent: true });
38
+ this._instance = getInstance();
39
+ } else {
40
+ this._instance = await startDashboardServer(paths, { port: 3120, silent: true });
41
+ }
42
+ return true;
43
+ }
44
+
45
+ async fetchApi(endpoint) {
46
+ if (!this._instance) return null;
47
+ const url = `${this.baseUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}token=${this.token}`;
48
+ return new Promise((resolve, reject) => {
49
+ http.get(url, (res) => {
50
+ let data = '';
51
+ res.on('data', chunk => data += chunk);
52
+ res.on('end', () => {
53
+ try { resolve(JSON.parse(data)); }
54
+ catch { resolve(null); }
55
+ });
56
+ }).on('error', () => resolve(null));
57
+ });
58
+ }
59
+
60
+ async getProjects() {
61
+ return this.fetchApi('/api/projects') || [];
62
+ }
63
+
64
+ async getPageData(projectId, scope) {
65
+ const scopeParam = scope ? `&scope=${scope}` : '';
66
+ return this.fetchApi(`/api/page-data?id=${projectId}${scopeParam}`);
67
+ }
68
+
69
+ async snapshotNow(projectPath) {
70
+ if (!projectPath) return;
71
+ try {
72
+ const { createGitSnapshot } = require('../../lib/core/snapshot');
73
+ const { loadConfig } = require('../../lib/utils');
74
+ const cfg = loadConfig(projectPath);
75
+ return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
76
+ } catch (e) {
77
+ return { status: 'error', error: e.message };
78
+ }
79
+ }
80
+
81
+ dispose() {
82
+ this._instance = null;
83
+ }
84
+ }
85
+
86
+ module.exports = { DashboardManager };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ const POLL_INTERVAL = 5000;
6
+
7
+ class Poller {
8
+ constructor(dashMgr) {
9
+ this._dashMgr = dashMgr;
10
+ this._timer = null;
11
+ this._listeners = [];
12
+ this._data = new Map();
13
+ }
14
+
15
+ get data() { return this._data; }
16
+
17
+ onChange(fn) {
18
+ this._listeners.push(fn);
19
+ return { dispose: () => { this._listeners = this._listeners.filter(l => l !== fn); } };
20
+ }
21
+
22
+ _emit() {
23
+ for (const fn of this._listeners) {
24
+ try { fn(this._data); } catch { /* listener error */ }
25
+ }
26
+ }
27
+
28
+ start() {
29
+ if (this._timer) return;
30
+ this._poll();
31
+ this._timer = setInterval(() => this._poll(), POLL_INTERVAL);
32
+ }
33
+
34
+ stop() {
35
+ if (this._timer) { clearInterval(this._timer); this._timer = null; }
36
+ }
37
+
38
+ async _poll() {
39
+ if (!this._dashMgr.running) return;
40
+ try {
41
+ const projects = await this._dashMgr.getProjects();
42
+ if (!Array.isArray(projects)) return;
43
+ for (const p of projects) {
44
+ const pageData = await this._dashMgr.getPageData(p.id, 'dashboard');
45
+ this._data.set(p.id, { ...p, dashboard: pageData?.dashboard || null });
46
+ }
47
+ this._emit();
48
+ } catch { /* non-critical */ }
49
+ }
50
+
51
+ async forceRefresh() {
52
+ await this._poll();
53
+ }
54
+
55
+ dispose() {
56
+ this.stop();
57
+ this._listeners = [];
58
+ }
59
+ }
60
+
61
+ module.exports = { Poller };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ class StatusBarController {
6
+ constructor(poller) {
7
+ this._item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
8
+ this._item.command = 'cursorGuard.openDashboard';
9
+ this._item.tooltip = 'Cursor Guard — click to open dashboard';
10
+ this._setIdle();
11
+ this._item.show();
12
+
13
+ this._sub = poller.onChange(data => this._update(data));
14
+ }
15
+
16
+ _setIdle() {
17
+ this._item.text = '$(shield) Guard';
18
+ this._item.backgroundColor = undefined;
19
+ }
20
+
21
+ _update(data) {
22
+ let hasAlert = false;
23
+ let watcherRunning = false;
24
+ let alertFileCount = 0;
25
+
26
+ for (const [, p] of data) {
27
+ const d = p.dashboard;
28
+ if (!d) continue;
29
+ if (d.alerts?.active) {
30
+ hasAlert = true;
31
+ alertFileCount = d.alerts.latest?.fileCount || 0;
32
+ }
33
+ if (d.watcher?.running) watcherRunning = true;
34
+ }
35
+
36
+ if (hasAlert) {
37
+ this._item.text = `$(warning) Guard: ${alertFileCount} files!`;
38
+ this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
39
+ this._item.tooltip = `Cursor Guard — ALERT: ${alertFileCount} files changed rapidly`;
40
+ } else if (watcherRunning) {
41
+ this._item.text = '$(shield) Guard: OK';
42
+ this._item.backgroundColor = undefined;
43
+ this._item.tooltip = 'Cursor Guard — watcher running, no alerts';
44
+ } else {
45
+ this._item.text = '$(shield) Guard';
46
+ this._item.backgroundColor = undefined;
47
+ this._item.tooltip = 'Cursor Guard — watcher not running';
48
+ }
49
+ }
50
+
51
+ dispose() {
52
+ this._sub?.dispose();
53
+ this._item.dispose();
54
+ }
55
+ }
56
+
57
+ module.exports = { StatusBarController };
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+
5
+ class GuardTreeView {
6
+ constructor(poller, dashMgr) {
7
+ this._poller = poller;
8
+ this._dashMgr = dashMgr;
9
+ this._onDidChange = new vscode.EventEmitter();
10
+ this.onDidChangeTreeData = this._onDidChange.event;
11
+
12
+ this._treeView = vscode.window.createTreeView('cursorGuardProjects', {
13
+ treeDataProvider: this,
14
+ showCollapseAll: true,
15
+ });
16
+
17
+ this._sub = poller.onChange(() => this._onDidChange.fire());
18
+ }
19
+
20
+ refresh() { this._onDidChange.fire(); }
21
+
22
+ getTreeItem(element) { return element; }
23
+
24
+ getChildren(element) {
25
+ if (!element) return this._getRootItems();
26
+ if (element.contextValue === 'project') return this._getProjectChildren(element.projectId);
27
+ return [];
28
+ }
29
+
30
+ _getRootItems() {
31
+ const data = this._poller.data;
32
+ if (data.size === 0) {
33
+ return [new TreeItem('No projects detected', 'info', { icon: 'info' })];
34
+ }
35
+ const items = [];
36
+ for (const [id, p] of data) {
37
+ const item = new TreeItem(p.name || id, 'project', {
38
+ icon: 'folder',
39
+ description: p.pathLabel,
40
+ collapsible: vscode.TreeItemCollapsibleState.Expanded,
41
+ });
42
+ item.projectId = id;
43
+ items.push(item);
44
+ }
45
+ return items;
46
+ }
47
+
48
+ _getProjectChildren(projectId) {
49
+ const p = this._poller.data.get(projectId);
50
+ if (!p?.dashboard) return [new TreeItem('Loading...', 'loading', { icon: 'loading~spin' })];
51
+ const d = p.dashboard;
52
+ const items = [];
53
+
54
+ const watcherStatus = d.watcher?.running ? 'Running' : 'Stopped';
55
+ const watcherIcon = d.watcher?.running ? 'eye' : 'eye-closed';
56
+ const watcherItem = new TreeItem(`Watcher: ${watcherStatus}`, 'watcher', { icon: watcherIcon });
57
+ if (d.watcher?.pid) watcherItem.description = `PID ${d.watcher.pid}`;
58
+ items.push(watcherItem);
59
+
60
+ if (d.lastBackup?.git) {
61
+ const ago = this._relativeTime(d.lastBackup.git.timestamp);
62
+ items.push(new TreeItem(`Last Backup: ${ago}`, 'backup', { icon: 'git-commit' }));
63
+ }
64
+
65
+ if (d.counts) {
66
+ const gitCount = d.counts.git?.commits || 0;
67
+ const shadowCount = d.counts.shadow?.snapshots || 0;
68
+ items.push(new TreeItem(`Backups: ${gitCount} git, ${shadowCount} shadow`, 'counts', { icon: 'database' }));
69
+ }
70
+
71
+ if (d.alerts?.active) {
72
+ const a = d.alerts.latest || {};
73
+ const alertItem = new TreeItem(
74
+ `ALERT: ${a.fileCount || '?'} files in ${a.windowSeconds || '?'}s`,
75
+ 'alert',
76
+ { icon: 'warning' }
77
+ );
78
+ alertItem.description = `threshold: ${a.threshold}`;
79
+ items.push(alertItem);
80
+ }
81
+
82
+ const health = d.health?.status || 'unknown';
83
+ const healthIcon = health === 'healthy' ? 'pass' : health === 'critical' ? 'error' : 'warning';
84
+ const healthItem = new TreeItem(`Health: ${health}`, 'health', { icon: healthIcon });
85
+ if (d.health?.issues?.length > 0) {
86
+ healthItem.description = d.health.issues[0];
87
+ }
88
+ items.push(healthItem);
89
+
90
+ return items;
91
+ }
92
+
93
+ _relativeTime(ts) {
94
+ const diff = Date.now() - new Date(ts).getTime();
95
+ const sec = Math.floor(diff / 1000);
96
+ if (sec < 60) return `${sec}s ago`;
97
+ const min = Math.floor(sec / 60);
98
+ if (min < 60) return `${min}m ago`;
99
+ const hr = Math.floor(min / 60);
100
+ return `${hr}h ago`;
101
+ }
102
+
103
+ dispose() {
104
+ this._sub?.dispose();
105
+ this._treeView.dispose();
106
+ this._onDidChange.dispose();
107
+ }
108
+ }
109
+
110
+ class TreeItem extends vscode.TreeItem {
111
+ constructor(label, contextValue, opts = {}) {
112
+ super(label, opts.collapsible || vscode.TreeItemCollapsibleState.None);
113
+ this.contextValue = contextValue;
114
+ if (opts.icon) this.iconPath = new vscode.ThemeIcon(opts.icon);
115
+ if (opts.description) this.description = opts.description;
116
+ }
117
+ }
118
+
119
+ module.exports = { GuardTreeView };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PUBLIC_DIR = path.resolve(__dirname, '..', '..', 'dashboard', 'public');
8
+
9
+ class WebViewProvider {
10
+ constructor(context, dashMgr) {
11
+ this._context = context;
12
+ this._dashMgr = dashMgr;
13
+ this._panel = null;
14
+ }
15
+
16
+ show() {
17
+ if (this._panel) {
18
+ this._panel.reveal();
19
+ return;
20
+ }
21
+
22
+ this._panel = vscode.window.createWebviewPanel(
23
+ 'cursorGuardDashboard',
24
+ 'Cursor Guard Dashboard',
25
+ vscode.ViewColumn.One,
26
+ {
27
+ enableScripts: true,
28
+ retainContextWhenHidden: true,
29
+ localResourceRoots: [vscode.Uri.file(PUBLIC_DIR)],
30
+ }
31
+ );
32
+
33
+ this._panel.webview.html = this._buildHtml(this._panel.webview);
34
+ this._panel.iconPath = new vscode.ThemeIcon('shield');
35
+
36
+ this._panel.onDidDispose(() => { this._panel = null; });
37
+
38
+ this._panel.webview.onDidReceiveMessage(msg => {
39
+ if (msg.type === 'copy') {
40
+ vscode.env.clipboard.writeText(msg.text);
41
+ vscode.window.showInformationMessage('Copied to clipboard');
42
+ }
43
+ });
44
+ }
45
+
46
+ _buildHtml(webview) {
47
+ const htmlPath = path.join(PUBLIC_DIR, 'index.html');
48
+ let html = fs.readFileSync(htmlPath, 'utf-8');
49
+
50
+ const styleUri = webview.asWebviewUri(vscode.Uri.file(path.join(PUBLIC_DIR, 'style.css')));
51
+ const scriptUri = webview.asWebviewUri(vscode.Uri.file(path.join(PUBLIC_DIR, 'app.js')));
52
+
53
+ html = html.replace(/href="style\.css"/g, `href="${styleUri}"`);
54
+ html = html.replace(/src="app\.js"/g, `src="${scriptUri}"`);
55
+
56
+ const baseUrl = this._dashMgr.baseUrl || '';
57
+ const token = this._dashMgr.token || '';
58
+ const nonce = _getNonce();
59
+
60
+ html = html.replace(
61
+ '</head>',
62
+ `<script nonce="${nonce}">
63
+ window.__GUARD_TOKEN__ = "${token}";
64
+ window.__GUARD_BASE_URL__ = "${baseUrl}";
65
+ window.__IN_VSCODE__ = true;
66
+ </script>
67
+ </head>`
68
+ );
69
+
70
+ return html;
71
+ }
72
+
73
+ dispose() {
74
+ if (this._panel) { this._panel.dispose(); this._panel = null; }
75
+ }
76
+ }
77
+
78
+ function _getNonce() {
79
+ let text = '';
80
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
81
+ for (let i = 0; i < 32; i++) text += chars.charAt(Math.floor(Math.random() * chars.length));
82
+ return text;
83
+ }
84
+
85
+ module.exports = { WebViewProvider };
@@ -0,0 +1,5 @@
1
+ ## Extension Icon
2
+
3
+ Place a 128x128 PNG file named `icon.png` in this directory for the VS Code marketplace listing.
4
+
5
+ The `guard-icon.svg` is used for the activity bar sidebar icon.
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
3
+ <path d="M9 12l2 2 4-4"/>
4
+ </svg>
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "cursor-guard-ide",
3
+ "displayName": "Cursor Guard",
4
+ "description": "AI code protection dashboard embedded in your IDE — real-time alerts, backup history, one-click snapshots",
5
+ "version": "4.7.0",
6
+ "publisher": "zhangqiang8vipp",
7
+ "license": "BUSL-1.1",
8
+ "engines": {
9
+ "vscode": "^1.74.0"
10
+ },
11
+ "categories": ["Other", "Visualization"],
12
+ "keywords": ["cursor", "ai-safety", "code-protection", "git-backup", "dashboard"],
13
+ "icon": "media/icon.png",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/zhangqiang8vipp/cursor-guard"
17
+ },
18
+ "main": "./extension.js",
19
+ "activationEvents": [
20
+ "workspaceContains:.cursor-guard.json"
21
+ ],
22
+ "contributes": {
23
+ "commands": [
24
+ {
25
+ "command": "cursorGuard.openDashboard",
26
+ "title": "Open Dashboard",
27
+ "category": "Cursor Guard",
28
+ "icon": "$(dashboard)"
29
+ },
30
+ {
31
+ "command": "cursorGuard.snapshotNow",
32
+ "title": "Snapshot Now",
33
+ "category": "Cursor Guard",
34
+ "icon": "$(device-camera)"
35
+ },
36
+ {
37
+ "command": "cursorGuard.startWatcher",
38
+ "title": "Start Watcher",
39
+ "category": "Cursor Guard",
40
+ "icon": "$(eye)"
41
+ },
42
+ {
43
+ "command": "cursorGuard.stopWatcher",
44
+ "title": "Stop Watcher",
45
+ "category": "Cursor Guard",
46
+ "icon": "$(eye-closed)"
47
+ },
48
+ {
49
+ "command": "cursorGuard.refreshTree",
50
+ "title": "Refresh",
51
+ "category": "Cursor Guard",
52
+ "icon": "$(refresh)"
53
+ }
54
+ ],
55
+ "viewsContainers": {
56
+ "activitybar": [
57
+ {
58
+ "id": "cursorGuard",
59
+ "title": "Cursor Guard",
60
+ "icon": "media/guard-icon.svg"
61
+ }
62
+ ]
63
+ },
64
+ "views": {
65
+ "cursorGuard": [
66
+ {
67
+ "id": "cursorGuardProjects",
68
+ "name": "Projects"
69
+ }
70
+ ]
71
+ },
72
+ "menus": {
73
+ "view/title": [
74
+ {
75
+ "command": "cursorGuard.openDashboard",
76
+ "when": "view == cursorGuardProjects",
77
+ "group": "navigation"
78
+ },
79
+ {
80
+ "command": "cursorGuard.refreshTree",
81
+ "when": "view == cursorGuardProjects",
82
+ "group": "navigation"
83
+ }
84
+ ]
85
+ }
86
+ }
87
+ }