cursor-guard 4.9.9 → 4.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +697 -697
- package/README.zh-CN.md +696 -696
- package/ROADMAP.md +1775 -1720
- package/SKILL.md +631 -629
- package/docs/RELEASE.md +197 -196
- package/docs/SNAPSHOT-BOOKMARK.md +47 -0
- package/package.json +70 -69
- package/references/dashboard/public/app.js +2079 -1832
- package/references/dashboard/public/style.css +1660 -1573
- package/references/dashboard/server.js +197 -4
- package/references/lib/core/backups.js +509 -492
- package/references/lib/core/core.test.js +1761 -1616
- package/references/lib/core/snapshot.js +441 -369
- package/references/mcp/mcp.test.js +381 -362
- package/references/mcp/server.js +404 -347
- package/references/vscode-extension/dist/{cursor-guard-ide-4.9.9.vsix → cursor-guard-ide-4.9.15.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +2079 -1832
- package/references/vscode-extension/dist/dashboard/public/style.css +1660 -1573
- package/references/vscode-extension/dist/dashboard/server.js +197 -4
- package/references/vscode-extension/dist/extension.js +780 -704
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-setup.js +201 -192
- package/references/vscode-extension/dist/lib/core/backups.js +509 -492
- package/references/vscode-extension/dist/lib/core/snapshot.js +441 -369
- package/references/vscode-extension/dist/lib/poller.js +161 -21
- package/references/vscode-extension/dist/lib/sidebar-webview.js +22 -0
- package/references/vscode-extension/dist/mcp/server.js +152 -35
- package/references/vscode-extension/dist/package.json +7 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +1775 -1720
- package/references/vscode-extension/dist/skill/SKILL.md +631 -629
- package/references/vscode-extension/extension.js +780 -704
- package/references/vscode-extension/lib/auto-setup.js +201 -192
- package/references/vscode-extension/lib/poller.js +161 -21
- package/references/vscode-extension/lib/sidebar-webview.js +22 -0
- package/references/vscode-extension/package.json +146 -140
|
@@ -1,704 +1,780 @@
|
|
|
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
|
-
const { getLocale } = require('./lib/locale');
|
|
15
|
-
|
|
16
|
-
let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider, _localeStorage;
|
|
17
|
-
const _preWarningBaselines = new Map();
|
|
18
|
-
const _preWarningTimers = new Map();
|
|
19
|
-
const _preWarningFingerprints = new Map();
|
|
20
|
-
const PRE_WARNING_DEBOUNCE_MS = 200;
|
|
21
|
-
const PRE_WARNING_POPUP_COOLDOWN_MS = 5000;
|
|
22
|
-
const PRE_WARNING_AUTO_CONTINUE_MS = 2000;
|
|
23
|
-
const PRE_WARNING_POPUP_TICK_MS = 250;
|
|
24
|
-
const PRE_WARNING_I18N = {
|
|
25
|
-
'en-US': {
|
|
26
|
-
'diff.title': 'Cursor Guard Pre-Warning: {file}',
|
|
27
|
-
'picker.title': 'Cursor Guard Review · auto-continue in {time}',
|
|
28
|
-
'picker.placeholder': 'Risky deletion detected in {file} · {summary}',
|
|
29
|
-
'picker.methods': 'methods',
|
|
30
|
-
'action.undo.label': '$(discard) Undo Change',
|
|
31
|
-
'action.undo.description': 'Revert this deletion now',
|
|
32
|
-
'action.undo.detail': 'Safest option. Restore the latest edit batch immediately.',
|
|
33
|
-
'action.diff.label': '$(diff) View Diff',
|
|
34
|
-
'action.diff.description': 'Inspect before and after',
|
|
35
|
-
'action.diff.detail': 'Open a diff view and keep the current edit for now.',
|
|
36
|
-
'action.keep.label': '$(check) Keep Changes',
|
|
37
|
-
'action.keep.description': 'Accept this edit',
|
|
38
|
-
'action.keep.detail': 'Dismiss this warning and continue working.',
|
|
39
|
-
'status.diffOpen': '$(shield) Cursor Guard review open for {file}',
|
|
40
|
-
'summary.methods.one': '{n} method removed',
|
|
41
|
-
'summary.methods.other': '{n} methods removed',
|
|
42
|
-
'summary.lines.one': '{n} line deleted',
|
|
43
|
-
'summary.lines.other': '{n} lines deleted',
|
|
44
|
-
'summary.risk': 'risk {n}%',
|
|
45
|
-
},
|
|
46
|
-
'zh-CN': {
|
|
47
|
-
'diff.title': 'Cursor Guard 预警对比:{file}',
|
|
48
|
-
'picker.title': 'Cursor Guard \u9884\u8b66 - {time} \u540e\u81ea\u52a8\u7ee7\u7eed',
|
|
49
|
-
'picker.placeholder': '\u68c0\u6d4b\u5230\u5220\u9664\u98ce\u9669\uff1a{file} - {summary}',
|
|
50
|
-
'picker.methods': '方法',
|
|
51
|
-
'action.undo.label': '$(discard) 撤销此次修改',
|
|
52
|
-
'action.undo.description': '立即回退这次删除',
|
|
53
|
-
'action.undo.detail': '这是最安全的选择,会马上撤销刚刚这一批编辑。',
|
|
54
|
-
'action.diff.label': '$(diff) 查看 Diff',
|
|
55
|
-
'action.diff.description': '先对比再决定',
|
|
56
|
-
'action.diff.detail': '打开前后差异视图,暂时保留当前修改。',
|
|
57
|
-
'action.keep.label': '$(check) 保留修改',
|
|
58
|
-
'action.keep.description': '接受这次编辑',
|
|
59
|
-
'action.keep.detail': '关闭这次预警并继续当前流程。',
|
|
60
|
-
'status.diffOpen': '$(shield) Cursor Guard 已打开预警对比:{file}',
|
|
61
|
-
'summary.methods.one': '删除了 {n} 个方法',
|
|
62
|
-
'summary.methods.other': '删除了 {n} 个方法',
|
|
63
|
-
'summary.lines.one': '删除了 {n} 行',
|
|
64
|
-
'summary.lines.other': '删除了 {n} 行',
|
|
65
|
-
'summary.risk': '风险 {n}%',
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function _docKey(document) {
|
|
70
|
-
return document.uri.toString();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function _uiLocale() {
|
|
74
|
-
return getLocale(_localeStorage);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function _tp(key, params) {
|
|
78
|
-
const locale = _uiLocale();
|
|
79
|
-
const dict = PRE_WARNING_I18N[locale] || PRE_WARNING_I18N['en-US'];
|
|
80
|
-
let value = dict[key] || PRE_WARNING_I18N['en-US'][key] || key;
|
|
81
|
-
for (const [name, replacement] of Object.entries(params || {})) {
|
|
82
|
-
value = value.replaceAll(`{${name}}`, String(replacement));
|
|
83
|
-
}
|
|
84
|
-
return value;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function _pluralKey(baseKey, count) {
|
|
88
|
-
return `${baseKey}.${count === 1 ? 'one' : 'other'}`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function _buildPreWarningSummary(warning) {
|
|
92
|
-
const parts = [];
|
|
93
|
-
if (warning.removedMethodCount > 0) {
|
|
94
|
-
parts.push(_tp(_pluralKey('summary.methods', warning.removedMethodCount), {
|
|
95
|
-
n: warning.removedMethodCount,
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
98
|
-
if (warning.deletedLines > 0) {
|
|
99
|
-
parts.push(_tp(_pluralKey('summary.lines', warning.deletedLines), {
|
|
100
|
-
n: warning.deletedLines,
|
|
101
|
-
}));
|
|
102
|
-
}
|
|
103
|
-
parts.push(_tp('summary.risk', { n: warning.riskPercent || 0 }));
|
|
104
|
-
return parts.join(_uiLocale() === 'zh-CN' ? ',' : ', ');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function _getPreWarningDeps() {
|
|
108
|
-
return {
|
|
109
|
-
loadConfig: require(guardPath('lib', 'utils')).loadConfig,
|
|
110
|
-
...require(guardPath('lib', 'core', 'pre-warning')),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function _getProjectInfo(document) {
|
|
115
|
-
if (!document || document.uri.scheme !== 'file') return null;
|
|
116
|
-
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
117
|
-
if (!folder) return null;
|
|
118
|
-
const projectPath = folder.uri.fsPath;
|
|
119
|
-
const relPath = path.relative(projectPath, document.uri.fsPath).replace(/\\/g, '/');
|
|
120
|
-
if (!relPath || relPath.startsWith('..')) return null;
|
|
121
|
-
return { projectPath, relPath };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function _seedBaseline(document) {
|
|
125
|
-
if (!document || document.uri.scheme !== 'file') return;
|
|
126
|
-
_preWarningBaselines.set(_docKey(document), document.getText());
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function _clearDocPreWarningState(document) {
|
|
130
|
-
const key = _docKey(document);
|
|
131
|
-
const timer = _preWarningTimers.get(key);
|
|
132
|
-
if (timer) clearTimeout(timer);
|
|
133
|
-
_preWarningTimers.delete(key);
|
|
134
|
-
_preWarningBaselines.delete(key);
|
|
135
|
-
_preWarningFingerprints.delete(key);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function _isLikelyDeleteBatch(event) {
|
|
139
|
-
const deletedChars = event.contentChanges.reduce((sum, change) => {
|
|
140
|
-
return sum + Math.max(0, change.rangeLength - (change.text || '').length);
|
|
141
|
-
}, 0);
|
|
142
|
-
const deletedLines = event.contentChanges.reduce((sum, change) => {
|
|
143
|
-
return sum + Math.max(0, change.range.end.line - change.range.start.line);
|
|
144
|
-
}, 0);
|
|
145
|
-
return deletedChars >= 8
|
|
146
|
-
|| deletedLines > 0
|
|
147
|
-
|| event.contentChanges.some(change => change.rangeLength >= 20 || (change.rangeLength > 0 && !change.text));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function _openPreWarningDiff(document, previousText) {
|
|
151
|
-
const beforeDoc = await vscode.workspace.openTextDocument({
|
|
152
|
-
language: document.languageId,
|
|
153
|
-
content: previousText,
|
|
154
|
-
});
|
|
155
|
-
await vscode.commands.executeCommand(
|
|
156
|
-
'vscode.diff',
|
|
157
|
-
beforeDoc.uri,
|
|
158
|
-
document.uri,
|
|
159
|
-
_tp('diff.title', { file: path.basename(document.uri.fsPath) })
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function _formatPreWarningCountdown(remainingMs) {
|
|
164
|
-
return `${Math.max(1, Math.ceil(remainingMs / 1000))}s`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function _showPreWarningPopup(document, relPath, previousText, warning, projectPath) {
|
|
168
|
-
const { clearPreWarning } = _getPreWarningDeps();
|
|
169
|
-
const summary = _buildPreWarningSummary(warning);
|
|
170
|
-
const methodHint = Array.isArray(warning.removedMethods) && warning.removedMethods.length > 0
|
|
171
|
-
? warning.removedMethods.slice(0, 3).map(m => `${m.name}:${m.lineNumber}`).join(', ')
|
|
172
|
-
: '';
|
|
173
|
-
const summaryLine = methodHint
|
|
174
|
-
? `${summary} - ${_tp('picker.methods')}: ${methodHint}`
|
|
175
|
-
: summary;
|
|
176
|
-
|
|
177
|
-
return new Promise((resolve) => {
|
|
178
|
-
const picker = vscode.window.createQuickPick();
|
|
179
|
-
const items = [
|
|
180
|
-
{
|
|
181
|
-
label: '$(discard) Undo Change',
|
|
182
|
-
description: 'Revert this deletion now',
|
|
183
|
-
detail: 'Safest option. Restore the latest edit batch immediately.',
|
|
184
|
-
action: 'undo',
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
label: '$(diff) View Diff',
|
|
188
|
-
description: 'Inspect before and after',
|
|
189
|
-
detail: 'Open a diff view and keep the current edit for now.',
|
|
190
|
-
action: 'diff',
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
label: '$(check) Keep Changes',
|
|
194
|
-
description: 'Accept this edit',
|
|
195
|
-
detail: 'Dismiss this warning and continue working.',
|
|
196
|
-
action: 'keep',
|
|
197
|
-
},
|
|
198
|
-
];
|
|
199
|
-
|
|
200
|
-
let settled = false;
|
|
201
|
-
let remainingMs = PRE_WARNING_AUTO_CONTINUE_MS;
|
|
202
|
-
let timer = null;
|
|
203
|
-
|
|
204
|
-
const finish = async (action) => {
|
|
205
|
-
if (settled) return;
|
|
206
|
-
settled = true;
|
|
207
|
-
if (timer) clearInterval(timer);
|
|
208
|
-
try { picker.hide(); } catch { /* ignore */ }
|
|
209
|
-
picker.dispose();
|
|
210
|
-
|
|
211
|
-
if (action === 'undo') {
|
|
212
|
-
await vscode.window.showTextDocument(document, { preview: false, preserveFocus: false });
|
|
213
|
-
await vscode.commands.executeCommand('undo');
|
|
214
|
-
clearPreWarning(projectPath, relPath);
|
|
215
|
-
if (poller) poller.forceRefresh();
|
|
216
|
-
resolve('undo');
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (action === 'diff') {
|
|
221
|
-
clearPreWarning(projectPath, relPath);
|
|
222
|
-
if (poller) poller.forceRefresh();
|
|
223
|
-
await _openPreWarningDiff(document, previousText);
|
|
224
|
-
vscode.window.setStatusBarMessage(
|
|
225
|
-
_tp('status.diffOpen', { file: path.basename(relPath) }),
|
|
226
|
-
2500
|
|
227
|
-
);
|
|
228
|
-
resolve('diff');
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
clearPreWarning(projectPath, relPath);
|
|
233
|
-
if (poller) poller.forceRefresh();
|
|
234
|
-
resolve(action);
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const updatePicker = () => {
|
|
238
|
-
picker.title = _tp('picker.title', {
|
|
239
|
-
time: _formatPreWarningCountdown(remainingMs),
|
|
240
|
-
});
|
|
241
|
-
picker.placeholder = _tp('picker.placeholder', {
|
|
242
|
-
file: relPath,
|
|
243
|
-
summary: summaryLine,
|
|
244
|
-
});
|
|
245
|
-
picker.items = [
|
|
246
|
-
{
|
|
247
|
-
label: _tp('action.undo.label'),
|
|
248
|
-
description: _tp('action.undo.description'),
|
|
249
|
-
detail: _tp('action.undo.detail'),
|
|
250
|
-
action: 'undo',
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
label: _tp('action.diff.label'),
|
|
254
|
-
description: _tp('action.diff.description'),
|
|
255
|
-
detail: _tp('action.diff.detail'),
|
|
256
|
-
action: 'diff',
|
|
257
|
-
},
|
|
258
|
-
{
|
|
259
|
-
label: _tp('action.keep.label'),
|
|
260
|
-
description: _tp('action.keep.description'),
|
|
261
|
-
detail: _tp('action.keep.detail'),
|
|
262
|
-
action: 'keep',
|
|
263
|
-
},
|
|
264
|
-
];
|
|
265
|
-
picker.matchOnDescription = true;
|
|
266
|
-
picker.matchOnDetail = true;
|
|
267
|
-
picker.ignoreFocusOut = false;
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
picker.onDidAccept(() => {
|
|
271
|
-
const selected = picker.selectedItems[0];
|
|
272
|
-
finish(selected?.action || 'keep').catch(() => resolve());
|
|
273
|
-
});
|
|
274
|
-
picker.onDidHide(() => {
|
|
275
|
-
if (!settled) {
|
|
276
|
-
finish('timeout').catch(() => resolve());
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
updatePicker();
|
|
281
|
-
picker.show();
|
|
282
|
-
|
|
283
|
-
timer = setInterval(() => {
|
|
284
|
-
remainingMs -= PRE_WARNING_POPUP_TICK_MS;
|
|
285
|
-
if (remainingMs <= 0) {
|
|
286
|
-
finish('timeout').catch(() => resolve());
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
updatePicker();
|
|
290
|
-
}, PRE_WARNING_POPUP_TICK_MS);
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function _assessPreWarning(document) {
|
|
295
|
-
if (!document || document.isClosed || document.uri.scheme !== 'file') return;
|
|
296
|
-
|
|
297
|
-
const info = _getProjectInfo(document);
|
|
298
|
-
if (!info) return;
|
|
299
|
-
|
|
300
|
-
const key = _docKey(document);
|
|
301
|
-
const previousText = _preWarningBaselines.get(key) ?? document.getText();
|
|
302
|
-
const deps = _getPreWarningDeps();
|
|
303
|
-
const { cfg } = deps.loadConfig(info.projectPath);
|
|
304
|
-
|
|
305
|
-
if (!deps.isPreWarningEnabled(cfg) || deps.shouldExcludePreWarning(info.relPath, cfg)) {
|
|
306
|
-
deps.clearPreWarning(info.projectPath, info.relPath);
|
|
307
|
-
_preWarningFingerprints.delete(key);
|
|
308
|
-
if (poller) poller.forceRefresh();
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const assessment = deps.assessDeletionRisk(previousText, document.getText(), {
|
|
313
|
-
threshold: cfg.pre_warning_threshold,
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
if (!assessment.triggered) {
|
|
317
|
-
deps.clearPreWarning(info.projectPath, info.relPath);
|
|
318
|
-
_preWarningFingerprints.delete(key);
|
|
319
|
-
if (poller) poller.forceRefresh();
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const warning = {
|
|
324
|
-
file: info.relPath,
|
|
325
|
-
mode: cfg.pre_warning_mode,
|
|
326
|
-
threshold: cfg.pre_warning_threshold,
|
|
327
|
-
deletedLines: assessment.deletedLines,
|
|
328
|
-
removedMethodCount: assessment.removedMethodCount,
|
|
329
|
-
removedMethods: assessment.removedMethods.slice(0, 10),
|
|
330
|
-
riskPercent: assessment.riskPercent,
|
|
331
|
-
summary: assessment.summary,
|
|
332
|
-
deletedLineSamples: assessment.deletedLineSamples,
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const fingerprint = JSON.stringify([
|
|
336
|
-
warning.file,
|
|
337
|
-
warning.deletedLines,
|
|
338
|
-
warning.removedMethodCount,
|
|
339
|
-
warning.riskPercent,
|
|
340
|
-
warning.removedMethods.map(m => m.name).join(','),
|
|
341
|
-
]);
|
|
342
|
-
const last = _preWarningFingerprints.get(key);
|
|
343
|
-
|
|
344
|
-
if (cfg.pre_warning_mode === 'popup') {
|
|
345
|
-
if (last?.suppressed) return;
|
|
346
|
-
if (last && last.fingerprint === fingerprint && Date.now() - last.at < PRE_WARNING_POPUP_COOLDOWN_MS) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
deps.recordPreWarning(info.projectPath, warning, {
|
|
352
|
-
setActive: cfg.pre_warning_mode === 'dashboard',
|
|
353
|
-
});
|
|
354
|
-
if (poller) poller.forceRefresh();
|
|
355
|
-
|
|
356
|
-
if (cfg.pre_warning_mode === 'silent') {
|
|
357
|
-
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
|
|
358
|
-
console.warn(`[cursor-guard] pre-warning ${info.relPath}: ${warning.summary}`);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (cfg.pre_warning_mode === 'popup') {
|
|
363
|
-
const action = await _showPreWarningPopup(document, info.relPath, previousText, warning, info.projectPath);
|
|
364
|
-
if (action === 'undo') {
|
|
365
|
-
_preWarningFingerprints.delete(key);
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: true });
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
vscode.window.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
} else {
|
|
465
|
-
vscode.window.showWarningMessage(
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
vscode.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
vscode.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
if (
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
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, installAgentSkill, detectIdeDir, getExtensionRoot } = require('./lib/auto-setup');
|
|
13
|
+
const { guardPath } = require('./lib/paths');
|
|
14
|
+
const { getLocale } = require('./lib/locale');
|
|
15
|
+
|
|
16
|
+
let dashMgr, poller, statusBar, treeView, webviewProvider, sidebarProvider, _localeStorage;
|
|
17
|
+
const _preWarningBaselines = new Map();
|
|
18
|
+
const _preWarningTimers = new Map();
|
|
19
|
+
const _preWarningFingerprints = new Map();
|
|
20
|
+
const PRE_WARNING_DEBOUNCE_MS = 200;
|
|
21
|
+
const PRE_WARNING_POPUP_COOLDOWN_MS = 5000;
|
|
22
|
+
const PRE_WARNING_AUTO_CONTINUE_MS = 2000;
|
|
23
|
+
const PRE_WARNING_POPUP_TICK_MS = 250;
|
|
24
|
+
const PRE_WARNING_I18N = {
|
|
25
|
+
'en-US': {
|
|
26
|
+
'diff.title': 'Cursor Guard Pre-Warning: {file}',
|
|
27
|
+
'picker.title': 'Cursor Guard Review · auto-continue in {time}',
|
|
28
|
+
'picker.placeholder': 'Risky deletion detected in {file} · {summary}',
|
|
29
|
+
'picker.methods': 'methods',
|
|
30
|
+
'action.undo.label': '$(discard) Undo Change',
|
|
31
|
+
'action.undo.description': 'Revert this deletion now',
|
|
32
|
+
'action.undo.detail': 'Safest option. Restore the latest edit batch immediately.',
|
|
33
|
+
'action.diff.label': '$(diff) View Diff',
|
|
34
|
+
'action.diff.description': 'Inspect before and after',
|
|
35
|
+
'action.diff.detail': 'Open a diff view and keep the current edit for now.',
|
|
36
|
+
'action.keep.label': '$(check) Keep Changes',
|
|
37
|
+
'action.keep.description': 'Accept this edit',
|
|
38
|
+
'action.keep.detail': 'Dismiss this warning and continue working.',
|
|
39
|
+
'status.diffOpen': '$(shield) Cursor Guard review open for {file}',
|
|
40
|
+
'summary.methods.one': '{n} method removed',
|
|
41
|
+
'summary.methods.other': '{n} methods removed',
|
|
42
|
+
'summary.lines.one': '{n} line deleted',
|
|
43
|
+
'summary.lines.other': '{n} lines deleted',
|
|
44
|
+
'summary.risk': 'risk {n}%',
|
|
45
|
+
},
|
|
46
|
+
'zh-CN': {
|
|
47
|
+
'diff.title': 'Cursor Guard 预警对比:{file}',
|
|
48
|
+
'picker.title': 'Cursor Guard \u9884\u8b66 - {time} \u540e\u81ea\u52a8\u7ee7\u7eed',
|
|
49
|
+
'picker.placeholder': '\u68c0\u6d4b\u5230\u5220\u9664\u98ce\u9669\uff1a{file} - {summary}',
|
|
50
|
+
'picker.methods': '方法',
|
|
51
|
+
'action.undo.label': '$(discard) 撤销此次修改',
|
|
52
|
+
'action.undo.description': '立即回退这次删除',
|
|
53
|
+
'action.undo.detail': '这是最安全的选择,会马上撤销刚刚这一批编辑。',
|
|
54
|
+
'action.diff.label': '$(diff) 查看 Diff',
|
|
55
|
+
'action.diff.description': '先对比再决定',
|
|
56
|
+
'action.diff.detail': '打开前后差异视图,暂时保留当前修改。',
|
|
57
|
+
'action.keep.label': '$(check) 保留修改',
|
|
58
|
+
'action.keep.description': '接受这次编辑',
|
|
59
|
+
'action.keep.detail': '关闭这次预警并继续当前流程。',
|
|
60
|
+
'status.diffOpen': '$(shield) Cursor Guard 已打开预警对比:{file}',
|
|
61
|
+
'summary.methods.one': '删除了 {n} 个方法',
|
|
62
|
+
'summary.methods.other': '删除了 {n} 个方法',
|
|
63
|
+
'summary.lines.one': '删除了 {n} 行',
|
|
64
|
+
'summary.lines.other': '删除了 {n} 行',
|
|
65
|
+
'summary.risk': '风险 {n}%',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function _docKey(document) {
|
|
70
|
+
return document.uri.toString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function _uiLocale() {
|
|
74
|
+
return getLocale(_localeStorage);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _tp(key, params) {
|
|
78
|
+
const locale = _uiLocale();
|
|
79
|
+
const dict = PRE_WARNING_I18N[locale] || PRE_WARNING_I18N['en-US'];
|
|
80
|
+
let value = dict[key] || PRE_WARNING_I18N['en-US'][key] || key;
|
|
81
|
+
for (const [name, replacement] of Object.entries(params || {})) {
|
|
82
|
+
value = value.replaceAll(`{${name}}`, String(replacement));
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _pluralKey(baseKey, count) {
|
|
88
|
+
return `${baseKey}.${count === 1 ? 'one' : 'other'}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _buildPreWarningSummary(warning) {
|
|
92
|
+
const parts = [];
|
|
93
|
+
if (warning.removedMethodCount > 0) {
|
|
94
|
+
parts.push(_tp(_pluralKey('summary.methods', warning.removedMethodCount), {
|
|
95
|
+
n: warning.removedMethodCount,
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
if (warning.deletedLines > 0) {
|
|
99
|
+
parts.push(_tp(_pluralKey('summary.lines', warning.deletedLines), {
|
|
100
|
+
n: warning.deletedLines,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
parts.push(_tp('summary.risk', { n: warning.riskPercent || 0 }));
|
|
104
|
+
return parts.join(_uiLocale() === 'zh-CN' ? ',' : ', ');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _getPreWarningDeps() {
|
|
108
|
+
return {
|
|
109
|
+
loadConfig: require(guardPath('lib', 'utils')).loadConfig,
|
|
110
|
+
...require(guardPath('lib', 'core', 'pre-warning')),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _getProjectInfo(document) {
|
|
115
|
+
if (!document || document.uri.scheme !== 'file') return null;
|
|
116
|
+
const folder = vscode.workspace.getWorkspaceFolder(document.uri);
|
|
117
|
+
if (!folder) return null;
|
|
118
|
+
const projectPath = folder.uri.fsPath;
|
|
119
|
+
const relPath = path.relative(projectPath, document.uri.fsPath).replace(/\\/g, '/');
|
|
120
|
+
if (!relPath || relPath.startsWith('..')) return null;
|
|
121
|
+
return { projectPath, relPath };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _seedBaseline(document) {
|
|
125
|
+
if (!document || document.uri.scheme !== 'file') return;
|
|
126
|
+
_preWarningBaselines.set(_docKey(document), document.getText());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _clearDocPreWarningState(document) {
|
|
130
|
+
const key = _docKey(document);
|
|
131
|
+
const timer = _preWarningTimers.get(key);
|
|
132
|
+
if (timer) clearTimeout(timer);
|
|
133
|
+
_preWarningTimers.delete(key);
|
|
134
|
+
_preWarningBaselines.delete(key);
|
|
135
|
+
_preWarningFingerprints.delete(key);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function _isLikelyDeleteBatch(event) {
|
|
139
|
+
const deletedChars = event.contentChanges.reduce((sum, change) => {
|
|
140
|
+
return sum + Math.max(0, change.rangeLength - (change.text || '').length);
|
|
141
|
+
}, 0);
|
|
142
|
+
const deletedLines = event.contentChanges.reduce((sum, change) => {
|
|
143
|
+
return sum + Math.max(0, change.range.end.line - change.range.start.line);
|
|
144
|
+
}, 0);
|
|
145
|
+
return deletedChars >= 8
|
|
146
|
+
|| deletedLines > 0
|
|
147
|
+
|| event.contentChanges.some(change => change.rangeLength >= 20 || (change.rangeLength > 0 && !change.text));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function _openPreWarningDiff(document, previousText) {
|
|
151
|
+
const beforeDoc = await vscode.workspace.openTextDocument({
|
|
152
|
+
language: document.languageId,
|
|
153
|
+
content: previousText,
|
|
154
|
+
});
|
|
155
|
+
await vscode.commands.executeCommand(
|
|
156
|
+
'vscode.diff',
|
|
157
|
+
beforeDoc.uri,
|
|
158
|
+
document.uri,
|
|
159
|
+
_tp('diff.title', { file: path.basename(document.uri.fsPath) })
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function _formatPreWarningCountdown(remainingMs) {
|
|
164
|
+
return `${Math.max(1, Math.ceil(remainingMs / 1000))}s`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function _showPreWarningPopup(document, relPath, previousText, warning, projectPath) {
|
|
168
|
+
const { clearPreWarning } = _getPreWarningDeps();
|
|
169
|
+
const summary = _buildPreWarningSummary(warning);
|
|
170
|
+
const methodHint = Array.isArray(warning.removedMethods) && warning.removedMethods.length > 0
|
|
171
|
+
? warning.removedMethods.slice(0, 3).map(m => `${m.name}:${m.lineNumber}`).join(', ')
|
|
172
|
+
: '';
|
|
173
|
+
const summaryLine = methodHint
|
|
174
|
+
? `${summary} - ${_tp('picker.methods')}: ${methodHint}`
|
|
175
|
+
: summary;
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
const picker = vscode.window.createQuickPick();
|
|
179
|
+
const items = [
|
|
180
|
+
{
|
|
181
|
+
label: '$(discard) Undo Change',
|
|
182
|
+
description: 'Revert this deletion now',
|
|
183
|
+
detail: 'Safest option. Restore the latest edit batch immediately.',
|
|
184
|
+
action: 'undo',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: '$(diff) View Diff',
|
|
188
|
+
description: 'Inspect before and after',
|
|
189
|
+
detail: 'Open a diff view and keep the current edit for now.',
|
|
190
|
+
action: 'diff',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
label: '$(check) Keep Changes',
|
|
194
|
+
description: 'Accept this edit',
|
|
195
|
+
detail: 'Dismiss this warning and continue working.',
|
|
196
|
+
action: 'keep',
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
let settled = false;
|
|
201
|
+
let remainingMs = PRE_WARNING_AUTO_CONTINUE_MS;
|
|
202
|
+
let timer = null;
|
|
203
|
+
|
|
204
|
+
const finish = async (action) => {
|
|
205
|
+
if (settled) return;
|
|
206
|
+
settled = true;
|
|
207
|
+
if (timer) clearInterval(timer);
|
|
208
|
+
try { picker.hide(); } catch { /* ignore */ }
|
|
209
|
+
picker.dispose();
|
|
210
|
+
|
|
211
|
+
if (action === 'undo') {
|
|
212
|
+
await vscode.window.showTextDocument(document, { preview: false, preserveFocus: false });
|
|
213
|
+
await vscode.commands.executeCommand('undo');
|
|
214
|
+
clearPreWarning(projectPath, relPath);
|
|
215
|
+
if (poller) poller.forceRefresh();
|
|
216
|
+
resolve('undo');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (action === 'diff') {
|
|
221
|
+
clearPreWarning(projectPath, relPath);
|
|
222
|
+
if (poller) poller.forceRefresh();
|
|
223
|
+
await _openPreWarningDiff(document, previousText);
|
|
224
|
+
vscode.window.setStatusBarMessage(
|
|
225
|
+
_tp('status.diffOpen', { file: path.basename(relPath) }),
|
|
226
|
+
2500
|
|
227
|
+
);
|
|
228
|
+
resolve('diff');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
clearPreWarning(projectPath, relPath);
|
|
233
|
+
if (poller) poller.forceRefresh();
|
|
234
|
+
resolve(action);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const updatePicker = () => {
|
|
238
|
+
picker.title = _tp('picker.title', {
|
|
239
|
+
time: _formatPreWarningCountdown(remainingMs),
|
|
240
|
+
});
|
|
241
|
+
picker.placeholder = _tp('picker.placeholder', {
|
|
242
|
+
file: relPath,
|
|
243
|
+
summary: summaryLine,
|
|
244
|
+
});
|
|
245
|
+
picker.items = [
|
|
246
|
+
{
|
|
247
|
+
label: _tp('action.undo.label'),
|
|
248
|
+
description: _tp('action.undo.description'),
|
|
249
|
+
detail: _tp('action.undo.detail'),
|
|
250
|
+
action: 'undo',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
label: _tp('action.diff.label'),
|
|
254
|
+
description: _tp('action.diff.description'),
|
|
255
|
+
detail: _tp('action.diff.detail'),
|
|
256
|
+
action: 'diff',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
label: _tp('action.keep.label'),
|
|
260
|
+
description: _tp('action.keep.description'),
|
|
261
|
+
detail: _tp('action.keep.detail'),
|
|
262
|
+
action: 'keep',
|
|
263
|
+
},
|
|
264
|
+
];
|
|
265
|
+
picker.matchOnDescription = true;
|
|
266
|
+
picker.matchOnDetail = true;
|
|
267
|
+
picker.ignoreFocusOut = false;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
picker.onDidAccept(() => {
|
|
271
|
+
const selected = picker.selectedItems[0];
|
|
272
|
+
finish(selected?.action || 'keep').catch(() => resolve());
|
|
273
|
+
});
|
|
274
|
+
picker.onDidHide(() => {
|
|
275
|
+
if (!settled) {
|
|
276
|
+
finish('timeout').catch(() => resolve());
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
updatePicker();
|
|
281
|
+
picker.show();
|
|
282
|
+
|
|
283
|
+
timer = setInterval(() => {
|
|
284
|
+
remainingMs -= PRE_WARNING_POPUP_TICK_MS;
|
|
285
|
+
if (remainingMs <= 0) {
|
|
286
|
+
finish('timeout').catch(() => resolve());
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
updatePicker();
|
|
290
|
+
}, PRE_WARNING_POPUP_TICK_MS);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function _assessPreWarning(document) {
|
|
295
|
+
if (!document || document.isClosed || document.uri.scheme !== 'file') return;
|
|
296
|
+
|
|
297
|
+
const info = _getProjectInfo(document);
|
|
298
|
+
if (!info) return;
|
|
299
|
+
|
|
300
|
+
const key = _docKey(document);
|
|
301
|
+
const previousText = _preWarningBaselines.get(key) ?? document.getText();
|
|
302
|
+
const deps = _getPreWarningDeps();
|
|
303
|
+
const { cfg } = deps.loadConfig(info.projectPath);
|
|
304
|
+
|
|
305
|
+
if (!deps.isPreWarningEnabled(cfg) || deps.shouldExcludePreWarning(info.relPath, cfg)) {
|
|
306
|
+
deps.clearPreWarning(info.projectPath, info.relPath);
|
|
307
|
+
_preWarningFingerprints.delete(key);
|
|
308
|
+
if (poller) poller.forceRefresh();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const assessment = deps.assessDeletionRisk(previousText, document.getText(), {
|
|
313
|
+
threshold: cfg.pre_warning_threshold,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (!assessment.triggered) {
|
|
317
|
+
deps.clearPreWarning(info.projectPath, info.relPath);
|
|
318
|
+
_preWarningFingerprints.delete(key);
|
|
319
|
+
if (poller) poller.forceRefresh();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const warning = {
|
|
324
|
+
file: info.relPath,
|
|
325
|
+
mode: cfg.pre_warning_mode,
|
|
326
|
+
threshold: cfg.pre_warning_threshold,
|
|
327
|
+
deletedLines: assessment.deletedLines,
|
|
328
|
+
removedMethodCount: assessment.removedMethodCount,
|
|
329
|
+
removedMethods: assessment.removedMethods.slice(0, 10),
|
|
330
|
+
riskPercent: assessment.riskPercent,
|
|
331
|
+
summary: assessment.summary,
|
|
332
|
+
deletedLineSamples: assessment.deletedLineSamples,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const fingerprint = JSON.stringify([
|
|
336
|
+
warning.file,
|
|
337
|
+
warning.deletedLines,
|
|
338
|
+
warning.removedMethodCount,
|
|
339
|
+
warning.riskPercent,
|
|
340
|
+
warning.removedMethods.map(m => m.name).join(','),
|
|
341
|
+
]);
|
|
342
|
+
const last = _preWarningFingerprints.get(key);
|
|
343
|
+
|
|
344
|
+
if (cfg.pre_warning_mode === 'popup') {
|
|
345
|
+
if (last?.suppressed) return;
|
|
346
|
+
if (last && last.fingerprint === fingerprint && Date.now() - last.at < PRE_WARNING_POPUP_COOLDOWN_MS) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
deps.recordPreWarning(info.projectPath, warning, {
|
|
352
|
+
setActive: cfg.pre_warning_mode === 'dashboard',
|
|
353
|
+
});
|
|
354
|
+
if (poller) poller.forceRefresh();
|
|
355
|
+
|
|
356
|
+
if (cfg.pre_warning_mode === 'silent') {
|
|
357
|
+
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
|
|
358
|
+
console.warn(`[cursor-guard] pre-warning ${info.relPath}: ${warning.summary}`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (cfg.pre_warning_mode === 'popup') {
|
|
363
|
+
const action = await _showPreWarningPopup(document, info.relPath, previousText, warning, info.projectPath);
|
|
364
|
+
if (action === 'undo') {
|
|
365
|
+
_preWarningFingerprints.delete(key);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: true });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_preWarningFingerprints.set(key, { fingerprint, at: Date.now(), suppressed: false });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** After autoSetup: toast when something changed, or once when Skill path is ready (locale-aware). */
|
|
376
|
+
function notifyGuardSkillSetup(vscode, context, setup) {
|
|
377
|
+
const locale = getLocale(context.globalState);
|
|
378
|
+
const isZh = locale === 'zh-CN';
|
|
379
|
+
const openLabel = isZh ? '打开 Skill 目录' : 'Open Skill Folder';
|
|
380
|
+
const { actions = [], skillPath } = setup || {};
|
|
381
|
+
|
|
382
|
+
const offerOpen = (msg) => {
|
|
383
|
+
if (!skillPath) {
|
|
384
|
+
vscode.window.showInformationMessage(msg);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
vscode.window.showInformationMessage(msg, openLabel).then((sel) => {
|
|
388
|
+
if (sel === openLabel) vscode.env.openExternal(vscode.Uri.file(skillPath));
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
if (actions.length > 0) {
|
|
393
|
+
const detail = actions.join(isZh ? '、' : ', ');
|
|
394
|
+
const msg = isZh
|
|
395
|
+
? `Cursor Guard:已自动完成首装(${detail})。Agent 可使用用户目录下的 cursor-guard Skill。`
|
|
396
|
+
: `Cursor Guard: first-time setup complete (${detail}). Agent Skill lives in your user skills folder (cursor-guard).`;
|
|
397
|
+
offerOpen(msg);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (skillPath && !context.globalState.get('cursorGuard.skillIdleHintShown')) {
|
|
402
|
+
void context.globalState.update('cursorGuard.skillIdleHintShown', true);
|
|
403
|
+
const msg = isZh
|
|
404
|
+
? `Cursor Guard:Agent Skill 已在安装扩展时自动配置(${skillPath})。命令面板可执行「Install Agent Skill」重新安装或打开目录。`
|
|
405
|
+
: `Cursor Guard: Agent Skill was auto-configured when you installed this extension (${skillPath}). Command Palette: "Install Agent Skill" to reinstall or open the folder.`;
|
|
406
|
+
offerOpen(msg);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function activate(context) {
|
|
411
|
+
const setup = await autoSetup(context, vscode);
|
|
412
|
+
_localeStorage = context.globalState;
|
|
413
|
+
notifyGuardSkillSetup(vscode, context, setup);
|
|
414
|
+
|
|
415
|
+
dashMgr = new DashboardManager();
|
|
416
|
+
poller = new Poller(dashMgr);
|
|
417
|
+
statusBar = new StatusBarController(poller);
|
|
418
|
+
treeView = new GuardTreeView(poller, dashMgr);
|
|
419
|
+
webviewProvider = new WebViewProvider(context, dashMgr);
|
|
420
|
+
sidebarProvider = new SidebarDashboardProvider(poller, context);
|
|
421
|
+
|
|
422
|
+
context.subscriptions.push(
|
|
423
|
+
vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
|
|
424
|
+
|
|
425
|
+
vscode.commands.registerCommand('cursorGuard.openDashboard', async () => {
|
|
426
|
+
if (!dashMgr.running) {
|
|
427
|
+
const action = await vscode.window.showWarningMessage(
|
|
428
|
+
'Cursor Guard: Dashboard server not running.',
|
|
429
|
+
'Start Server', 'Cancel'
|
|
430
|
+
);
|
|
431
|
+
if (action === 'Start Server') {
|
|
432
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
433
|
+
if (folders) {
|
|
434
|
+
await dashMgr.ensureRunning(folders.map(f => f.uri.fsPath));
|
|
435
|
+
}
|
|
436
|
+
if (!dashMgr.running) {
|
|
437
|
+
vscode.window.showErrorMessage('Cursor Guard: failed to start dashboard server.');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
webviewProvider.show();
|
|
445
|
+
}),
|
|
446
|
+
|
|
447
|
+
vscode.commands.registerCommand('cursorGuard.snapshotNow', async () => {
|
|
448
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
449
|
+
if (!folders || folders.length === 0) return;
|
|
450
|
+
const projectPath = folders[0].uri.fsPath;
|
|
451
|
+
const result = await dashMgr.snapshotNow(projectPath);
|
|
452
|
+
if (result?.status === 'created') {
|
|
453
|
+
const n = result.changedCount ?? 0;
|
|
454
|
+
const msg = result.bookmark
|
|
455
|
+
? 'Cursor Guard: bookmark snapshot saved (tree unchanged — timeline records intent & time).'
|
|
456
|
+
: n > 0
|
|
457
|
+
? `Cursor Guard: snapshot created (${n} file change(s))`
|
|
458
|
+
: 'Cursor Guard: snapshot created (restore point saved; no file changes since last snapshot)';
|
|
459
|
+
vscode.window.showInformationMessage(msg);
|
|
460
|
+
} else if (result?.status === 'unchanged' || result?.status === 'skipped') {
|
|
461
|
+
vscode.window.showInformationMessage(
|
|
462
|
+
`Cursor Guard: no snapshot created (${result.reason || 'unchanged'})`
|
|
463
|
+
);
|
|
464
|
+
} else if (result?.status === 'error') {
|
|
465
|
+
vscode.window.showWarningMessage(`Cursor Guard: ${result.error}`);
|
|
466
|
+
} else {
|
|
467
|
+
vscode.window.showWarningMessage(`Cursor Guard: snapshot returned status "${result?.status || 'unknown'}"`);
|
|
468
|
+
}
|
|
469
|
+
poller.forceRefresh();
|
|
470
|
+
}),
|
|
471
|
+
|
|
472
|
+
vscode.commands.registerCommand('cursorGuard.startWatcher', async () => {
|
|
473
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
474
|
+
if (!folders || folders.length === 0) {
|
|
475
|
+
vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const projectPath = folders[0].uri.fsPath;
|
|
479
|
+
const existingPid = dashMgr.getWatcherPid(projectPath);
|
|
480
|
+
if (existingPid) {
|
|
481
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher already running (PID ${existingPid})`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const pid = dashMgr.startWatcher(projectPath);
|
|
485
|
+
if (pid) {
|
|
486
|
+
vscode.window.showInformationMessage(`Cursor Guard: watcher started (PID ${pid})`);
|
|
487
|
+
setTimeout(() => poller.forceRefresh(), 2000);
|
|
488
|
+
} else {
|
|
489
|
+
vscode.window.showWarningMessage('Cursor Guard: failed to start watcher');
|
|
490
|
+
}
|
|
491
|
+
}),
|
|
492
|
+
|
|
493
|
+
vscode.commands.registerCommand('cursorGuard.stopWatcher', async () => {
|
|
494
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
495
|
+
if (!folders || folders.length === 0) return;
|
|
496
|
+
const projectPath = folders[0].uri.fsPath;
|
|
497
|
+
const stopped = dashMgr.stopWatcher(projectPath);
|
|
498
|
+
if (stopped) {
|
|
499
|
+
vscode.window.showInformationMessage('Cursor Guard: watcher stopped');
|
|
500
|
+
setTimeout(() => poller.forceRefresh(), 1000);
|
|
501
|
+
} else {
|
|
502
|
+
vscode.window.showWarningMessage('Cursor Guard: no running watcher found');
|
|
503
|
+
}
|
|
504
|
+
}),
|
|
505
|
+
|
|
506
|
+
vscode.commands.registerCommand('cursorGuard.refreshTree', () => {
|
|
507
|
+
poller.forceRefresh();
|
|
508
|
+
treeView.refresh();
|
|
509
|
+
}),
|
|
510
|
+
|
|
511
|
+
vscode.commands.registerCommand('cursorGuard.quickRestore', async () => {
|
|
512
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
513
|
+
if (!folders || folders.length === 0) return;
|
|
514
|
+
if (!dashMgr.running) {
|
|
515
|
+
vscode.window.showWarningMessage('Cursor Guard: dashboard not running. Cannot list backups.');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const projects = await dashMgr.fetchApi('/api/projects');
|
|
519
|
+
if (!projects || projects.length === 0) return;
|
|
520
|
+
const pid = projects[0].id;
|
|
521
|
+
const pageData = await dashMgr.getFullPageData(pid);
|
|
522
|
+
const backups = (pageData?.backups || []).slice(0, 8);
|
|
523
|
+
if (backups.length === 0) {
|
|
524
|
+
vscode.window.showInformationMessage('Cursor Guard: no backups available to restore from.');
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const items = backups.map(b => {
|
|
528
|
+
const time = b.timestamp ? new Date(b.timestamp).toLocaleString() : '?';
|
|
529
|
+
const files = b.filesChanged ? `${b.filesChanged} files` : '';
|
|
530
|
+
const summary = b.summary ? b.summary.slice(0, 60) : '';
|
|
531
|
+
return {
|
|
532
|
+
label: `$(git-commit) ${time}`,
|
|
533
|
+
description: `${b.type || 'auto'} - ${files}`,
|
|
534
|
+
detail: summary,
|
|
535
|
+
hash: b.commitHash,
|
|
536
|
+
};
|
|
537
|
+
});
|
|
538
|
+
const selected = await vscode.window.showQuickPick(items, {
|
|
539
|
+
placeHolder: 'Select a backup to restore from',
|
|
540
|
+
title: 'Cursor Guard: Quick Restore',
|
|
541
|
+
});
|
|
542
|
+
if (selected && selected.hash) {
|
|
543
|
+
const url = `${dashMgr.baseUrl}?token=${dashMgr.token}`;
|
|
544
|
+
vscode.env.openExternal(vscode.Uri.parse(url));
|
|
545
|
+
vscode.window.showInformationMessage(
|
|
546
|
+
`Cursor Guard: opening dashboard for restore. Selected backup: ${selected.hash.slice(0, 7)}`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}),
|
|
550
|
+
|
|
551
|
+
vscode.commands.registerCommand('cursorGuard.doctor', async () => {
|
|
552
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
553
|
+
if (!folders || folders.length === 0) return;
|
|
554
|
+
const projectPath = folders[0].uri.fsPath;
|
|
555
|
+
try {
|
|
556
|
+
const { runDiagnostics } = require(guardPath('lib', 'core', 'doctor'));
|
|
557
|
+
const result = runDiagnostics(projectPath);
|
|
558
|
+
const passed = result.checks.filter(c => c.status === 'PASS').length;
|
|
559
|
+
const warned = result.checks.filter(c => c.status === 'WARN').length;
|
|
560
|
+
const failed = result.checks.filter(c => c.status === 'FAIL').length;
|
|
561
|
+
const msg = `Doctor: ${passed} passed, ${warned} warnings, ${failed} failed`;
|
|
562
|
+
if (failed > 0) {
|
|
563
|
+
vscode.window.showErrorMessage(`Cursor Guard: ${msg}`);
|
|
564
|
+
} else if (warned > 0) {
|
|
565
|
+
vscode.window.showWarningMessage(`Cursor Guard: ${msg}`);
|
|
566
|
+
} else {
|
|
567
|
+
vscode.window.showInformationMessage(`Cursor Guard: ${msg}`);
|
|
568
|
+
}
|
|
569
|
+
} catch (e) {
|
|
570
|
+
vscode.window.showErrorMessage(`Cursor Guard Doctor: ${e.message}`);
|
|
571
|
+
}
|
|
572
|
+
}),
|
|
573
|
+
|
|
574
|
+
vscode.commands.registerCommand('cursorGuard.setupSkill', async () => {
|
|
575
|
+
const extRoot = getExtensionRoot(context);
|
|
576
|
+
const { homePath, dirName } = detectIdeDir(vscode);
|
|
577
|
+
let r;
|
|
578
|
+
try {
|
|
579
|
+
r = installAgentSkill(extRoot, homePath, dirName);
|
|
580
|
+
} catch (e) {
|
|
581
|
+
vscode.window.showErrorMessage(`Cursor Guard: Install Agent Skill failed — ${e.message}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const loc = getLocale(context.globalState);
|
|
585
|
+
const isZh = loc === 'zh-CN';
|
|
586
|
+
const openLabel = isZh ? '打开 Skill 目录' : 'Open Skill Folder';
|
|
587
|
+
if (!r.skillPath && r.actions.length === 0) {
|
|
588
|
+
vscode.window.showWarningMessage(
|
|
589
|
+
isZh
|
|
590
|
+
? 'Cursor Guard:扩展包内未找到 SKILL.md,无法安装 Agent Skill。'
|
|
591
|
+
: 'Cursor Guard: bundled SKILL.md not found; cannot install Agent Skill.'
|
|
592
|
+
);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (r.actions.length > 0) {
|
|
596
|
+
const detail = r.actions.join(isZh ? '、' : ', ');
|
|
597
|
+
const msg = isZh
|
|
598
|
+
? `Cursor Guard:已更新 Agent Skill(${detail})。路径:${r.skillPath}`
|
|
599
|
+
: `Cursor Guard: Agent Skill updated (${detail}). Path: ${r.skillPath}`;
|
|
600
|
+
vscode.window.showInformationMessage(msg, openLabel).then((sel) => {
|
|
601
|
+
if (sel === openLabel && r.skillPath) vscode.env.openExternal(vscode.Uri.file(r.skillPath));
|
|
602
|
+
});
|
|
603
|
+
} else {
|
|
604
|
+
const msg = isZh
|
|
605
|
+
? `Cursor Guard:Agent Skill 已就绪,无需变更(${r.skillPath})。`
|
|
606
|
+
: `Cursor Guard: Agent Skill already up to date (${r.skillPath}).`;
|
|
607
|
+
vscode.window.showInformationMessage(msg, openLabel).then((sel) => {
|
|
608
|
+
if (sel === openLabel && r.skillPath) vscode.env.openExternal(vscode.Uri.file(r.skillPath));
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}),
|
|
612
|
+
|
|
613
|
+
vscode.commands.registerCommand('cursorGuard.addToProtect', (uri) => {
|
|
614
|
+
_modifyGuardConfig(uri, 'protect');
|
|
615
|
+
}),
|
|
616
|
+
|
|
617
|
+
vscode.commands.registerCommand('cursorGuard.addToIgnore', (uri) => {
|
|
618
|
+
_modifyGuardConfig(uri, 'ignore');
|
|
619
|
+
}),
|
|
620
|
+
|
|
621
|
+
statusBar,
|
|
622
|
+
poller,
|
|
623
|
+
treeView,
|
|
624
|
+
webviewProvider,
|
|
625
|
+
sidebarProvider,
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
const started = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
629
|
+
if (started) {
|
|
630
|
+
poller.start();
|
|
631
|
+
vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
let _fsRefreshTimer = null;
|
|
635
|
+
const _scheduleRefresh = () => {
|
|
636
|
+
if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
|
|
637
|
+
_fsRefreshTimer = setTimeout(() => poller.forceRefresh(), 1500);
|
|
638
|
+
};
|
|
639
|
+
const fileWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, false, false);
|
|
640
|
+
fileWatcher.onDidChange(_scheduleRefresh);
|
|
641
|
+
fileWatcher.onDidCreate(_scheduleRefresh);
|
|
642
|
+
fileWatcher.onDidDelete(_scheduleRefresh);
|
|
643
|
+
context.subscriptions.push(fileWatcher);
|
|
644
|
+
|
|
645
|
+
for (const document of vscode.workspace.textDocuments) {
|
|
646
|
+
_seedBaseline(document);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
context.subscriptions.push(
|
|
650
|
+
vscode.workspace.onDidOpenTextDocument(document => {
|
|
651
|
+
_seedBaseline(document);
|
|
652
|
+
}),
|
|
653
|
+
vscode.workspace.onDidCloseTextDocument(document => {
|
|
654
|
+
const info = _getProjectInfo(document);
|
|
655
|
+
_clearDocPreWarningState(document);
|
|
656
|
+
if (info) {
|
|
657
|
+
const { clearPreWarning } = _getPreWarningDeps();
|
|
658
|
+
clearPreWarning(info.projectPath, info.relPath);
|
|
659
|
+
if (poller) poller.forceRefresh();
|
|
660
|
+
}
|
|
661
|
+
}),
|
|
662
|
+
vscode.workspace.onDidSaveTextDocument(document => {
|
|
663
|
+
const info = _getProjectInfo(document);
|
|
664
|
+
_seedBaseline(document);
|
|
665
|
+
_preWarningFingerprints.delete(_docKey(document));
|
|
666
|
+
if (info) {
|
|
667
|
+
const { clearPreWarning } = _getPreWarningDeps();
|
|
668
|
+
clearPreWarning(info.projectPath, info.relPath);
|
|
669
|
+
if (poller) poller.forceRefresh();
|
|
670
|
+
}
|
|
671
|
+
}),
|
|
672
|
+
vscode.workspace.onDidChangeTextDocument(event => {
|
|
673
|
+
const info = _getProjectInfo(event.document);
|
|
674
|
+
if (!info || !_isLikelyDeleteBatch(event)) return;
|
|
675
|
+
|
|
676
|
+
const key = _docKey(event.document);
|
|
677
|
+
const existing = _preWarningTimers.get(key);
|
|
678
|
+
if (existing) clearTimeout(existing);
|
|
679
|
+
|
|
680
|
+
const timer = setTimeout(() => {
|
|
681
|
+
_preWarningTimers.delete(key);
|
|
682
|
+
_assessPreWarning(event.document).catch(err => {
|
|
683
|
+
console.warn(`[cursor-guard] pre-warning failed for ${info.relPath}: ${err.message}`);
|
|
684
|
+
});
|
|
685
|
+
}, PRE_WARNING_DEBOUNCE_MS);
|
|
686
|
+
|
|
687
|
+
_preWarningTimers.set(key, timer);
|
|
688
|
+
}),
|
|
689
|
+
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
690
|
+
const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
691
|
+
if (restarted && !poller._timer) poller.start();
|
|
692
|
+
poller.forceRefresh();
|
|
693
|
+
})
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function _modifyGuardConfig(uri, field) {
|
|
698
|
+
const folders = vscode.workspace.workspaceFolders;
|
|
699
|
+
if (!folders || folders.length === 0) {
|
|
700
|
+
vscode.window.showWarningMessage('Cursor Guard: no workspace folder open.');
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let targetUri = uri;
|
|
705
|
+
if (!targetUri) {
|
|
706
|
+
const editor = vscode.window.activeTextEditor;
|
|
707
|
+
if (editor) targetUri = editor.document.uri;
|
|
708
|
+
}
|
|
709
|
+
if (!targetUri) {
|
|
710
|
+
vscode.window.showWarningMessage('Cursor Guard: no file or folder selected.');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const wsRoot = folders[0].uri.fsPath;
|
|
715
|
+
const configPath = path.join(wsRoot, '.cursor-guard.json');
|
|
716
|
+
const targetPath = targetUri.fsPath;
|
|
717
|
+
|
|
718
|
+
const relative = path.relative(wsRoot, targetPath).replace(/\\/g, '/');
|
|
719
|
+
if (!relative || relative.startsWith('..')) {
|
|
720
|
+
vscode.window.showWarningMessage('Cursor Guard: selected path is outside the workspace.');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let isDir = false;
|
|
725
|
+
try { isDir = fs.statSync(targetPath).isDirectory(); } catch { /* file */ }
|
|
726
|
+
const pattern = isDir ? `${relative}/**` : relative;
|
|
727
|
+
|
|
728
|
+
const action = field === 'protect' ? 'Add to Protected' : 'Exclude from Protection';
|
|
729
|
+
const pick = await vscode.window.showQuickPick(
|
|
730
|
+
[
|
|
731
|
+
{ label: pattern, description: isDir ? 'directory glob' : 'exact file' },
|
|
732
|
+
{ label: `${path.basename(targetPath)}`, description: 'filename only (matches anywhere)' },
|
|
733
|
+
...(isDir ? [] : [{ label: `*.${path.extname(targetPath).slice(1)}`, description: 'file extension' }]),
|
|
734
|
+
{ label: '$(edit) Custom pattern...', description: 'enter your own glob', custom: true },
|
|
735
|
+
],
|
|
736
|
+
{ placeHolder: `${action}: choose a pattern`, title: `Cursor Guard: ${action}` }
|
|
737
|
+
);
|
|
738
|
+
if (!pick) return;
|
|
739
|
+
|
|
740
|
+
let chosenPattern = pick.label;
|
|
741
|
+
if (pick.custom) {
|
|
742
|
+
const input = await vscode.window.showInputBox({
|
|
743
|
+
prompt: `Enter a glob pattern to ${field === 'protect' ? 'protect' : 'exclude'}`,
|
|
744
|
+
value: pattern,
|
|
745
|
+
});
|
|
746
|
+
if (!input) return;
|
|
747
|
+
chosenPattern = input;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let config = {};
|
|
751
|
+
if (fs.existsSync(configPath)) {
|
|
752
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch { config = {}; }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!Array.isArray(config[field])) config[field] = [];
|
|
756
|
+
|
|
757
|
+
if (config[field].includes(chosenPattern)) {
|
|
758
|
+
vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" already in ${field} list.`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
config[field].push(chosenPattern);
|
|
763
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
764
|
+
|
|
765
|
+
const label = field === 'protect' ? 'Protected' : 'Excluded';
|
|
766
|
+
vscode.window.showInformationMessage(`Cursor Guard: "${chosenPattern}" added to ${label} list.`);
|
|
767
|
+
|
|
768
|
+
if (poller) poller.forceRefresh();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function deactivate() {
|
|
772
|
+
if (poller) poller.dispose();
|
|
773
|
+
if (statusBar) statusBar.dispose();
|
|
774
|
+
if (treeView) treeView.dispose();
|
|
775
|
+
if (webviewProvider) webviewProvider.dispose();
|
|
776
|
+
if (sidebarProvider) sidebarProvider.dispose();
|
|
777
|
+
if (dashMgr) dashMgr.dispose();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
module.exports = { activate, deactivate };
|