cursor-guard 4.7.0 → 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 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
@@ -388,6 +397,7 @@ The skill activates on these signals:
388
397
  | `references/bin/cursor-guard-doctor.js` | CLI entry: `npx cursor-guard-doctor` |
389
398
  | `references/dashboard/server.js` | Dashboard HTTP server + REST API |
390
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 |
391
401
  | `references/auto-backup.ps1` / `.sh` | Thin wrappers (Windows / macOS+Linux) |
392
402
  | `references/guard-doctor.ps1` / `.sh` | Thin wrappers (Windows / macOS+Linux) |
393
403
  | `references/recovery.md` | Recovery command templates |
@@ -400,6 +410,33 @@ The skill activates on these signals:
400
410
 
401
411
  ## Changelog
402
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
+
403
440
  ### v4.4.0 — V4 Final
404
441
 
405
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
@@ -489,6 +526,16 @@ The skill activates on these signals:
489
526
 
490
527
  ---
491
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
+
492
539
  ## License
493
540
 
494
- MIT
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
@@ -388,6 +397,7 @@ code --install-extension .
388
397
  | `references/bin/cursor-guard-doctor.js` | CLI 入口:`npx cursor-guard-doctor` |
389
398
  | `references/dashboard/server.js` | 仪表盘 HTTP 服务 + REST API |
390
399
  | `references/dashboard/public/` | 仪表盘 Web UI(index.html、style.css、app.js) |
400
+ | `references/vscode-extension/` | IDE 扩展:WebView 仪表盘、状态栏、侧边栏树、命令面板 |
391
401
  | `references/auto-backup.ps1` / `.sh` | 薄封装(Windows / macOS+Linux) |
392
402
  | `references/guard-doctor.ps1` / `.sh` | 薄封装(Windows / macOS+Linux) |
393
403
  | `references/recovery.md` | 恢复命令模板 |
@@ -400,6 +410,33 @@ code --install-extension .
400
410
 
401
411
  ## 更新日志
402
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
+
403
440
  ### v4.4.0 — V4 收官版
404
441
 
405
442
  - **修复**:首次快照现在会生成 "Added N: file1, file2, ..." 摘要,而不是空白——之前第一次备份因为没有 parent tree 对比所以 summary 始终为空
@@ -489,6 +526,16 @@ code --install-extension .
489
526
 
490
527
  ---
491
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
+
492
539
  ## 许可证
493
540
 
494
- MIT
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.0`
7
- > **文档状态**:`V2` ~ `V4.7.0` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
6
+ > **当前版本**:`V4.7.1`
7
+ > **文档状态**:`V2` ~ `V4.7.1` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -734,6 +734,16 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
734
734
  }
735
735
  ```
736
736
 
737
+ ### V4.7.1:IDE 插件 Bug 修复 + UX 增强 ✅
738
+
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
+
737
747
  ### V4.7.0:IDE 集成(VSCode/Cursor Extension) ✅
738
748
 
739
749
  | 组件 | 说明 |
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.7.0",
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",
@@ -51,6 +51,7 @@
51
51
  "references/mcp/",
52
52
  "references/dashboard/",
53
53
  "references/vscode-extension/",
54
+ "media/",
54
55
  "references/config-reference.md",
55
56
  "references/config-reference.zh-CN.md",
56
57
  "references/cursor-guard.example.json",
@@ -40,16 +40,38 @@ async function activate(context) {
40
40
  poller.forceRefresh();
41
41
  }),
42
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
- );
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
+ }
47
62
  }),
48
63
 
49
- vscode.commands.registerCommand('cursorGuard.stopWatcher', () => {
50
- vscode.window.showInformationMessage(
51
- 'Cursor Guard: stop the watcher by terminating its terminal process (Ctrl+C).'
52
- );
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
+ }
53
75
  }),
54
76
 
55
77
  vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const http = require('http');
6
+ const { spawn } = require('child_process');
6
7
 
7
8
  const CONFIG_FILE = '.cursor-guard.json';
8
9
 
@@ -71,13 +72,53 @@ class DashboardManager {
71
72
  try {
72
73
  const { createGitSnapshot } = require('../../lib/core/snapshot');
73
74
  const { loadConfig } = require('../../lib/utils');
74
- const cfg = loadConfig(projectPath);
75
+ const { cfg } = loadConfig(projectPath);
75
76
  return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
76
77
  } catch (e) {
77
78
  return { status: 'error', error: e.message };
78
79
  }
79
80
  }
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
+
81
122
  dispose() {
82
123
  this._instance = null;
83
124
  }
@@ -14,16 +14,19 @@ class StatusBarController {
14
14
  }
15
15
 
16
16
  _setIdle() {
17
- this._item.text = '$(shield) Guard';
17
+ this._item.text = '$(shield) Guard: init...';
18
18
  this._item.backgroundColor = undefined;
19
+ this._item.color = new vscode.ThemeColor('statusBar.foreground');
19
20
  }
20
21
 
