cursor-guard 4.5.9 → 4.6.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/ROADMAP.md +36 -2
- package/package.json +1 -1
- package/references/dashboard/public/app.js +94 -32
- package/references/lib/core/dashboard.js +5 -5
package/ROADMAP.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
|
|
4
4
|
> 每一代向下兼容,低版本功能永远不废弃。
|
|
5
5
|
>
|
|
6
|
-
> **当前版本**:`V4.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.
|
|
6
|
+
> **当前版本**:`V4.6.1`
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.6.1` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -465,6 +465,8 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
465
465
|
| V4.5.6 | **Bug 修复 + 告警 UX + init 优化**:见下方详细说明 | ✅ |
|
|
466
466
|
| V4.5.7 | **文件详情 Modal 修复 + Dashboard 端口复用**:见下方详细说明 | ✅ |
|
|
467
467
|
| V4.5.8 | **Dashboard 版本更新检测 + 一键重启**:见下方详细说明 | ✅ |
|
|
468
|
+
| V4.6.0 | **告警 UX 全面升级**:见下方详细说明 | ✅ |
|
|
469
|
+
| V4.6.1 | **告警历史弹窗化**:见下方详细说明 | ✅ |
|
|
468
470
|
|
|
469
471
|
#### V4.4.1 详细内容
|
|
470
472
|
|
|
@@ -685,6 +687,29 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
685
687
|
| 重启流程 | 前端发 POST `/api/restart` → 显示"正在重启..." → 每 500ms 轮询 `/api/version`(最多 10s)→ 服务就绪后 `location.reload()` 自动刷新页面 |
|
|
686
688
|
| 双语支持 | 新增 `upgrade.*` 共 8 组 i18n key(banner/dismiss/restartNow/restart/hint/restarting/waiting/failed) |
|
|
687
689
|
|
|
690
|
+
#### V4.6.0 详细内容
|
|
691
|
+
|
|
692
|
+
**告警 UX 全面升级**:
|
|
693
|
+
|
|
694
|
+
| 修复项 | 说明 |
|
|
695
|
+
|------|------|
|
|
696
|
+
| 倒计时秒级实时更新 | `state.alertExpiresAt` 在 `renderAlertCard` 时记录过期时间;已有的 1 秒定时器 `updateRefreshDisplay` 中实时更新 `.alert-countdown` 元素,无需额外 `setInterval` |
|
|
697
|
+
| 告警文件详情复制/恢复操作 | 告警详情弹窗现在传入 `commitHash`(从 `alerts.latest.commitHash` 或 `'HEAD'` 回退),使每行文件的"复制恢复命令"按钮正常显示和工作 |
|
|
698
|
+
| 备份过时阈值修正 | 阈值从 `interval * 5 / 60` 分钟改为 `max(interval * 10, 300)` 秒(至少 5 分钟);仅在 watcher 正在运行时才检查过时状态;文案从"stale"改为中性的"Last git backup: X ago" |
|
|
699
|
+
| 告警历史常驻入口 | `buildAlertHistoryHtml()` 提取为独立函数,在活跃告警和无告警两种状态下都渲染"历史 N 条"折叠按钮,告警激活时也能查看历史 |
|
|
700
|
+
| 告警历史 localStorage 持久化 | 新增 `loadAlertHistory()` / `saveAlertHistory()`,使用 `localStorage` key `cursorGuard_alertHistory` 持久化最近 20 条告警历史,刷新页面不丢失 |
|
|
701
|
+
|
|
702
|
+
#### V4.6.1 详细内容
|
|
703
|
+
|
|
704
|
+
**告警历史弹窗化**:
|
|
705
|
+
|
|
706
|
+
| 改动 | 说明 |
|
|
707
|
+
|------|------|
|
|
708
|
+
| 历史按钮改为弹窗触发 | "历史 N 条"按钮点击后打开全屏 Modal(复用 `file-modal`),不再内联折叠展开 |
|
|
709
|
+
| 历史表格化展示 | Modal 内以表格呈现所有历史告警:触发时间、详情、文件类型分布、文件列表按钮 |
|
|
710
|
+
| 嵌套文件详情 | 每条历史告警的"查看文件"按钮可再打开文件详情 Modal,查看具体变更文件列表 |
|
|
711
|
+
| 新增 i18n keys | `alert.col.detail` / `alert.col.breakdown` / `alert.col.files`(中英双语) |
|
|
712
|
+
|
|
688
713
|
#### V4.5.x 新增配置参考
|
|
689
714
|
|
|
690
715
|
| 字段 | 类型 | 默认值 | 引入版本 | 说明 |
|
|
@@ -709,6 +734,15 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
709
734
|
}
|
|
710
735
|
```
|
|
711
736
|
|
|
737
|
+
### V4.7 规划:IDE 集成(VSCode/Cursor Extension)
|
|
738
|
+
|
|
739
|
+
| 项目 | 说明 |
|
|
740
|
+
|------|------|
|
|
741
|
+
| 定位 | 将 Dashboard 从浏览器迁移到 IDE 内置 WebView,减少上下文切换 |
|
|
742
|
+
| 实现方式 | VSCode Extension + WebView Panel,复用现有 `dashboard/public/` 前端代码 |
|
|
743
|
+
| 核心能力 | IDE 侧边栏入口、状态栏告警指示器、WebView 内嵌 Dashboard、一键快照按钮 |
|
|
744
|
+
| 状态 | 🔮 规划中 |
|
|
745
|
+
|
|
712
746
|
### V4 不做的事
|
|
713
747
|
|
|
714
748
|
- 不做自动恢复(恢复永远需要人确认,这是产品底线)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.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",
|
|
@@ -52,6 +52,9 @@ const I18N = {
|
|
|
52
52
|
'alert.col.file': 'File',
|
|
53
53
|
'alert.col.action': 'Action',
|
|
54
54
|
'alert.col.changes':'Changes',
|
|
55
|
+
'alert.col.detail': 'Detail',
|
|
56
|
+
'alert.col.breakdown': 'Breakdown',
|
|
57
|
+
'alert.col.files': 'Files',
|
|
55
58
|
'alert.action.modified': 'Modified',
|
|
56
59
|
'alert.action.added': 'Added',
|
|
57
60
|
'alert.action.deleted': 'Deleted',
|
|
@@ -276,6 +279,9 @@ const I18N = {
|
|
|
276
279
|
'alert.col.file': '文件',
|
|
277
280
|
'alert.col.action': '操作',
|
|
278
281
|
'alert.col.changes':'变化量',
|
|
282
|
+
'alert.col.detail': '详情',
|
|
283
|
+
'alert.col.breakdown': '文件类型',
|
|
284
|
+
'alert.col.files': '文件列表',
|
|
279
285
|
'alert.action.modified': '修改',
|
|
280
286
|
'alert.action.added': '新增',
|
|
281
287
|
'alert.action.deleted': '删除',
|
|
@@ -470,10 +476,23 @@ const state = {
|
|
|
470
476
|
tickTimer: null,
|
|
471
477
|
lastRefreshAt: null,
|
|
472
478
|
drawerOpen: null,
|
|
473
|
-
alertHistory:
|
|
479
|
+
alertHistory: loadAlertHistory(),
|
|
480
|
+
alertExpiresAt: null,
|
|
474
481
|
};
|
|
475
482
|
|
|
476
483
|
const REFRESH_MS = 15000;
|
|
484
|
+
const ALERT_HISTORY_KEY = 'cursorGuard_alertHistory';
|
|
485
|
+
|
|
486
|
+
function loadAlertHistory() {
|
|
487
|
+
try {
|
|
488
|
+
const raw = localStorage.getItem(ALERT_HISTORY_KEY);
|
|
489
|
+
return raw ? JSON.parse(raw) : [];
|
|
490
|
+
} catch { return []; }
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function saveAlertHistory() {
|
|
494
|
+
try { localStorage.setItem(ALERT_HISTORY_KEY, JSON.stringify(state.alertHistory)); } catch { /* quota */ }
|
|
495
|
+
}
|
|
477
496
|
|
|
478
497
|
/* ── DOM helpers ──────────────────────────────────────────── */
|
|
479
498
|
|
|
@@ -737,9 +756,17 @@ async function manualRefresh() {
|
|
|
737
756
|
|
|
738
757
|
function updateRefreshDisplay() {
|
|
739
758
|
const el = $('#last-refresh');
|
|
740
|
-
if (
|
|
741
|
-
|
|
742
|
-
|
|
759
|
+
if (el && state.lastRefreshAt) {
|
|
760
|
+
const sec = Math.floor((Date.now() - state.lastRefreshAt) / 1000);
|
|
761
|
+
el.textContent = `${t('topbar.lastRefresh')}: ${sec}s`;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const cdEl = document.querySelector('.alert-countdown');
|
|
765
|
+
if (cdEl && state.alertExpiresAt) {
|
|
766
|
+
const remain = Math.max(0, state.alertExpiresAt - Date.now());
|
|
767
|
+
const s = Math.ceil(remain / 1000);
|
|
768
|
+
cdEl.textContent = s > 60 ? `${Math.floor(s / 60)}m ${s % 60}s` : `${s}s`;
|
|
769
|
+
}
|
|
743
770
|
}
|
|
744
771
|
|
|
745
772
|
/* ── Rendering: Top bar ───────────────────────────────────── */
|
|
@@ -889,33 +916,66 @@ function alertFileBreakdown(files) {
|
|
|
889
916
|
return t('alert.breakdown', { added, modified, deleted });
|
|
890
917
|
}
|
|
891
918
|
|
|
919
|
+
function buildAlertHistoryHtml() {
|
|
920
|
+
if (state.alertHistory.length === 0) return '';
|
|
921
|
+
const count = state.alertHistory.length;
|
|
922
|
+
return `
|
|
923
|
+
<div class="alert-history-toggle-wrap">
|
|
924
|
+
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-modal>${t('alert.historyCount', { n: count })}</button>
|
|
925
|
+
</div>`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function openAlertHistoryModal() {
|
|
929
|
+
const list = [...state.alertHistory].reverse();
|
|
930
|
+
if (list.length === 0) return;
|
|
931
|
+
$('#file-modal-title').textContent = t('alert.history');
|
|
932
|
+
const body = $('#file-modal-body');
|
|
933
|
+
|
|
934
|
+
const rows = list.map((h, i) => {
|
|
935
|
+
const breakdown = alertFileBreakdown(h.files);
|
|
936
|
+
const fileCount = Array.isArray(h.files) ? h.files.length : 0;
|
|
937
|
+
return `<tr>
|
|
938
|
+
<td class="text-mono">${esc(formatTime(h.timestamp))}</td>
|
|
939
|
+
<td>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</td>
|
|
940
|
+
<td>${breakdown ? esc(breakdown) : '-'}</td>
|
|
941
|
+
<td>${fileCount > 0 ? `<button class="modal-restore-btn" data-history-files="${i}">${t('alert.viewFiles', { n: fileCount })}</button>` : '-'}</td>
|
|
942
|
+
</tr>`;
|
|
943
|
+
}).join('');
|
|
944
|
+
|
|
945
|
+
body.innerHTML = `<table>
|
|
946
|
+
<thead><tr>
|
|
947
|
+
<th>${t('alert.triggered')}</th>
|
|
948
|
+
<th>${t('alert.col.detail')}</th>
|
|
949
|
+
<th>${t('alert.col.breakdown')}</th>
|
|
950
|
+
<th>${t('alert.col.files')}</th>
|
|
951
|
+
</tr></thead>
|
|
952
|
+
<tbody>${rows}</tbody>
|
|
953
|
+
</table>`;
|
|
954
|
+
|
|
955
|
+
body.addEventListener('click', (e) => {
|
|
956
|
+
const btn = e.target.closest('[data-history-files]');
|
|
957
|
+
if (btn) {
|
|
958
|
+
const idx = parseInt(btn.dataset.historyFiles);
|
|
959
|
+
const h = list[idx];
|
|
960
|
+
if (h?.files?.length > 0) {
|
|
961
|
+
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
962
|
+
openFileModal(t('modal.alertFiles'), h.files, proj, '');
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
$('#file-modal-overlay').classList.add('active');
|
|
968
|
+
document.body.style.overflow = 'hidden';
|
|
969
|
+
}
|
|
970
|
+
|
|
892
971
|
function renderAlertCard(alerts) {
|
|
893
972
|
const el = $('#card-alert');
|
|
894
973
|
if (!alerts?.active) {
|
|
895
|
-
|
|
896
|
-
if (state.alertHistory.length > 0) {
|
|
897
|
-
const count = state.alertHistory.length;
|
|
898
|
-
const rows = state.alertHistory.slice(-5).reverse().map(h => {
|
|
899
|
-
const breakdown = alertFileBreakdown(h.files);
|
|
900
|
-
return `<div class="alert-history-row text-sm text-muted">
|
|
901
|
-
<span class="alert-history-time">${esc(formatTime(h.timestamp))}</span>
|
|
902
|
-
<span>${t('alert.detail', { count: h.fileCount, window: h.windowSeconds, threshold: h.threshold })}</span>
|
|
903
|
-
${breakdown ? `<span class="alert-history-breakdown">${esc(breakdown)}</span>` : ''}
|
|
904
|
-
<span class="badge badge-expired">${t('alert.expired')}</span>
|
|
905
|
-
</div>`;
|
|
906
|
-
}).join('');
|
|
907
|
-
historyHtml = `
|
|
908
|
-
<div class="alert-history-toggle-wrap">
|
|
909
|
-
<button class="alert-history-toggle-btn text-sm text-muted" data-alert-history-toggle>${t('alert.historyCount', { n: count })}</button>
|
|
910
|
-
</div>
|
|
911
|
-
<div class="alert-history alert-history-collapsed">
|
|
912
|
-
<div class="alert-history-label text-sm">${t('alert.history')}</div>${rows}
|
|
913
|
-
</div>`;
|
|
914
|
-
}
|
|
974
|
+
state.alertExpiresAt = null;
|
|
915
975
|
el.innerHTML = `
|
|
916
976
|
<div class="card-label">${t('alert.title')}</div>
|
|
917
977
|
<div class="card-status"><span class="status-dot status-healthy"></span><span>${t('alert.none')}</span></div>
|
|
918
|
-
${
|
|
978
|
+
${buildAlertHistoryHtml()}
|
|
919
979
|
`;
|
|
920
980
|
return;
|
|
921
981
|
}
|
|
@@ -925,10 +985,12 @@ function renderAlertCard(alerts) {
|
|
|
925
985
|
if (a.timestamp && !state.alertHistory.some(h => h.timestamp === a.timestamp)) {
|
|
926
986
|
state.alertHistory.push({ timestamp: a.timestamp, fileCount: a.fileCount, windowSeconds: a.windowSeconds, threshold: a.threshold, expiresAt: a.expiresAt, files: a.files });
|
|
927
987
|
if (state.alertHistory.length > 20) state.alertHistory = state.alertHistory.slice(-20);
|
|
988
|
+
saveAlertHistory();
|
|
928
989
|
}
|
|
929
990
|
|
|
930
991
|
const triggeredAt = a.timestamp ? formatTime(a.timestamp) : '-';
|
|
931
992
|
const expiresAt = a.expiresAt ? new Date(a.expiresAt) : null;
|
|
993
|
+
state.alertExpiresAt = expiresAt ? expiresAt.getTime() : null;
|
|
932
994
|
const remainMs = expiresAt ? expiresAt.getTime() - Date.now() : 0;
|
|
933
995
|
const remainSec = Math.max(0, Math.ceil(remainMs / 1000));
|
|
934
996
|
const remainMin = Math.floor(remainSec / 60);
|
|
@@ -956,6 +1018,7 @@ function renderAlertCard(alerts) {
|
|
|
956
1018
|
<div class="alert-detail-row alert-suggestion text-sm text-muted">${t('alert.suggestion')}</div>
|
|
957
1019
|
</div>
|
|
958
1020
|
${filesHtml}
|
|
1021
|
+
${buildAlertHistoryHtml()}
|
|
959
1022
|
`;
|
|
960
1023
|
}
|
|
961
1024
|
|
|
@@ -1547,13 +1610,11 @@ function setupEvents() {
|
|
|
1547
1610
|
if (backup) openRestoreDrawer(backup);
|
|
1548
1611
|
});
|
|
1549
1612
|
|
|
1550
|
-
// Alert history
|
|
1613
|
+
// Alert history modal + file modal (event delegation)
|
|
1551
1614
|
$('#card-alert').addEventListener('click', (e) => {
|
|
1552
|
-
const
|
|
1553
|
-
if (
|
|
1554
|
-
|
|
1555
|
-
const history = card?.querySelector('.alert-history');
|
|
1556
|
-
if (history) history.classList.toggle('alert-history-collapsed');
|
|
1615
|
+
const historyBtn = e.target.closest('[data-alert-history-modal]');
|
|
1616
|
+
if (historyBtn) {
|
|
1617
|
+
openAlertHistoryModal();
|
|
1557
1618
|
return;
|
|
1558
1619
|
}
|
|
1559
1620
|
const modalBtn = e.target.closest('[data-alert-files-modal]');
|
|
@@ -1562,7 +1623,8 @@ function setupEvents() {
|
|
|
1562
1623
|
const files = alerts?.latest?.files || [];
|
|
1563
1624
|
if (files.length > 0) {
|
|
1564
1625
|
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1565
|
-
|
|
1626
|
+
const hash = alerts?.latest?.commitHash || 'HEAD';
|
|
1627
|
+
openFileModal(t('modal.alertFiles'), files, proj, hash);
|
|
1566
1628
|
}
|
|
1567
1629
|
return;
|
|
1568
1630
|
}
|
|
@@ -168,12 +168,12 @@ function getDashboard(projectDir) {
|
|
|
168
168
|
issues.push(`Disk space low (${status.disk.freeGB} GB free)`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
if (status.lastBackup.git) {
|
|
171
|
+
if (status.lastBackup.git && status.watcher.running) {
|
|
172
172
|
const lastTs = new Date(status.lastBackup.git.timestamp).getTime();
|
|
173
|
-
const
|
|
174
|
-
const staleThreshold = Math.
|
|
175
|
-
if (
|
|
176
|
-
issues.push(`Last git backup
|
|
173
|
+
const staleSec = (Date.now() - lastTs) / 1000;
|
|
174
|
+
const staleThreshold = Math.max(cfg.auto_backup_interval_seconds * 10, 300);
|
|
175
|
+
if (staleSec > staleThreshold) {
|
|
176
|
+
issues.push(`Last git backup: ${relativeTime(status.lastBackup.git.timestamp)}`);
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
|