cursor-guard 4.9.9 → 4.9.12
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/ROADMAP.md +43 -5
- package/package.json +69 -69
- package/references/dashboard/public/app.js +234 -16
- package/references/dashboard/public/style.css +56 -1
- package/references/dashboard/server.js +197 -4
- package/references/lib/core/backups.js +36 -21
- package/references/lib/core/core.test.js +1723 -1616
- package/references/lib/core/snapshot.js +51 -4
- package/references/vscode-extension/{dist/cursor-guard-ide-4.9.9.vsix → cursor-guard-ide-4.9.12.vsix} +0 -0
- package/references/vscode-extension/dist/cursor-guard-ide-4.9.12.vsix +0 -0
- package/references/vscode-extension/dist/dashboard/public/app.js +234 -16
- package/references/vscode-extension/dist/dashboard/public/style.css +56 -1
- package/references/vscode-extension/dist/dashboard/server.js +197 -4
- package/references/vscode-extension/dist/guard-version.json +1 -1
- package/references/vscode-extension/dist/lib/core/backups.js +36 -21
- package/references/vscode-extension/dist/lib/core/snapshot.js +51 -4
- 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 +77 -26
- package/references/vscode-extension/dist/package.json +1 -1
- package/references/vscode-extension/dist/skill/ROADMAP.md +43 -5
- 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 +140 -140
|
@@ -14,6 +14,7 @@ function coreDeps() {
|
|
|
14
14
|
runDiagnostics: require('../lib/core/doctor').runDiagnostics,
|
|
15
15
|
listBackups: require('../lib/core/backups').listBackups,
|
|
16
16
|
getBackupFiles: require('../lib/core/backups').getBackupFiles,
|
|
17
|
+
clearAlert: require('../lib/core/anomaly').clearAlert,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -196,7 +197,21 @@ function handleApi(pathname, query, registry, res, req) {
|
|
|
196
197
|
return json(res, { error: `Project directory not accessible: ${project.pathLabel}` }, 500);
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
const { getDashboard, runDiagnostics, listBackups, getBackupFiles } = coreDeps();
|
|
200
|
+
const { getDashboard, runDiagnostics, listBackups, getBackupFiles, clearAlert } = coreDeps();
|
|
201
|
+
|
|
202
|
+
if (pathname === '/api/dismiss-alert') {
|
|
203
|
+
if (req.method !== 'POST') {
|
|
204
|
+
res.writeHead(405);
|
|
205
|
+
return res.end('Method Not Allowed');
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
clearAlert(pp);
|
|
209
|
+
if (_instance?.hub && id) broadcastGuardChanged(_instance.hub, id);
|
|
210
|
+
return json(res, { ok: true });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
return json(res, { ok: false, error: e.message }, 500);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
200
215
|
|
|
201
216
|
if (pathname === '/api/page-data') {
|
|
202
217
|
const scope = query.get('scope');
|
|
@@ -245,6 +260,164 @@ function handleApi(pathname, query, registry, res, req) {
|
|
|
245
260
|
return notFound(res);
|
|
246
261
|
}
|
|
247
262
|
|
|
263
|
+
/* ── Live push (SSE) + fs.watch on Guard refs / backups ─────── */
|
|
264
|
+
|
|
265
|
+
function broadcastGuardChanged(hub, projectId) {
|
|
266
|
+
if (!hub?.sseClients?.size) return;
|
|
267
|
+
const line = `data: ${JSON.stringify({ type: 'guard-changed', projectId })}\n\n`;
|
|
268
|
+
for (const c of hub.sseClients) {
|
|
269
|
+
try {
|
|
270
|
+
if (c.projectId && c.projectId !== projectId) continue;
|
|
271
|
+
c.res.write(line);
|
|
272
|
+
} catch {
|
|
273
|
+
hub.sseClients.delete(c);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function teardownGuardWatchersOnly(hub) {
|
|
279
|
+
if (hub._reattachTimer) {
|
|
280
|
+
clearTimeout(hub._reattachTimer);
|
|
281
|
+
hub._reattachTimer = null;
|
|
282
|
+
}
|
|
283
|
+
if (hub.guardDebounce) {
|
|
284
|
+
for (const t of hub.guardDebounce.values()) clearTimeout(t);
|
|
285
|
+
hub.guardDebounce.clear();
|
|
286
|
+
}
|
|
287
|
+
for (const w of hub.guardWatchers) {
|
|
288
|
+
try { w.close(); } catch { /* ignore */ }
|
|
289
|
+
}
|
|
290
|
+
hub.guardWatchers = [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function tryWatchDirRecursive(dir, cb) {
|
|
294
|
+
try {
|
|
295
|
+
return fs.watch(dir, { recursive: true }, cb);
|
|
296
|
+
} catch {
|
|
297
|
+
try {
|
|
298
|
+
return fs.watch(dir, cb);
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* When Git refs under refs/guard/, shadow backup dir, or alert/lock change, notify SSE clients.
|
|
307
|
+
*/
|
|
308
|
+
function attachGuardWatchers(hub) {
|
|
309
|
+
const { isGitRepo, gitDir: getGitDir } = require('../lib/utils');
|
|
310
|
+
teardownGuardWatchersOnly(hub);
|
|
311
|
+
if (!hub.guardDebounce) hub.guardDebounce = new Map();
|
|
312
|
+
|
|
313
|
+
function schedule(projectId) {
|
|
314
|
+
const prev = hub.guardDebounce.get(projectId);
|
|
315
|
+
if (prev) clearTimeout(prev);
|
|
316
|
+
hub.guardDebounce.set(projectId, setTimeout(() => {
|
|
317
|
+
hub.guardDebounce.delete(projectId);
|
|
318
|
+
broadcastGuardChanged(hub, projectId);
|
|
319
|
+
}, 400));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const proj of hub.registry.values()) {
|
|
323
|
+
const pp = proj._path;
|
|
324
|
+
const pid = proj.id;
|
|
325
|
+
|
|
326
|
+
if (isGitRepo(pp)) {
|
|
327
|
+
const gDir = getGitDir(pp);
|
|
328
|
+
if (gDir && fs.existsSync(gDir)) {
|
|
329
|
+
try {
|
|
330
|
+
const w = fs.watch(gDir, (ev, fname) => {
|
|
331
|
+
if (fname === 'cursor-guard-alert.json' || fname === 'cursor-guard.lock') schedule(pid);
|
|
332
|
+
});
|
|
333
|
+
hub.guardWatchers.push(w);
|
|
334
|
+
} catch { /* ignore */ }
|
|
335
|
+
|
|
336
|
+
const refsDir = path.join(gDir, 'refs');
|
|
337
|
+
if (fs.existsSync(refsDir)) {
|
|
338
|
+
try {
|
|
339
|
+
const w = fs.watch(refsDir, (ev, fname) => {
|
|
340
|
+
if (!fname) return;
|
|
341
|
+
if (fname === 'guard' || fname.startsWith('guard')) {
|
|
342
|
+
schedule(pid);
|
|
343
|
+
if (!hub._reattachTimer) {
|
|
344
|
+
hub._reattachTimer = setTimeout(() => {
|
|
345
|
+
hub._reattachTimer = null;
|
|
346
|
+
attachGuardWatchers(hub);
|
|
347
|
+
}, 600);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
hub.guardWatchers.push(w);
|
|
352
|
+
} catch { /* ignore */ }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const guardDir = path.join(gDir, 'refs', 'guard');
|
|
356
|
+
if (fs.existsSync(guardDir)) {
|
|
357
|
+
const w = tryWatchDirRecursive(guardDir, () => schedule(pid));
|
|
358
|
+
if (w) hub.guardWatchers.push(w);
|
|
359
|
+
const preRestoreDir = path.join(guardDir, 'pre-restore');
|
|
360
|
+
if (fs.existsSync(preRestoreDir)) {
|
|
361
|
+
const w2 = tryWatchDirRecursive(preRestoreDir, () => schedule(pid));
|
|
362
|
+
if (w2) hub.guardWatchers.push(w2);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const backupDir = path.join(pp, '.cursor-guard-backup');
|
|
369
|
+
if (fs.existsSync(backupDir)) {
|
|
370
|
+
const w = tryWatchDirRecursive(backupDir, () => schedule(pid));
|
|
371
|
+
if (w) hub.guardWatchers.push(w);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function teardownLivePush(hub) {
|
|
377
|
+
teardownGuardWatchersOnly(hub);
|
|
378
|
+
for (const c of hub.sseClients) {
|
|
379
|
+
try { c.res.end(); } catch { /* ignore */ }
|
|
380
|
+
}
|
|
381
|
+
hub.sseClients.clear();
|
|
382
|
+
if (hub.ssePingTimer) {
|
|
383
|
+
clearInterval(hub.ssePingTimer);
|
|
384
|
+
hub.ssePingTimer = null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function openSseStream(query, hub, res, req) {
|
|
389
|
+
const subscribeId = query.get('id') || null;
|
|
390
|
+
res.writeHead(200, {
|
|
391
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
392
|
+
'Cache-Control': 'no-store, no-cache',
|
|
393
|
+
'Connection': 'keep-alive',
|
|
394
|
+
'X-Accel-Buffering': 'no',
|
|
395
|
+
'Access-Control-Allow-Origin': '*',
|
|
396
|
+
});
|
|
397
|
+
res.write(': connected\n\n');
|
|
398
|
+
const client = { res, projectId: subscribeId };
|
|
399
|
+
hub.sseClients.add(client);
|
|
400
|
+
req.on('close', () => {
|
|
401
|
+
hub.sseClients.delete(client);
|
|
402
|
+
if (hub.sseClients.size === 0 && hub.ssePingTimer) {
|
|
403
|
+
clearInterval(hub.ssePingTimer);
|
|
404
|
+
hub.ssePingTimer = null;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
if (!hub.ssePingTimer) {
|
|
408
|
+
hub.ssePingTimer = setInterval(() => {
|
|
409
|
+
if (hub.sseClients.size === 0) return;
|
|
410
|
+
for (const c of hub.sseClients) {
|
|
411
|
+
try {
|
|
412
|
+
c.res.write(': ping\n\n');
|
|
413
|
+
} catch {
|
|
414
|
+
hub.sseClients.delete(c);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}, 25000);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
248
421
|
/* ── Server (singleton) ─────────────────────────────────────── */
|
|
249
422
|
|
|
250
423
|
let _instance = null;
|
|
@@ -280,8 +453,11 @@ function _mergeProjects(registry, paths) {
|
|
|
280
453
|
function startDashboardServer(paths, opts = {}) {
|
|
281
454
|
if (_instance) {
|
|
282
455
|
const added = _mergeProjects(_instance.registry, paths);
|
|
283
|
-
if (added > 0
|
|
284
|
-
|
|
456
|
+
if (added > 0) {
|
|
457
|
+
attachGuardWatchers(_instance.hub);
|
|
458
|
+
if (!opts.silent) {
|
|
459
|
+
console.log(` [dashboard] Hot-added ${added} project(s) — total: ${_instance.registry.size} on port ${_instance.port}`);
|
|
460
|
+
}
|
|
285
461
|
}
|
|
286
462
|
return Promise.resolve(_instance);
|
|
287
463
|
}
|
|
@@ -290,6 +466,14 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
290
466
|
const silent = opts.silent || false;
|
|
291
467
|
const registry = buildRegistry(paths);
|
|
292
468
|
const token = crypto.randomBytes(16).toString('hex');
|
|
469
|
+
const hub = {
|
|
470
|
+
registry,
|
|
471
|
+
token,
|
|
472
|
+
sseClients: new Set(),
|
|
473
|
+
guardWatchers: [],
|
|
474
|
+
guardDebounce: new Map(),
|
|
475
|
+
ssePingTimer: null,
|
|
476
|
+
};
|
|
293
477
|
|
|
294
478
|
return new Promise((resolve, reject) => {
|
|
295
479
|
let currentPort = port;
|
|
@@ -326,6 +510,13 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
326
510
|
res.writeHead(403);
|
|
327
511
|
return res.end('Forbidden: invalid token');
|
|
328
512
|
}
|
|
513
|
+
if (parsed.pathname === '/api/events') {
|
|
514
|
+
if (req.method !== 'GET') {
|
|
515
|
+
res.writeHead(405);
|
|
516
|
+
return res.end('Method Not Allowed');
|
|
517
|
+
}
|
|
518
|
+
return openSseStream(parsed.searchParams, hub, res, req);
|
|
519
|
+
}
|
|
329
520
|
handleApi(parsed.pathname, parsed.searchParams, registry, res, req);
|
|
330
521
|
} else {
|
|
331
522
|
if (req.method !== 'GET') { res.writeHead(405); return res.end('Method Not Allowed'); }
|
|
@@ -345,7 +536,8 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
345
536
|
|
|
346
537
|
server.on('listening', () => {
|
|
347
538
|
const addr = server.address();
|
|
348
|
-
_instance = { server, port: addr.port, registry, token };
|
|
539
|
+
_instance = { server, port: addr.port, registry, token, hub };
|
|
540
|
+
attachGuardWatchers(hub);
|
|
349
541
|
if (!silent) {
|
|
350
542
|
console.log('');
|
|
351
543
|
console.log(' Cursor Guard Dashboard');
|
|
@@ -371,6 +563,7 @@ async function restartDashboard() {
|
|
|
371
563
|
const paths = [..._instance.registry.values()].map(p => p._path);
|
|
372
564
|
const port = _instance.port;
|
|
373
565
|
|
|
566
|
+
if (_instance.hub) teardownLivePush(_instance.hub);
|
|
374
567
|
_instance.server.close();
|
|
375
568
|
_instance = null;
|
|
376
569
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.9.
|
|
1
|
+
{"version":"4.9.12"}
|
|
@@ -51,6 +51,8 @@ const TRAILER_MAP = {
|
|
|
51
51
|
'From': { key: 'from' },
|
|
52
52
|
'Restore-To': { key: 'restoreTo' },
|
|
53
53
|
'File': { key: 'restoreFile' },
|
|
54
|
+
'Guard-Diff-Base': { key: 'guardDiffBase' },
|
|
55
|
+
'Guard-Scope': { key: 'guardScope' },
|
|
54
56
|
};
|
|
55
57
|
|
|
56
58
|
function parseCommitTrailers(body) {
|
|
@@ -61,7 +63,9 @@ function parseCommitTrailers(body) {
|
|
|
61
63
|
const m = line.match(pattern);
|
|
62
64
|
if (m) {
|
|
63
65
|
const def = TRAILER_MAP[m[1]];
|
|
64
|
-
|
|
66
|
+
const raw = m[2].replace(/\r/g, '');
|
|
67
|
+
const val = def.parse ? def.parse(raw) : raw.trim();
|
|
68
|
+
result[def.key] = typeof val === 'string' ? val.trim() : val;
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
return result;
|
|
@@ -158,26 +162,37 @@ function listBackups(projectDir, opts = {}) {
|
|
|
158
162
|
}
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
//
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
165
|
+
// Manual / IDE snapshot ref (full history on dedicated ref; no subject grep so test/custom messages still list)
|
|
166
|
+
const snapRef = 'refs/guard/snapshot';
|
|
167
|
+
const snapshotExists = git(['rev-parse', '--verify', snapRef], { cwd: projectDir, allowFail: true });
|
|
168
|
+
if (snapshotExists) {
|
|
169
|
+
const snapLogArgs = ['log', snapRef, '--format=%H\x1f%aI\x1f%B\x1e', `-${limit}`];
|
|
170
|
+
if (opts.before) snapLogArgs.push(`--before=${opts.before}`);
|
|
171
|
+
if (opts.file) snapLogArgs.push('--', opts.file);
|
|
172
|
+
const snapOut = git(snapLogArgs, { cwd: projectDir, allowFail: true });
|
|
173
|
+
if (snapOut) {
|
|
174
|
+
for (const record of snapOut.split('\x1e').filter(r => r.trim())) {
|
|
175
|
+
const parts = record.split('\x1f');
|
|
176
|
+
if (parts.length < 3) continue;
|
|
177
|
+
const hash = parts[0].trim();
|
|
178
|
+
const timestamp = parts[1];
|
|
179
|
+
const body = parts[2];
|
|
180
|
+
const subject = body.split('\n')[0];
|
|
181
|
+
const trailers = parseCommitTrailers(body);
|
|
182
|
+
if (beforeDate && timestamp) {
|
|
183
|
+
const ms = Date.parse(timestamp);
|
|
184
|
+
if (!isNaN(ms) && ms > beforeDate.getTime()) continue;
|
|
185
|
+
}
|
|
186
|
+
sources.push({
|
|
187
|
+
type: 'git-snapshot',
|
|
188
|
+
ref: snapRef,
|
|
189
|
+
commitHash: hash,
|
|
190
|
+
shortHash: hash.substring(0, 7),
|
|
191
|
+
timestamp,
|
|
192
|
+
message: subject || undefined,
|
|
193
|
+
...trailers,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
181
196
|
}
|
|
182
197
|
}
|
|
183
198
|
}
|
|
@@ -14,6 +14,36 @@ function formatTimestamp(d) {
|
|
|
14
14
|
return `${d.getFullYear()}${pad(d.getMonth()+1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const REF_GUARD_AUTO_BACKUP = 'refs/guard/auto-backup';
|
|
18
|
+
const REF_GUARD_SNAPSHOT = 'refs/guard/snapshot';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parent commit for the next Guard Git snapshot (first parent of `commit-tree`).
|
|
22
|
+
*
|
|
23
|
+
* For **refs/guard/auto-backup** and **refs/guard/snapshot** only: pick whichever tip is
|
|
24
|
+
* **newer in commit time** between the two refs. That matches the human reading: "changes
|
|
25
|
+
* since the **last** Guard backup, automatic or manual" — one shared baseline for +/- and file counts.
|
|
26
|
+
*
|
|
27
|
+
* For any **other** `branchRef` (e.g. tests using refs/guard/test-*): chain that ref only.
|
|
28
|
+
*/
|
|
29
|
+
function resolveGuardParentHash(cwd, branchRef) {
|
|
30
|
+
if (branchRef !== REF_GUARD_AUTO_BACKUP && branchRef !== REF_GUARD_SNAPSHOT) {
|
|
31
|
+
return git(['rev-parse', '--verify', branchRef], { cwd, allowFail: true });
|
|
32
|
+
}
|
|
33
|
+
const autoH = git(['rev-parse', '--verify', REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
|
|
34
|
+
const snapH = git(['rev-parse', '--verify', REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
|
|
35
|
+
if (!autoH && !snapH) return null;
|
|
36
|
+
if (!autoH) return snapH;
|
|
37
|
+
if (!snapH) return autoH;
|
|
38
|
+
const commitUnix = h => {
|
|
39
|
+
const s = git(['log', '-1', '--format=%ct', h], { cwd, allowFail: true });
|
|
40
|
+
return s ? parseInt(String(s).trim(), 10) : 0;
|
|
41
|
+
};
|
|
42
|
+
const tAuto = commitUnix(autoH);
|
|
43
|
+
const tSnap = commitUnix(snapH);
|
|
44
|
+
return tSnap > tAuto ? snapH : autoH;
|
|
45
|
+
}
|
|
46
|
+
|
|
17
47
|
function listIndexFiles(cwd, env) {
|
|
18
48
|
try {
|
|
19
49
|
const out = execFileSync('git', ['ls-files', '--cached'], {
|
|
@@ -94,6 +124,8 @@ function buildCommitMessage(ts, opts) {
|
|
|
94
124
|
* @param {boolean} [opts.allowEmptyTree] - If true, still create a commit when the snapshot tree equals the previous ref (empty / bookmark commit). Auto-backup should omit this; explicit manual snapshots should set it.
|
|
95
125
|
* @param {boolean} [opts.fullWorkspaceSnapshot] - If true, ignore `cfg.protect` when building the snapshot tree (still apply `ignore` / secrets). Use for IDE/MCP "snapshot everything" so edits outside protect patterns are not invisible to the snapshot.
|
|
96
126
|
* @returns {{ status: 'created'|'skipped'|'error', commitHash?: string, shortHash?: string, fileCount?: number, reason?: string, error?: string, secretsExcluded?: string[] }}
|
|
127
|
+
* @remarks For refs/guard/auto-backup and refs/guard/snapshot, the first parent is always the
|
|
128
|
+
* newer of those two tips (by commit time), so incremental stats mean "since last Guard backup" in the human sense.
|
|
97
129
|
*/
|
|
98
130
|
function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
99
131
|
const branchRef = opts.branchRef || 'refs/guard/auto-backup';
|
|
@@ -111,7 +143,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
111
143
|
try { fs.unlinkSync(guardIndexLock); } catch { /* doesn't exist */ }
|
|
112
144
|
|
|
113
145
|
try {
|
|
114
|
-
const parentHash =
|
|
146
|
+
const parentHash = resolveGuardParentHash(cwd, branchRef);
|
|
115
147
|
|
|
116
148
|
if (narrowProtect) {
|
|
117
149
|
// protect uses strict matching (full path only, no basename fallback)
|
|
@@ -120,7 +152,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
120
152
|
pruneIndexFiles(cwd, env, f => !matchesAny(cfg.protect, f, { strict: true }));
|
|
121
153
|
} else {
|
|
122
154
|
if (parentHash) {
|
|
123
|
-
execFileSync('git', ['read-tree',
|
|
155
|
+
execFileSync('git', ['read-tree', parentHash], { cwd, env, stdio: 'pipe' });
|
|
124
156
|
}
|
|
125
157
|
execFileSync('git', ['add', '-A'], { cwd, env, stdio: 'pipe' });
|
|
126
158
|
}
|
|
@@ -133,7 +165,7 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
133
165
|
|
|
134
166
|
const newTree = execFileSync('git', ['write-tree'], { cwd, env, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
135
167
|
const parentTree = parentHash
|
|
136
|
-
? git(['rev-parse', `${
|
|
168
|
+
? git(['rev-parse', `${parentHash}^{tree}`], { cwd, allowFail: true })
|
|
137
169
|
: null;
|
|
138
170
|
|
|
139
171
|
if (newTree === parentTree && !opts.allowEmptyTree) {
|
|
@@ -246,7 +278,22 @@ function createGitSnapshot(projectDir, cfg, opts = {}) {
|
|
|
246
278
|
}
|
|
247
279
|
|
|
248
280
|
const ts = formatTimestamp(new Date());
|
|
249
|
-
|
|
281
|
+
let msg = buildCommitMessage(ts, opts);
|
|
282
|
+
|
|
283
|
+
const autoTip = git(['rev-parse', '--verify', REF_GUARD_AUTO_BACKUP], { cwd, allowFail: true });
|
|
284
|
+
const snapTip = git(['rev-parse', '--verify', REF_GUARD_SNAPSHOT], { cwd, allowFail: true });
|
|
285
|
+
const autoTipTrim = autoTip ? String(autoTip).trim() : '';
|
|
286
|
+
const snapTipTrim = snapTip ? String(snapTip).trim() : '';
|
|
287
|
+
let diffBaseLabel = 'initial';
|
|
288
|
+
if (parentHash) {
|
|
289
|
+
if (parentHash === autoTipTrim) diffBaseLabel = 'auto-backup';
|
|
290
|
+
else if (parentHash === snapTipTrim) diffBaseLabel = 'snapshot';
|
|
291
|
+
else diffBaseLabel = 'other';
|
|
292
|
+
}
|
|
293
|
+
const scopeTrailer = narrowProtect ? 'narrow' : 'full';
|
|
294
|
+
const guardBlock = `Guard-Diff-Base: ${diffBaseLabel}\nGuard-Scope: ${scopeTrailer}`;
|
|
295
|
+
msg = msg.includes('\n\n') ? `${msg}\n${guardBlock}` : `${msg}\n\n${guardBlock}`;
|
|
296
|
+
|
|
250
297
|
const commitArgs = parentHash
|
|
251
298
|
? ['commit-tree', newTree, '-p', parentHash, '-m', msg]
|
|
252
299
|
: ['commit-tree', newTree, '-m', msg];
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { guardPath } = require('./paths');
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/** Safety net if fs.watch misses (platform limits); primary updates are filesystem events. */
|
|
8
|
+
const FALLBACK_POLL_MS = 180000;
|
|
9
|
+
|
|
10
|
+
function tryWatchDirRecursive(dir, cb) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.watch(dir, { recursive: true }, cb);
|
|
13
|
+
} catch {
|
|
14
|
+
try {
|
|
15
|
+
return fs.watch(dir, cb);
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
6
21
|
|
|
7
22
|
class Poller {
|
|
8
23
|
constructor(dashMgr) {
|
|
@@ -11,6 +26,12 @@ class Poller {
|
|
|
11
26
|
this._listeners = [];
|
|
12
27
|
this._data = new Map();
|
|
13
28
|
this._pollRunning = false;
|
|
29
|
+
this._pollAgainAfter = false;
|
|
30
|
+
this._pollWaiters = [];
|
|
31
|
+
this._fsWatchers = [];
|
|
32
|
+
this._watchedRegistryKey = '';
|
|
33
|
+
this._fsDebounceTimer = null;
|
|
34
|
+
this._reattachTimer = null;
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
get data() { return this._data; }
|
|
@@ -26,10 +47,107 @@ class Poller {
|
|
|
26
47
|
}
|
|
27
48
|
}
|
|
28
49
|
|
|
50
|
+
_teardownFsWatchers() {
|
|
51
|
+
if (this._reattachTimer) {
|
|
52
|
+
clearTimeout(this._reattachTimer);
|
|
53
|
+
this._reattachTimer = null;
|
|
54
|
+
}
|
|
55
|
+
if (this._fsDebounceTimer) {
|
|
56
|
+
clearTimeout(this._fsDebounceTimer);
|
|
57
|
+
this._fsDebounceTimer = null;
|
|
58
|
+
}
|
|
59
|
+
for (const w of this._fsWatchers) {
|
|
60
|
+
try { w.close(); } catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
this._fsWatchers = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_scheduleFsRefresh() {
|
|
66
|
+
if (this._fsDebounceTimer) clearTimeout(this._fsDebounceTimer);
|
|
67
|
+
this._fsDebounceTimer = setTimeout(() => {
|
|
68
|
+
this._fsDebounceTimer = null;
|
|
69
|
+
void this.forceRefresh();
|
|
70
|
+
}, 400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_syncFileWatchers() {
|
|
74
|
+
const reg = this._dashMgr?.registry;
|
|
75
|
+
if (!reg) return;
|
|
76
|
+
const key = [...reg.entries()].map(([k, v]) => `${k}:${v._path}`).sort().join('|');
|
|
77
|
+
if (key === this._watchedRegistryKey) return;
|
|
78
|
+
this._watchedRegistryKey = key;
|
|
79
|
+
|
|
80
|
+
this._teardownFsWatchers();
|
|
81
|
+
|
|
82
|
+
let utils;
|
|
83
|
+
try {
|
|
84
|
+
utils = require(guardPath('lib', 'utils'));
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { isGitRepo, gitDir: getGitDir } = utils;
|
|
89
|
+
|
|
90
|
+
for (const proj of reg.values()) {
|
|
91
|
+
const pp = proj._path;
|
|
92
|
+
const pid = proj.id;
|
|
93
|
+
|
|
94
|
+
if (isGitRepo(pp)) {
|
|
95
|
+
const gDir = getGitDir(pp);
|
|
96
|
+
if (gDir && fs.existsSync(gDir)) {
|
|
97
|
+
try {
|
|
98
|
+
const w = fs.watch(gDir, (ev, fname) => {
|
|
99
|
+
if (fname === 'cursor-guard-alert.json' || fname === 'cursor-guard.lock') {
|
|
100
|
+
this._scheduleFsRefresh();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
this._fsWatchers.push(w);
|
|
104
|
+
} catch { /* ignore */ }
|
|
105
|
+
|
|
106
|
+
const refsDir = path.join(gDir, 'refs');
|
|
107
|
+
if (fs.existsSync(refsDir)) {
|
|
108
|
+
try {
|
|
109
|
+
const w = fs.watch(refsDir, (ev, fname) => {
|
|
110
|
+
if (!fname) return;
|
|
111
|
+
if (fname === 'guard' || fname.startsWith('guard')) {
|
|
112
|
+
this._scheduleFsRefresh();
|
|
113
|
+
if (!this._reattachTimer) {
|
|
114
|
+
this._reattachTimer = setTimeout(() => {
|
|
115
|
+
this._reattachTimer = null;
|
|
116
|
+
this._watchedRegistryKey = '';
|
|
117
|
+
this._syncFileWatchers();
|
|
118
|
+
}, 600);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
this._fsWatchers.push(w);
|
|
123
|
+
} catch { /* ignore */ }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const guardDir = path.join(gDir, 'refs', 'guard');
|
|
127
|
+
if (fs.existsSync(guardDir)) {
|
|
128
|
+
const w = tryWatchDirRecursive(guardDir, () => this._scheduleFsRefresh());
|
|
129
|
+
if (w) this._fsWatchers.push(w);
|
|
130
|
+
const preRestoreDir = path.join(guardDir, 'pre-restore');
|
|
131
|
+
if (fs.existsSync(preRestoreDir)) {
|
|
132
|
+
const w2 = tryWatchDirRecursive(preRestoreDir, () => this._scheduleFsRefresh());
|
|
133
|
+
if (w2) this._fsWatchers.push(w2);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const backupDir = path.join(pp, '.cursor-guard-backup');
|
|
140
|
+
if (fs.existsSync(backupDir)) {
|
|
141
|
+
const w = tryWatchDirRecursive(backupDir, () => this._scheduleFsRefresh());
|
|
142
|
+
if (w) this._fsWatchers.push(w);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
29
147
|
start() {
|
|
30
148
|
if (this._timer) return;
|
|
31
|
-
this._poll();
|
|
32
|
-
this._timer = setInterval(() => this._poll(),
|
|
149
|
+
void this._poll();
|
|
150
|
+
this._timer = setInterval(() => void this._poll(), FALLBACK_POLL_MS);
|
|
33
151
|
}
|
|
34
152
|
|
|
35
153
|
stop() {
|
|
@@ -37,33 +155,55 @@ class Poller {
|
|
|
37
155
|
}
|
|
38
156
|
|
|
39
157
|
async _poll() {
|
|
40
|
-
if (this._pollRunning)
|
|
158
|
+
if (this._pollRunning) {
|
|
159
|
+
this._pollAgainAfter = true;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
41
162
|
this._pollRunning = true;
|
|
42
163
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
164
|
+
do {
|
|
165
|
+
this._pollAgainAfter = false;
|
|
166
|
+
if (!this._dashMgr.running) break;
|
|
167
|
+
const projects = await this._dashMgr.getProjects();
|
|
168
|
+
if (!Array.isArray(projects)) break;
|
|
169
|
+
for (const p of projects) {
|
|
170
|
+
const fullData = await this._dashMgr.getFullPageData(p.id);
|
|
171
|
+
this._data.set(p.id, {
|
|
172
|
+
...p,
|
|
173
|
+
dashboard: fullData?.dashboard || null,
|
|
174
|
+
backups: fullData?.backups || [],
|
|
175
|
+
scope: fullData?.scope || null,
|
|
176
|
+
doctor: fullData?.doctor || null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
this._emit();
|
|
180
|
+
} while (this._pollAgainAfter);
|
|
57
181
|
} catch { /* non-critical */ }
|
|
58
|
-
finally {
|
|
182
|
+
finally {
|
|
183
|
+
this._pollRunning = false;
|
|
184
|
+
const waiters = this._pollWaiters.splice(0);
|
|
185
|
+
for (const fn of waiters) {
|
|
186
|
+
try { fn(); } catch { /* ignore */ }
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
this._syncFileWatchers();
|
|
190
|
+
} catch { /* ignore */ }
|
|
191
|
+
}
|
|
59
192
|
}
|
|
60
193
|
|
|
61
194
|
async forceRefresh() {
|
|
62
|
-
|
|
195
|
+
return new Promise(resolve => {
|
|
196
|
+
this._pollWaiters.push(resolve);
|
|
197
|
+
this._pollAgainAfter = true;
|
|
198
|
+
if (!this._pollRunning) {
|
|
199
|
+
void this._poll();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
63
202
|
}
|
|
64
203
|
|
|
65
204
|
dispose() {
|
|
66
205
|
this.stop();
|
|
206
|
+
this._teardownFsWatchers();
|
|
67
207
|
this._listeners = [];
|
|
68
208
|
}
|
|
69
209
|
}
|