cursor-guard 4.9.0 → 4.9.6

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 (54) hide show
  1. package/README.md +94 -28
  2. package/README.zh-CN.md +91 -25
  3. package/ROADMAP.md +51 -9
  4. package/SKILL.md +32 -22
  5. package/package.json +1 -1
  6. package/references/config-reference.md +68 -7
  7. package/references/config-reference.zh-CN.md +68 -7
  8. package/references/cursor-guard.example.json +11 -7
  9. package/references/cursor-guard.schema.json +30 -7
  10. package/references/dashboard/public/app.js +73 -27
  11. package/references/dashboard/public/index.html +8 -7
  12. package/references/lib/auto-backup.js +40 -2
  13. package/references/lib/core/backups.js +46 -16
  14. package/references/lib/core/core.test.js +101 -22
  15. package/references/lib/core/dashboard.js +37 -23
  16. package/references/lib/core/doctor.js +19 -13
  17. package/references/lib/core/pre-warning.js +296 -0
  18. package/references/lib/core/snapshot.js +24 -2
  19. package/references/lib/core/status.js +15 -7
  20. package/references/lib/utils.js +46 -20
  21. package/references/mcp/mcp.test.js +60 -12
  22. package/references/mcp/server.js +72 -60
  23. package/references/quickstart.zh-CN.md +46 -21
  24. package/references/vscode-extension/build-vsix.js +4 -3
  25. package/references/vscode-extension/dist/LICENSE +65 -0
  26. package/references/vscode-extension/dist/{cursor-guard-ide-4.9.0.vsix → cursor-guard-ide-4.9.6.vsix} +0 -0
  27. package/references/vscode-extension/dist/dashboard/public/app.js +73 -27
  28. package/references/vscode-extension/dist/dashboard/public/index.html +8 -7
  29. package/references/vscode-extension/dist/extension.js +498 -296
  30. package/references/vscode-extension/dist/guard-version.json +1 -1
  31. package/references/vscode-extension/dist/lib/auto-backup.js +40 -2
  32. package/references/vscode-extension/dist/lib/core/backups.js +46 -16
  33. package/references/vscode-extension/dist/lib/core/dashboard.js +37 -23
  34. package/references/vscode-extension/dist/lib/core/doctor.js +19 -13
  35. package/references/vscode-extension/dist/lib/core/pre-warning.js +296 -0
  36. package/references/vscode-extension/dist/lib/core/snapshot.js +24 -2
  37. package/references/vscode-extension/dist/lib/core/status.js +15 -7
  38. package/references/vscode-extension/dist/lib/sidebar-webview.js +502 -433
  39. package/references/vscode-extension/dist/lib/status-bar.js +95 -68
  40. package/references/vscode-extension/dist/lib/tree-view.js +174 -114
  41. package/references/vscode-extension/dist/lib/utils.js +46 -20
  42. package/references/vscode-extension/dist/mcp/server.js +393 -30
  43. package/references/vscode-extension/dist/package.json +1 -1
  44. package/references/vscode-extension/dist/skill/ROADMAP.md +51 -9
  45. package/references/vscode-extension/dist/skill/SKILL.md +32 -22
  46. package/references/vscode-extension/dist/skill/config-reference.md +68 -7
  47. package/references/vscode-extension/dist/skill/config-reference.zh-CN.md +68 -7
  48. package/references/vscode-extension/dist/skill/cursor-guard.example.json +11 -7
  49. package/references/vscode-extension/dist/skill/cursor-guard.schema.json +30 -7
  50. package/references/vscode-extension/extension.js +498 -296
  51. package/references/vscode-extension/lib/sidebar-webview.js +502 -433
  52. package/references/vscode-extension/lib/status-bar.js +95 -68
  53. package/references/vscode-extension/lib/tree-view.js +174 -114
  54. package/references/vscode-extension/package.json +1 -1
