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 +24 -1
- package/README.zh-CN.md +24 -1
- package/ROADMAP.md +13 -8
- package/package.json +2 -1
- package/references/dashboard/public/app.js +20 -13
- package/references/vscode-extension/.vscodeignore +4 -0
- package/references/vscode-extension/extension.js +89 -0
- package/references/vscode-extension/lib/dashboard-manager.js +86 -0
- package/references/vscode-extension/lib/poller.js +61 -0
- package/references/vscode-extension/lib/status-bar.js +57 -0
- package/references/vscode-extension/lib/tree-view.js +119 -0
- package/references/vscode-extension/lib/webview-provider.js +85 -0
- package/references/vscode-extension/media/ICON_README.md +5 -0
- package/references/vscode-extension/media/guard-icon.svg +4 -0
- package/references/vscode-extension/media/icon.png +0 -0
- package/references/vscode-extension/package.json +87 -0
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.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
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
|
|
737
|
+
### V4.7.0:IDE 集成(VSCode/Cursor Extension) ✅
|
|
738
738
|
|
|
739
|
-
|
|
|
739
|
+
| 组件 | 说明 |
|
|
740
740
|
|------|------|
|
|
741
|
-
|
|
|
742
|
-
|
|
|
743
|
-
|
|
|
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.
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
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,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 };
|
|
Binary file
|
|
@@ -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
|
+
}
|