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