cursor-guard 4.7.0 → 4.7.3
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 +48 -1
- package/README.zh-CN.md +48 -1
- package/ROADMAP.md +36 -2
- package/media/alipay.jpg +0 -0
- package/media/wechat-pay.png +0 -0
- package/package.json +2 -1
- package/references/vscode-extension/extension.js +37 -9
- package/references/vscode-extension/lib/dashboard-manager.js +53 -1
- package/references/vscode-extension/lib/poller.js +8 -2
- package/references/vscode-extension/lib/sidebar-webview.js +383 -0
- package/references/vscode-extension/lib/status-bar.js +16 -5
- package/references/vscode-extension/lib/tree-view.js +56 -61
- package/references/vscode-extension/lib/webview-provider.js +11 -1
- package/references/vscode-extension/package.json +5 -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.7.
|
|
6
|
+
> **当前版本**:`V4.7.3`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.7.3` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -734,6 +734,40 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
734
734
|
}
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
+
### V4.7.3:可视化图表侧边栏 ✅
|
|
738
|
+
|
|
739
|
+
| 组件 | 说明 |
|
|
740
|
+
|------|------|
|
|
741
|
+
| **`sidebar-webview.js`** | 新增 `WebviewViewProvider`,在 Activity Bar 侧边栏底部渲染自定义 HTML/CSS 图表面板 |
|
|
742
|
+
| **状态徽章行** | 4 个彩色卡片一行排列:Watcher(绿/红)、Alerts(红/绿)、Health(绿/黄/红)、Files(蓝) |
|
|
743
|
+
| **告警横幅** | 活跃告警时显示红色横幅:文件数 + 窗口时间 + 阈值 + 倒计时 |
|
|
744
|
+
| **进度条图表** | Backup Statistics 区域用 4 根彩色进度条展示:Git 备份数、Shadow 快照数、Git 磁盘、Shadow 磁盘,附系统剩余空间 |
|
|
745
|
+
| **备份时间线** | Recent Backups 区域用圆点 + 时间 + 类型 + 摘要的紧凑列表展示最近 6 条备份 |
|
|
746
|
+
| **健康问题列表** | 逐条展示 health issues,黄/红圆点标注严重度 |
|
|
747
|
+
| **保护范围标签** | 用绿色/红色药丸标签展示 protect/ignore 规则 |
|
|
748
|
+
| **快捷操作栏** | 4 个 hover 高亮按钮:Dashboard / Snapshot / Start / Stop |
|
|
749
|
+
| **数据推送** | Poller 每 5 秒通过 `postMessage` 推送完整数据到 WebviewView,实时更新所有图表 |
|
|
750
|
+
| **TreeView 精简** | 上方 TreeView 只保留项目概要(名称/状态/操作),详细数据全在图表面板 |
|
|
751
|
+
|
|
752
|
+
### V4.7.2:Sidebar Mini Dashboard + Watcher 修复 ✅
|
|
753
|
+
|
|
754
|
+
| 修复/增强 | 说明 |
|
|
755
|
+
|----------|------|
|
|
756
|
+
| **Watcher 无限重启修复** | `startWatcher` 误用 `lib/auto-backup.js`(库模块),改为正确的 CLI 入口 `bin/cursor-guard-backup.js`;新增防重复检查(已有进程则跳过) |
|
|
757
|
+
| **TreeView Mini Dashboard** | 侧边栏从简单列表重构为 7 个信息密集的折叠区:Watcher 详情(PID/启动时间/策略)、告警详情(倒计时/文件列表 top 10)、近期备份(最近 8 条 + 摘要/时间/类型)、统计(git/shadow 数量 + 磁盘占用 + 系统剩余空间)、健康检查(逐条 issue)、保护范围(protect/ignore 规则 + 文件数)、快捷操作 |
|
|
758
|
+
| **Poller 完整数据** | 后台轮询从只拉 `dashboard` scope 改为拉取完整 page data(含 backups/scope/doctor),驱动 TreeView 展示更多信息 |
|
|
759
|
+
| **彩色区分** | 7 个 section 使用不同 ThemeColor(绿=正常/红=告警/蓝=备份/紫=统计/橙=配置/黄=警告),项目节点根据状态显示 Protected/Unprotected/ALERT |
|
|
760
|
+
|
|
761
|
+
### V4.7.1:IDE 插件 Bug 修复 + UX 增强 ✅
|
|
762
|
+
|
|
763
|
+
| 修复/增强 | 说明 |
|
|
764
|
+
|----------|------|
|
|
765
|
+
| **P1 Bug 修复** | `snapshotNow` 中 `loadConfig` 返回 `{cfg, loaded, error, warnings}`,但代码直接传了整个对象。修复为解构 `const { cfg } = loadConfig()` |
|
|
766
|
+
| **WebView CSP** | WebView 缺少 Content-Security-Policy meta 标签,导致 `fetch` 请求被浏览器安全策略阻止(failed to fetch)。添加 CSP 允许 `connect-src` 到 Dashboard Server |
|
|
767
|
+
| **一键启动/停止 Watcher** | `Start Watcher` / `Stop Watcher` 命令从提示文字改为实际 `spawn` 子进程启动和 `SIGTERM` 停止 |
|
|
768
|
+
| **TreeView 彩色化** | 所有图标使用 `ThemeColor`(绿色=正常、红色=告警/停止、蓝色=备份、紫色=统计)。项目节点根据状态显示 Protected/Unprotected/ALERT。新增 Quick Actions 折叠区(Open Dashboard / Snapshot Now / Start/Stop Watcher / Refresh) |
|
|
769
|
+
| **StatusBar 增强** | Watcher 未运行时显示 `$(eye-closed) Guard: Unprotected`;告警时图标动画 `$(bell~spin)` |
|
|
770
|
+
|
|
737
771
|
### V4.7.0:IDE 集成(VSCode/Cursor Extension) ✅
|
|
738
772
|
|
|
739
773
|
| 组件 | 说明 |
|
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.7.
|
|
3
|
+
"version": "4.7.3",
|
|
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",
|
|
@@ -6,8 +6,9 @@ const { WebViewProvider } = require('./lib/webview-provider');
|
|
|
6
6
|
const { StatusBarController } = require('./lib/status-bar');
|
|
7
7
|
const { GuardTreeView } = require('./lib/tree-view');
|
|
8
8
|
const { Poller } = require('./lib/poller');
|
|
9
|
+
const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
|
|
9
10
|
|
|
10
|
-
let dashMgr, poller, statusBar, treeView, webviewProvider;
|
|
11
|
+
let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider;
|
|
11
12
|
|
|
12
13
|
async function activate(context) {
|
|
13
14
|
dashMgr = new DashboardManager();
|
|
@@ -15,8 +16,11 @@ async function activate(context) {
|
|
|
15
16
|
statusBar = new StatusBarController(poller);
|
|
16
17
|
treeView = new GuardTreeView(poller, dashMgr);
|
|
17
18
|
webviewProvider = new WebViewProvider(context, dashMgr);
|
|
19
|
+
sidebarProvider = new SidebarDashboardProvider(poller);
|
|
18
20
|
|
|
19
21
|
context.subscriptions.push(
|
|
22
|
+
vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
|
|
23
|
+
|
|
20
24
|
vscode.commands.registerCommand('cursorGuard.openDashboard', () => {
|
|
21
25
|
if (!dashMgr.running) {
|
|
22
26
|
vscode.window.showWarningMessage('Cursor Guard: no projects detected. Add .cursor-guard.json to your workspace.');
|
|
@@ -40,16 +44,38 @@ async function activate(context) {
|
|
|
40
44
|
poller.forceRefresh();
|
|
41
45
|
}),
|
|
42
46
|
|
|
43
|
-
vscode.commands.registerCommand('cursorGuard.startWatcher', () => {
|
|
44
|
-
vscode.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
vscode.commands.registerCommand('cursorGuard.startWatcher', async () => {
|
|
48
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
49
|
+
if (!folders || folders.length === 0) {
|
|
50
|
+
vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const projectPath = folders[0].uri.fsPath;
|
|
54
|
+
const existingPid = dashMgr.getWatcherPid(projectPath);
|
|
55
|
+
if (existingPid) {
|
|
56
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher already running (PID ${existingPid})`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const pid = dashMgr.startWatcher(projectPath);
|
|
60
|
+
if (pid) {
|
|
61
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher started (PID ${pid})`);
|
|
62
|
+
setTimeout(() => poller.forceRefresh(), 2000);
|
|
63
|
+
} else {
|
|
64
|
+
vscode.window.showWarningMessage('Cursor Guard: failed to start watcher');
|
|
65
|
+
}
|
|
47
66
|
}),
|
|
48
67
|
|
|
49
|
-
vscode.commands.registerCommand('cursorGuard.stopWatcher', () => {
|
|
50
|
-
vscode.
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
vscode.commands.registerCommand('cursorGuard.stopWatcher', async () => {
|
|
69
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
70
|
+
if (!folders || folders.length === 0) return;
|
|
71
|
+
const projectPath = folders[0].uri.fsPath;
|
|
72
|
+
const stopped = dashMgr.stopWatcher(projectPath);
|
|
73
|
+
if (stopped) {
|
|
74
|
+
vscode.window.showInformationMessage('Cursor Guard: watcher stopped');
|
|
75
|
+
setTimeout(() => poller.forceRefresh(), 1000);
|
|
76
|
+
} else {
|
|
77
|
+
vscode.window.showWarningMessage('Cursor Guard: no running watcher found');
|
|
78
|
+
}
|
|
53
79
|
}),
|
|
54
80
|
|
|
55
81
|
vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
|
|
@@ -61,6 +87,7 @@ async function activate(context) {
|
|
|
61
87
|
poller,
|
|
62
88
|
treeView,
|
|
63
89
|
webviewProvider,
|
|
90
|
+
sidebarProvider,
|
|
64
91
|
);
|
|
65
92
|
|
|
66
93
|
const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
@@ -83,6 +110,7 @@ function deactivate() {
|
|
|
83
110
|
if (statusBar) statusBar.dispose();
|
|
84
111
|
if (treeView) treeView.dispose();
|
|
85
112
|
if (webviewProvider) webviewProvider.dispose();
|
|
113
|
+
if (sidebarProvider) sidebarProvider.dispose();
|
|
86
114
|
if (dashMgr) dashMgr.dispose();
|
|
87
115
|
}
|
|
88
116
|
|
|
@@ -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
|
|
|
@@ -66,18 +67,69 @@ class DashboardManager {
|
|
|
66
67
|
return this.fetchApi(`/api/page-data?id=${projectId}${scopeParam}`);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
async getFullPageData(projectId) {
|
|
71
|
+
return this.fetchApi(`/api/page-data?id=${projectId}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getBackupFiles(projectId, hash) {
|
|
75
|
+
return this.fetchApi(`/api/backup-files?id=${projectId}&hash=${hash}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
async snapshotNow(projectPath) {
|
|
70
79
|
if (!projectPath) return;
|
|
71
80
|
try {
|
|
72
81
|
const { createGitSnapshot } = require('../../lib/core/snapshot');
|
|
73
82
|
const { loadConfig } = require('../../lib/utils');
|
|
74
|
-
const cfg = loadConfig(projectPath);
|
|
83
|
+
const { cfg } = loadConfig(projectPath);
|
|
75
84
|
return createGitSnapshot(projectPath, cfg, { message: 'guard: manual snapshot via IDE extension' });
|
|
76
85
|
} catch (e) {
|
|
77
86
|
return { status: 'error', error: e.message };
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
startWatcher(projectPath) {
|
|
91
|
+
if (!projectPath) return null;
|
|
92
|
+
const existingPid = this.getWatcherPid(projectPath);
|
|
93
|
+
if (existingPid) return existingPid;
|
|
94
|
+
const cliScript = path.resolve(__dirname, '..', '..', 'bin', 'cursor-guard-backup.js');
|
|
95
|
+
const child = spawn(process.execPath, [cliScript, '--path', projectPath], {
|
|
96
|
+
cwd: projectPath,
|
|
97
|
+
stdio: 'ignore',
|
|
98
|
+
detached: true,
|
|
99
|
+
env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
|
|
100
|
+
});
|
|
101
|
+
child.unref();
|
|
102
|
+
return child.pid;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stopWatcher(projectPath) {
|
|
106
|
+
if (!projectPath) return false;
|
|
107
|
+
try {
|
|
108
|
+
const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
|
|
109
|
+
if (!fs.existsSync(lockPath)) return false;
|
|
110
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
111
|
+
if (lockData.pid) {
|
|
112
|
+
process.kill(lockData.pid, 'SIGTERM');
|
|
113
|
+
try { fs.unlinkSync(lockPath); } catch { /* ok */ }
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ok */ }
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
getWatcherPid(projectPath) {
|
|
121
|
+
try {
|
|
122
|
+
const lockPath = path.join(projectPath, '.cursor-guard-backup.lock');
|
|
123
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
124
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
|
|
125
|
+
if (lockData.pid) {
|
|
126
|
+
process.kill(lockData.pid, 0);
|
|
127
|
+
return lockData.pid;
|
|
128
|
+
}
|
|
129
|
+
} catch { /* not running */ }
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
81
133
|
dispose() {
|
|
82
134
|
this._instance = null;
|
|
83
135
|
}
|
|
@@ -41,8 +41,14 @@ class Poller {
|
|
|
41
41
|
const projects = await this._dashMgr.getProjects();
|
|
42
42
|
if (!Array.isArray(projects)) return;
|
|
43
43
|
for (const p of projects) {
|
|
44
|
-
const
|
|
45
|
-
this._data.set(p.id, {
|
|
44
|
+
const fullData = await this._dashMgr.getFullPageData(p.id);
|
|
45
|
+
this._data.set(p.id, {
|
|
46
|
+
...p,
|
|
47
|
+
dashboard: fullData?.dashboard || null,
|
|
48
|
+
backups: fullData?.backups || [],
|
|
49
|
+
scope: fullData?.scope || null,
|
|
50
|
+
doctor: fullData?.doctor || null,
|
|
51
|
+
});
|
|
46
52
|
}
|
|
47
53
|
this._emit();
|
|
48
54
|
} catch { /* non-critical */ }
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const vscode = require('vscode');
|
|
4
|
+
|
|
5
|
+
class SidebarDashboardProvider {
|
|
6
|
+
constructor(poller) {
|
|
7
|
+
this._poller = poller;
|
|
8
|
+
this._view = null;
|
|
9
|
+
this._sub = poller.onChange(data => this._push(data));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
resolveWebviewView(webviewView) {
|
|
13
|
+
this._view = webviewView;
|
|
14
|
+
webviewView.webview.options = { enableScripts: true };
|
|
15
|
+
webviewView.webview.html = _getHtml();
|
|
16
|
+
|
|
17
|
+
webviewView.webview.onDidReceiveMessage(msg => {
|
|
18
|
+
if (msg.cmd === 'ready') this._push(this._poller.data);
|
|
19
|
+
if (msg.cmd === 'exec') vscode.commands.executeCommand(msg.command);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
webviewView.onDidChangeVisibility(() => {
|
|
23
|
+
if (webviewView.visible) this._push(this._poller.data);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_push(data) {
|
|
28
|
+
if (!this._view?.visible) return;
|
|
29
|
+
const payload = {};
|
|
30
|
+
for (const [id, p] of data) {
|
|
31
|
+
payload[id] = {
|
|
32
|
+
name: p.name || id,
|
|
33
|
+
dashboard: p.dashboard,
|
|
34
|
+
backups: (p.backups || []).slice(0, 6),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
this._view.webview.postMessage({ type: 'update', data: payload });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dispose() {
|
|
41
|
+
this._sub?.dispose();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _getHtml() {
|
|
46
|
+
return `<!DOCTYPE html>
|
|
47
|
+
<html lang="en">
|
|
48
|
+
<head>
|
|
49
|
+
<meta charset="UTF-8">
|
|
50
|
+
<style>
|
|
51
|
+
:root {
|
|
52
|
+
--bg: #1e1e2e;
|
|
53
|
+
--surface: #282838;
|
|
54
|
+
--border: #383850;
|
|
55
|
+
--text: #cdd6f4;
|
|
56
|
+
--dim: #6c7086;
|
|
57
|
+
--green: #a6e3a1;
|
|
58
|
+
--red: #f38ba8;
|
|
59
|
+
--yellow: #f9e2af;
|
|
60
|
+
--blue: #89b4fa;
|
|
61
|
+
--purple: #cba6f7;
|
|
62
|
+
--orange: #fab387;
|
|
63
|
+
--teal: #94e2d5;
|
|
64
|
+
--radius: 6px;
|
|
65
|
+
}
|
|
66
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
67
|
+
body {
|
|
68
|
+
font: 11px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
69
|
+
color: var(--text);
|
|
70
|
+
background: transparent;
|
|
71
|
+
padding: 8px;
|
|
72
|
+
}
|
|
73
|
+
.card {
|
|
74
|
+
background: var(--surface);
|
|
75
|
+
border: 1px solid var(--border);
|
|
76
|
+
border-radius: var(--radius);
|
|
77
|
+
padding: 8px 10px;
|
|
78
|
+
margin-bottom: 6px;
|
|
79
|
+
}
|
|
80
|
+
.card-title {
|
|
81
|
+
font-size: 9px;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
text-transform: uppercase;
|
|
84
|
+
letter-spacing: 0.8px;
|
|
85
|
+
color: var(--dim);
|
|
86
|
+
margin-bottom: 6px;
|
|
87
|
+
}
|
|
88
|
+
.status-row {
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: 6px;
|
|
91
|
+
margin-bottom: 6px;
|
|
92
|
+
}
|
|
93
|
+
.status-badge {
|
|
94
|
+
flex: 1;
|
|
95
|
+
text-align: center;
|
|
96
|
+
padding: 6px 4px;
|
|
97
|
+
border-radius: var(--radius);
|
|
98
|
+
background: var(--bg);
|
|
99
|
+
border: 1px solid var(--border);
|
|
100
|
+
}
|
|
101
|
+
.status-badge .icon { font-size: 16px; display: block; }
|
|
102
|
+
.status-badge .label { font-size: 9px; color: var(--dim); margin-top: 2px; }
|
|
103
|
+
.status-badge .value { font-size: 11px; font-weight: 700; }
|
|
104
|
+
.status-badge.ok { border-color: var(--green); }
|
|
105
|
+
.status-badge.ok .value { color: var(--green); }
|
|
106
|
+
.status-badge.warn { border-color: var(--yellow); }
|
|
107
|
+
.status-badge.warn .value { color: var(--yellow); }
|
|
108
|
+
.status-badge.danger { border-color: var(--red); }
|
|
109
|
+
.status-badge.danger .value { color: var(--red); }
|
|
110
|
+
.status-badge.info { border-color: var(--blue); }
|
|
111
|
+
.status-badge.info .value { color: var(--blue); }
|
|
112
|
+
|
|
113
|
+
.alert-bar {
|
|
114
|
+
background: rgba(243,139,168,0.15);
|
|
115
|
+
border: 1px solid var(--red);
|
|
116
|
+
border-radius: var(--radius);
|
|
117
|
+
padding: 6px 10px;
|
|
118
|
+
margin-bottom: 6px;
|
|
119
|
+
text-align: center;
|
|
120
|
+
}
|
|
121
|
+
.alert-bar .alert-title { color: var(--red); font-weight: 700; font-size: 12px; }
|
|
122
|
+
.alert-bar .alert-detail { color: var(--dim); font-size: 10px; margin-top: 2px; }
|
|
123
|
+
.alert-bar.hidden { display: none; }
|
|
124
|
+
|
|
125
|
+
.bar-group { margin-bottom: 4px; }
|
|
126
|
+
.bar-label {
|
|
127
|
+
display: flex;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
font-size: 10px;
|
|
130
|
+
margin-bottom: 2px;
|
|
131
|
+
}
|
|
132
|
+
.bar-label .name { color: var(--text); }
|
|
133
|
+
.bar-label .val { color: var(--dim); font-weight: 600; }
|
|
134
|
+
.bar-track {
|
|
135
|
+
height: 6px;
|
|
136
|
+
background: var(--bg);
|
|
137
|
+
border-radius: 3px;
|
|
138
|
+
overflow: hidden;
|
|
139
|
+
}
|
|
140
|
+
.bar-fill {
|
|
141
|
+
height: 100%;
|
|
142
|
+
border-radius: 3px;
|
|
143
|
+
transition: width 0.4s ease;
|
|
144
|
+
}
|
|
145
|
+
.bar-fill.blue { background: var(--blue); }
|
|
146
|
+
.bar-fill.purple { background: var(--purple); }
|
|
147
|
+
.bar-fill.green { background: var(--green); }
|
|
148
|
+
.bar-fill.orange { background: var(--orange); }
|
|
149
|
+
.bar-fill.teal { background: var(--teal); }
|
|
150
|
+
|
|
151
|
+
.backup-list { list-style: none; }
|
|
152
|
+
.backup-item {
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
padding: 3px 0;
|
|
157
|
+
border-bottom: 1px solid var(--border);
|
|
158
|
+
font-size: 10px;
|
|
159
|
+
}
|
|
160
|
+
.backup-item:last-child { border: none; }
|
|
161
|
+
.backup-dot {
|
|
162
|
+
width: 6px; height: 6px;
|
|
163
|
+
border-radius: 50%;
|
|
164
|
+
flex-shrink: 0;
|
|
165
|
+
}
|
|
166
|
+
.backup-dot.auto { background: var(--blue); }
|
|
167
|
+
.backup-dot.snapshot { background: var(--purple); }
|
|
168
|
+
.backup-dot.restore { background: var(--orange); }
|
|
169
|
+
.backup-time { color: var(--dim); white-space: nowrap; }
|
|
170
|
+
.backup-type { font-weight: 600; min-width: 36px; }
|
|
171
|
+
.backup-summary { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
|
172
|
+
|
|
173
|
+
.scope-tags { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 4px; }
|
|
174
|
+
.scope-tag {
|
|
175
|
+
font-size: 9px;
|
|
176
|
+
padding: 1px 6px;
|
|
177
|
+
border-radius: 10px;
|
|
178
|
+
background: var(--bg);
|
|
179
|
+
}
|
|
180
|
+
.scope-tag.protect { color: var(--green); border: 1px solid var(--green); }
|
|
181
|
+
.scope-tag.ignore { color: var(--red); border: 1px solid var(--red); }
|
|
182
|
+
|
|
183
|
+
.health-row { display: flex; align-items: center; gap: 4px; font-size: 10px; padding: 2px 0; }
|
|
184
|
+
.health-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
185
|
+
|
|
186
|
+
.actions-row { display: flex; gap: 4px; flex-wrap: wrap; }
|
|
187
|
+
.action-btn {
|
|
188
|
+
flex: 1;
|
|
189
|
+
min-width: 70px;
|
|
190
|
+
padding: 5px 4px;
|
|
191
|
+
font-size: 9px;
|
|
192
|
+
font-weight: 600;
|
|
193
|
+
text-align: center;
|
|
194
|
+
border: 1px solid var(--border);
|
|
195
|
+
border-radius: var(--radius);
|
|
196
|
+
background: var(--bg);
|
|
197
|
+
color: var(--text);
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
transition: all 0.15s;
|
|
200
|
+
}
|
|
201
|
+
.action-btn:hover { border-color: var(--blue); color: var(--blue); }
|
|
202
|
+
|
|
203
|
+
.empty-state {
|
|
204
|
+
text-align: center;
|
|
205
|
+
padding: 20px;
|
|
206
|
+
color: var(--dim);
|
|
207
|
+
font-size: 11px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.ring-chart {
|
|
211
|
+
position: relative;
|
|
212
|
+
width: 56px; height: 56px;
|
|
213
|
+
margin: 0 auto 4px;
|
|
214
|
+
}
|
|
215
|
+
.ring-chart svg { transform: rotate(-90deg); }
|
|
216
|
+
.ring-chart .ring-bg { stroke: var(--bg); }
|
|
217
|
+
.ring-chart .ring-fill { transition: stroke-dashoffset 0.6s ease; }
|
|
218
|
+
.ring-label {
|
|
219
|
+
position: absolute;
|
|
220
|
+
top: 50%; left: 50%;
|
|
221
|
+
transform: translate(-50%, -50%);
|
|
222
|
+
font-size: 12px;
|
|
223
|
+
font-weight: 700;
|
|
224
|
+
}
|
|
225
|
+
</style>
|
|
226
|
+
</head>
|
|
227
|
+
<body>
|
|
228
|
+
<div id="root">
|
|
229
|
+
<div class="empty-state">Waiting for data...</div>
|
|
230
|
+
</div>
|
|
231
|
+
<script>
|
|
232
|
+
const vscode = acquireVsCodeApi();
|
|
233
|
+
window.addEventListener('message', e => {
|
|
234
|
+
if (e.data.type === 'update') render(e.data.data);
|
|
235
|
+
});
|
|
236
|
+
vscode.postMessage({ cmd: 'ready' });
|
|
237
|
+
|
|
238
|
+
function render(projects) {
|
|
239
|
+
const ids = Object.keys(projects);
|
|
240
|
+
if (ids.length === 0) {
|
|
241
|
+
document.getElementById('root').innerHTML = '<div class="empty-state">No projects detected</div>';
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let html = '';
|
|
245
|
+
for (const id of ids) {
|
|
246
|
+
const p = projects[id];
|
|
247
|
+
const d = p.dashboard;
|
|
248
|
+
if (!d) { html += '<div class="empty-state">Loading ' + esc(p.name) + '...</div>'; continue; }
|
|
249
|
+
html += renderProject(p.name, d, p.backups || []);
|
|
250
|
+
}
|
|
251
|
+
html += renderActions();
|
|
252
|
+
document.getElementById('root').innerHTML = html;
|
|
253
|
+
document.querySelectorAll('.action-btn').forEach(btn => {
|
|
254
|
+
btn.addEventListener('click', () => vscode.postMessage({ cmd: 'exec', command: btn.dataset.cmd }));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function renderProject(name, d, backups) {
|
|
259
|
+
let h = '';
|
|
260
|
+
|
|
261
|
+
// Status badges row
|
|
262
|
+
const wOk = d.watcher?.running;
|
|
263
|
+
const hasAlert = d.alerts?.active;
|
|
264
|
+
const health = d.health?.status || 'unknown';
|
|
265
|
+
|
|
266
|
+
h += '<div class="status-row">';
|
|
267
|
+
h += badge(wOk ? '👁' : '🚫', 'Watcher', wOk ? 'Running' : 'Stopped', wOk ? 'ok' : 'danger');
|
|
268
|
+
h += badge(hasAlert ? '🔔' : '✅', 'Alerts', hasAlert ? (d.alerts.latest?.fileCount || '!') : 'None', hasAlert ? 'danger' : 'ok');
|
|
269
|
+
h += badge('💚', 'Health', health, health === 'healthy' ? 'ok' : health === 'critical' ? 'danger' : 'warn');
|
|
270
|
+
h += badge('📁', 'Files', d.protectionScope?.fileCount || 0, 'info');
|
|
271
|
+
h += '</div>';
|
|
272
|
+
|
|
273
|
+
// Alert bar
|
|
274
|
+
if (hasAlert) {
|
|
275
|
+
const a = d.alerts.latest;
|
|
276
|
+
const remain = a.expiresAt ? Math.max(0, Math.ceil((new Date(a.expiresAt).getTime() - Date.now()) / 1000)) : 0;
|
|
277
|
+
const display = remain > 60 ? Math.floor(remain/60) + 'm ' + (remain%60) + 's' : remain + 's';
|
|
278
|
+
h += '<div class="alert-bar">';
|
|
279
|
+
h += '<div class="alert-title">⚠ ' + (a.fileCount||'?') + ' files changed in ' + (a.windowSeconds||'?') + 's</div>';
|
|
280
|
+
h += '<div class="alert-detail">Threshold: ' + (a.threshold||'?') + ' · Expires: ' + display + '</div>';
|
|
281
|
+
h += '</div>';
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Backup stats bars
|
|
285
|
+
const gitC = d.counts?.git?.commits || 0;
|
|
286
|
+
const shadowC = d.counts?.shadow?.snapshots || 0;
|
|
287
|
+
const maxC = Math.max(gitC, shadowC, 1);
|
|
288
|
+
const gitDisk = d.diskUsage?.git?.display || '0B';
|
|
289
|
+
const shadowDisk = d.diskUsage?.shadow?.display || '0B';
|
|
290
|
+
const gitBytes = d.diskUsage?.git?.bytes || 0;
|
|
291
|
+
const shadowBytes = d.diskUsage?.shadow?.bytes || 0;
|
|
292
|
+
const maxBytes = Math.max(gitBytes, shadowBytes, 1);
|
|
293
|
+
|
|
294
|
+
h += '<div class="card">';
|
|
295
|
+
h += '<div class="card-title">Backup Statistics</div>';
|
|
296
|
+
h += bar('Git backups', gitC, gitC / maxC * 100, 'blue');
|
|
297
|
+
h += bar('Shadow snapshots', shadowC, shadowC / maxC * 100, 'purple');
|
|
298
|
+
h += bar('Git disk', gitDisk, gitBytes / maxBytes * 100, 'teal');
|
|
299
|
+
h += bar('Shadow disk', shadowDisk, shadowBytes / maxBytes * 100, 'orange');
|
|
300
|
+
if (d.disk) {
|
|
301
|
+
h += '<div class="bar-label" style="margin-top:4px"><span class="name">System free</span><span class="val">' + d.disk.freeGB + ' GB</span></div>';
|
|
302
|
+
}
|
|
303
|
+
h += '</div>';
|
|
304
|
+
|
|
305
|
+
// Recent backups timeline
|
|
306
|
+
h += '<div class="card">';
|
|
307
|
+
h += '<div class="card-title">Recent Backups</div>';
|
|
308
|
+
if (backups.length === 0) {
|
|
309
|
+
h += '<div style="color:var(--dim);font-size:10px">No backups yet</div>';
|
|
310
|
+
} else {
|
|
311
|
+
h += '<ul class="backup-list">';
|
|
312
|
+
for (const b of backups) {
|
|
313
|
+
const time = b.timestamp ? new Date(b.timestamp).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '?';
|
|
314
|
+
const type = b.type || 'auto';
|
|
315
|
+
const dotClass = type === 'git-snapshot' ? 'snapshot' : type === 'pre-restore' ? 'restore' : 'auto';
|
|
316
|
+
const typeLabel = type === 'git-snapshot' ? 'snap' : type === 'pre-restore' ? 'pre-rst' : 'auto';
|
|
317
|
+
const summary = b.summary ? truncate(b.summary, 30) : '';
|
|
318
|
+
const files = b.filesChanged ? b.filesChanged + ' files' : '';
|
|
319
|
+
h += '<li class="backup-item">';
|
|
320
|
+
h += '<span class="backup-dot ' + dotClass + '"></span>';
|
|
321
|
+
h += '<span class="backup-time">' + time + '</span>';
|
|
322
|
+
h += '<span class="backup-type">' + typeLabel + '</span>';
|
|
323
|
+
h += '<span class="backup-summary">' + esc(files + (files && summary ? ' · ' : '') + summary) + '</span>';
|
|
324
|
+
h += '</li>';
|
|
325
|
+
}
|
|
326
|
+
h += '</ul>';
|
|
327
|
+
}
|
|
328
|
+
h += '</div>';
|
|
329
|
+
|
|
330
|
+
// Health issues
|
|
331
|
+
if (d.health?.issues?.length > 0) {
|
|
332
|
+
h += '<div class="card">';
|
|
333
|
+
h += '<div class="card-title">Health Issues</div>';
|
|
334
|
+
for (const issue of d.health.issues) {
|
|
335
|
+
const critical = issue.includes('critically') || issue.includes('requires Git');
|
|
336
|
+
h += '<div class="health-row"><span class="health-dot" style="background:' + (critical ? 'var(--red)' : 'var(--yellow)') + '"></span>' + esc(issue) + '</div>';
|
|
337
|
+
}
|
|
338
|
+
h += '</div>';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Protection scope
|
|
342
|
+
h += '<div class="card">';
|
|
343
|
+
h += '<div class="card-title">Protection Scope</div>';
|
|
344
|
+
const protect = d.protectionScope?.protect || ['**'];
|
|
345
|
+
const ignore = d.protectionScope?.ignore || [];
|
|
346
|
+
h += '<div style="font-size:10px;margin-bottom:4px">' + (d.protectionScope?.fileCount || 0) + ' files monitored</div>';
|
|
347
|
+
h += '<div class="scope-tags">';
|
|
348
|
+
for (const p of protect) h += '<span class="scope-tag protect">✓ ' + esc(p) + '</span>';
|
|
349
|
+
for (const i of ignore.slice(0, 6)) h += '<span class="scope-tag ignore">✗ ' + esc(i) + '</span>';
|
|
350
|
+
if (ignore.length > 6) h += '<span class="scope-tag ignore">+' + (ignore.length - 6) + ' more</span>';
|
|
351
|
+
h += '</div></div>';
|
|
352
|
+
|
|
353
|
+
return h;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderActions() {
|
|
357
|
+
return '<div class="card"><div class="card-title">Quick Actions</div><div class="actions-row">'
|
|
358
|
+
+ '<button class="action-btn" data-cmd="cursorGuard.openDashboard">🖥 Dashboard</button>'
|
|
359
|
+
+ '<button class="action-btn" data-cmd="cursorGuard.snapshotNow">📸 Snapshot</button>'
|
|
360
|
+
+ '<button class="action-btn" data-cmd="cursorGuard.startWatcher">▶ Start</button>'
|
|
361
|
+
+ '<button class="action-btn" data-cmd="cursorGuard.stopWatcher">⏹ Stop</button>'
|
|
362
|
+
+ '</div></div>';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function badge(icon, label, value, cls) {
|
|
366
|
+
return '<div class="status-badge ' + cls + '">'
|
|
367
|
+
+ '<span class="icon">' + icon + '</span>'
|
|
368
|
+
+ '<span class="value">' + esc(String(value)) + '</span>'
|
|
369
|
+
+ '<span class="label">' + label + '</span>'
|
|
370
|
+
+ '</div>';
|
|
371
|
+
}
|
|
372
|
+
function bar(name, val, pct, color) {
|
|
373
|
+
return '<div class="bar-group"><div class="bar-label"><span class="name">' + name + '</span><span class="val">' + val + '</span></div>'
|
|
374
|
+
+ '<div class="bar-track"><div class="bar-fill ' + color + '" style="width:' + Math.max(pct, 2) + '%"></div></div></div>';
|
|
375
|
+
}
|
|
376
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
377
|
+
function truncate(s, n) { return s.length > n ? s.slice(0, n) + '...' : s; }
|
|
378
|
+
</script>
|
|
379
|
+
</body>
|
|
380
|
+
</html>`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = { SidebarDashboardProvider };
|
|
@@ -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 = `$(
|
|
40
|
+
this._item.text = `$(bell~spin) Guard: ${alertFileCount} files!`;
|
|
38
41
|
this._item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
|
|
39
|
-
this._item.
|
|
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.
|
|
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.
|
|
57
|
+
this._item.color = new vscode.ThemeColor('statusBar.foreground');
|
|
58
|
+
this._item.tooltip = 'Cursor Guard — no projects detected';
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
61
|
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
4
|
|
|
5
|
+
const C = {
|
|
6
|
+
green: new vscode.ThemeColor('charts.green'),
|
|
7
|
+
red: new vscode.ThemeColor('charts.red'),
|
|
8
|
+
yellow: new vscode.ThemeColor('charts.yellow'),
|
|
9
|
+
blue: new vscode.ThemeColor('charts.blue'),
|
|
10
|
+
purple: new vscode.ThemeColor('charts.purple'),
|
|
11
|
+
orange: new vscode.ThemeColor('charts.orange'),
|
|
12
|
+
};
|
|
13
|
+
|
|
5
14
|
class GuardTreeView {
|
|
6
15
|
constructor(poller, dashMgr) {
|
|
7
16
|
this._poller = poller;
|
|
@@ -11,32 +20,37 @@ class GuardTreeView {
|
|
|
11
20
|
|
|
12
21
|
this._treeView = vscode.window.createTreeView('cursorGuardProjects', {
|
|
13
22
|
treeDataProvider: this,
|
|
14
|
-
showCollapseAll:
|
|
23
|
+
showCollapseAll: false,
|
|
15
24
|
});
|
|
16
25
|
|
|
17
26
|
this._sub = poller.onChange(() => this._onDidChange.fire());
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
refresh() { this._onDidChange.fire(); }
|
|
30
|
+
getTreeItem(el) { return el; }
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!element) return this._getRootItems();
|
|
26
|
-
if (element.contextValue === 'project') return this._getProjectChildren(element.projectId);
|
|
32
|
+
getChildren(el) {
|
|
33
|
+
if (!el) return this._getRootItems();
|
|
34
|
+
if (el.contextValue === 'project') return this._getProjectStatus(el.projectId);
|
|
27
35
|
return [];
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
_getRootItems() {
|
|
31
39
|
const data = this._poller.data;
|
|
32
40
|
if (data.size === 0) {
|
|
33
|
-
return [
|
|
41
|
+
return [_item('No projects detected', 'info', { icon: 'info', color: C.yellow, desc: 'Add .cursor-guard.json' })];
|
|
34
42
|
}
|
|
35
43
|
const items = [];
|
|
36
44
|
for (const [id, p] of data) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
const d = p.dashboard;
|
|
46
|
+
const hasAlert = d?.alerts?.active;
|
|
47
|
+
const watcherOk = d?.watcher?.running;
|
|
48
|
+
const color = hasAlert ? C.red : watcherOk ? C.green : C.yellow;
|
|
49
|
+
const status = hasAlert ? `ALERT ${d.alerts.latest?.fileCount || ''} files` : watcherOk ? 'Protected' : 'Unprotected';
|
|
50
|
+
const item = _item(p.name || id, 'project', {
|
|
51
|
+
icon: hasAlert ? 'bell' : watcherOk ? 'shield' : 'eye-closed',
|
|
52
|
+
color,
|
|
53
|
+
desc: status,
|
|
40
54
|
collapsible: vscode.TreeItemCollapsibleState.Expanded,
|
|
41
55
|
});
|
|
42
56
|
item.projectId = id;
|
|
@@ -45,59 +59,41 @@ class GuardTreeView {
|
|
|
45
59
|
return items;
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
const p = this._poller.data.get(
|
|
50
|
-
if (!p?.dashboard) return [
|
|
62
|
+
_getProjectStatus(pid) {
|
|
63
|
+
const p = this._poller.data.get(pid);
|
|
64
|
+
if (!p?.dashboard) return [_item('Loading...', 'loading', { icon: 'loading~spin', color: C.blue })];
|
|
51
65
|
const d = p.dashboard;
|
|
52
66
|
const items = [];
|
|
53
67
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
items.push(new TreeItem(`Last Backup: ${ago}`, 'backup', { icon: 'git-commit' }));
|
|
68
|
+
if (d.watcher?.running) {
|
|
69
|
+
const w = _item('Watcher: Running', 'watcher', { icon: 'eye', color: C.green, desc: `PID ${d.watcher.pid || '?'}` });
|
|
70
|
+
items.push(w);
|
|
71
|
+
} else {
|
|
72
|
+
const w = _item('Watcher: Stopped', 'watcher', { icon: 'eye-closed', color: C.red });
|
|
73
|
+
w.command = { command: 'cursorGuard.startWatcher', title: 'Start Watcher' };
|
|
74
|
+
w.tooltip = 'Click to start watcher';
|
|
75
|
+
items.push(w);
|
|
63
76
|
}
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
}
|
|
78
|
+
const gitC = d.counts?.git?.commits || 0;
|
|
79
|
+
const shadowC = d.counts?.shadow?.snapshots || 0;
|
|
80
|
+
const lastAgo = d.lastBackup?.git?.relativeTime || 'never';
|
|
81
|
+
items.push(_item(`Backups: ${gitC + shadowC}`, 'stat', { icon: 'history', color: C.blue, desc: `last ${lastAgo}` }));
|
|
81
82
|
|
|
82
83
|
const health = d.health?.status || 'unknown';
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
healthItem.description = d.health.issues[0];
|
|
87
|
-
}
|
|
88
|
-
items.push(healthItem);
|
|
84
|
+
const hColor = health === 'healthy' ? C.green : health === 'critical' ? C.red : C.yellow;
|
|
85
|
+
const hIcon = health === 'healthy' ? 'pass-filled' : health === 'critical' ? 'error' : 'warning';
|
|
86
|
+
items.push(_item(`Health: ${health}`, 'health', { icon: hIcon, color: hColor }));
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
const openItem = _item('Open Dashboard', 'action', { icon: 'browser', color: C.blue });
|
|
89
|
+
openItem.command = { command: 'cursorGuard.openDashboard', title: 'Open' };
|
|
90
|
+
items.push(openItem);
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (min < 60) return `${min}m ago`;
|
|
99
|
-
const hr = Math.floor(min / 60);
|
|
100
|
-
return `${hr}h ago`;
|
|
92
|
+
const snapItem = _item('Snapshot Now', 'action', { icon: 'device-camera', color: C.purple });
|
|
93
|
+
snapItem.command = { command: 'cursorGuard.snapshotNow', title: 'Snap' };
|
|
94
|
+
items.push(snapItem);
|
|
95
|
+
|
|
96
|
+
return items;
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
dispose() {
|
|
@@ -107,13 +103,12 @@ class GuardTreeView {
|
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
106
|
+
function _item(label, ctx, opts = {}) {
|
|
107
|
+
const ti = new vscode.TreeItem(label, opts.collapsible || vscode.TreeItemCollapsibleState.None);
|
|
108
|
+
ti.contextValue = ctx;
|
|
109
|
+
if (opts.icon) ti.iconPath = new vscode.ThemeIcon(opts.icon, opts.color);
|
|
110
|
+
if (opts.desc) ti.description = opts.desc;
|
|
111
|
+
return ti;
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
module.exports = { GuardTreeView };
|
|
@@ -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
|
-
`<
|
|
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;
|