cursor-guard 4.2.2 → 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 +9 -0
- package/package.json +1 -1
- package/references/dashboard/public/app.js +55 -0
- package/references/dashboard/public/style.css +8 -3
- package/references/lib/auto-backup.js +36 -2
- package/references/lib/core/backups.js +44 -13
- package/references/lib/core/restore.js +4 -2
- package/references/lib/core/snapshot.js +26 -1
- package/references/mcp/server.js +1 -0
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
|
+
"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",
|
|
@@ -88,6 +88,18 @@ const I18N = {
|
|
|
88
88
|
'drawer.field.hash': 'Commit Hash',
|
|
89
89
|
'drawer.field.path': 'Path',
|
|
90
90
|
'drawer.field.message': 'Message',
|
|
91
|
+
'drawer.field.filesChanged': 'Files Changed',
|
|
92
|
+
'drawer.field.summary': 'Change Summary',
|
|
93
|
+
'drawer.field.trigger': 'Trigger',
|
|
94
|
+
'trigger.auto': 'Auto (scheduled)',
|
|
95
|
+
'trigger.manual': 'Manual (agent)',
|
|
96
|
+
'trigger.pre-restore': 'Pre-Restore',
|
|
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',
|
|
91
103
|
|
|
92
104
|
'error.fetchFailed': 'Failed to fetch data',
|
|
93
105
|
'error.sectionFailed': 'This section failed to load',
|
|
@@ -254,6 +266,18 @@ const I18N = {
|
|
|
254
266
|
'drawer.field.hash': '提交 Hash',
|
|
255
267
|
'drawer.field.path': '路径',
|
|
256
268
|
'drawer.field.message': '消息',
|
|
269
|
+
'drawer.field.filesChanged': '变更文件数',
|
|
270
|
+
'drawer.field.summary': '变更摘要',
|
|
271
|
+
'drawer.field.trigger': '触发方式',
|
|
272
|
+
'trigger.auto': '自动(定时)',
|
|
273
|
+
'trigger.manual': '手动(Agent)',
|
|
274
|
+
'trigger.pre-restore': '恢复前快照',
|
|
275
|
+
'backups.col.summary': '变更',
|
|
276
|
+
'summary.modified': '修改',
|
|
277
|
+
'summary.added': '新增',
|
|
278
|
+
'summary.deleted': '删除',
|
|
279
|
+
'summary.renamed': '重命名',
|
|
280
|
+
'summary.files': '个文件',
|
|
257
281
|
|
|
258
282
|
'error.fetchFailed': '数据拉取失败',
|
|
259
283
|
'error.sectionFailed': '此区块加载失败',
|
|
@@ -779,6 +803,31 @@ function renderFilterBar() {
|
|
|
779
803
|
).join('');
|
|
780
804
|
}
|
|
781
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
|
+
|
|
815
|
+
function formatSummaryCell(b) {
|
|
816
|
+
const parts = [];
|
|
817
|
+
if (b.filesChanged != null) parts.push(`<span class="text-sm">${b.filesChanged} ${t('summary.files')}</span>`);
|
|
818
|
+
if (b.trigger) parts.push(`<span class="badge badge-trigger">${t('trigger.' + b.trigger)}</span>`);
|
|
819
|
+
if (b.summary) {
|
|
820
|
+
const translated = translateSummary(b.summary);
|
|
821
|
+
const short = translated.length > 80 ? translated.substring(0, 77) + '...' : translated;
|
|
822
|
+
parts.push(`<span class="text-muted text-sm">${esc(short)}</span>`);
|
|
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
|
+
}
|
|
828
|
+
return parts.length > 0 ? parts.join(' ') : '<span class="text-muted text-sm">-</span>';
|
|
829
|
+
}
|
|
830
|
+
|
|
782
831
|
function renderBackupTable(backups) {
|
|
783
832
|
if (!Array.isArray(backups)) {
|
|
784
833
|
$('#backup-table-wrap').innerHTML = `<div class="error-panel">${t('error.sectionFailed')}</div>`;
|
|
@@ -795,10 +844,12 @@ function renderBackupTable(backups) {
|
|
|
795
844
|
|
|
796
845
|
const rows = state.filteredBackups.map((b, i) => {
|
|
797
846
|
const badgeClass = b.type.startsWith('git') ? (b.type.includes('pre') ? 'badge-pre' : 'badge-git') : (b.type.includes('pre') ? 'badge-pre' : 'badge-shadow');
|
|
847
|
+
const summaryCell = formatSummaryCell(b);
|
|
798
848
|
return `<tr data-bi="${i}">
|
|
799
849
|
<td><div>${esc(formatTime(b.timestamp))}</div><div class="text-muted text-sm">${esc(relativeTime(b.timestamp))}</div></td>
|
|
800
850
|
<td><span class="badge ${badgeClass}">${t('type.' + b.type)}</span></td>
|
|
801
851
|
<td class="text-mono">${esc(b.shortHash || b.timestamp || '-')}</td>
|
|
852
|
+
<td class="backup-summary-cell">${summaryCell}</td>
|
|
802
853
|
</tr>`;
|
|
803
854
|
}).join('');
|
|
804
855
|
|
|
@@ -808,6 +859,7 @@ function renderBackupTable(backups) {
|
|
|
808
859
|
<th>${t('backups.col.time')}</th>
|
|
809
860
|
<th>${t('backups.col.type')}</th>
|
|
810
861
|
<th>${t('backups.col.ref')}</th>
|
|
862
|
+
<th>${t('backups.col.summary')}</th>
|
|
811
863
|
</tr></thead>
|
|
812
864
|
<tbody>${rows}</tbody>
|
|
813
865
|
</table>
|
|
@@ -895,10 +947,13 @@ function openRestoreDrawer(backup) {
|
|
|
895
947
|
{ key: 'drawer.field.time', val: formatTime(backup.timestamp) },
|
|
896
948
|
{ key: 'drawer.field.type', val: t('type.' + backup.type) },
|
|
897
949
|
];
|
|
950
|
+
if (backup.trigger) fields.push({ key: 'drawer.field.trigger', val: t('trigger.' + backup.trigger) });
|
|
951
|
+
if (backup.filesChanged != null) fields.push({ key: 'drawer.field.filesChanged', val: String(backup.filesChanged) });
|
|
898
952
|
if (backup.ref) fields.push({ key: 'drawer.field.ref', val: backup.ref });
|
|
899
953
|
if (backup.commitHash) fields.push({ key: 'drawer.field.hash', val: backup.commitHash });
|
|
900
954
|
if (backup.path) fields.push({ key: 'drawer.field.path', val: backup.path });
|
|
901
955
|
if (backup.message) fields.push({ key: 'drawer.field.message', val: backup.message });
|
|
956
|
+
if (backup.summary) fields.push({ key: 'drawer.field.summary', val: translateSummary(backup.summary) });
|
|
902
957
|
|
|
903
958
|
const refText = backup.ref || backup.shortHash || backup.timestamp || '';
|
|
904
959
|
const jsonText = JSON.stringify(backup, null, 2);
|
|
@@ -244,9 +244,14 @@ main {
|
|
|
244
244
|
.badge-warn { background: var(--yellow-bg); color: var(--yellow); }
|
|
245
245
|
.badge-fail { background: var(--red-bg); color: var(--red); }
|
|
246
246
|
|
|
247
|
-
.badge-git
|
|
248
|
-
.badge-shadow
|
|
249
|
-
.badge-pre
|
|
247
|
+
.badge-git { background: var(--blue-bg); color: var(--blue); }
|
|
248
|
+
.badge-shadow { background: var(--amber-bg); color: var(--amber); }
|
|
249
|
+
.badge-pre { background: var(--purple-bg); color: var(--purple); }
|
|
250
|
+
.badge-trigger { background: var(--bg-tertiary); color: var(--text-secondary); font-size: 0.7rem; }
|
|
251
|
+
|
|
252
|
+
.backup-summary-cell { max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
253
|
+
.backup-summary-cell .badge-trigger { margin-right: 4px; }
|
|
254
|
+
.backup-summary-cell .text-sm { font-size: 0.75rem; }
|
|
250
255
|
|
|
251
256
|
/* ── Stats Row ────────────────────────────────────────────── */
|
|
252
257
|
|
|
@@ -210,10 +210,10 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
210
210
|
|
|
211
211
|
// V4: Record change event and check for anomalies
|
|
212
212
|
let changedFileCount = 0;
|
|
213
|
+
let porcelain = '';
|
|
213
214
|
if (repo) {
|
|
214
215
|
// Use execFileSync directly — git() helper's trim() strips leading spaces
|
|
215
216
|
// from porcelain output, corrupting the first line when it starts with ' '.
|
|
216
|
-
let porcelain = '';
|
|
217
217
|
try {
|
|
218
218
|
porcelain = execFileSync('git', ['status', '--porcelain'], {
|
|
219
219
|
cwd: projectDir, stdio: 'pipe', encoding: 'utf-8',
|
|
@@ -260,7 +260,41 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
260
260
|
|
|
261
261
|
// Git snapshot via Core
|
|
262
262
|
if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
|
|
263
|
-
const
|
|
263
|
+
const context = { trigger: 'auto', changedFileCount };
|
|
264
|
+
if (porcelain) {
|
|
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
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const snapResult = createGitSnapshot(projectDir, cfg, { branchRef, context });
|
|
264
298
|
if (snapResult.status === 'created') {
|
|
265
299
|
let msg = `Git snapshot ${snapResult.shortHash} (${snapResult.fileCount} files)`;
|
|
266
300
|
if (snapResult.secretsExcluded) {
|
|
@@ -41,6 +41,20 @@ function entryToMs(entry) {
|
|
|
41
41
|
return d ? d.getTime() : 0;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function parseCommitTrailers(body) {
|
|
45
|
+
if (!body) return {};
|
|
46
|
+
const result = {};
|
|
47
|
+
for (const line of body.split('\n')) {
|
|
48
|
+
const m = line.match(/^(Files-Changed|Summary|Trigger):\s*(.+)$/);
|
|
49
|
+
if (m) {
|
|
50
|
+
const key = m[1] === 'Files-Changed' ? 'filesChanged'
|
|
51
|
+
: m[1] === 'Summary' ? 'summary' : 'trigger';
|
|
52
|
+
result[key] = key === 'filesChanged' ? parseInt(m[2], 10) : m[2];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// ── List backups ────────────────────────────────────────────────
|
|
45
59
|
|
|
46
60
|
/**
|
|
@@ -52,7 +66,7 @@ function entryToMs(entry) {
|
|
|
52
66
|
* @param {string} [opts.file] - Filter to commits touching this relative path
|
|
53
67
|
* @param {string} [opts.before] - Time boundary (e.g. '10 minutes ago', ISO string)
|
|
54
68
|
* @param {number} [opts.limit=20] - Max total results
|
|
55
|
-
* @returns {{ sources: Array<{type: string, ref?: string, commitHash?: string, shortHash?: string, timestamp?: string, message?: string, path?: string}> }}
|
|
69
|
+
* @returns {{ sources: Array<{type: string, ref?: string, commitHash?: string, shortHash?: string, timestamp?: string, message?: string, path?: string, filesChanged?: number, summary?: string, trigger?: string}> }}
|
|
56
70
|
*/
|
|
57
71
|
function listBackups(projectDir, opts = {}) {
|
|
58
72
|
const limit = opts.limit || 20;
|
|
@@ -74,24 +88,27 @@ function listBackups(projectDir, opts = {}) {
|
|
|
74
88
|
const autoRef = 'refs/guard/auto-backup';
|
|
75
89
|
const autoExists = git(['rev-parse', '--verify', autoRef], { cwd: projectDir, allowFail: true });
|
|
76
90
|
if (autoExists) {
|
|
77
|
-
const logArgs = ['log', autoRef,
|
|
91
|
+
const logArgs = ['log', autoRef, '--format=%H\x1f%aI\x1f%B\x1e', `-${limit}`, '--grep=^guard:'];
|
|
78
92
|
if (opts.before) logArgs.push(`--before=${opts.before}`);
|
|
79
93
|
if (opts.file) logArgs.push('--', opts.file);
|
|
80
94
|
const out = git(logArgs, { cwd: projectDir, allowFail: true });
|
|
81
95
|
if (out) {
|
|
82
|
-
for (const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
const hash =
|
|
86
|
-
const timestamp =
|
|
87
|
-
const
|
|
96
|
+
for (const record of out.split('\x1e').filter(r => r.trim())) {
|
|
97
|
+
const parts = record.split('\x1f');
|
|
98
|
+
if (parts.length < 3) continue;
|
|
99
|
+
const hash = parts[0].trim();
|
|
100
|
+
const timestamp = parts[1];
|
|
101
|
+
const body = parts[2];
|
|
102
|
+
const subject = body.split('\n')[0];
|
|
103
|
+
const trailers = parseCommitTrailers(body);
|
|
88
104
|
sources.push({
|
|
89
105
|
type: 'git-auto-backup',
|
|
90
106
|
ref: autoRef,
|
|
91
107
|
commitHash: hash,
|
|
92
108
|
shortHash: hash.substring(0, 7),
|
|
93
109
|
timestamp,
|
|
94
|
-
message,
|
|
110
|
+
message: subject,
|
|
111
|
+
...trailers,
|
|
95
112
|
});
|
|
96
113
|
}
|
|
97
114
|
}
|
|
@@ -112,20 +129,32 @@ function listBackups(projectDir, opts = {}) {
|
|
|
112
129
|
const ms = Date.parse(timestamp);
|
|
113
130
|
if (!isNaN(ms) && ms > beforeDate.getTime()) continue;
|
|
114
131
|
}
|
|
115
|
-
|
|
132
|
+
const entry = {
|
|
116
133
|
type: 'git-pre-restore',
|
|
117
134
|
ref,
|
|
118
135
|
commitHash: hash,
|
|
119
136
|
shortHash: hash.substring(0, 7),
|
|
120
137
|
timestamp,
|
|
121
|
-
}
|
|
138
|
+
};
|
|
139
|
+
const prBody = git(['log', '-1', '--format=%B', hash], { cwd: projectDir, allowFail: true });
|
|
140
|
+
if (prBody) {
|
|
141
|
+
const prSubject = prBody.split('\n')[0];
|
|
142
|
+
if (prSubject) entry.message = prSubject;
|
|
143
|
+
Object.assign(entry, parseCommitTrailers(prBody));
|
|
144
|
+
}
|
|
145
|
+
sources.push(entry);
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
|
|
125
149
|
// Agent snapshot ref
|
|
126
150
|
const snapshotHash = git(['rev-parse', '--verify', 'refs/guard/snapshot'], { cwd: projectDir, allowFail: true });
|
|
127
151
|
if (snapshotHash) {
|
|
128
|
-
const
|
|
152
|
+
const snapLog = git(['log', '-1', '--format=%aI\x1f%B', 'refs/guard/snapshot'], { cwd: projectDir, allowFail: true });
|
|
153
|
+
const snapParts = snapLog ? snapLog.split('\x1f') : [];
|
|
154
|
+
const ts = snapParts[0] || null;
|
|
155
|
+
const snapBody = snapParts[1] || '';
|
|
156
|
+
const snapTrailers = parseCommitTrailers(snapBody);
|
|
157
|
+
const snapSubject = snapBody.split('\n')[0] || '';
|
|
129
158
|
const include = !beforeDate || (ts && Date.parse(ts) <= beforeDate.getTime());
|
|
130
159
|
if (include) {
|
|
131
160
|
sources.push({
|
|
@@ -133,7 +162,9 @@ function listBackups(projectDir, opts = {}) {
|
|
|
133
162
|
ref: 'refs/guard/snapshot',
|
|
134
163
|
commitHash: snapshotHash,
|
|
135
164
|
shortHash: snapshotHash.substring(0, 7),
|
|
136
|
-
timestamp: ts
|
|
165
|
+
timestamp: ts,
|
|
166
|
+
message: snapSubject || undefined,
|
|
167
|
+
...snapTrailers,
|
|
137
168
|
});
|
|
138
169
|
}
|
|
139
170
|
}
|
|
@@ -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, '/');
|
|
@@ -370,8 +370,10 @@ function createPreRestoreSnapshot(projectDir, scope) {
|
|
|
370
370
|
return { status: 'skipped', reason: 'no changes to preserve' };
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
+
let msg = `guard: pre-restore snapshot ${ts}`;
|
|
374
|
+
msg += '\n\nTrigger: pre-restore';
|
|
373
375
|
const commitHash = execFileSync('git', [
|
|
374
|
-
'commit-tree', tree, '-p', head, '-m',
|
|
376
|
+
'commit-tree', tree, '-p', head, '-m', msg,
|
|
375
377
|
], { cwd, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
376
378
|
|
|
377
379
|
if (!commitHash) return { status: 'error', error: 'commit-tree returned empty' };
|
|
@@ -52,6 +52,24 @@ function removeSecretsFromIndex(secretsPatterns, cwd, env) {
|
|
|
52
52
|
return excluded;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// ── Commit message builder ──────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function buildCommitMessage(ts, opts) {
|
|
58
|
+
if (opts.message && !opts.context) return opts.message;
|
|
59
|
+
|
|
60
|
+
const ctx = opts.context || {};
|
|
61
|
+
const countTag = ctx.changedFileCount ? ` (${ctx.changedFileCount} files)` : '';
|
|
62
|
+
const subject = opts.message || `guard: auto-backup ${ts}${countTag}`;
|
|
63
|
+
|
|
64
|
+
const trailers = [];
|
|
65
|
+
if (ctx.changedFileCount != null) trailers.push(`Files-Changed: ${ctx.changedFileCount}`);
|
|
66
|
+
if (ctx.summary) trailers.push(`Summary: ${ctx.summary}`);
|
|
67
|
+
if (ctx.trigger) trailers.push(`Trigger: ${ctx.trigger}`);
|
|
68
|
+
|
|
69
|
+
if (trailers.length === 0) return subject;
|
|
70
|
+
return subject + '\n\n' + trailers.join('\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
// ── Git snapshot ────────────────────────────────────────────────
|
|
56
74
|
|
|
57
75
|
/**
|
|
@@ -63,6 +81,10 @@ function removeSecretsFromIndex(secretsPatterns, cwd, env) {
|
|
|
63
81
|
* @param {object} [opts]
|
|
64
82
|
* @param {string} [opts.branchRef='refs/guard/auto-backup']
|
|
65
83
|
* @param {string} [opts.message] - Commit message (auto-generated if omitted)
|
|
84
|
+
* @param {object} [opts.context] - Backup context metadata
|
|
85
|
+
* @param {string} [opts.context.trigger] - 'auto' | 'manual' | 'pre-restore'
|
|
86
|
+
* @param {number} [opts.context.changedFileCount] - Number of changed files
|
|
87
|
+
* @param {string} [opts.context.summary] - Short change summary (e.g. "M src/app.js, A new.ts")
|
|
66
88
|
* @returns {{ status: 'created'|'skipped'|'error', commitHash?: string, shortHash?: string, fileCount?: number, reason?: string, error?: string, secretsExcluded?: string[] }}
|
|
67
89
|
*/
|
|
68
90
|
function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
@@ -72,9 +94,11 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
72
94
|
if (!gDir) return { status: 'error', error: 'not a git repository' };
|
|
73
95
|
|
|
74
96
|
const guardIndex = path.join(gDir, 'cursor-guard-index');
|
|
97
|
+
const guardIndexLock = guardIndex + '.lock';
|
|
75
98
|
const env = { ...process.env, GIT_INDEX_FILE: guardIndex };
|
|
76
99
|
|
|
77
100
|
try { fs.unlinkSync(guardIndex); } catch { /* doesn't exist */ }
|
|
101
|
+
try { fs.unlinkSync(guardIndexLock); } catch { /* doesn't exist */ }
|
|
78
102
|
|
|
79
103
|
try {
|
|
80
104
|
const parentHash = git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
|
|
@@ -109,7 +133,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
const ts = formatTimestamp(new Date());
|
|
112
|
-
const msg = opts
|
|
136
|
+
const msg = buildCommitMessage(ts, opts);
|
|
113
137
|
const commitArgs = parentHash
|
|
114
138
|
? ['commit-tree', newTree, '-p', parentHash, '-m', msg]
|
|
115
139
|
: ['commit-tree', newTree, '-m', msg];
|
|
@@ -142,6 +166,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
142
166
|
return { status: 'error', error: e.message };
|
|
143
167
|
} finally {
|
|
144
168
|
try { fs.unlinkSync(guardIndex); } catch { /* ignore */ }
|
|
169
|
+
try { fs.unlinkSync(guardIndexLock); } catch { /* ignore */ }
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
172
|
|
package/references/mcp/server.js
CHANGED