cursor-guard 4.7.6 → 4.7.9
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/ROADMAP.md +24 -2
- package/package.json +1 -1
- package/references/dashboard/public/app.js +16 -3
- package/references/dashboard/server.js +11 -0
- package/references/lib/core/dashboard.js +8 -1
- package/references/lib/core/doctor.js +1 -1
- package/references/lib/core/snapshot.js +3 -5
- package/references/lib/utils.js +12 -5
- package/references/vscode-extension/dist/{cursor-guard-ide-4.7.5.vsix → cursor-guard-ide-4.7.8.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +16 -3
- package/references/vscode-extension/dist/dashboard/server.js +11 -0
- package/references/vscode-extension/dist/extension.js +165 -3
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/core/dashboard.js +10 -9
- package/references/vscode-extension/dist/lib/core/doctor.js +1 -1
- package/references/vscode-extension/dist/lib/core/snapshot.js +3 -5
- package/references/vscode-extension/dist/lib/dashboard-manager.js +7 -0
- package/references/vscode-extension/dist/lib/sidebar-webview.js +272 -222
- package/references/vscode-extension/dist/lib/utils.js +12 -5
- package/references/vscode-extension/dist/lib/webview-provider.js +70 -27
- package/references/vscode-extension/dist/package.json +49 -2
- package/references/vscode-extension/dist/skill/ROADMAP.md +34 -2
- package/references/vscode-extension/extension.js +101 -3
- package/references/vscode-extension/lib/dashboard-manager.js +7 -0
- package/references/vscode-extension/lib/sidebar-webview.js +129 -5
- package/references/vscode-extension/lib/webview-provider.js +48 -30
- package/references/vscode-extension/package.json +37 -2
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.9`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.7.8` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -734,6 +734,28 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
734
734
|
}
|
|
735
735
|
```
|
|
736
736
|
|
|
737
|
+
### V4.7.8:告警倒计时实时更新 + Open Dashboard CORS/Fallback 修复 ✅
|
|
738
|
+
|
|
739
|
+
| 修复/增强 | 说明 |
|
|
740
|
+
|----------|------|
|
|
741
|
+
| **告警倒计时秒级实时更新** | 侧边栏告警 "Expires: Xm Ys" 之前只在 poller 推数据时(每 5 秒)刷新一次。新增独立 `setInterval(1000)` 定时器,每秒从 DOM `data-expires` 属性读取 `expiresAt` 时间戳并实时更新 `.alert-countdown` 文本 |
|
|
742
|
+
| **Dashboard Server CORS 支持** | API 响应头新增 `Access-Control-Allow-Origin: *`;新增 `OPTIONS` preflight 处理(204 响应 + 完整 CORS 头)。解决 WebView 跨域 fetch 被浏览器/IDE 安全策略拦截的问题 |
|
|
743
|
+
| **WebView CSP 策略放宽** | `connect-src` 从只允许固定 `baseUrl` 改为 `http://127.0.0.1:* http://localhost:*`;`script-src` 增加 `'unsafe-inline'` 兼容旧 Cursor 版本 |
|
|
744
|
+
| **Open Dashboard 启动容错** | Server 未运行时提供 "Start Server" 按钮自动启动,不再直接报错退出。`DashboardManager` 新增 `ensureRunning()` 方法 |
|
|
745
|
+
| **WebView Fetch 错误降级** | 前端 `fetchJson` 连续 3 次失败后通过 `postMessage({ type: 'fetchError' })` 通知 host,自动关闭 WebView 面板并 fallback 到浏览器打开 |
|
|
746
|
+
| **侧边栏 Protection Scope 卡片** | 新增独立保护范围卡片:顶部三 chip 显示 🛡️ N protected / 🚫 N excluded / N total;下方按 Protect/Ignore 分组展示匹配模式标签(绿色/红色),最多 6 个,超出显示 "+N more"。后端 `getDashboard` 新增 `totalFiles`、`excludedCount` 字段 |
|
|
747
|
+
| **protect 匹配语义修正(破坏性修复)** | `matchesAny` 新增 `{ strict: true }` 模式。**protect 规则不再匹配 basename**(仅匹配完整相对路径),`*.js` 只保护根目录 js 文件,要保护所有深度需写 `**/*.js`。**ignore 保留 basename 匹配**(宽松排除更安全)。修复了 `*.json` / `*.js` 意外保护 `node_modules/`、`.cursor/` 等无关目录的问题。`filterFiles`、`pruneIndexFiles`(snapshot.js)、`doctor.js` 统一使用 strict 模式。新增 4 组 strict 模式单元测试 |
|
|
748
|
+
|
|
749
|
+
### V4.7.7:右键菜单动态 Protect/Ignore ✅
|
|
750
|
+
|
|
751
|
+
| 组件 | 说明 |
|
|
752
|
+
|------|------|
|
|
753
|
+
| **Explorer 右键菜单** | 文件资源管理器中右键文件或文件夹,出现 "Cursor Guard: Add to Protected" 和 "Cursor Guard: Exclude from Protection" 两个菜单项 |
|
|
754
|
+
| **编辑器标签右键** | 编辑器标签页右键同样支持 Protect/Ignore 操作 |
|
|
755
|
+
| **模式选择 QuickPick** | 点击后弹出 QuickPick,提供 4 种模式选择:精确路径(`src/auth.ts`)、目录 glob(`src/**`)、文件名匹配(`auth.ts`)、扩展名匹配(`*.ts`)、自定义 glob 输入 |
|
|
756
|
+
| **配置自动写入** | 选择后自动写入 `.cursor-guard.json` 的 `protect` 或 `ignore` 数组,不覆盖已有配置,重复检测 |
|
|
757
|
+
| **即时生效** | 修改后自动触发 Poller 刷新,侧边栏实时更新保护范围 |
|
|
758
|
+
|
|
737
759
|
### V4.7.6:侧边栏 UX 重构 + Health 修复 + Open Dashboard 修复 ✅
|
|
738
760
|
|
|
739
761
|
| 修复/增强 | 说明 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.9",
|
|
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",
|
|
@@ -673,13 +673,26 @@ function relativeTime(ts) {
|
|
|
673
673
|
|
|
674
674
|
/* ── Data fetching ────────────────────────────────────────── */
|
|
675
675
|
|
|
676
|
+
let _fetchFailCount = 0;
|
|
676
677
|
async function fetchJson(url) {
|
|
677
678
|
const base = window.__GUARD_BASE_URL__ || '';
|
|
678
679
|
const sep = url.includes('?') ? '&' : '?';
|
|
679
680
|
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
681
|
+
try {
|
|
682
|
+
const r = await fetch(base + url + tokenParam);
|
|
683
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
684
|
+
_fetchFailCount = 0;
|
|
685
|
+
return r.json();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
_fetchFailCount++;
|
|
688
|
+
if (window.__IN_VSCODE__ && _fetchFailCount >= 3) {
|
|
689
|
+
try {
|
|
690
|
+
const api = window.__vscodeApi || (window.__vscodeApi = acquireVsCodeApi());
|
|
691
|
+
api.postMessage({ type: 'fetchError', detail: err.message });
|
|
692
|
+
} catch { /* ignore */ }
|
|
693
|
+
}
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
683
696
|
}
|
|
684
697
|
|
|
685
698
|
async function loadProjects() {
|
|
@@ -103,6 +103,7 @@ function json(res, data, status = 200) {
|
|
|
103
103
|
'Content-Type': 'application/json; charset=utf-8',
|
|
104
104
|
'Content-Length': Buffer.byteLength(body),
|
|
105
105
|
'Cache-Control': 'no-store',
|
|
106
|
+
'Access-Control-Allow-Origin': '*',
|
|
106
107
|
});
|
|
107
108
|
res.end(body);
|
|
108
109
|
}
|
|
@@ -301,6 +302,16 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
301
302
|
return res.end('Forbidden: invalid host');
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
if (req.method === 'OPTIONS') {
|
|
306
|
+
res.writeHead(204, {
|
|
307
|
+
'Access-Control-Allow-Origin': '*',
|
|
308
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
309
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
310
|
+
'Access-Control-Max-Age': '86400',
|
|
311
|
+
});
|
|
312
|
+
return res.end();
|
|
313
|
+
}
|
|
314
|
+
|
|
304
315
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
305
316
|
res.writeHead(405);
|
|
306
317
|
return res.end('Method Not Allowed');
|
|
@@ -139,11 +139,18 @@ function getDashboard(projectDir) {
|
|
|
139
139
|
protect: cfg.protect.length > 0 ? cfg.protect : ['**'],
|
|
140
140
|
ignore: cfg.ignore,
|
|
141
141
|
fileCount: 0,
|
|
142
|
+
totalFiles: 0,
|
|
143
|
+
excludedCount: 0,
|
|
144
|
+
protectPatterns: cfg.protect.length > 0 ? cfg.protect.length : 1,
|
|
145
|
+
ignorePatterns: cfg.ignore.length,
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
try {
|
|
145
149
|
const allFiles = walkDir(projectDir, projectDir);
|
|
146
|
-
|
|
150
|
+
const protectedFiles = filterFiles(allFiles, cfg);
|
|
151
|
+
protectionScope.totalFiles = allFiles.length;
|
|
152
|
+
protectionScope.fileCount = protectedFiles.length;
|
|
153
|
+
protectionScope.excludedCount = allFiles.length - protectedFiles.length;
|
|
147
154
|
} catch { /* ignore */ }
|
|
148
155
|
|
|
149
156
|
// ── Health assessment ───────────────────────────────────────
|
|
@@ -197,7 +197,7 @@ function runDiagnostics(projectDir) {
|
|
|
197
197
|
const allFiles = walkDir(projectDir, projectDir);
|
|
198
198
|
let protectedCount = 0;
|
|
199
199
|
for (const f of allFiles) {
|
|
200
|
-
if (matchesAny(cfg.protect, f.rel)) protectedCount++;
|
|
200
|
+
if (matchesAny(cfg.protect, f.rel, { strict: true })) protectedCount++;
|
|
201
201
|
}
|
|
202
202
|
check('Protect patterns', 'PASS', `${protectedCount} / ${allFiles.length} files matched by protect patterns`);
|
|
203
203
|
}
|
|
@@ -110,12 +110,10 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
110
110
|
const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
|
|
111
111
|
|
|
112
112
|
if (cfg.protect.length > 0) {
|
|
113
|
-
//
|
|
114
|
-
// root-
|
|
115
|
-
// "settings.json" matches "src/settings.json"). Pruning via matchesAny
|
|
116
|
-
// keeps the semantics consistent with filterFiles().
|
|
113
|
+
// protect uses strict matching (full path only, no basename fallback)
|
|
114
|
+
// so *.js only matches root-level js files, not nested ones
|
|
117
115
|
execFileSync('git', ['add', '-A'], { cwd, env, stdio: 'pipe' });
|
|
118
|
-
pruneIndexFiles(cwd, env, f => !matchesAny(cfg.protect, f));
|
|
116
|
+
pruneIndexFiles(cwd, env, f => !matchesAny(cfg.protect, f, { strict: true }));
|
|
119
117
|
} else {
|
|
120
118
|
if (parentHash) {
|
|
121
119
|
execFileSync('git', ['read-tree', branchRef], { cwd, env, stdio: 'pipe' });
|
package/references/lib/utils.js
CHANGED
|
@@ -36,12 +36,19 @@ function globMatch(pattern, relPath) {
|
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Check if a relative file path matches any pattern in a list.
|
|
39
|
-
*
|
|
39
|
+
*
|
|
40
|
+
* @param {string[]} patterns
|
|
41
|
+
* @param {string} relPath
|
|
42
|
+
* @param {{ strict?: boolean }} [opts]
|
|
43
|
+
* strict = true → only match against full relPath (for `protect`)
|
|
44
|
+
* strict = false → also match against basename (for `ignore` / `secrets`)
|
|
40
45
|
*/
|
|
41
|
-
function matchesAny(patterns, relPath) {
|
|
42
|
-
const
|
|
46
|
+
function matchesAny(patterns, relPath, opts) {
|
|
47
|
+
const strict = opts?.strict === true;
|
|
48
|
+
const leaf = strict ? null : path.basename(relPath);
|
|
43
49
|
for (const pat of patterns) {
|
|
44
|
-
if (globMatch(pat, relPath)
|
|
50
|
+
if (globMatch(pat, relPath)) return true;
|
|
51
|
+
if (!strict && globMatch(pat, leaf)) return true;
|
|
45
52
|
}
|
|
46
53
|
return false;
|
|
47
54
|
}
|
|
@@ -427,7 +434,7 @@ function parseArgs(argv) {
|
|
|
427
434
|
function filterFiles(files, cfg) {
|
|
428
435
|
let result = files;
|
|
429
436
|
if (cfg.protect.length > 0) {
|
|
430
|
-
result = result.filter(f => matchesAny(cfg.protect, f.rel));
|
|
437
|
+
result = result.filter(f => matchesAny(cfg.protect, f.rel, { strict: true }));
|
|
431
438
|
}
|
|
432
439
|
result = result.filter(f => {
|
|
433
440
|
if (cfg.ignore.length > 0 && matchesAny(cfg.ignore, f.rel)) return false;
|
package/references/vscode-extension/dist/{cursor-guard-ide-4.7.5.vsix → cursor-guard-ide-4.7.8.vsix}
RENAMED
|
Binary file
|
|
@@ -673,13 +673,26 @@ function relativeTime(ts) {
|
|
|
673
673
|
|
|
674
674
|
/* ── Data fetching ────────────────────────────────────────── */
|
|
675
675
|
|
|
676
|
+
let _fetchFailCount = 0;
|
|
676
677
|
async function fetchJson(url) {
|
|
677
678
|
const base = window.__GUARD_BASE_URL__ || '';
|
|
678
679
|
const sep = url.includes('?') ? '&' : '?';
|
|
679
680
|
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
681
|
+
try {
|
|
682
|
+
const r = await fetch(base + url + tokenParam);
|
|
683
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
684
|
+
_fetchFailCount = 0;
|
|
685
|
+
return r.json();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
_fetchFailCount++;
|
|
688
|
+
if (window.__IN_VSCODE__ && _fetchFailCount >= 3) {
|
|
689
|
+
try {
|
|
690
|
+
const api = window.__vscodeApi || (window.__vscodeApi = acquireVsCodeApi());
|
|
691
|
+
api.postMessage({ type: 'fetchError', detail: err.message });
|
|
692
|
+
} catch { /* ignore */ }
|
|
693
|
+
}
|
|
694
|
+
throw err;
|
|
695
|
+
}
|
|
683
696
|
}
|
|
684
697
|
|
|
685
698
|
async function loadProjects() {
|
|
@@ -103,6 +103,7 @@ function json(res, data, status = 200) {
|
|
|
103
103
|
'Content-Type': 'application/json; charset=utf-8',
|
|
104
104
|
'Content-Length': Buffer.byteLength(body),
|
|
105
105
|
'Cache-Control': 'no-store',
|
|
106
|
+
'Access-Control-Allow-Origin': '*',
|
|
106
107
|
});
|
|
107
108
|
res.end(body);
|
|
108
109
|
}
|
|
@@ -301,6 +302,16 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
301
302
|
return res.end('Forbidden: invalid host');
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
if (req.method === 'OPTIONS') {
|
|
306
|
+
res.writeHead(204, {
|
|
307
|
+
'Access-Control-Allow-Origin': '*',
|
|
308
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
309
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
310
|
+
'Access-Control-Max-Age': '86400',
|
|
311
|
+
});
|
|
312
|
+
return res.end();
|
|
313
|
+
}
|
|
314
|
+
|
|
304
315
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
305
316
|
res.writeHead(405);
|
|
306
317
|
return res.end('Method Not Allowed');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const vscode = require('vscode');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
const { DashboardManager } = require('./lib/dashboard-manager');
|
|
5
7
|
const { WebViewProvider } = require('./lib/webview-provider');
|
|
6
8
|
const { StatusBarController } = require('./lib/status-bar');
|
|
@@ -8,6 +10,7 @@ const { GuardTreeView } = require('./lib/tree-view');
|
|
|
8
10
|
const { Poller } = require('./lib/poller');
|
|
9
11
|
const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
|
|
10
12
|
const { autoSetup } = require('./lib/auto-setup');
|
|
13
|
+
const { guardPath } = require('./lib/paths');
|
|
11
14
|
|
|
12
15
|
let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider;
|
|
13
16
|
|
|
@@ -24,10 +27,24 @@ async function activate(context) {
|
|
|
24
27
|
context.subscriptions.push(
|
|
25
28
|
vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
|
|
26
29
|
|
|
27
|
-
vscode.commands.registerCommand('cursorGuard.openDashboard', () => {
|
|
30
|
+
vscode.commands.registerCommand('cursorGuard.openDashboard', async () => {
|
|
28
31
|
if (!dashMgr.running) {
|
|
29
|
-
vscode.window.showWarningMessage(
|
|
30
|
-
|
|
32
|
+
const action = await vscode.window.showWarningMessage(
|
|
33
|
+
'Cursor Guard: Dashboard server not running.',
|
|
34
|
+
'Start Server', 'Cancel'
|
|
35
|
+
);
|
|
36
|
+
if (action === 'Start Server') {
|
|
37
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
38
|
+
if (folders) {
|
|
39
|
+
await dashMgr.ensureRunning(folders.map(f => f.uri.fsPath));
|
|
40
|
+
}
|
|
41
|
+
if (!dashMgr.running) {
|
|
42
|
+
vscode.window.showErrorMessage('Cursor Guard: failed to start dashboard server.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
31
48
|
}
|
|
32
49
|
webviewProvider.show();
|
|
33
50
|
}),
|
|
@@ -88,6 +105,77 @@ async function activate(context) {
|
|
|
88
105
|
treeView.refresh();
|
|
89
106
|
}),
|
|
90
107
|
|
|
108
|
+
vscode.commands.registerCommand('cursorGuard.quickRestore', async () => {
|
|
109
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
110
|
+
if (!folders || folders.length === 0) return;
|
|
111
|
+
if (!dashMgr.running) {
|
|
112
|
+
vscode.window.showWarningMessage('Cursor Guard: dashboard not running. Cannot list backups.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const projects = await dashMgr.fetchApi('/api/projects');
|
|
116
|
+
if (!projects || projects.length === 0) return;
|
|
117
|
+
const pid = projects[0].id;
|
|
118
|
+
const pageData = await dashMgr.getFullPageData(pid);
|
|
119
|
+
const backups = (pageData?.backups || []).slice(0, 8);
|
|
120
|
+
if (backups.length === 0) {
|
|
121
|
+
vscode.window.showInformationMessage('Cursor Guard: no backups available to restore from.');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const items = backups.map(b => {
|
|
125
|
+
const time = b.timestamp ? new Date(b.timestamp).toLocaleString() : '?';
|
|
126
|
+
const files = b.filesChanged ? `${b.filesChanged} files` : '';
|
|
127
|
+
const summary = b.summary ? b.summary.slice(0, 60) : '';
|
|
128
|
+
return {
|
|
129
|
+
label: `$(git-commit) ${time}`,
|
|
130
|
+
description: `${b.type || 'auto'} · ${files}`,
|
|
131
|
+
detail: summary,
|
|
132
|
+
hash: b.commitHash,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
136
|
+
placeHolder: 'Select a backup to restore from',
|
|
137
|
+
title: 'Cursor Guard: Quick Restore',
|
|
138
|
+
});
|
|
139
|
+
if (selected && selected.hash) {
|
|
140
|
+
const url = `${dashMgr.baseUrl}?token=${dashMgr.token}`;
|
|
141
|
+
vscode.env.openExternal(vscode.Uri.parse(url));
|
|
142
|
+
vscode.window.showInformationMessage(
|
|
143
|
+
`Cursor Guard: opening dashboard for restore. Selected backup: ${selected.hash.slice(0, 7)}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}),
|
|
147
|
+
|
|
148
|
+
vscode.commands.registerCommand('cursorGuard.doctor', async () => {
|
|
149
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
150
|
+
if (!folders || folders.length === 0) return;
|
|
151
|
+
const projectPath = folders[0].uri.fsPath;
|
|
152
|
+
try {
|
|
153
|
+
const { runDiagnostics } = require(guardPath('lib', 'core', 'doctor'));
|
|
154
|
+
const result = runDiagnostics(projectPath);
|
|
155
|
+
const passed = result.checks.filter(c => c.status === 'PASS').length;
|
|
156
|
+
const warned = result.checks.filter(c => c.status === 'WARN').length;
|
|
157
|
+
const failed = result.checks.filter(c => c.status === 'FAIL').length;
|
|
158
|
+
const msg = `Doctor: ${passed} passed, ${warned} warnings, ${failed} failed`;
|
|
159
|
+
if (failed > 0) {
|
|
160
|
+
vscode.window.showErrorMessage(`Cursor Guard: ${msg}`);
|
|
161
|
+
} else if (warned > 0) {
|
|
162
|
+
vscode.window.showWarningMessage(`Cursor Guard: ${msg}`);
|
|
163
|
+
} else {
|
|
164
|
+
vscode.window.showInformationMessage(`Cursor Guard: ${msg} ✓`);
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
vscode.window.showErrorMessage(`Cursor Guard Doctor: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
|
|
171
|
+
vscode.commands.registerCommand('cursorGuard.addToProtect', (uri) => {
|
|
172
|
+
_modifyGuardConfig(uri, 'protect');
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
vscode.commands.registerCommand('cursorGuard.addToIgnore', (uri) => {
|
|
176
|
+
_modifyGuardConfig(uri, 'ignore');
|
|
177
|
+
}),
|
|
178
|
+
|
|
91
179
|
statusBar,
|
|
92
180
|
poller,
|
|
93
181
|
treeView,
|
|
@@ -110,6 +198,80 @@ async function activate(context) {
|
|
|
110
198
|
);
|
|
111
199
|
}
|
|
112
200
|
|
|
201
|
+
async function _modifyGuardConfig(uri, field) {
|
|
202
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
203
|
+
if (!folders || folders.length === 0) {
|
|
204
|
+
vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let targetUri = uri;
|
|
209
|
+
if (!targetUri) {
|
|
210
|
+
const editor = vscode.window.activeTextEditor;
|
|
211
|
+
if (editor) targetUri = editor.document.uri;
|
|
212
|
+
}
|
|
213
|
+
if (!targetUri) {
|
|
214
|
+
vscode.window.showWarningMessage('Cursor Guard: no file or folder selected.');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const wsRoot = folders[0].uri.fsPath;
|
|
219
|
+
const configPath = path.join(wsRoot, '.cursor-guard.json');
|
|
220
|
+
const targetPath = targetUri.fsPath;
|
|
221
|
+
|
|
222
|
+
const relative = path.relative(wsRoot, targetPath).replace(/\\/g, '/');
|
|
223
|
+
if (!relative || relative.startsWith('..')) {
|
|
224
|
+
vscode.window.showWarningMessage('Cursor Guard: selected path is outside the workspace.');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let isDir = false;
|
|
229
|
+
try { isDir = fs.statSync(targetPath).isDirectory(); } catch { /* file */ }
|
|
230
|
+
const pattern = isDir ? `${relative}/**` : relative;
|
|
231
|
+
|
|
232
|
+
const action = field === 'protect' ? 'Add to Protected' : 'Exclude from Protection';
|
|
233
|
+
const pick = await vscode.window.showQuickPick(
|
|
234
|
+
[
|
|
235
|
+
{ label: pattern, description: isDir ? 'directory glob' : 'exact file' },
|
|
236
|
+
{ label: `${path.basename(targetPath)}`, description: 'filename only (matches anywhere)' },
|
|
237
|
+
...(isDir ? [] : [{ label: `*.${path.extname(targetPath).slice(1)}`, description: 'file extension' }]),
|
|
238
|
+
{ label: '$(edit) Custom pattern...', description: 'enter your own glob', custom: true },
|
|
239
|
+
],
|
|
240
|
+
{ placeHolder: `${action}: choose a pattern`, title: `Cursor Guard: ${action}` }
|
|
241
|
+
);
|
|
242
|
+
if (!pick) return;
|
|
243
|
+
|
|
244
|
+
let chosenPattern = pick.label;
|
|
245
|
+
if (pick.custom) {
|
|
246
|
+
const input = await vscode.window.showInputBox({
|
|
247
|
+
prompt: `Enter a glob pattern to ${field === 'protect' ? 'protect' : 'exclude'}`,
|
|
248
|
+
value: pattern,
|
|
249
|
+
});
|
|
250
|
+
if (!input) return;
|
|
251
|
+
chosenPattern = input;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let config = {};
|
|
255
|
+
if (fs.existsSync(configPath)) {
|
|
256
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { config = {}; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!Array.isArray(config[field])) config[field] = [];
|
|
260
|
+
|
|
261
|
+
if (config[field].includes(chosenPattern)) {
|
|
262
|
+
vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" already in ${field} list.`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
config[field].push(chosenPattern);
|
|
267
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
268
|
+
|
|
269
|
+
const label = field === 'protect' ? 'Protected' : 'Excluded';
|
|
270
|
+
vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" added to ${label} list.`);
|
|
271
|
+
|
|
272
|
+
if (poller) poller.forceRefresh();
|
|
273
|
+
}
|
|
274
|
+
|
|
113
275
|
function deactivate() {
|
|
114
276
|
if (poller) poller.dispose();
|
|
115
277
|
if (statusBar) statusBar.dispose();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.7.
|
|
1
|
+
{"version":"4.7.8"}
|
|
@@ -139,11 +139,18 @@ function getDashboard(projectDir) {
|
|
|
139
139
|
protect: cfg.protect.length > 0 ? cfg.protect : ['**'],
|
|
140
140
|
ignore: cfg.ignore,
|
|
141
141
|
fileCount: 0,
|
|
142
|
+
totalFiles: 0,
|
|
143
|
+
excludedCount: 0,
|
|
144
|
+
protectPatterns: cfg.protect.length > 0 ? cfg.protect.length : 1,
|
|
145
|
+
ignorePatterns: cfg.ignore.length,
|
|
142
146
|
};
|
|
143
147
|
|
|
144
148
|
try {
|
|
145
149
|
const allFiles = walkDir(projectDir, projectDir);
|
|
146
|
-
|
|
150
|
+
const protectedFiles = filterFiles(allFiles, cfg);
|
|
151
|
+
protectionScope.totalFiles = allFiles.length;
|
|
152
|
+
protectionScope.fileCount = protectedFiles.length;
|
|
153
|
+
protectionScope.excludedCount = allFiles.length - protectedFiles.length;
|
|
147
154
|
} catch { /* ignore */ }
|
|
148
155
|
|
|
149
156
|
// ── Health assessment ───────────────────────────────────────
|
|
@@ -168,14 +175,8 @@ function getDashboard(projectDir) {
|
|
|
168
175
|
issues.push(`Disk space low (${status.disk.freeGB} GB free)`);
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const staleSec = (Date.now() - lastTs) / 1000;
|
|
174
|
-
const staleThreshold = Math.max(cfg.auto_backup_interval_seconds * 10, 300);
|
|
175
|
-
if (staleSec > staleThreshold) {
|
|
176
|
-
issues.push(`Last git backup: ${relativeTime(status.lastBackup.git.timestamp)}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
178
|
+
// Last backup time is purely informational — no changes = no backup is normal behavior.
|
|
179
|
+
// Health warnings should only reflect actionable problems (watcher down, disk full, no repo).
|
|
179
180
|
|
|
180
181
|
let healthStatus = 'healthy';
|
|
181
182
|
if (issues.length > 0) healthStatus = 'warning';
|
|
@@ -197,7 +197,7 @@ function runDiagnostics(projectDir) {
|
|
|
197
197
|
const allFiles = walkDir(projectDir, projectDir);
|
|
198
198
|
let protectedCount = 0;
|
|
199
199
|
for (const f of allFiles) {
|
|
200
|
-
if (matchesAny(cfg.protect, f.rel)) protectedCount++;
|
|
200
|
+
if (matchesAny(cfg.protect, f.rel, { strict: true })) protectedCount++;
|
|
201
201
|
}
|
|
202
202
|
check('Protect patterns', 'PASS', `${protectedCount} / ${allFiles.length} files matched by protect patterns`);
|
|
203
203
|
}
|
|
@@ -110,12 +110,10 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
110
110
|
const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
|
|
111
111
|
|
|
112
112
|
if (cfg.protect.length > 0) {
|
|
113
|
-
//
|
|
114
|
-
// root-
|
|
115
|
-
// "settings.json" matches "src/settings.json"). Pruning via matchesAny
|
|
116
|
-
// keeps the semantics consistent with filterFiles().
|
|
113
|
+
// protect uses strict matching (full path only, no basename fallback)
|
|
114
|
+
// so *.js only matches root-level js files, not nested ones
|
|
117
115
|
execFileSync('git', ['add', '-A'], { cwd, env, stdio: 'pipe' });
|
|
118
|
-
pruneIndexFiles(cwd, env, f => !matchesAny(cfg.protect, f));
|
|
116
|
+
pruneIndexFiles(cwd, env, f => !matchesAny(cfg.protect, f, { strict: true }));
|
|
119
117
|
} else {
|
|
120
118
|
if (parentHash) {
|
|
121
119
|
execFileSync('git', ['read-tree', branchRef], { cwd, env, stdio: 'pipe' });
|
|
@@ -29,6 +29,13 @@ class DashboardManager {
|
|
|
29
29
|
return this.start(paths);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
async ensureRunning(paths) {
|
|
33
|
+
if (this._instance) return true;
|
|
34
|
+
const configPaths = paths.filter(p => fs.existsSync(path.join(p, CONFIG_FILE)));
|
|
35
|
+
if (configPaths.length === 0) return false;
|
|
36
|
+
return this.start(configPaths);
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
async start(paths) {
|
|
33
40
|
if (!this._serverModule) {
|
|
34
41
|
this._serverModule = require(guardPath('dashboard', 'server'));
|