@@ -1,296 +1,498 @@
1
- 'use strict';
2
-
3
- const vscode = require('vscode');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { DashboardManager } = require('./lib/dashboard-manager');
7
- const { WebViewProvider } = require('./lib/webview-provider');
8
- const { StatusBarController } = require('./lib/status-bar');
9
- const { GuardTreeView } = require('./lib/tree-view');
10
- const { Poller } = require('./lib/poller');
11
- const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
12
- const { autoSetup } = require('./lib/auto-setup');
13
- const { guardPath } = require('./lib/paths');
14
-
15
- let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider;
16
-
17
- async function activate(context) {
18
- await autoSetup(context, vscode);
19
-
20
- dashMgr = new DashboardManager();
21
- poller = new Poller(dashMgr);
22
- statusBar = new StatusBarController(poller);
23
- treeView = new GuardTreeView(poller, dashMgr);
24
- webviewProvider = new WebViewProvider(context, dashMgr);
25
- sidebarProvider = new SidebarDashboardProvider(poller);
26
-
27
- context.subscriptions.push(
28
- vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
29
-
30
- vscode.commands.registerCommand('cursorGuard.openDashboard', async () => {
31
- if (!dashMgr.running) {
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
- }
48
- }
49
- webviewProvider.show();
50
- }),
51
-
52
- vscode.commands.registerCommand('cursorGuard.snapshotNow', async () => {
53
- const folders = vscode.workspace.workspaceFolders;
54
- if (!folders || folders.length === 0) return;
55
- const projectPath = folders[0].uri.fsPath;
56
- const result = await dashMgr.snapshotNow(projectPath);
57
- if (result?.status === 'created') {
58
- vscode.window.showInformationMessage(`Cursor Guard: snapshot created (${result.changedCount || 0} changes)`);
59
- } else if (result?.status === 'unchanged' || result?.status === 'skipped') {
60
- vscode.window.showInformationMessage('Cursor Guard: no changes to snapshot');
61
- } else if (result?.status === 'error') {
62
- vscode.window.showWarningMessage(`Cursor Guard: ${result.error}`);
63
- } else {
64
- vscode.window.showWarningMessage(`Cursor Guard: snapshot returned status "${result?.status || 'unknown'}"`);
65
- }
66
- poller.forceRefresh();
67
- }),
68
-
69
- vscode.commands.registerCommand('cursorGuard.startWatcher', async () => {
70
- const folders = vscode.workspace.workspaceFolders;
71
- if (!folders || folders.length === 0) {
72
- vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
73
- return;
74
- }
75
- const projectPath = folders[0].uri.fsPath;
76
- const existingPid = dashMgr.getWatcherPid(projectPath);
77
- if (existingPid) {
78
- vscode.window.showInformationMessage(`Cursor Guard: watcher already running (PID ${existingPid})`);
79
- return;
80
- }
81
- const pid = dashMgr.startWatcher(projectPath);
82
- if (pid) {
83
- vscode.window.showInformationMessage(`Cursor Guard: watcher started (PID ${pid})`);
84
- setTimeout(() => poller.forceRefresh(), 2000);
85
- } else {
86
- vscode.window.showWarningMessage('Cursor Guard: failed to start watcher');
87
- }
88
- }),
89
-
90
- vscode.commands.registerCommand('cursorGuard.stopWatcher', async () => {
91
- const folders = vscode.workspace.workspaceFolders;
92
- if (!folders || folders.length === 0) return;
93
- const projectPath = folders[0].uri.fsPath;
94
- const stopped = dashMgr.stopWatcher(projectPath);
95
- if (stopped) {
96
- vscode.window.showInformationMessage('Cursor Guard: watcher stopped');
97
- setTimeout(() => poller.forceRefresh(), 1000);
98
- } else {
99
- vscode.window.showWarningMessage('Cursor Guard: no running watcher found');
100
- }
101
- }),
102
-
103
- vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
104
- poller.forceRefresh();
105
- treeView.refresh();
106
- }),
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
-
179
- statusBar,
180
- poller,
181
- treeView,
182
- webviewProvider,
183
- sidebarProvider,
184
- );
185
-
186
- const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
187
- if (started) {
188
- poller.start();
189
- vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
190
- }
191
-
192
- // Event-driven UI refresh: FileSystemWatcher triggers immediate poller refresh
193
- let _fsRefreshTimer = null;
194
- const _scheduleRefresh = () => {
195
- if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
196
- _fsRefreshTimer = setTimeout(() => poller.forceRefresh(), 1500);
197
- };
198
- const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, false, false);
199
- fileWatcher.onDidChange(_scheduleRefresh);
200
- fileWatcher.onDidCreate(_scheduleRefresh);
201
- fileWatcher.onDidDelete(_scheduleRefresh);
202
- context.subscriptions.push(fileWatcher);
203
-
204
- context.subscriptions.push(
205
- vscode.workspace.onDidChangeWorkspaceFolders(async () => {
206
- const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
207
- if (restarted && !poller._timer) poller.start();
208
- poller.forceRefresh();
209
- })
210
- );
211
- }
212
-
213
- async function _modifyGuardConfig(uri, field) {
214
- const folders = vscode.workspace.workspaceFolders;
215
- if (!folders || folders.length === 0) {
216
- vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
217
- return;
218
- }
219
-
220
- let targetUri = uri;
221
- if (!targetUri) {
222
- const editor = vscode.window.activeTextEditor;
223
- if (editor) targetUri = editor.document.uri;
224
- }
225
- if (!targetUri) {
226
- vscode.window.showWarningMessage('Cursor Guard: no file or folder selected.');
227
- return;
228
- }
229
-
230
- const wsRoot = folders[0].uri.fsPath;
231
- const configPath = path.join(wsRoot, '.cursor-guard.json');
232
- const targetPath = targetUri.fsPath;
233
-
234
- const relative = path.relative(wsRoot, targetPath).replace(/\\/g, '/');
235
- if (!relative || relative.startsWith('..')) {
236
- vscode.window.showWarningMessage('Cursor Guard: selected path is outside the workspace.');
237
- return;
238
- }
239
-
240
- let isDir = false;
241
- try { isDir = fs.statSync(targetPath).isDirectory(); } catch { /* file */ }
242
- const pattern = isDir ? `${relative}/**` : relative;
243
-
244
- const action = field === 'protect' ? 'Add to Protected' : 'Exclude from Protection';
245
- const pick = await vscode.window.showQuickPick(
246
- [
247
- { label: pattern, description: isDir ? 'directory glob' : 'exact file' },
248
- { label: `${path.basename(targetPath)}`, description: 'filename only (matches anywhere)' },
249
- ...(isDir ? [] : [{ label: `*.${path.extname(targetPath).slice(1)}`, description: 'file extension' }]),
250
- { label: '$(edit) Custom pattern...', description: 'enter your own glob', custom: true },
251
- ],
252
- { placeHolder: `${action}: choose a pattern`, title: `Cursor Guard: ${action}` }
253
- );
254
- if (!pick) return;
255
-
256
- let chosenPattern = pick.label;
257
- if (pick.custom) {
258
- const input = await vscode.window.showInputBox({
259
- prompt: `Enter a glob pattern to ${field === 'protect' ? 'protect' : 'exclude'}`,
260
- value: pattern,
261
- });
262
- if (!input) return;
263
- chosenPattern = input;
264
- }
265
-
266
- let config = {};
267
- if (fs.existsSync(configPath)) {
268
- try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { config = {}; }
269
- }
270
-
271
- if (!Array.isArray(config[field])) config[field] = [];
272
-
273
- if (config[field].includes(chosenPattern)) {
274
- vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" already in ${field} list.`);
275
- return;
276
- }
277
-
278
- config[field].push(chosenPattern);
279
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
280
-
281
- const label = field === 'protect' ? 'Protected' : 'Excluded';
282
- vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" added to ${label} list.`);
283
-
284
- if (poller) poller.forceRefresh();
285
- }
286
-
287
- function deactivate() {
288
- if (poller) poller.dispose();
289
- if (statusBar) statusBar.dispose();
290
- if (treeView) treeView.dispose();
291
- if (webviewProvider) webviewProvider.dispose();
292
- if (sidebarProvider) sidebarProvider.dispose();
293
- if (dashMgr) dashMgr.dispose();
294
- }
295
-
296
- module.exports = { activate, deactivate };
1
+ 'use strict';
2
+
3
+ const vscode = require('vscode');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { DashboardManager } = require('./lib/dashboard-manager');
7
+ const { WebViewProvider } = require('./lib/webview-provider');
8
+ const { StatusBarController } = require('./lib/status-bar');
9
+ const { GuardTreeView } = require('./lib/tree-view');
10
+ const { Poller } = require('./lib/poller');
11
+ const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
12
+ const { autoSetup } = require('./lib/auto-setup');
13
+ const { guardPath } = require('./lib/paths');
14
+
15
+ let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider;
16
+ const _preWarningBaselines = new Map();
17
+ const _preWarningTimers = new Map();
18
+ const _preWarningFingerprints = new Map();
19
+ const PRE_WARNING_DEBOUNCE_MS = 200;
20
+ const PRE_WARNING_POPUP_COOLDOWN_MS = 5000;
21
+
22
+ function _docKey(document) {
23
+ return document.uri.toString();
24
+ }
25
+
26
+ function _getPreWarningDeps() {
27
+ return {
28
+ loadConfig: require(guardPath('lib', 'utils')).loadConfig,
29
+ ...require(guardPath('lib', 'core', 'pre-warning')),
30
+ };
31
+ }
32
+
33
+ function _getProjectInfo(document) {
34
+ if (!document || document.uri.scheme !== 'file') return null;
35
+ const folder = vscode.workspace.getWorkspaceFolder(document.uri);
36
+ if (!folder) return null;
37
+ const projectPath = folder.uri.fsPath;
38
+ const relPath = path.relative(projectPath, document.uri.fsPath).replace(/\\/g, '/');
39
+ if (!relPath || relPath.startsWith('..')) return null;
40
+ return { projectPath, relPath };
41
+ }
42
+
43
+ function _seedBaseline(document) {
44
+ if (!document || document.uri.scheme !== 'file') return;
45
+ _preWarningBaselines.set(_docKey(document), document.getText());
46
+ }
47
+
48
+ function _clearDocPreWarningState(document) {
49
+ const key = _docKey(document);
50
+ const timer = _preWarningTimers.get(key);
51
+ if (timer) clearTimeout(timer);
52
+ _preWarningTimers.delete(key);
53
+ _preWarningBaselines.delete(key);
54
+ _preWarningFingerprints.delete(key);
55
+ }
56
+
57
+ function _isLikelyDeleteBatch(event) {
58
+ const deletedChars = event.contentChanges.reduce((sum, change) => {
59
+ return sum + Math.max(0, change.rangeLength - (change.text || '').length);
60
+ }, 0);
61
+ const deletedLines = event.contentChanges.reduce((sum, change) => {
62
+ return sum + Math.max(0, change.range.end.line - change.range.start.line);
63
+ }, 0);
64
+ return deletedChars >= 8
65
+ || deletedLines > 0
66
+ || event.contentChanges.some(change => change.rangeLength >= 20 || (change.rangeLength > 0 && !change.text));
67
+ }
68
+
69
+ async function _openPreWarningDiff(document, previousText) {
70
+ const beforeDoc = await vscode.workspace.openTextDocument({
71
+ language: document.languageId,
72
+ content: previousText,
73
+ });
74
+ await vscode.commands.executeCommand(
75
+ 'vscode.diff',
76
+ beforeDoc.uri,
77
+ document.uri,
78
+ `Cursor Guard Pre-Warning: ${path.basename(document.uri.fsPath)}`
79
+ );
80
+ }
81
+
82
+ async function _showPreWarningPopup(document, relPath, previousText, warning, projectPath) {
83
+ const methodHint = Array.isArray(warning.removedMethods) && warning.removedMethods.length > 0
84
+ ? ` - ${warning.removedMethods.slice(0, 3).map(m => `${m.name}:${m.lineNumber}`).join(', ')}`
85
+ : '';
86
+ const action = await vscode.window.showWarningMessage(
87
+ `Cursor Guard: risky deletion detected in ${relPath} - ${warning.summary}${methodHint}`,
88
+ { modal: true },
89
+ 'Undo Change',
90
+ 'View Diff',
91
+ 'Keep Changes'
92
+ );
93
+
94
+ if (action === 'Undo Change') {
95
+ await vscode.window.showTextDocument(document, { preview: false, preserveFocus: false });
96
+ await vscode.commands.executeCommand('undo');
97
+ const { clearPreWarning } = _getPreWarningDeps();
98
+ clearPreWarning(projectPath, relPath);
99
+ if (poller) poller.forceRefresh();
100
+ return;
101
+ }
102
+
103
+ if (action === 'View Diff') {
104
+ await _openPreWarningDiff(document, previousText);
105
+ }
106
+ }
107
+
108
+ async function _assessPreWarning(document) {
109
+ if (!document || document.isClosed || document.uri.scheme !== 'file') return;
110
+
111
+ const info = _getProjectInfo(document);
112
+ if (!info) return;
113
+
114
+ const key = _docKey(document);
115
+ const previousText = _preWarningBaselines.get(key) ?? document.getText();
116
+ const deps = _getPreWarningDeps();
117
+ const { cfg } = deps.loadConfig(info.projectPath);
118
+
119
+ if (!deps.isPreWarningEnabled(cfg) || deps.shouldExcludePreWarning(info.relPath, cfg)) {
120
+ deps.clearPreWarning(info.projectPath, info.relPath);
121
+ _preWarningFingerprints.delete(key);
122
+ if (poller) poller.forceRefresh();
123
+ return;
124
+ }
125
+
126
+ const assessment = deps.assessDeletionRisk(previousText, document.getText(), {
127
+ threshold: cfg.pre_warning_threshold,
128
+ });
129
+
130
+ if (!assessment.triggered) {
131
+ deps.clearPreWarning(info.projectPath, info.relPath);
132
+ _preWarningFingerprints.delete(key);
133
+ if (poller) poller.forceRefresh();
134
+ return;
135
+ }
136
+
137
+ const warning = {
138
+ file: info.relPath,
139
+ mode: cfg.pre_warning_mode,
140
+ threshold: cfg.pre_warning_threshold,
141
+ deletedLines: assessment.deletedLines,
142
+ removedMethodCount: assessment.removedMethodCount,
143
+ removedMethods: assessment.removedMethods.slice(0, 10),
144
+ riskPercent: assessment.riskPercent,
145
+ summary: assessment.summary,
146
+ deletedLineSamples: assessment.deletedLineSamples,
147
+ };
148
+
149
+ const fingerprint = JSON.stringify([
150
+ warning.file,
151
+ warning.deletedLines,
152
+ warning.removedMethodCount,
153
+ warning.riskPercent,
154
+ warning.removedMethods.map(m => m.name).join(','),
155
+ ]);
156
+ const last = _preWarningFingerprints.get(key);
157
+ _preWarningFingerprints.set(key, { fingerprint, at: Date.now() });
158
+
159
+ deps.recordPreWarning(info.projectPath, warning, {
160
+ setActive: cfg.pre_warning_mode !== 'silent',
161
+ });
162
+ if (poller) poller.forceRefresh();
163
+
164
+ if (cfg.pre_warning_mode === 'silent') {
165
+ console.warn(`[cursor-guard] pre-warning ${info.relPath}: ${warning.summary}`);
166
+ return;
167
+ }
168
+
169
+ if (cfg.pre_warning_mode === 'popup') {
170
+ if (last && last.fingerprint === fingerprint && Date.now() - last.at < PRE_WARNING_POPUP_COOLDOWN_MS) {
171
+ return;
172
+ }
173
+ await _showPreWarningPopup(document, info.relPath, previousText, warning, info.projectPath);
174
+ }
175
+ }
176
+
177
+ async function activate(context) {
178
+ await autoSetup(context, vscode);
179
+
180
+ dashMgr = new DashboardManager();
181
+ poller = new Poller(dashMgr);
182
+ statusBar = new StatusBarController(poller);
183
+ treeView = new GuardTreeView(poller, dashMgr);
184
+ webviewProvider = new WebViewProvider(context, dashMgr);
185
+ sidebarProvider = new SidebarDashboardProvider(poller);
186
+
187
+ context.subscriptions.push(
188
+ vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
189
+
190
+ vscode.commands.registerCommand('cursorGuard.openDashboard', async () => {
191
+ if (!dashMgr.running) {
192
+ const action = await vscode.window.showWarningMessage(
193
+ 'Cursor Guard: Dashboard server not running.',
194
+ 'Start Server', 'Cancel'
195
+ );
196
+ if (action === 'Start Server') {
197
+ const folders = vscode.workspace.workspaceFolders;
198
+ if (folders) {
199
+ await dashMgr.ensureRunning(folders.map(f => f.uri.fsPath));
200
+ }
201
+ if (!dashMgr.running) {
202
+ vscode.window.showErrorMessage('Cursor Guard: failed to start dashboard server.');
203
+ return;
204
+ }
205
+ } else {
206
+ return;
207
+ }
208
+ }
209
+ webviewProvider.show();
210
+ }),
211
+
212
+ vscode.commands.registerCommand('cursorGuard.snapshotNow', async () => {
213
+ const folders = vscode.workspace.workspaceFolders;
214
+ if (!folders || folders.length === 0) return;
215
+ const projectPath = folders[0].uri.fsPath;
216
+ const result = await dashMgr.snapshotNow(projectPath);
217
+ if (result?.status === 'created') {
218
+ vscode.window.showInformationMessage(`Cursor Guard: snapshot created (${result.changedCount || 0} changes)`);
219
+ } else if (result?.status === 'unchanged' || result?.status === 'skipped') {
220
+ vscode.window.showInformationMessage('Cursor Guard: no changes to snapshot');
221
+ } else if (result?.status === 'error') {
222
+ vscode.window.showWarningMessage(`Cursor Guard: ${result.error}`);
223
+ } else {
224
+ vscode.window.showWarningMessage(`Cursor Guard: snapshot returned status "${result?.status || 'unknown'}"`);
225
+ }
226
+ poller.forceRefresh();
227
+ }),
228
+
229
+ vscode.commands.registerCommand('cursorGuard.startWatcher', async () => {
230
+ const folders = vscode.workspace.workspaceFolders;
231
+ if (!folders || folders.length === 0) {
232
+ vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
233
+ return;
234
+ }
235
+ const projectPath = folders[0].uri.fsPath;
236
+ const existingPid = dashMgr.getWatcherPid(projectPath);
237
+ if (existingPid) {
238
+ vscode.window.showInformationMessage(`Cursor Guard: watcher already running (PID ${existingPid})`);
239
+ return;
240
+ }
241
+ const pid = dashMgr.startWatcher(projectPath);
242
+ if (pid) {
243
+ vscode.window.showInformationMessage(`Cursor Guard: watcher started (PID ${pid})`);
244
+ setTimeout(() => poller.forceRefresh(), 2000);
245
+ } else {
246
+ vscode.window.showWarningMessage('Cursor Guard: failed to start watcher');
247
+ }
248
+ }),
249
+
250
+ vscode.commands.registerCommand('cursorGuard.stopWatcher', async () => {
251
+ const folders = vscode.workspace.workspaceFolders;
252
+ if (!folders || folders.length === 0) return;
253
+ const projectPath = folders[0].uri.fsPath;
254
+ const stopped = dashMgr.stopWatcher(projectPath);
255
+ if (stopped) {
256
+ vscode.window.showInformationMessage('Cursor Guard: watcher stopped');
257
+ setTimeout(() => poller.forceRefresh(), 1000);
258
+ } else {
259
+ vscode.window.showWarningMessage('Cursor Guard: no running watcher found');
260
+ }
261
+ }),
262
+
263
+ vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
264
+ poller.forceRefresh();
265
+ treeView.refresh();
266
+ }),
267
+
268
+ vscode.commands.registerCommand('cursorGuard.quickRestore', async () => {
269
+ const folders = vscode.workspace.workspaceFolders;
270
+ if (!folders || folders.length === 0) return;
271
+ if (!dashMgr.running) {
272
+ vscode.window.showWarningMessage('Cursor Guard: dashboard not running. Cannot list backups.');
273
+ return;
274
+ }
275
+ const projects = await dashMgr.fetchApi('/api/projects');
276
+ if (!projects || projects.length === 0) return;
277
+ const pid = projects[0].id;
278
+ const pageData = await dashMgr.getFullPageData(pid);
279
+ const backups = (pageData?.backups || []).slice(0, 8);
280
+ if (backups.length === 0) {
281
+ vscode.window.showInformationMessage('Cursor Guard: no backups available to restore from.');
282
+ return;
283
+ }
284
+ const items = backups.map(b => {
285
+ const time = b.timestamp ? new Date(b.timestamp).toLocaleString() : '?';
286
+ const files = b.filesChanged ? `${b.filesChanged} files` : '';
287
+ const summary = b.summary ? b.summary.slice(0, 60) : '';
288
+ return {
289
+ label: `$(git-commit) ${time}`,
290
+ description: `${b.type || 'auto'} - ${files}`,
291
+ detail: summary,
292
+ hash: b.commitHash,
293
+ };
294
+ });
295
+ const selected = await vscode.window.showQuickPick(items, {
296
+ placeHolder: 'Select a backup to restore from',
297
+ title: 'Cursor Guard: Quick Restore',
298
+ });
299
+ if (selected && selected.hash) {
300
+ const url = `${dashMgr.baseUrl}?token=${dashMgr.token}`;
301
+ vscode.env.openExternal(vscode.Uri.parse(url));
302
+ vscode.window.showInformationMessage(
303
+ `Cursor Guard: opening dashboard for restore. Selected backup: ${selected.hash.slice(0, 7)}`
304
+ );
305
+ }
306
+ }),
307
+
308
+ vscode.commands.registerCommand('cursorGuard.doctor', async () => {
309
+ const folders = vscode.workspace.workspaceFolders;
310
+ if (!folders || folders.length === 0) return;
311
+ const projectPath = folders[0].uri.fsPath;
312
+ try {
313
+ const { runDiagnostics } = require(guardPath('lib', 'core', 'doctor'));
314
+ const result = runDiagnostics(projectPath);
315
+ const passed = result.checks.filter(c => c.status === 'PASS').length;
316
+ const warned = result.checks.filter(c => c.status === 'WARN').length;
317
+ const failed = result.checks.filter(c => c.status === 'FAIL').length;
318
+ const msg = `Doctor: ${passed} passed, ${warned} warnings, ${failed} failed`;
319
+ if (failed > 0) {
320
+ vscode.window.showErrorMessage(`Cursor Guard: ${msg}`);
321
+ } else if (warned > 0) {
322
+ vscode.window.showWarningMessage(`Cursor Guard: ${msg}`);
323
+ } else {
324
+ vscode.window.showInformationMessage(`Cursor Guard: ${msg}`);
325
+ }
326
+ } catch (e) {
327
+ vscode.window.showErrorMessage(`Cursor Guard Doctor: ${e.message}`);
328
+ }
329
+ }),
330
+
331
+ vscode.commands.registerCommand('cursorGuard.addToProtect', (uri) => {
332
+ _modifyGuardConfig(uri, 'protect');
333
+ }),
334
+
335
+ vscode.commands.registerCommand('cursorGuard.addToIgnore', (uri) => {
336
+ _modifyGuardConfig(uri, 'ignore');
337
+ }),
338
+
339
+ statusBar,
340
+ poller,
341
+ treeView,
342
+ webviewProvider,
343
+ sidebarProvider,
344
+ );
345
+
346
+ const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
347
+ if (started) {
348
+ poller.start();
349
+ vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
350
+ }
351
+
352
+ let _fsRefreshTimer = null;
353
+ const _scheduleRefresh = () => {
354
+ if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
355
+ _fsRefreshTimer = setTimeout(() => poller.forceRefresh(), 1500);
356
+ };
357
+ const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, false, false);
358
+ fileWatcher.onDidChange(_scheduleRefresh);
359
+ fileWatcher.onDidCreate(_scheduleRefresh);
360
+ fileWatcher.onDidDelete(_scheduleRefresh);
361
+ context.subscriptions.push(fileWatcher);
362
+
363
+ for (const document of vscode.workspace.textDocuments) {
364
+ _seedBaseline(document);
365
+ }
366
+
367
+ context.subscriptions.push(
368
+ vscode.workspace.onDidOpenTextDocument(document => {
369
+ _seedBaseline(document);
370
+ }),
371
+ vscode.workspace.onDidCloseTextDocument(document => {
372
+ const info = _getProjectInfo(document);
373
+ _clearDocPreWarningState(document);
374
+ if (info) {
375
+ const { clearPreWarning } = _getPreWarningDeps();
376
+ clearPreWarning(info.projectPath, info.relPath);
377
+ if (poller) poller.forceRefresh();
378
+ }
379
+ }),
380
+ vscode.workspace.onDidSaveTextDocument(document => {
381
+ const info = _getProjectInfo(document);
382
+ _seedBaseline(document);
383
+ _preWarningFingerprints.delete(_docKey(document));
384
+ if (info) {
385
+ const { clearPreWarning } = _getPreWarningDeps();
386
+ clearPreWarning(info.projectPath, info.relPath);
387
+ if (poller) poller.forceRefresh();
388
+ }
389
+ }),
390
+ vscode.workspace.onDidChangeTextDocument(event => {
391
+ const info = _getProjectInfo(event.document);
392
+ if (!info || !_isLikelyDeleteBatch(event)) return;
393
+
394
+ const key = _docKey(event.document);
395
+ const existing = _preWarningTimers.get(key);
396
+ if (existing) clearTimeout(existing);
397
+
398
+ const timer = setTimeout(() => {
399
+ _preWarningTimers.delete(key);
400
+ _assessPreWarning(event.document).catch(err => {
401
+ console.warn(`[cursor-guard] pre-warning failed for ${info.relPath}: ${err.message}`);
402
+ });
403
+ }, PRE_WARNING_DEBOUNCE_MS);
404
+
405
+ _preWarningTimers.set(key, timer);
406
+ }),
407
+ vscode.workspace.onDidChangeWorkspaceFolders(async () => {
408
+ const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
409
+ if (restarted && !poller._timer) poller.start();
410
+ poller.forceRefresh();
411
+ })
412
+ );
413
+ }
414
+
415
+ async function _modifyGuardConfig(uri, field) {
416
+ const folders = vscode.workspace.workspaceFolders;
417
+ if (!folders || folders.length === 0) {
418
+ vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
419
+ return;
420
+ }
421
+
422
+ let targetUri = uri;
423
+ if (!targetUri) {
424
+ const editor = vscode.window.activeTextEditor;
425
+ if (editor) targetUri = editor.document.uri;
426
+ }
427
+ if (!targetUri) {
428
+ vscode.window.showWarningMessage('Cursor Guard: no file or folder selected.');
429
+ return;
430
+ }
431
+
432
+ const wsRoot = folders[0].uri.fsPath;
433
+ const configPath = path.join(wsRoot, '.cursor-guard.json');
434
+ const targetPath = targetUri.fsPath;
435
+
436
+ const relative = path.relative(wsRoot, targetPath).replace(/\\/g, '/');
437
+ if (!relative || relative.startsWith('..')) {
438
+ vscode.window.showWarningMessage('Cursor Guard: selected path is outside the workspace.');
439
+ return;
440
+ }
441
+
442
+ let isDir = false;
443
+ try { isDir = fs.statSync(targetPath).isDirectory(); } catch { /* file */ }
444
+ const pattern = isDir ? `${relative}/**` : relative;
445
+
446
+ const action = field === 'protect' ? 'Add to Protected' : 'Exclude from Protection';
447
+ const pick = await vscode.window.showQuickPick(
448
+ [
449
+ { label: pattern, description: isDir ? 'directory glob' : 'exact file' },
450
+ { label: `${path.basename(targetPath)}`, description: 'filename only (matches anywhere)' },
451
+ ...(isDir ? [] : [{ label: `*.${path.extname(targetPath).slice(1)}`, description: 'file extension' }]),
452
+ { label: '$(edit) Custom pattern...', description: 'enter your own glob', custom: true },
453
+ ],
454
+ { placeHolder: `${action}: choose a pattern`, title: `Cursor Guard: ${action}` }
455
+ );
456
+ if (!pick) return;
457
+
458
+ let chosenPattern = pick.label;
459
+ if (pick.custom) {
460
+ const input = await vscode.window.showInputBox({
461
+ prompt: `Enter a glob pattern to ${field === 'protect' ? 'protect' : 'exclude'}`,
462
+ value: pattern,
463
+ });
464
+ if (!input) return;
465
+ chosenPattern = input;
466
+ }
467
+
468
+ let config = {};
469
+ if (fs.existsSync(configPath)) {
470
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { config = {}; }
471
+ }
472
+
473
+ if (!Array.isArray(config[field])) config[field] = [];
474
+
475
+ if (config[field].includes(chosenPattern)) {
476
+ vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" already in ${field} list.`);
477
+ return;
478
+ }
479
+
480
+ config[field].push(chosenPattern);
481
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
482
+
483
+ const label = field === 'protect' ? 'Protected' : 'Excluded';
484
+ vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" added to ${label} list.`);
485
+
486
+ if (poller) poller.forceRefresh();
487
+ }
488
+
489
+ function deactivate() {
490
+ if (poller) poller.dispose();
491
+ if (statusBar) statusBar.dispose();
492
+ if (treeView) treeView.dispose();
493
+ if (webviewProvider) webviewProvider.dispose();
494
+ if (sidebarProvider) sidebarProvider.dispose();
495
+ if (dashMgr) dashMgr.dispose();
496
+ }
497
+
498
+ module.exports = { activate, deactivate };