21
22
  _update(data) {
22
23
  let hasAlert = false;
23
24
  let watcherRunning = false;
24
25
  let alertFileCount = 0;
26
+ let projectCount = 0;
25
27
 
26
28
  for (const [, p] of data) {
29
+ projectCount++;
27
30
  const d = p.dashboard;
28
31
  if (!d) continue;
29
32
  if (d.alerts?.active) {
@@ -34,17 +37,25 @@ class StatusBarController {
34
37
  }
35
38
 
36
39
  if (hasAlert) {
37
- this._item.text = `$(warning) Guard: ${alertFileCount} files!`;
40
+ this._item.text = `$(bell~spin) Guard: ${alertFileCount} files!`;
38
41
  this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
39
- this._item.tooltip = `Cursor Guard — ALERT: ${alertFileCount} files changed rapidly`;
42
+ this._item.color = undefined;
43
+ this._item.tooltip = `Cursor Guard — ALERT: ${alertFileCount} files changed rapidly. Click to open dashboard.`;
40
44
  } else if (watcherRunning) {
41
45
  this._item.text = '$(shield) Guard: OK';
42
46
  this._item.backgroundColor = undefined;
43
- this._item.tooltip = 'Cursor Guard — watcher running, no alerts';
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.';
44
54
  } else {
45
55
  this._item.text = '$(shield) Guard';
46
56
  this._item.backgroundColor = undefined;
47
- this._item.tooltip = 'Cursor Guard — watcher not running';
57
+ this._item.color = new vscode.ThemeColor('statusBar.foreground');
58
+ this._item.tooltip = 'Cursor Guard — no projects detected';
48
59
  }
49
60
  }
50
61
 
@@ -24,48 +24,106 @@ class GuardTreeView {
24
24
  getChildren(element) {
25
25
  if (!element) return this._getRootItems();
26
26
  if (element.contextValue === 'project') return this._getProjectChildren(element.projectId);
27
+ if (element.contextValue === 'actions') return this._getActionItems();
27
28
  return [];
28
29
  }
29
30
 
30
31
  _getRootItems() {
31
32
  const data = this._poller.data;
32
- if (data.size === 0) {
33
- return [new TreeItem('No projects detected', 'info', { icon: 'info' })];
34
- }
35
33
  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,
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',
41
39
  });
42
- item.projectId = id;
43
- items.push(item);
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
+ }
44
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
+
45
73
  return items;
46
74
  }
47
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
+
48
105
  _getProjectChildren(projectId) {
49
106
  const p = this._poller.data.get(projectId);
50
- if (!p?.dashboard) return [new TreeItem('Loading...', 'loading', { icon: 'loading~spin' })];
107
+ if (!p?.dashboard) {
108
+ return [new TreeItem('Loading...', 'loading', {
109
+ icon: new vscode.ThemeIcon('loading~spin', new vscode.ThemeColor('charts.blue')),
110
+ })];
111
+ }
51
112
  const d = p.dashboard;
52
113
  const items = [];
53
114
 
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' }));
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);
69
127
  }
70
128
 
71
129
  if (d.alerts?.active) {
@@ -73,18 +131,44 @@ class GuardTreeView {
73
131
  const alertItem = new TreeItem(
74
132
  `ALERT: ${a.fileCount || '?'} files in ${a.windowSeconds || '?'}s`,
75
133
  'alert',
76
- { icon: 'warning' }
134
+ {
135
+ icon: new vscode.ThemeIcon('bell', new vscode.ThemeColor('charts.red')),
136
+ description: `threshold: ${a.threshold}`,
137
+ }
77
138
  );
78
- alertItem.description = `threshold: ${a.threshold}`;
79
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
+ }));
80
144
  }
81
145
 
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];
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
+ }));
87
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
+ });
88
172
  items.push(healthItem);
89
173
 
90
174
  return items;
@@ -111,7 +195,7 @@ class TreeItem extends vscode.TreeItem {
111
195
  constructor(label, contextValue, opts = {}) {
112
196
  super(label, opts.collapsible || vscode.TreeItemCollapsibleState.None);
113
197
  this.contextValue = contextValue;
114
- if (opts.icon) this.iconPath = new vscode.ThemeIcon(opts.icon);
198
+ if (opts.icon) this.iconPath = opts.icon;
115
199
  if (opts.description) this.description = opts.description;
116
200
  }
117
201
  }
@@ -57,9 +57,19 @@ class WebViewProvider {
57
57
  const token = this._dashMgr.token || '';
58
58
  const nonce = _getNonce();
59
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
+
60
69
  html = html.replace(
61
70
  '</head>',
62
- `<script nonce="${nonce}">
71
+ `<meta http-equiv="Content-Security-Policy" content="${csp}">
72
+ <script nonce="${nonce}">
63
73
  window.__GUARD_TOKEN__ = "${token}";
64
74
  window.__GUARD_BASE_URL__ = "${baseUrl}";
65
75
  window.__IN_VSCODE__ = true;