cursor-guard 4.3.0 → 4.3.1
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/SKILL.md
CHANGED
|
@@ -146,6 +146,15 @@ When the target file of an edit **falls outside the protected scope**, the agent
|
|
|
146
146
|
**Before any High-risk operation on a protected file:**
|
|
147
147
|
|
|
148
148
|
> **MCP shortcut**: if `snapshot_now` tool is available, call it with `{ "path": "<project>", "strategy": "git" }` instead of the shell commands below. The tool handles temp index, secrets exclusion, and ref creation internally, and returns `{ "git": { "status": "created", "commitHash": "...", "shortHash": "..." } }`. Report the `shortHash` to the user and proceed.
|
|
149
|
+
>
|
|
150
|
+
> **Best practice — descriptive messages**: Always provide a meaningful `message` parameter that describes *why* this snapshot is being created and *what* changes are at risk. This message appears in the dashboard restore-point list, helping users identify which snapshot to restore from. Example:
|
|
151
|
+
> ```json
|
|
152
|
+
> {
|
|
153
|
+
> "path": "/project",
|
|
154
|
+
> "strategy": "git",
|
|
155
|
+
> "message": "guard: before refactoring auth middleware — moving session logic from app.js to middleware/auth.js"
|
|
156
|
+
> }
|
|
157
|
+
> ```
|
|
149
158
|
|
|
150
159
|
Use a **temporary index and dedicated ref** so the user's staged/unstaged state is never touched:
|
|
151
160
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -95,6 +95,11 @@ const I18N = {
|
|
|
95
95
|
'trigger.manual': 'Manual (agent)',
|
|
96
96
|
'trigger.pre-restore': 'Pre-Restore',
|
|
97
97
|
'backups.col.summary': 'Changes',
|
|
98
|
+
'summary.modified': 'Modified',
|
|
99
|
+
'summary.added': 'Added',
|
|
100
|
+
'summary.deleted': 'Deleted',
|
|
101
|
+
'summary.renamed': 'Renamed',
|
|
102
|
+
'summary.files': 'files',
|
|
98
103
|
|
|
99
104
|
'error.fetchFailed': 'Failed to fetch data',
|
|
100
105
|
'error.sectionFailed': 'This section failed to load',
|
|
@@ -268,6 +273,11 @@ const I18N = {
|
|
|
268
273
|
'trigger.manual': '手动(Agent)',
|
|
269
274
|
'trigger.pre-restore': '恢复前快照',
|
|
270
275
|
'backups.col.summary': '变更',
|
|
276
|
+
'summary.modified': '修改',
|
|
277
|
+
'summary.added': '新增',
|
|
278
|
+
'summary.deleted': '删除',
|
|
279
|
+
'summary.renamed': '重命名',
|
|
280
|
+
'summary.files': '个文件',
|
|
271
281
|
|
|
272
282
|
'error.fetchFailed': '数据拉取失败',
|
|
273
283
|
'error.sectionFailed': '此区块加载失败',
|
|
@@ -793,14 +803,28 @@ function renderFilterBar() {
|
|
|
793
803
|
).join('');
|
|
794
804
|
}
|
|
795
805
|
|
|
806
|
+
function translateSummary(raw) {
|
|
807
|
+
if (!raw) return raw;
|
|
808
|
+
return raw
|
|
809
|
+
.replace(/\bModified (\d+)/g, (_, n) => `${t('summary.modified')} ${n}`)
|
|
810
|
+
.replace(/\bAdded (\d+)/g, (_, n) => `${t('summary.added')} ${n}`)
|
|
811
|
+
.replace(/\bDeleted (\d+)/g, (_, n) => `${t('summary.deleted')} ${n}`)
|
|
812
|
+
.replace(/\bRenamed (\d+)/g, (_, n) => `${t('summary.renamed')} ${n}`);
|
|
813
|
+
}
|
|
814
|
+
|
|
796
815
|
function formatSummaryCell(b) {
|
|
797
816
|
const parts = [];
|
|
798
|
-
if (b.filesChanged != null) parts.push(`<span class="text-sm">${b.filesChanged} files</span>`);
|
|
817
|
+
if (b.filesChanged != null) parts.push(`<span class="text-sm">${b.filesChanged} ${t('summary.files')}</span>`);
|
|
799
818
|
if (b.trigger) parts.push(`<span class="badge badge-trigger">${t('trigger.' + b.trigger)}</span>`);
|
|
800
819
|
if (b.summary) {
|
|
801
|
-
const
|
|
820
|
+
const translated = translateSummary(b.summary);
|
|
821
|
+
const short = translated.length > 80 ? translated.substring(0, 77) + '...' : translated;
|
|
802
822
|
parts.push(`<span class="text-muted text-sm">${esc(short)}</span>`);
|
|
803
823
|
}
|
|
824
|
+
if (b.message && !b.message.startsWith('guard:')) {
|
|
825
|
+
const msgShort = b.message.length > 50 ? b.message.substring(0, 47) + '...' : b.message;
|
|
826
|
+
parts.push(`<span class="text-muted text-sm">${esc(msgShort)}</span>`);
|
|
827
|
+
}
|
|
804
828
|
return parts.length > 0 ? parts.join(' ') : '<span class="text-muted text-sm">-</span>';
|
|
805
829
|
}
|
|
806
830
|
|
|
@@ -929,7 +953,7 @@ function openRestoreDrawer(backup) {
|
|
|
929
953
|
if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
|
|
930
954
|
if (backup.path) fields.push({ key: 'drawer.field.path', val: backup.path });
|
|
931
955
|
if (backup.message) fields.push({ key: 'drawer.field.message', val: backup.message });
|
|
932
|
-
if (backup.summary) fields.push({ key: 'drawer.field.summary', val: backup.summary });
|
|
956
|
+
if (backup.summary) fields.push({ key: 'drawer.field.summary', val: translateSummary(backup.summary) });
|
|
933
957
|
|
|
934
958
|
const refText = backup.ref || backup.shortHash || backup.timestamp || '';
|
|
935
959
|
const jsonText = JSON.stringify(backup, null, 2);
|
|
@@ -262,8 +262,37 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
262
262
|
if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
|
|
263
263
|
const context = { trigger: 'auto', changedFileCount };
|
|
264
264
|
if (porcelain) {
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
let pLines = porcelain.split('\n').filter(Boolean);
|
|
266
|
+
if (cfg.protect.length > 0 || cfg.ignore.length > 0) {
|
|
267
|
+
pLines = pLines.filter(line => {
|
|
268
|
+
const filePart = line.substring(3);
|
|
269
|
+
const arrowIdx = filePart.indexOf(' -> ');
|
|
270
|
+
const raw = arrowIdx >= 0 ? filePart.substring(arrowIdx + 4) : filePart;
|
|
271
|
+
const rel = unquoteGitPath(raw);
|
|
272
|
+
const fakeFile = { rel, full: path.join(projectDir, rel) };
|
|
273
|
+
return filterFiles([fakeFile], cfg).length > 0;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (pLines.length > 0) {
|
|
277
|
+
const groups = { M: [], A: [], D: [], R: [] };
|
|
278
|
+
for (const l of pLines) {
|
|
279
|
+
const code = l.substring(0, 2).trim();
|
|
280
|
+
const filePart = l.substring(3);
|
|
281
|
+
const arrowIdx = filePart.indexOf(' -> ');
|
|
282
|
+
const file = arrowIdx >= 0 ? filePart.substring(arrowIdx + 4) : filePart;
|
|
283
|
+
const key = code.startsWith('R') ? 'R'
|
|
284
|
+
: code.includes('D') ? 'D'
|
|
285
|
+
: code.includes('A') || code === '??' ? 'A'
|
|
286
|
+
: 'M';
|
|
287
|
+
groups[key].push(file);
|
|
288
|
+
}
|
|
289
|
+
const parts = [];
|
|
290
|
+
if (groups.M.length) parts.push(`Modified ${groups.M.length}: ${groups.M.slice(0, 5).join(', ')}`);
|
|
291
|
+
if (groups.A.length) parts.push(`Added ${groups.A.length}: ${groups.A.slice(0, 5).join(', ')}`);
|
|
292
|
+
if (groups.D.length) parts.push(`Deleted ${groups.D.length}: ${groups.D.slice(0, 5).join(', ')}`);
|
|
293
|
+
if (groups.R.length) parts.push(`Renamed ${groups.R.length}: ${groups.R.slice(0, 5).join(', ')}`);
|
|
294
|
+
context.summary = parts.join('; ');
|
|
295
|
+
}
|
|
267
296
|
}
|
|
268
297
|
const snapResult = createGitSnapshot(projectDir, cfg, { branchRef, context });
|
|
269
298
|
if (snapResult.status === 'created') {
|
|
@@ -21,7 +21,7 @@ function validateRelativePath(file) {
|
|
|
21
21
|
const VALID_SHADOW_SOURCE = /^\d{8}_\d{6}(_\d{3})?$|^pre-restore-\d{8}_\d{6}(_\d{3})?$/;
|
|
22
22
|
|
|
23
23
|
const TOOL_DIRS = ['.cursor/', '.cursor\\'];
|
|
24
|
-
const GUARD_CONFIGS = ['.cursor-guard.json'];
|
|
24
|
+
const GUARD_CONFIGS = ['.cursor-guard.json', '.gitignore'];
|
|
25
25
|
|
|
26
26
|
function isToolPath(filePath) {
|
|
27
27
|
const normalized = filePath.replace(/\\/g, '/');
|
|
@@ -94,9 +94,11 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
94
94
|
if (!gDir) return { status: 'error', error: 'not a git repository' };
|
|
95
95
|
|
|
96
96
|
const guardIndex = path.join(gDir, 'cursor-guard-index');
|
|
97
|
+
const guardIndexLock = guardIndex + '.lock';
|
|
97
98
|
const env = { ...process.env, GIT_INDEX_FILE: guardIndex };
|
|
98
99
|
|
|
99
100
|
try { fs.unlinkSync(guardIndex); } catch { /* doesn't exist */ }
|
|
101
|
+
try { fs.unlinkSync(guardIndexLock); } catch { /* doesn't exist */ }
|
|
100
102
|
|
|
101
103
|
try {
|
|
102
104
|
const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
|
|
@@ -164,6 +166,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
164
166
|
return { status: 'error', error: e.message };
|
|
165
167
|
} finally {
|
|
166
168
|
try { fs.unlinkSync(guardIndex); } catch { /* ignore */ }
|
|
169
|
+
try { fs.unlinkSync(guardIndexLock); } catch { /* ignore */ }
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|