cursor-guard 4.9.1 → 4.9.8
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 +130 -10
- package/README.zh-CN.md +130 -10
- package/ROADMAP.md +65 -8
- package/SKILL.md +32 -22
- package/package.json +3 -2
- package/references/config-reference.md +68 -7
- package/references/config-reference.zh-CN.md +68 -7
- package/references/cursor-guard.example.json +11 -7
- package/references/cursor-guard.schema.json +30 -7
- package/references/dashboard/public/app.js +73 -27
- package/references/dashboard/public/index.html +8 -7
- package/references/lib/auto-backup.js +40 -2
- package/references/lib/core/backups.js +46 -16
- package/references/lib/core/core.test.js +101 -22
- package/references/lib/core/dashboard.js +37 -23
- package/references/lib/core/doctor.js +19 -13
- package/references/lib/core/pre-warning.js +296 -0
- package/references/lib/core/snapshot.js +24 -2
- package/references/lib/core/status.js +15 -7
- package/references/lib/utils.js +46 -20
- package/references/mcp/mcp.test.js +60 -12
- package/references/mcp/server.js +72 -60
- package/references/quickstart.zh-CN.md +46 -21
- package/references/vscode-extension/build-vsix.js +4 -1
- package/references/vscode-extension/dist/LICENSE +65 -0
- package/references/vscode-extension/dist/{cursor-guard-ide-4.9.1.vsix → cursor-guard-ide-4.9.8.vsix} +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +73 -27
- package/references/vscode-extension/dist/dashboard/public/index.html +8 -7
- package/references/vscode-extension/dist/extension.js +406 -5
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/auto-backup.js +40 -2
- package/references/vscode-extension/dist/lib/core/backups.js +46 -16
- package/references/vscode-extension/dist/lib/core/dashboard.js +37 -23
- package/references/vscode-extension/dist/lib/core/doctor.js +19 -13
- package/references/vscode-extension/dist/lib/core/pre-warning.js +296 -0
- package/references/vscode-extension/dist/lib/core/snapshot.js +24 -2
- package/references/vscode-extension/dist/lib/core/status.js +15 -7
- package/references/vscode-extension/dist/lib/dashboard-manager.js +102 -52
- package/references/vscode-extension/dist/lib/locale.js +36 -0
- package/references/vscode-extension/dist/lib/sidebar-webview.js +1027 -281
- package/references/vscode-extension/dist/lib/status-bar.js +95 -68
- package/references/vscode-extension/dist/lib/tree-view.js +174 -114
- package/references/vscode-extension/dist/lib/utils.js +46 -20
- package/references/vscode-extension/dist/mcp/server.js +395 -31
- package/references/vscode-extension/dist/media/brand-placeholder.png +0 -0
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +65 -8
- package/references/vscode-extension/dist/skill/SKILL.md +32 -22
- package/references/vscode-extension/dist/skill/config-reference.md +68 -7
- package/references/vscode-extension/dist/skill/config-reference.zh-CN.md +68 -7
- package/references/vscode-extension/dist/skill/cursor-guard.example.json +11 -7
- package/references/vscode-extension/dist/skill/cursor-guard.schema.json +30 -7
- package/references/vscode-extension/extension.js +406 -5
- package/references/vscode-extension/lib/dashboard-manager.js +102 -52
- package/references/vscode-extension/lib/locale.js +36 -0
- package/references/vscode-extension/lib/sidebar-webview.js +1027 -281
- package/references/vscode-extension/lib/status-bar.js +95 -68
- package/references/vscode-extension/lib/tree-view.js +174 -114
- package/references/vscode-extension/media/brand-placeholder.png +0 -0
- package/references/vscode-extension/package.json +1 -1
|
@@ -11,18 +11,377 @@ const { Poller } = require('./lib/poller');
|
|
|
11
11
|
const { SidebarDashboardProvider } = require('./lib/sidebar-webview');
|
|
12
12
|
const { autoSetup } = require('./lib/auto-setup');
|
|
13
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;
|
|
14
299
|
|
|
15
|
-
|
|
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
|
+
}
|
|
16
374
|
|
|
17
375
|
async function activate(context) {
|
|
18
376
|
await autoSetup(context, vscode);
|
|
377
|
+
_localeStorage = context.globalState;
|
|
19
378
|
|
|
20
379
|
dashMgr = new DashboardManager();
|
|
21
380
|
poller = new Poller(dashMgr);
|
|
22
381
|
statusBar = new StatusBarController(poller);
|
|
23
382
|
treeView = new GuardTreeView(poller, dashMgr);
|
|
24
383
|
webviewProvider = new WebViewProvider(context, dashMgr);
|
|
25
|
-
sidebarProvider = new SidebarDashboardProvider(poller);
|
|
384
|
+
sidebarProvider = new SidebarDashboardProvider(poller, context);
|
|
26
385
|
|
|
27
386
|
context.subscriptions.push(
|
|
28
387
|
vscode.window.registerWebviewViewProvider('cursorGuardDashboard', sidebarProvider),
|
|
@@ -127,7 +486,7 @@ async function activate(context) {
|
|
|
127
486
|
const summary = b.summary ? b.summary.slice(0, 60) : '';
|
|
128
487
|
return {
|
|
129
488
|
label: `$(git-commit) ${time}`,
|
|
130
|
-
description: `${b.type || 'auto'}
|
|
489
|
+
description: `${b.type || 'auto'} - ${files}`,
|
|
131
490
|
detail: summary,
|
|
132
491
|
hash: b.commitHash,
|
|
133
492
|
};
|
|
@@ -161,7 +520,7 @@ async function activate(context) {
|
|
|
161
520
|
} else if (warned > 0) {
|
|
162
521
|
vscode.window.showWarningMessage(`Cursor Guard: ${msg}`);
|
|
163
522
|
} else {
|
|
164
|
-
vscode.window.showInformationMessage(`Cursor Guard: ${msg}
|
|
523
|
+
vscode.window.showInformationMessage(`Cursor Guard: ${msg}`);
|
|
165
524
|
}
|
|
166
525
|
} catch (e) {
|
|
167
526
|
vscode.window.showErrorMessage(`Cursor Guard Doctor: ${e.message}`);
|
|
@@ -189,7 +548,6 @@ async function activate(context) {
|
|
|
189
548
|
vscode.window.showInformationMessage(`Cursor Guard: dashboard started on port ${dashMgr.port}`);
|
|
190
549
|
}
|
|
191
550
|
|
|
192
|
-
// Event-driven UI refresh: FileSystemWatcher triggers immediate poller refresh
|
|
193
551
|
let _fsRefreshTimer = null;
|
|
194
552
|
const _scheduleRefresh = () => {
|
|
195
553
|
if (_fsRefreshTimer) clearTimeout(_fsRefreshTimer);
|
|
@@ -201,7 +559,50 @@ async function activate(context) {
|
|
|
201
559
|
fileWatcher.onDidDelete(_scheduleRefresh);
|
|
202
560
|
context.subscriptions.push(fileWatcher);
|
|
203
561
|
|
|
562
|
+
for (const document of vscode.workspace.textDocuments) {
|
|
563
|
+
_seedBaseline(document);
|
|
564
|
+
}
|
|
565
|
+
|
|
204
566
|
context.subscriptions.push(
|
|
567
|
+
vscode.workspace.onDidOpenTextDocument(document => {
|
|
568
|
+
_seedBaseline(document);
|
|
569
|
+
}),
|
|
570
|
+
vscode.workspace.onDidCloseTextDocument(document => {
|
|
571
|
+
const info = _getProjectInfo(document);
|
|
572
|
+
_clearDocPreWarningState(document);
|
|
573
|
+
if (info) {
|
|
574
|
+
const { clearPreWarning } = _getPreWarningDeps();
|
|
575
|
+
clearPreWarning(info.projectPath, info.relPath);
|
|
576
|
+
if (poller) poller.forceRefresh();
|
|
577
|
+
}
|
|
578
|
+
}),
|
|
579
|
+
vscode.workspace.onDidSaveTextDocument(document => {
|
|
580
|
+
const info = _getProjectInfo(document);
|
|
581
|
+
_seedBaseline(document);
|
|
582
|
+
_preWarningFingerprints.delete(_docKey(document));
|
|
583
|
+
if (info) {
|
|
584
|
+
const { clearPreWarning } = _getPreWarningDeps();
|
|
585
|
+
clearPreWarning(info.projectPath, info.relPath);
|
|
586
|
+
if (poller) poller.forceRefresh();
|
|
587
|
+
}
|
|
588
|
+
}),
|
|
589
|
+
vscode.workspace.onDidChangeTextDocument(event => {
|
|
590
|
+
const info = _getProjectInfo(event.document);
|
|
591
|
+
if (!info || !_isLikelyDeleteBatch(event)) return;
|
|
592
|
+
|
|
593
|
+
const key = _docKey(event.document);
|
|
594
|
+
const existing = _preWarningTimers.get(key);
|
|
595
|
+
if (existing) clearTimeout(existing);
|
|
596
|
+
|
|
597
|
+
const timer = setTimeout(() => {
|
|
598
|
+
_preWarningTimers.delete(key);
|
|
599
|
+
_assessPreWarning(event.document).catch(err => {
|
|
600
|
+
console.warn(`[cursor-guard] pre-warning failed for ${info.relPath}: ${err.message}`);
|
|
601
|
+
});
|
|
602
|
+
}, PRE_WARNING_DEBOUNCE_MS);
|
|
603
|
+
|
|
604
|
+
_preWarningTimers.set(key, timer);
|
|
605
|
+
}),
|
|
205
606
|
vscode.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
206
607
|
const restarted = await dashMgr.autoStart(vscode.workspace.workspaceFolders);
|
|
207
608
|
if (restarted && !poller._timer) poller.start();
|
|
@@ -3,16 +3,18 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const http = require('http');
|
|
6
|
-
const { spawn } = require('child_process');
|
|
7
|
-
const { guardPath } = require('./paths');
|
|
8
|
-
|
|
9
|
-
const CONFIG_FILE = '.cursor-guard.json';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
this.
|
|
15
|
-
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const { guardPath } = require('./paths');
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILE = '.cursor-guard.json';
|
|
10
|
+
const WATCHER_START_GRACE_MS = 8000;
|
|
11
|
+
|
|
12
|
+
class DashboardManager {
|
|
13
|
+
constructor() {
|
|
14
|
+
this._instance = null;
|
|
15
|
+
this._serverModule = null;
|
|
16
|
+
this._startingWatchers = new Map();
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
get running() { return !!this._instance; }
|
|
18
20
|
get port() { return this._instance?.port; }
|
|
@@ -83,7 +85,7 @@ class DashboardManager {
|
|
|
83
85
|
return this.fetchApi(`/api/backup-files?id=${projectId}&hash=${hash}`);
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
async snapshotNow(projectPath) {
|
|
88
|
+
async snapshotNow(projectPath) {
|
|
87
89
|
if (!projectPath) return;
|
|
88
90
|
try {
|
|
89
91
|
const { createGitSnapshot } = require(guardPath('lib', 'core', 'snapshot'));
|
|
@@ -93,50 +95,98 @@ class DashboardManager {
|
|
|
93
95
|
} catch (e) {
|
|
94
96
|
return { status: 'error', error: e.message };
|
|
95
97
|
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_getWatcherLockPath(projectPath) {
|
|
101
|
+
try {
|
|
102
|
+
const { gitAvailable, isGitRepo, gitDir: getGitDir } = require(guardPath('lib', 'utils'));
|
|
103
|
+
const repo = gitAvailable() && isGitRepo(projectPath);
|
|
104
|
+
if (repo) {
|
|
105
|
+
const gDir = getGitDir(projectPath);
|
|
106
|
+
if (gDir) return path.join(gDir, 'cursor-guard.lock');
|
|
107
|
+
}
|
|
108
|
+
} catch { /* ignore */ }
|
|
109
|
+
return path.join(projectPath, '.cursor-guard-backup', 'cursor-guard.lock');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_getPendingWatcherPid(projectPath) {
|
|
113
|
+
const pending = this._startingWatchers.get(projectPath);
|
|
114
|
+
if (!pending) return null;
|
|
115
|
+
try {
|
|
116
|
+
process.kill(pending.pid, 0);
|
|
117
|
+
return pending.pid;
|
|
118
|
+
} catch {
|
|
119
|
+
this._startingWatchers.delete(projectPath);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_clearPendingWatcher(projectPath, pid) {
|
|
125
|
+
const pending = this._startingWatchers.get(projectPath);
|
|
126
|
+
if (!pending) return;
|
|
127
|
+
if (pid == null || pending.pid === pid) {
|
|
128
|
+
this._startingWatchers.delete(projectPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
startWatcher(projectPath) {
|
|
133
|
+
if (!projectPath) return null;
|
|
134
|
+
const existingPid = this.getWatcherPid(projectPath);
|
|
135
|
+
if (existingPid) return existingPid;
|
|
136
|
+
const cliScript = guardPath('bin', 'cursor-guard-backup.js');
|
|
103
137
|
const child = spawn(process.execPath, [cliScript, '--path', projectPath], {
|
|
104
138
|
cwd: projectPath,
|
|
105
139
|
stdio: 'ignore',
|
|
106
|
-
detached: true,
|
|
107
|
-
env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
|
|
108
|
-
});
|
|
109
|
-
child.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
detached: true,
|
|
141
|
+
env: { ...process.env, GUARD_SPAWNED_BY_EXT: '1' },
|
|
142
|
+
});
|
|
143
|
+
this._startingWatchers.set(projectPath, { pid: child.pid, startedAt: Date.now() });
|
|
144
|
+
const clearPending = () => this._clearPendingWatcher(projectPath, child.pid);
|
|
145
|
+
child.once('exit', clearPending);
|
|
146
|
+
child.once('error', clearPending);
|
|
147
|
+
setTimeout(clearPending, WATCHER_START_GRACE_MS);
|
|
148
|
+
child.unref();
|
|
149
|
+
return child.pid;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
stopWatcher(projectPath) {
|
|
153
|
+
if (!projectPath) return false;
|
|
154
|
+
const pendingPid = this._getPendingWatcherPid(projectPath);
|
|
155
|
+
if (pendingPid) {
|
|
156
|
+
try { process.kill(pendingPid, 'SIGTERM'); } catch { /* ignore */ }
|
|
157
|
+
this._clearPendingWatcher(projectPath, pendingPid);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const lockPath = this._getWatcherLockPath(projectPath);
|
|
161
|
+
if (!fs.existsSync(lockPath)) return false;
|
|
162
|
+
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
163
|
+
const pidMatch = content.match(/pid=(\d+)/);
|
|
164
|
+
if (pidMatch) {
|
|
165
|
+
process.kill(parseInt(pidMatch[1], 10), 'SIGTERM');
|
|
166
|
+
}
|
|
167
|
+
try { fs.unlinkSync(lockPath); } catch { /* ok */ }
|
|
168
|
+
return true;
|
|
169
|
+
} catch { /* ok */ }
|
|
170
|
+
return !!pendingPid;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
getWatcherPid(projectPath) {
|
|
174
|
+
const pendingPid = this._getPendingWatcherPid(projectPath);
|
|
175
|
+
if (pendingPid) return pendingPid;
|
|
176
|
+
try {
|
|
177
|
+
const lockPath = this._getWatcherLockPath(projectPath);
|
|
178
|
+
if (!fs.existsSync(lockPath)) return null;
|
|
179
|
+
const content = fs.readFileSync(lockPath, 'utf-8');
|
|
180
|
+
const pidMatch = content.match(/pid=(\d+)/);
|
|
181
|
+
if (pidMatch) {
|
|
182
|
+
const pid = parseInt(pidMatch[1], 10);
|
|
183
|
+
process.kill(pid, 0);
|
|
184
|
+
this._clearPendingWatcher(projectPath, pid);
|
|
185
|
+
return pid;
|
|
186
|
+
}
|
|
187
|
+
} catch { /* not running */ }
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
140
190
|
|
|
141
191
|
dispose() {
|
|
142
192
|
this._instance = null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const vscode = require('vscode');
|
|
4
|
+
|
|
5
|
+
const EXTENSION_LOCALE_KEY = 'cursorGuard.locale';
|
|
6
|
+
|
|
7
|
+
function normalizeLocale(locale) {
|
|
8
|
+
return locale === 'zh-CN' ? 'zh-CN' : 'en-US';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function detectLocale() {
|
|
12
|
+
return normalizeLocale(
|
|
13
|
+
(vscode.env.language || '').toLowerCase().startsWith('zh') ? 'zh-CN' : 'en-US'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getLocale(storage) {
|
|
18
|
+
if (!storage || typeof storage.get !== 'function') return detectLocale();
|
|
19
|
+
return normalizeLocale(storage.get(EXTENSION_LOCALE_KEY) || detectLocale());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function setLocale(storage, locale) {
|
|
23
|
+
const normalized = normalizeLocale(locale);
|
|
24
|
+
if (storage && typeof storage.update === 'function') {
|
|
25
|
+
await storage.update(EXTENSION_LOCALE_KEY, normalized);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
EXTENSION_LOCALE_KEY,
|
|
32
|
+
normalizeLocale,
|
|
33
|
+
detectLocale,
|
|
34
|
+
getLocale,
|
|
35
|
+
setLocale,
|
|
36
|
+
};
|