cursor-guard 4.6.2 → 4.7.1
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 +72 -2
- package/README.zh-CN.md +72 -2
- package/ROADMAP.md +23 -8
- package/media/alipay.jpg +0 -0
- package/media/wechat-pay.png +0 -0
- package/package.json +3 -1
- package/references/dashboard/public/app.js +20 -13
- package/references/vscode-extension/.vscodeignore +4 -0
- package/references/vscode-extension/extension.js +111 -0
- package/references/vscode-extension/lib/dashboard-manager.js +127 -0
- package/references/vscode-extension/lib/poller.js +61 -0
- package/references/vscode-extension/lib/status-bar.js +68 -0
- package/references/vscode-extension/lib/tree-view.js +203 -0
- package/references/vscode-extension/lib/webview-provider.js +95 -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
|
@@ -25,6 +25,10 @@ When Cursor's AI agent edits your files, there's a risk of accidental overwrites
|
|
|
25
25
|
- **Proactive change-velocity alerts (V4)** — Auto-detects abnormal file change patterns and raises risk warnings
|
|
26
26
|
- **Backup health dashboard (V4)** — One-call comprehensive view: strategy, counts, disk usage, protection scope, health status
|
|
27
27
|
- **Web dashboard (V4.2)** — Local read-only web UI at `http://127.0.0.1:3120` — see health, backups, restore points, diagnostics, protection scope at a glance. Dual-language (zh-CN / en-US), auto-refresh every 15s, multi-project support
|
|
28
|
+
- **IDE extension (V4.7)** — Full dashboard embedded in VSCode/Cursor/Windsurf as a WebView tab + status bar alert indicator + sidebar project tree. No browser needed
|
|
29
|
+
- **One-click hot restart (V4.5.8)** — Dashboard detects new versions and offers in-place server restart without losing state
|
|
30
|
+
- **Shadow incremental hard links (V4.5.4)** — Unchanged files are hard-linked to save disk space and I/O
|
|
31
|
+
- **Strong protection mode (V4.5.4)** — `always_watch: true` auto-starts watcher with MCP server, ensuring zero protection gaps
|
|
28
32
|
|
|
29
33
|
---
|
|
30
34
|
|
|
@@ -124,6 +128,11 @@ After installation, your directory structure should look like this:
|
|
|
124
128
|
│ └── app.js
|
|
125
129
|
├── mcp/
|
|
126
130
|
│ └── server.js # MCP Server (9 tools)
|
|
131
|
+
├── vscode-extension/ # IDE Extension (V4.7)
|
|
132
|
+
│ ├── extension.js # Extension entry point
|
|
133
|
+
│ ├── package.json # Extension manifest
|
|
134
|
+
│ ├── lib/ # Modules (dashboard-manager, webview, status-bar, tree-view, poller)
|
|
135
|
+
│ └── media/ # Icons (SVG + PNG)
|
|
127
136
|
├── bin/
|
|
128
137
|
│ ├── cursor-guard-backup.js # CLI: npx cursor-guard-backup
|
|
129
138
|
│ ├── cursor-guard-doctor.js # CLI: npx cursor-guard-doctor
|
|
@@ -274,7 +283,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
|
|
|
274
283
|
node references\dashboard\server.js --path "D:\MyProject"
|
|
275
284
|
```
|
|
276
285
|
|
|
277
|
-
Then open `http://127.0.0.1:3120` in your browser.
|
|
286
|
+
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
287
|
|
|
279
288
|
Features:
|
|
280
289
|
|
|
@@ -287,6 +296,29 @@ Features:
|
|
|
287
296
|
- **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
297
|
- **Zero extra dependencies** — uses Node.js built-in `http` module + existing cursor-guard core modules
|
|
289
298
|
|
|
299
|
+
### IDE Extension (VSCode / Cursor)
|
|
300
|
+
|
|
301
|
+
Embed the full dashboard directly inside your IDE — no browser needed.
|
|
302
|
+
|
|
303
|
+
The extension is located at `references/vscode-extension/`. To install:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# From the cursor-guard skill directory
|
|
307
|
+
cd references/vscode-extension
|
|
308
|
+
# Install as a development extension in your IDE
|
|
309
|
+
code --install-extension .
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Features:
|
|
313
|
+
|
|
314
|
+
- **WebView Dashboard** — full dashboard embedded as an editor tab, identical to the browser version
|
|
315
|
+
- **Status Bar Indicator** — shows `Guard: OK` (green) or `Guard: 22 files!` (yellow) in real-time
|
|
316
|
+
- **Sidebar TreeView** — activity bar icon with project list, watcher status, backup stats, alerts, health
|
|
317
|
+
- **Command Palette** — `Cursor Guard: Open Dashboard`, `Snapshot Now`, `Start Watcher`, `Refresh`
|
|
318
|
+
- **Auto-activation** — detects `.cursor-guard.json` in workspace, starts dashboard server automatically
|
|
319
|
+
- **Multi-project** — hot-loads all workspace folders with `.cursor-guard.json`
|
|
320
|
+
- **Compatible** — works with VSCode ^1.74.0, Cursor, Windsurf, and all VSCode-based IDEs
|
|
321
|
+
|
|
290
322
|
---
|
|
291
323
|
|
|
292
324
|
## Recovery
|
|
@@ -365,6 +397,7 @@ The skill activates on these signals:
|
|
|
365
397
|
| `references/bin/cursor-guard-doctor.js` | CLI entry: `npx cursor-guard-doctor` |
|
|
366
398
|
| `references/dashboard/server.js` | Dashboard HTTP server + REST API |
|
|
367
399
|
| `references/dashboard/public/` | Dashboard web UI (index.html, style.css, app.js) |
|
|
400
|
+
| `references/vscode-extension/` | IDE Extension: WebView dashboard, status bar, sidebar tree, commands |
|
|
368
401
|
| `references/auto-backup.ps1` / `.sh` | Thin wrappers (Windows / macOS+Linux) |
|
|
369
402
|
| `references/guard-doctor.ps1` / `.sh` | Thin wrappers (Windows / macOS+Linux) |
|
|
370
403
|
| `references/recovery.md` | Recovery command templates |
|
|
@@ -377,6 +410,33 @@ The skill activates on these signals:
|
|
|
377
410
|
|
|
378
411
|
## Changelog
|
|
379
412
|
|
|
413
|
+
### v4.7.0 — IDE Extension
|
|
414
|
+
|
|
415
|
+
- **Feature**: VSCode/Cursor/Windsurf extension — full dashboard as WebView tab, status bar alert indicator, sidebar TreeView with project status, Command Palette integration
|
|
416
|
+
- **Feature**: Auto-activation on `.cursor-guard.json` detection, dashboard server runs in extension host process (zero subprocess overhead)
|
|
417
|
+
- **Adapt**: `fetchJson()` supports `__GUARD_BASE_URL__` for WebView; `copyText()` bridges to `vscode.env.clipboard` when in IDE
|
|
418
|
+
|
|
419
|
+
### v4.6.x — Alert UX Overhaul
|
|
420
|
+
|
|
421
|
+
- **Fix**: Alert countdown now updates every second (was only on 15s page refresh)
|
|
422
|
+
- **Fix**: Alert file details modal now shows per-file "Copy Restore Command" buttons
|
|
423
|
+
- **Fix**: Backup stale threshold changed to `max(interval*10, 300)s` (min 5 min); only checks when watcher is running
|
|
424
|
+
- **Feature**: Alert history always accessible (both active and no-alert states), persisted in `localStorage`
|
|
425
|
+
- **Feature**: Alert history as modal dialog with nested file detail drill-down
|
|
426
|
+
|
|
427
|
+
### v4.5.x — Protection Hardening
|
|
428
|
+
|
|
429
|
+
- **Fix**: Shadow hard-link ordering bug (previous snapshot was always empty directory)
|
|
430
|
+
- **Fix**: `changedFiles` now filters ignored paths from git diff output
|
|
431
|
+
- **Feature**: Alert structured file list — per-file path, action, +/- lines, sortable tables
|
|
432
|
+
- **Feature**: Shadow incremental hard links — unchanged files linked to previous snapshot, saving disk space
|
|
433
|
+
- **Feature**: `always_watch: true` config — watcher auto-starts with MCP server, zero protection gaps
|
|
434
|
+
- **Feature**: Dashboard server singleton — multiple projects share one port, hot-add new projects
|
|
435
|
+
- **Feature**: Dashboard version detection + one-click hot restart (`/api/restart` endpoint)
|
|
436
|
+
- **Feature**: File detail modal with per-file restore command copy buttons
|
|
437
|
+
- **Feature**: `cursor-guard-init` auto-creates `.cursor-guard.json`; `backup_interval_seconds` alias supported
|
|
438
|
+
- **License**: Changed from MIT to BSL 1.1 (source available, commercial use requires author authorization)
|
|
439
|
+
|
|
380
440
|
### v4.4.0 — V4 Final
|
|
381
441
|
|
|
382
442
|
- **Fix**: First snapshot now generates "Added N: file1, file2, ..." summary instead of blank — previously the very first backup had no summary because there was no parent tree to diff against
|
|
@@ -466,6 +526,16 @@ The skill activates on these signals:
|
|
|
466
526
|
|
|
467
527
|
---
|
|
468
528
|
|
|
529
|
+
## Support / Donate
|
|
530
|
+
|
|
531
|
+
This is an independent open-source project maintained by a solo developer. If Cursor Guard has saved your code or your time, consider buying me a coffee :)
|
|
532
|
+
|
|
533
|
+
| WeChat Pay | Alipay |
|
|
534
|
+
|:---:|:---:|
|
|
535
|
+
| <img src="media/wechat-pay.png" alt="WeChat Pay" width="200"> | <img src="media/alipay.jpg" alt="Alipay" width="200"> |
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
469
539
|
## License
|
|
470
540
|
|
|
471
|
-
|
|
541
|
+
[BSL 1.1 (Business Source License)](LICENSE) — Source code is freely available for viewing, modification, and non-commercial use. Commercial use requires authorization from the author. Auto-converts to Apache 2.0 on 2056-03-22.
|
package/README.zh-CN.md
CHANGED
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
- **主动变更频率告警(V4)** — 自动检测异常文件变更模式并发出风险预警
|
|
26
26
|
- **备份健康看板(V4)** — 一次调用全面查看:策略、数量、磁盘占用、保护范围、健康状态
|
|
27
27
|
- **Web 仪表盘(V4.2)** — 本地只读 Web 页面 `http://127.0.0.1:3120`——健康状态、备份、恢复点、诊断、保护范围一目了然。中英双语、每 15 秒自动刷新、支持多项目监控
|
|
28
|
+
- **IDE 扩展(V4.7)** — 完整仪表盘嵌入 VSCode/Cursor/Windsurf,WebView 标签页 + 状态栏告警指示器 + 侧边栏项目树。无需打开浏览器
|
|
29
|
+
- **一键热重启(V4.5.8)** — 仪表盘检测到新版本时可原地重启服务,不丢失状态
|
|
30
|
+
- **Shadow 增量硬链接(V4.5.4)** — 未变更文件硬链接到上次快照,节省磁盘空间和 I/O
|
|
31
|
+
- **强保护模式(V4.5.4)** — `always_watch: true` 让 watcher 随 MCP server 自动启动,确保零保护缺口
|
|
28
32
|
|
|
29
33
|
---
|
|
30
34
|
|
|
@@ -124,6 +128,11 @@ git clone https://github.com/zhangqiang8vipp/cursor-guard.git .cursor/skills/cur
|
|
|
124
128
|
│ └── app.js
|
|
125
129
|
├── mcp/
|
|
126
130
|
│ └── server.js # MCP Server(9 个工具)
|
|
131
|
+
├── vscode-extension/ # IDE 扩展(V4.7)
|
|
132
|
+
│ ├── extension.js # 扩展入口
|
|
133
|
+
│ ├── package.json # 扩展清单
|
|
134
|
+
│ ├── lib/ # 模块(dashboard-manager、webview、status-bar、tree-view、poller)
|
|
135
|
+
│ └── media/ # 图标(SVG + PNG)
|
|
127
136
|
├── bin/
|
|
128
137
|
│ ├── cursor-guard-backup.js # CLI:npx cursor-guard-backup
|
|
129
138
|
│ ├── cursor-guard-doctor.js # CLI:npx cursor-guard-doctor
|
|
@@ -274,7 +283,7 @@ npx cursor-guard-dashboard --path /my/project --port 8080
|
|
|
274
283
|
node references\dashboard\server.js --path "D:\MyProject"
|
|
275
284
|
```
|
|
276
285
|
|
|
277
|
-
然后在浏览器打开 `http://127.0.0.1:3120
|
|
286
|
+
然后在浏览器打开 `http://127.0.0.1:3120`。也可以使用 **IDE 扩展**(见下方)将仪表盘直接嵌入编辑器。
|
|
278
287
|
|
|
279
288
|
特性:
|
|
280
289
|
|
|
@@ -287,6 +296,29 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
287
296
|
- **安全性** — 仅绑定 `127.0.0.1`(不暴露到局域网)、API 使用项目 ID 而非原始路径、静态文件服务严格限制在 `public/` 目录
|
|
288
297
|
- **零额外依赖** — 使用 Node.js 内置 `http` 模块 + cursor-guard 已有核心模块
|
|
289
298
|
|
|
299
|
+
### IDE 扩展(VSCode / Cursor)
|
|
300
|
+
|
|
301
|
+
将完整仪表盘直接嵌入 IDE 内部,无需打开浏览器。
|
|
302
|
+
|
|
303
|
+
扩展位于 `references/vscode-extension/`。安装方式:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
# 从 cursor-guard skill 目录
|
|
307
|
+
cd references/vscode-extension
|
|
308
|
+
# 作为开发扩展安装到 IDE
|
|
309
|
+
code --install-extension .
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
功能:
|
|
313
|
+
|
|
314
|
+
- **WebView 仪表盘** — 完整仪表盘作为编辑器标签页嵌入,与浏览器版本完全一致
|
|
315
|
+
- **状态栏指示器** — 实时显示 `Guard: OK`(绿色)或 `Guard: 22 files!`(黄色告警)
|
|
316
|
+
- **侧边栏 TreeView** — Activity Bar 图标,树形展示项目列表、Watcher 状态、备份统计、告警、健康评估
|
|
317
|
+
- **命令面板** — `Cursor Guard: Open Dashboard`、`Snapshot Now`、`Start Watcher`、`Refresh`
|
|
318
|
+
- **自动激活** — 检测到工作区有 `.cursor-guard.json` 时自动启动 Dashboard 服务
|
|
319
|
+
- **多项目** — 热加载所有包含 `.cursor-guard.json` 的工作区文件夹
|
|
320
|
+
- **兼容性** — 支持 VSCode ^1.74.0、Cursor、Windsurf 及所有基于 VSCode 的 IDE
|
|
321
|
+
|
|
290
322
|
---
|
|
291
323
|
|
|
292
324
|
## 恢复
|
|
@@ -365,6 +397,7 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
365
397
|
| `references/bin/cursor-guard-doctor.js` | CLI 入口:`npx cursor-guard-doctor` |
|
|
366
398
|
| `references/dashboard/server.js` | 仪表盘 HTTP 服务 + REST API |
|
|
367
399
|
| `references/dashboard/public/` | 仪表盘 Web UI(index.html、style.css、app.js) |
|
|
400
|
+
| `references/vscode-extension/` | IDE 扩展:WebView 仪表盘、状态栏、侧边栏树、命令面板 |
|
|
368
401
|
| `references/auto-backup.ps1` / `.sh` | 薄封装(Windows / macOS+Linux) |
|
|
369
402
|
| `references/guard-doctor.ps1` / `.sh` | 薄封装(Windows / macOS+Linux) |
|
|
370
403
|
| `references/recovery.md` | 恢复命令模板 |
|
|
@@ -377,6 +410,33 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
377
410
|
|
|
378
411
|
## 更新日志
|
|
379
412
|
|
|
413
|
+
### v4.7.0 — IDE 扩展
|
|
414
|
+
|
|
415
|
+
- **功能**:VSCode/Cursor/Windsurf 扩展 — 完整仪表盘作为 WebView 标签页嵌入,状态栏告警指示器,侧边栏 TreeView 项目状态,命令面板集成
|
|
416
|
+
- **功能**:检测到 `.cursor-guard.json` 自动激活,Dashboard 服务在扩展宿主进程内运行(零额外进程开销)
|
|
417
|
+
- **适配**:`fetchJson()` 支持 `__GUARD_BASE_URL__` 用于 WebView;`copyText()` 在 IDE 中通过 `postMessage` 桥接到 `vscode.env.clipboard`
|
|
418
|
+
|
|
419
|
+
### v4.6.x — 告警 UX 大优化
|
|
420
|
+
|
|
421
|
+
- **修复**:告警倒计时现在每秒更新(之前仅在 15 秒页面刷新时更新)
|
|
422
|
+
- **修复**:告警文件详情弹窗支持每文件「复制恢复命令」按钮
|
|
423
|
+
- **修复**:备份过时阈值改为 `max(interval*10, 300)` 秒(至少 5 分钟);仅在 watcher 运行时检查
|
|
424
|
+
- **功能**:告警历史始终可访问(无论是否有活跃告警),使用 `localStorage` 持久化
|
|
425
|
+
- **功能**:告警历史作为弹窗展示,支持嵌套查看文件详情
|
|
426
|
+
|
|
427
|
+
### v4.5.x — 保护加固
|
|
428
|
+
|
|
429
|
+
- **修复**:Shadow 硬链接顺序 bug(上次快照总是空目录)
|
|
430
|
+
- **修复**:`changedFiles` 现在过滤忽略路径
|
|
431
|
+
- **功能**:告警结构化文件列表 — 每文件路径、操作、+/- 行数,支持排序
|
|
432
|
+
- **功能**:Shadow 增量硬链接 — 未变更文件链接到上次快照,节省磁盘空间
|
|
433
|
+
- **功能**:`always_watch: true` 配置 — watcher 随 MCP server 自动启动,零保护缺口
|
|
434
|
+
- **功能**:Dashboard 服务单例 — 多项目共享一个端口,热加载新项目
|
|
435
|
+
- **功能**:Dashboard 版本检测 + 一键热重启(`/api/restart` 端点)
|
|
436
|
+
- **功能**:文件详情弹窗 + 每文件恢复命令复制按钮
|
|
437
|
+
- **功能**:`cursor-guard-init` 自动创建 `.cursor-guard.json`;支持 `backup_interval_seconds` 别名
|
|
438
|
+
- **许可证**:从 MIT 变更为 BSL 1.1(源码可见,商业使用需作者授权)
|
|
439
|
+
|
|
380
440
|
### v4.4.0 — V4 收官版
|
|
381
441
|
|
|
382
442
|
- **修复**:首次快照现在会生成 "Added N: file1, file2, ..." 摘要,而不是空白——之前第一次备份因为没有 parent tree 对比所以 summary 始终为空
|
|
@@ -466,6 +526,16 @@ node references\dashboard\server.js --path "D:\MyProject"
|
|
|
466
526
|
|
|
467
527
|
---
|
|
468
528
|
|
|
529
|
+
## 支持 / 捐赠
|
|
530
|
+
|
|
531
|
+
这是一个独立开发者维护的开源项目。如果 Cursor Guard 拯救过你的代码或节省了你的时间,欢迎请我喝杯咖啡 :)
|
|
532
|
+
|
|
533
|
+
| 微信支付 | 支付宝 |
|
|
534
|
+
|:---:|:---:|
|
|
535
|
+
| <img src="media/wechat-pay.png" alt="微信支付" width="200"> | <img src="media/alipay.jpg" alt="支付宝" width="200"> |
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
469
539
|
## 许可证
|
|
470
540
|
|
|
471
|
-
|
|
541
|
+
[BSL 1.1(商业源代码许可证)](LICENSE) — 源代码自由查看、修改和非商业使用。商业使用需获得作者授权。2056-03-22 之后自动转为 Apache 2.0。
|
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.1`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.7.1` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -734,14 +734,29 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
734
734
|
}
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
-
### V4.7
|
|
737
|
+
### V4.7.1:IDE 插件 Bug 修复 + UX 增强 ✅
|
|
738
738
|
|
|
739
|
-
|
|
|
739
|
+
| 修复/增强 | 说明 |
|
|
740
|
+
|----------|------|
|
|
741
|
+
| **P1 Bug 修复** | `snapshotNow` 中 `loadConfig` 返回 `{cfg, loaded, error, warnings}`,但代码直接传了整个对象。修复为解构 `const { cfg } = loadConfig()` |
|
|
742
|
+
| **WebView CSP** | WebView 缺少 Content-Security-Policy meta 标签,导致 `fetch` 请求被浏览器安全策略阻止(failed to fetch)。添加 CSP 允许 `connect-src` 到 Dashboard Server |
|
|
743
|
+
| **一键启动/停止 Watcher** | `Start Watcher` / `Stop Watcher` 命令从提示文字改为实际 `spawn` 子进程启动和 `SIGTERM` 停止 |
|
|
744
|
+
| **TreeView 彩色化** | 所有图标使用 `ThemeColor`(绿色=正常、红色=告警/停止、蓝色=备份、紫色=统计)。项目节点根据状态显示 Protected/Unprotected/ALERT。新增 Quick Actions 折叠区(Open Dashboard / Snapshot Now / Start/Stop Watcher / Refresh) |
|
|
745
|
+
| **StatusBar 增强** | Watcher 未运行时显示 `$(eye-closed) Guard: Unprotected`;告警时图标动画 `$(bell~spin)` |
|
|
746
|
+
|
|
747
|
+
### V4.7.0:IDE 集成(VSCode/Cursor Extension) ✅
|
|
748
|
+
|
|
749
|
+
| 组件 | 说明 |
|
|
740
750
|
|------|------|
|
|
741
|
-
|
|
|
742
|
-
|
|
|
743
|
-
|
|
|
744
|
-
|
|
|
751
|
+
| `extension.js` | 扩展入口。自动检测 `.cursor-guard.json` 激活,启动内嵌 Dashboard Server,注册所有命令和视图 |
|
|
752
|
+
| `dashboard-manager.js` | 复用现有 `dashboard/server.js` 单例模式,在扩展宿主进程内直接 require,零额外开销。支持多 workspace folder 热加载 |
|
|
753
|
+
| `webview-provider.js` | WebView Panel 管理。加载 `dashboard/public/` 前端,通过 `asWebviewUri()` 转换资源路径,注入 `__GUARD_TOKEN__` + `__GUARD_BASE_URL__` + `__IN_VSCODE__` |
|
|
754
|
+
| `status-bar.js` | 状态栏告警指示器。正常时显示 `$(shield) Guard: OK`,告警时黄色背景 `$(warning) Guard: 22 files!`,点击打开 Dashboard |
|
|
755
|
+
| `tree-view.js` | Activity Bar 侧边栏。树形展示项目列表,每个项目下显示 Watcher 状态、最近备份时间、备份统计、活跃告警、健康评估 |
|
|
756
|
+
| `poller.js` | 后台每 5 秒轮询 `/api/page-data`,驱动状态栏和 TreeView 实时更新 |
|
|
757
|
+
| `app.js` 适配 | `fetchJson` 支持 `window.__GUARD_BASE_URL__` 前缀(兼容浏览器和 WebView);`copyText` 在 VSCode 中通过 `postMessage` 桥接到 `vscode.env.clipboard` |
|
|
758
|
+
| 命令面板 | `Open Dashboard` / `Snapshot Now` / `Start/Stop Watcher` / `Refresh` |
|
|
759
|
+
| 兼容性 | VSCode ^1.74.0,覆盖 Cursor、Windsurf 等所有 VSCode 衍生 IDE |
|
|
745
760
|
|
|
746
761
|
### V4 不做的事
|
|
747
762
|
|
package/media/alipay.jpg
ADDED
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.1",
|
|
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,8 @@
|
|
|
50
50
|
"references/lib/core/",
|
|
51
51
|
"references/mcp/",
|
|
52
52
|
"references/dashboard/",
|
|
53
|
+
"references/vscode-extension/",
|
|
54
|
+
"media/",
|
|
53
55
|
"references/config-reference.md",
|
|
54
56
|
"references/config-reference.zh-CN.md",
|
|
55
57
|
"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,111 @@
|
|
|
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', async () => {
|
|
44
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
45
|
+
if (!folders || folders.length === 0) {
|
|
46
|
+
vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const projectPath = folders[0].uri.fsPath;
|
|
50
|
+
const existingPid = dashMgr.getWatcherPid(projectPath);
|
|
51
|
+
if (existingPid) {
|
|
52
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher already running (PID ${existingPid})`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const pid = dashMgr.startWatcher(projectPath);
|
|
56
|
+
if (pid) {
|
|
57
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher started (PID ${pid})`);
|
|
58
|
+
setTimeout(() => poller.forceRefresh(), 2000);
|
|
59
|
+
} else {
|
|
60
|
+
vscode.window.showWarningMessage('Cursor Guard: failed to start watcher');
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
vscode.commands.registerCommand('cursorGuard.stopWatcher', async () => {
|
|
65
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
66
|
+
if (!folders || folders.length === 0) return;
|
|
67
|
+
const projectPath = folders[0].uri.fsPath;
|
|
68
|
+
const stopped = dashMgr.stopWatcher(projectPath);
|
|
69
|
+
if (stopped) {
|
|
70
|
+
vscode.window.showInformationMessage('Cursor Guard: watcher stopped');
|
|
71
|
+
setTimeout(() => poller.forceRefresh(), 1000);
|
|
72
|
+
} else {
|
|
73
|
+
vscode.window.showWarningMessage('Cursor Guard: no running watcher found');
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
|
|
77
|
+
vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
|
|
78
|
+
poller.forceRefresh();
|
|
79
|
+
treeView.refresh();
|
|
80
|
+
}),
|
|
81
|
+
|
|
82
|
+
statusBar,
|
|
83
|
+
poller,
|
|
84
|
+
treeView,
|
|
85
|
+
webviewProvider,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
89
|
+
if (started) {
|
|
90
|
+
poller.start();
|
|
91
|
+
vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
context.subscriptions.push(
|
|
95
|
+
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
96
|
+
const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
97
|
+
if (restarted && !poller._timer) poller.start();
|
|
98
|
+
poller.forceRefresh();
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deactivate() {
|
|
104
|
+
if (poller) poller.dispose();
|
|
105
|
+
if (statusBar) statusBar.dispose();
|
|
106
|
+
if (treeView) treeView.dispose();
|
|
107
|
+
if (webviewProvider) webviewProvider.dispose();
|
|
108
|
+
if (dashMgr) dashMgr.dispose();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { activate, deactivate };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE = '.cursor-guard.json';
|
|
9
|
+
|
|
10
|
+
class DashboardManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this._instance = null;
|
|
13
|
+
this._serverModule = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get running() { return !!this._instance; }
|
|
17
|
+
get port() { return this._instance?.port; }
|
|
18
|
+
get token() { return this._instance?.token; }
|
|
19
|
+
get baseUrl() { return this._instance ? `http://127.0.0.1:${this._instance.port}` : null; }
|
|
20
|
+
get registry() { return this._instance?.registry; }
|
|
21
|
+
|
|
22
|
+
async autoStart(workspaceFolders) {
|
|
23
|
+
if (!workspaceFolders || workspaceFolders.length === 0) return false;
|
|
24
|
+
const paths = workspaceFolders
|
|
25
|
+
.map(f => f.uri.fsPath)
|
|
26
|
+
.filter(p => fs.existsSync(path.join(p, CONFIG_FILE)));
|
|
27
|
+
if (paths.length === 0) return false;
|
|
28
|
+
return this.start(paths);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start(paths) {
|
|
32
|
+
if (!this._serverModule) {
|
|
33
|
+
this._serverModule = require('../../dashboard/server');
|
|
34
|
+
}
|
|
35
|
+
const { startDashboardServer, getInstance } = this._serverModule;
|
|
36
|
+
const existing = getInstance();
|
|
37
|
+
if (existing) {
|
|
38
|
+
await startDashboardServer(paths, { silent: true });
|
|
39
|
+
this._instance = getInstance();
|
|
40
|
+
} else {
|
|
41
|
+
this._instance = await startDashboardServer(paths, { port: 3120, silent: true });
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetchApi(endpoint) {
|
|
47
|
+
if (!this._instance) return null;
|
|
48
|
+
const url = `${this.baseUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}token=${this.token}`;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
http.get(url, (res) => {
|
|
51
|
+
let data = '';
|
|
52
|
+
res.on('data', chunk => data += chunk);
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
try { resolve(JSON.parse(data)); }
|
|
55
|
+
catch { resolve(null); }
|
|
56
|
+
});
|
|
57
|
+
}).on('error', () => resolve(null));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async getProjects() {
|
|
62
|
+
return this.fetchApi('/api/projects') || [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getPageData(projectId, scope) {
|
|
66
|
+
const scopeParam = scope ? `&scope=${scope}` : '';
|
|
67
|
+
return this.fetchApi(`/api/page-data?id=${projectId}${scopeParam}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async snapshotNow(projectPath) {
|
|
71
|
+
if (!projectPath) return;
|
|
72
|
+
try {
|
|
73
|
+
const { createGitSnapshot } = require('../../lib/core/snapshot');
|
|
74
|
+
const { loadConfig } = require('../../lib/utils');
|
|
75
|
+
const { cfg } = loadConfig(projectPath);
|
|
76
|
+
return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { status: 'error', error: e.message };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
startWatcher(projectPath) {
|
|
83
|
+
if (!projectPath) return null;
|
|
84
|
+
const backupScript = path.resolve(__dirname, '..', '..', 'lib', 'auto-backup.js');
|
|
85
|
+
const child = spawn(process.execPath, [backupScript, '--path', projectPath], {
|
|
86
|
+
cwd: projectPath,
|
|
87
|
+
stdio: 'ignore',
|
|
88
|
+
detached: true,
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
return child.pid;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stopWatcher(projectPath) {
|
|
95
|
+
if (!projectPath) return false;
|
|
96
|
+
try {
|
|
97
|
+
const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
|
|
98
|
+
if (!fs.existsSync(lockPath)) return false;
|
|
99
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
100
|
+
if (lockData.pid) {
|
|
101
|
+
process.kill(lockData.pid, 'SIGTERM');
|
|
102
|
+
try { fs.unlinkSync(lockPath); } catch { /* ok */ }
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
} catch { /* ok */ }
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getWatcherPid(projectPath) {
|
|
110
|
+
try {
|
|
111
|
+
const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
|
|
112
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
113
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
114
|
+
if (lockData.pid) {
|
|
115
|
+
process.kill(lockData.pid, 0);
|
|
116
|
+
return lockData.pid;
|
|
117
|
+
}
|
|
118
|
+
} catch { /* not running */ }
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dispose() {
|
|
123
|
+
this._instance = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
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,68 @@
|
|
|
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: init...';
|
|
18
|
+
this._item.backgroundColor = undefined;
|
|
19
|
+
this._item.color = new vscode.ThemeColor('statusBar.foreground');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_update(data) {
|
|
23
|
+
let hasAlert = false;
|
|
24
|
+
let watcherRunning = false;
|
|
25
|
+
let alertFileCount = 0;
|
|
26
|
+
let projectCount = 0;
|
|
27
|
+
|
|
28
|
+
for (const [, p] of data) {
|
|
29
|
+
projectCount++;
|
|
30
|
+
const d = p.dashboard;
|
|
31
|
+
if (!d) continue;
|
|
32
|
+
if (d.alerts?.active) {
|
|
33
|
+
hasAlert = true;
|
|
34
|
+
alertFileCount = d.alerts.latest?.fileCount || 0;
|
|
35
|
+
}
|
|
36
|
+
if (d.watcher?.running) watcherRunning = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (hasAlert) {
|
|
40
|
+
this._item.text = `$(bell~spin) Guard: ${alertFileCount} files!`;
|
|
41
|
+
this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
|
42
|
+
this._item.color = undefined;
|
|
43
|
+
this._item.tooltip = `Cursor Guard — ALERT: ${alertFileCount} files changed rapidly. Click to open dashboard.`;
|
|
44
|
+
} else if (watcherRunning) {
|
|
45
|
+
this._item.text = '$(shield) Guard: OK';
|
|
46
|
+
this._item.backgroundColor = undefined;
|
|
47
|
+
this._item.color = new vscode.ThemeColor('statusBar.foreground');
|
|
48
|
+
this._item.tooltip = 'Cursor Guard — watcher running, no alerts. Click to open dashboard.';
|
|
49
|
+
} else if (projectCount > 0) {
|
|
50
|
+
this._item.text = '$(eye-closed) Guard: Unprotected';
|
|
51
|
+
this._item.backgroundColor = undefined;
|
|
52
|
+
this._item.color = new vscode.ThemeColor('statusBar.foreground');
|
|
53
|
+
this._item.tooltip = 'Cursor Guard — watcher NOT running. Click to open dashboard, or use Command Palette > Start Watcher.';
|
|
54
|
+
} else {
|
|
55
|
+
this._item.text = '$(shield) Guard';
|
|
56
|
+
this._item.backgroundColor = undefined;
|
|
57
|
+
this._item.color = new vscode.ThemeColor('statusBar.foreground');
|
|
58
|
+
this._item.tooltip = 'Cursor Guard — no projects detected';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dispose() {
|
|
63
|
+
this._sub?.dispose();
|
|
64
|
+
this._item.dispose();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { StatusBarController };
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
if (element.contextValue === 'actions') return this._getActionItems();
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_getRootItems() {
|
|
32
|
+
const data = this._poller.data;
|
|
33
|
+
const items = [];
|
|
34
|
+
|
|
35
|
+
if (data.size === 0) {
|
|
36
|
+
const hint = new TreeItem('No projects detected', 'info', {
|
|
37
|
+
icon: new vscode.ThemeIcon('info', new vscode.ThemeColor('charts.yellow')),
|
|
38
|
+
description: 'Add .cursor-guard.json',
|
|
39
|
+
});
|
|
40
|
+
items.push(hint);
|
|
41
|
+
} else {
|
|
42
|
+
for (const [id, p] of data) {
|
|
43
|
+
const d = p.dashboard;
|
|
44
|
+
const hasAlert = d?.alerts?.active;
|
|
45
|
+
const watcherOk = d?.watcher?.running;
|
|
46
|
+
const iconColor = hasAlert
|
|
47
|
+
? new vscode.ThemeColor('charts.red')
|
|
48
|
+
: watcherOk
|
|
49
|
+
? new vscode.ThemeColor('charts.green')
|
|
50
|
+
: new vscode.ThemeColor('charts.yellow');
|
|
51
|
+
const icon = hasAlert
|
|
52
|
+
? new vscode.ThemeIcon('shield', iconColor)
|
|
53
|
+
: watcherOk
|
|
54
|
+
? new vscode.ThemeIcon('shield', iconColor)
|
|
55
|
+
: new vscode.ThemeIcon('shield', iconColor);
|
|
56
|
+
|
|
57
|
+
const item = new TreeItem(p.name || id, 'project', {
|
|
58
|
+
icon,
|
|
59
|
+
description: hasAlert ? 'ALERT' : watcherOk ? 'Protected' : 'Unprotected',
|
|
60
|
+
collapsible: vscode.TreeItemCollapsibleState.Expanded,
|
|
61
|
+
});
|
|
62
|
+
item.projectId = id;
|
|
63
|
+
items.push(item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const actionsItem = new TreeItem('Quick Actions', 'actions', {
|
|
68
|
+
icon: new vscode.ThemeIcon('zap', new vscode.ThemeColor('charts.blue')),
|
|
69
|
+
collapsible: vscode.TreeItemCollapsibleState.Collapsed,
|
|
70
|
+
});
|
|
71
|
+
items.push(actionsItem);
|
|
72
|
+
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_getActionItems() {
|
|
77
|
+
const openDash = new TreeItem('Open Dashboard', 'action', {
|
|
78
|
+
icon: new vscode.ThemeIcon('dashboard', new vscode.ThemeColor('charts.blue')),
|
|
79
|
+
});
|
|
80
|
+
openDash.command = { command: 'cursorGuard.openDashboard', title: 'Open Dashboard' };
|
|
81
|
+
|
|
82
|
+
const snapshot = new TreeItem('Snapshot Now', 'action', {
|
|
83
|
+
icon: new vscode.ThemeIcon('device-camera', new vscode.ThemeColor('charts.purple')),
|
|
84
|
+
});
|
|
85
|
+
snapshot.command = { command: 'cursorGuard.snapshotNow', title: 'Snapshot Now' };
|
|
86
|
+
|
|
87
|
+
const startW = new TreeItem('Start Watcher', 'action', {
|
|
88
|
+
icon: new vscode.ThemeIcon('play', new vscode.ThemeColor('charts.green')),
|
|
89
|
+
});
|
|
90
|
+
startW.command = { command: 'cursorGuard.startWatcher', title: 'Start Watcher' };
|
|
91
|
+
|
|
92
|
+
const stopW = new TreeItem('Stop Watcher', 'action', {
|
|
93
|
+
icon: new vscode.ThemeIcon('debug-stop', new vscode.ThemeColor('charts.red')),
|
|
94
|
+
});
|
|
95
|
+
stopW.command = { command: 'cursorGuard.stopWatcher', title: 'Stop Watcher' };
|
|
96
|
+
|
|
97
|
+
const refresh = new TreeItem('Refresh', 'action', {
|
|
98
|
+
icon: new vscode.ThemeIcon('refresh', new vscode.ThemeColor('charts.orange')),
|
|
99
|
+
});
|
|
100
|
+
refresh.command = { command: 'cursorGuard.refreshTree', title: 'Refresh' };
|
|
101
|
+
|
|
102
|
+
return [openDash, snapshot, startW, stopW, refresh];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_getProjectChildren(projectId) {
|
|
106
|
+
const p = this._poller.data.get(projectId);
|
|
107
|
+
if (!p?.dashboard) {
|
|
108
|
+
return [new TreeItem('Loading...', 'loading', {
|
|
109
|
+
icon: new vscode.ThemeIcon('loading~spin', new vscode.ThemeColor('charts.blue')),
|
|
110
|
+
})];
|
|
111
|
+
}
|
|
112
|
+
const d = p.dashboard;
|
|
113
|
+
const items = [];
|
|
114
|
+
|
|
115
|
+
if (d.watcher?.running) {
|
|
116
|
+
const w = new TreeItem('Watcher: Running', 'watcher', {
|
|
117
|
+
icon: new vscode.ThemeIcon('eye', new vscode.ThemeColor('charts.green')),
|
|
118
|
+
description: d.watcher.pid ? `PID ${d.watcher.pid}` : '',
|
|
119
|
+
});
|
|
120
|
+
items.push(w);
|
|
121
|
+
} else {
|
|
122
|
+
const w = new TreeItem('Watcher: Stopped', 'watcher', {
|
|
123
|
+
icon: new vscode.ThemeIcon('eye-closed', new vscode.ThemeColor('charts.red')),
|
|
124
|
+
description: 'Click Quick Actions > Start',
|
|
125
|
+
});
|
|
126
|
+
items.push(w);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (d.alerts?.active) {
|
|
130
|
+
const a = d.alerts.latest || {};
|
|
131
|
+
const alertItem = new TreeItem(
|
|
132
|
+
`ALERT: ${a.fileCount || '?'} files in ${a.windowSeconds || '?'}s`,
|
|
133
|
+
'alert',
|
|
134
|
+
{
|
|
135
|
+
icon: new vscode.ThemeIcon('bell', new vscode.ThemeColor('charts.red')),
|
|
136
|
+
description: `threshold: ${a.threshold}`,
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
items.push(alertItem);
|
|
140
|
+
} else {
|
|
141
|
+
items.push(new TreeItem('No alerts', 'noalert', {
|
|
142
|
+
icon: new vscode.ThemeIcon('check', new vscode.ThemeColor('charts.green')),
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (d.lastBackup?.git) {
|
|
147
|
+
const ago = this._relativeTime(d.lastBackup.git.timestamp);
|
|
148
|
+
items.push(new TreeItem(`Last Backup: ${ago}`, 'backup', {
|
|
149
|
+
icon: new vscode.ThemeIcon('git-commit', new vscode.ThemeColor('charts.blue')),
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (d.counts) {
|
|
154
|
+
const gitCount = d.counts.git?.commits || 0;
|
|
155
|
+
const shadowCount = d.counts.shadow?.snapshots || 0;
|
|
156
|
+
items.push(new TreeItem(`Git: ${gitCount} Shadow: ${shadowCount}`, 'counts', {
|
|
157
|
+
icon: new vscode.ThemeIcon('database', new vscode.ThemeColor('charts.purple')),
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const health = d.health?.status || 'unknown';
|
|
162
|
+
const healthColor = health === 'healthy'
|
|
163
|
+
? new vscode.ThemeColor('charts.green')
|
|
164
|
+
: health === 'critical'
|
|
165
|
+
? new vscode.ThemeColor('charts.red')
|
|
166
|
+
: new vscode.ThemeColor('charts.yellow');
|
|
167
|
+
const healthIcon = health === 'healthy' ? 'pass-filled' : health === 'critical' ? 'error' : 'warning';
|
|
168
|
+
const healthItem = new TreeItem(`Health: ${health}`, 'health', {
|
|
169
|
+
icon: new vscode.ThemeIcon(healthIcon, healthColor),
|
|
170
|
+
description: d.health?.issues?.length > 0 ? d.health.issues[0] : '',
|
|
171
|
+
});
|
|
172
|
+
items.push(healthItem);
|
|
173
|
+
|
|
174
|
+
return items;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_relativeTime(ts) {
|
|
178
|
+
const diff = Date.now() - new Date(ts).getTime();
|
|
179
|
+
const sec = Math.floor(diff / 1000);
|
|
180
|
+
if (sec < 60) return `${sec}s ago`;
|
|
181
|
+
const min = Math.floor(sec / 60);
|
|
182
|
+
if (min < 60) return `${min}m ago`;
|
|
183
|
+
const hr = Math.floor(min / 60);
|
|
184
|
+
return `${hr}h ago`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
dispose() {
|
|
188
|
+
this._sub?.dispose();
|
|
189
|
+
this._treeView.dispose();
|
|
190
|
+
this._onDidChange.dispose();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
class TreeItem extends vscode.TreeItem {
|
|
195
|
+
constructor(label, contextValue, opts = {}) {
|
|
196
|
+
super(label, opts.collapsible || vscode.TreeItemCollapsibleState.None);
|
|
197
|
+
this.contextValue = contextValue;
|
|
198
|
+
if (opts.icon) this.iconPath = opts.icon;
|
|
199
|
+
if (opts.description) this.description = opts.description;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { GuardTreeView };
|
|
@@ -0,0 +1,95 @@
|
|
|
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
|
+
const csp = [
|
|
61
|
+
`default-src 'none'`,
|
|
62
|
+
`style-src ${webview.cspSource} 'unsafe-inline'`,
|
|
63
|
+
`script-src 'nonce-${nonce}' ${webview.cspSource}`,
|
|
64
|
+
`font-src ${webview.cspSource}`,
|
|
65
|
+
`img-src ${webview.cspSource} data:`,
|
|
66
|
+
`connect-src ${baseUrl}`,
|
|
67
|
+
].join('; ');
|
|
68
|
+
|
|
69
|
+
html = html.replace(
|
|
70
|
+
'</head>',
|
|
71
|
+
`<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
72
|
+
<script nonce="${nonce}">
|
|
73
|
+
window.__GUARD_TOKEN__ = "${token}";
|
|
74
|
+
window.__GUARD_BASE_URL__ = "${baseUrl}";
|
|
75
|
+
window.__IN_VSCODE__ = true;
|
|
76
|
+
</script>
|
|
77
|
+
</head>`
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return html;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dispose() {
|
|
84
|
+
if (this._panel) { this._panel.dispose(); this._panel = null; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _getNonce() {
|
|
89
|
+
let text = '';
|
|
90
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
91
|
+
for (let i = 0; i < 32; i++) text += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
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
|
+
}
|