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.
Files changed (27) hide show
  1. package/ROADMAP.md +24 -2
  2. package/package.json +1 -1
  3. package/references/dashboard/public/app.js +16 -3
  4. package/references/dashboard/server.js +11 -0
  5. package/references/lib/core/dashboard.js +8 -1
  6. package/references/lib/core/doctor.js +1 -1
  7. package/references/lib/core/snapshot.js +3 -5
  8. package/references/lib/utils.js +12 -5
  9. package/references/vscode-extension/dist/{cursor-guard-ide-4.7.5.vsix → cursor-guard-ide-4.7.8.vsix} +0 -0
  10. package/references/vscode-extension/dist/dashboard/public/app.js +16 -3
  11. package/references/vscode-extension/dist/dashboard/server.js +11 -0
  12. package/references/vscode-extension/dist/extension.js +165 -3
  13. package/references/vscode-extension/dist/guard-version.json +1 -1
  14. package/references/vscode-extension/dist/lib/core/dashboard.js +10 -9
  15. package/references/vscode-extension/dist/lib/core/doctor.js +1 -1
  16. package/references/vscode-extension/dist/lib/core/snapshot.js +3 -5
  17. package/references/vscode-extension/dist/lib/dashboard-manager.js +7 -0
  18. package/references/vscode-extension/dist/lib/sidebar-webview.js +272 -222
  19. package/references/vscode-extension/dist/lib/utils.js +12 -5
  20. package/references/vscode-extension/dist/lib/webview-provider.js +70 -27
  21. package/references/vscode-extension/dist/package.json +49 -2
  22. package/references/vscode-extension/dist/skill/ROADMAP.md +34 -2
  23. package/references/vscode-extension/extension.js +101 -3
  24. package/references/vscode-extension/lib/dashboard-manager.js +7 -0
  25. package/references/vscode-extension/lib/sidebar-webview.js +129 -5
  26. package/references/vscode-extension/lib/webview-provider.js +48 -30
  27. 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.6`
7
- > **文档状态**:`V2` ~ `V4.7.6` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
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.6",
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
- const r = await fetch(base + url + tokenParam);
681
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
682
- return r.json();
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
- protectionScope.fileCount = filterFiles(allFiles, cfg).length;
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
- // Add everything then prune 'git add -- <pattern>' treats bare names as
114
- // root-relative pathspecs, but matchesAny() also checks basenames (e.g.
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' });
@@ -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
- * Also checks leaf filename for patterns like "*.log".
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 leaf = path.basename(relPath);
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) || globMatch(pat, leaf)) return true;
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;
@@ -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
- const r = await fetch(base + url + tokenParam);
681
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
682
- return r.json();
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('Cursor Guard: no projects detected. Add .cursor-guard.json to your workspace.');
30
- return;
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.5"}
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
- protectionScope.fileCount = filterFiles(allFiles, cfg).length;
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
- if (status.lastBackup.git && status.watcher.running) {
172
- const lastTs = new Date(status.lastBackup.git.timestamp).getTime();
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
- // Add everything then prune 'git add -- <pattern>' treats bare names as
114
- // root-relative pathspecs, but matchesAny() also checks basenames (e.g.
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'));