cursor-guard 4.5.7 → 4.5.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/ROADMAP.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
> 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
|
|
4
4
|
> 每一代向下兼容,低版本功能永远不废弃。
|
|
5
5
|
>
|
|
6
|
-
> **当前版本**:`V4.5.
|
|
7
|
-
> **文档状态**:`V2` ~ `V4.5.
|
|
6
|
+
> **当前版本**:`V4.5.8`(V4 最终版)
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.5.8` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -464,6 +464,7 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
464
464
|
| V4.5.4 | **Shadow 硬链接增量优化 + always_watch 强保护模式**:见下方详细说明 | ✅ |
|
|
465
465
|
| V4.5.6 | **Bug 修复 + 告警 UX + init 优化**:见下方详细说明 | ✅ |
|
|
466
466
|
| V4.5.7 | **文件详情 Modal 修复 + Dashboard 端口复用**:见下方详细说明 | ✅ |
|
|
467
|
+
| V4.5.8 | **Dashboard 版本更新检测**:见下方详细说明 | ✅ |
|
|
467
468
|
|
|
468
469
|
#### V4.4.1 详细内容
|
|
469
470
|
|
|
@@ -671,6 +672,17 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
671
672
|
| 透明生效 | registry 是引用传递,已运行的 HTTP handler 闭包自动读取最新 registry,无需重启服务 |
|
|
672
673
|
| 导出 `getInstance()` | 外部可通过 `getInstance()` 获取当前运行实例的 server/port/registry 信息 |
|
|
673
674
|
|
|
675
|
+
#### V4.5.8 详细内容
|
|
676
|
+
|
|
677
|
+
**Dashboard 版本更新检测**:
|
|
678
|
+
|
|
679
|
+
| 组件 | 说明 |
|
|
680
|
+
|------|------|
|
|
681
|
+
| `/api/version` 端点 | 新增无需 project id 的公共 API。返回 `serverVersion`(进程启动时 require cache 锁定)和 `installedVersion`(实时从磁盘读取 `package.json`),以及 `updateAvailable` 布尔值 |
|
|
682
|
+
| 前端版本检查 | `init()` 完成后异步调用 `checkServerVersion()`,非阻塞,失败静默 |
|
|
683
|
+
| 升级横幅 | `updateAvailable === true` 时,在 topbar 下方滑入黄色横幅,显示版本差异和重启提示。带"如何重启"按钮(展开命令示例)和关闭按钮 |
|
|
684
|
+
| 双语支持 | 新增 `upgrade.banner` / `upgrade.dismiss` / `upgrade.restart` / `upgrade.hint` 四组 i18n key |
|
|
685
|
+
|
|
674
686
|
#### V4.5.x 新增配置参考
|
|
675
687
|
|
|
676
688
|
| 字段 | 类型 | 默认值 | 引入版本 | 说明 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.8",
|
|
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",
|
|
@@ -140,6 +140,11 @@ const I18N = {
|
|
|
140
140
|
'error.sectionFailed': 'This section failed to load',
|
|
141
141
|
'empty.noData': 'No data available',
|
|
142
142
|
|
|
143
|
+
'upgrade.banner': 'New version {installed} available (current server: {server}). Please restart the Dashboard service to load the latest features.',
|
|
144
|
+
'upgrade.dismiss': 'Dismiss',
|
|
145
|
+
'upgrade.restart': 'How to restart',
|
|
146
|
+
'upgrade.hint': 'Stop the current process (Ctrl+C), then run: cursor-guard-backup --path <dir> --dashboard',
|
|
147
|
+
|
|
143
148
|
'strategy.git': 'Git',
|
|
144
149
|
'strategy.shadow': 'Shadow',
|
|
145
150
|
'strategy.both': 'Both',
|
|
@@ -355,6 +360,11 @@ const I18N = {
|
|
|
355
360
|
'error.sectionFailed': '此区块加载失败',
|
|
356
361
|
'empty.noData': '暂无数据',
|
|
357
362
|
|
|
363
|
+
'upgrade.banner': '检测到新版本 {installed}(当前服务: {server}),请重启 Dashboard 服务以加载最新功能',
|
|
364
|
+
'upgrade.dismiss': '关闭',
|
|
365
|
+
'upgrade.restart': '如何重启',
|
|
366
|
+
'upgrade.hint': '停止当前进程 (Ctrl+C),然后运行: cursor-guard-backup --path <目录> --dashboard',
|
|
367
|
+
|
|
358
368
|
'strategy.git': 'Git',
|
|
359
369
|
'strategy.shadow': '影子',
|
|
360
370
|
'strategy.both': '双重',
|
|
@@ -1580,6 +1590,38 @@ function setupEvents() {
|
|
|
1580
1590
|
});
|
|
1581
1591
|
}
|
|
1582
1592
|
|
|
1593
|
+
/* ── Version Check ────────────────────────────────────────── */
|
|
1594
|
+
|
|
1595
|
+
async function checkServerVersion() {
|
|
1596
|
+
try {
|
|
1597
|
+
const data = await fetchJson('/api/version');
|
|
1598
|
+
if (data.updateAvailable) showUpgradeBanner(data);
|
|
1599
|
+
} catch { /* non-critical */ }
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function showUpgradeBanner(data) {
|
|
1603
|
+
if ($('#upgrade-banner')) return;
|
|
1604
|
+
const banner = document.createElement('div');
|
|
1605
|
+
banner.id = 'upgrade-banner';
|
|
1606
|
+
banner.className = 'upgrade-banner';
|
|
1607
|
+
banner.innerHTML = `
|
|
1608
|
+
<span class="upgrade-banner-text">${t('upgrade.banner', { installed: esc(data.installedVersion), server: esc(data.serverVersion) })}</span>
|
|
1609
|
+
<button class="upgrade-banner-hint-btn" title="${t('upgrade.hint')}">${t('upgrade.restart')}</button>
|
|
1610
|
+
<button class="upgrade-banner-close" aria-label="${t('upgrade.dismiss')}">×</button>
|
|
1611
|
+
`;
|
|
1612
|
+
const topbar = $('#topbar');
|
|
1613
|
+
topbar.parentNode.insertBefore(banner, topbar.nextSibling);
|
|
1614
|
+
banner.querySelector('.upgrade-banner-close').addEventListener('click', () => banner.remove());
|
|
1615
|
+
banner.querySelector('.upgrade-banner-hint-btn').addEventListener('click', () => {
|
|
1616
|
+
const hint = banner.querySelector('.upgrade-banner-hint');
|
|
1617
|
+
if (hint) { hint.remove(); return; }
|
|
1618
|
+
const el = document.createElement('div');
|
|
1619
|
+
el.className = 'upgrade-banner-hint';
|
|
1620
|
+
el.innerHTML = `<code>${t('upgrade.hint')}</code>`;
|
|
1621
|
+
banner.appendChild(el);
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1583
1625
|
/* ── Init ─────────────────────────────────────────────────── */
|
|
1584
1626
|
|
|
1585
1627
|
async function init() {
|
|
@@ -1595,6 +1637,7 @@ async function init() {
|
|
|
1595
1637
|
await loadPageData({ progressive: true });
|
|
1596
1638
|
renderAll();
|
|
1597
1639
|
startRefresh();
|
|
1640
|
+
checkServerVersion();
|
|
1598
1641
|
} catch (e) {
|
|
1599
1642
|
showGlobalError(e.message);
|
|
1600
1643
|
}
|
|
@@ -118,6 +118,74 @@ body {
|
|
|
118
118
|
|
|
119
119
|
#last-refresh { font-size: 12px; white-space: nowrap; opacity: .7; }
|
|
120
120
|
|
|
121
|
+
/* ── Upgrade Banner ──────────────────────────────────────── */
|
|
122
|
+
|
|
123
|
+
.upgrade-banner {
|
|
124
|
+
position: fixed;
|
|
125
|
+
top: var(--topbar-h);
|
|
126
|
+
left: 0; right: 0;
|
|
127
|
+
background: var(--yellow-bg);
|
|
128
|
+
border-bottom: 1px solid var(--yellow);
|
|
129
|
+
color: var(--yellow);
|
|
130
|
+
padding: 8px 24px;
|
|
131
|
+
font-size: 13px;
|
|
132
|
+
font-weight: 500;
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 12px;
|
|
136
|
+
z-index: 49;
|
|
137
|
+
animation: slideDown 0.3s ease;
|
|
138
|
+
}
|
|
139
|
+
@keyframes slideDown {
|
|
140
|
+
from { transform: translateY(-100%); opacity: 0; }
|
|
141
|
+
to { transform: translateY(0); opacity: 1; }
|
|
142
|
+
}
|
|
143
|
+
.upgrade-banner-text {
|
|
144
|
+
flex: 1;
|
|
145
|
+
}
|
|
146
|
+
.upgrade-banner-hint-btn {
|
|
147
|
+
padding: 3px 10px;
|
|
148
|
+
font-size: 11px;
|
|
149
|
+
border: 1px solid var(--yellow);
|
|
150
|
+
border-radius: var(--radius-sm);
|
|
151
|
+
background: transparent;
|
|
152
|
+
color: var(--yellow);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
white-space: nowrap;
|
|
155
|
+
transition: all 0.15s;
|
|
156
|
+
}
|
|
157
|
+
.upgrade-banner-hint-btn:hover {
|
|
158
|
+
background: var(--yellow);
|
|
159
|
+
color: var(--bg);
|
|
160
|
+
}
|
|
161
|
+
.upgrade-banner-close {
|
|
162
|
+
background: none;
|
|
163
|
+
border: none;
|
|
164
|
+
color: var(--yellow);
|
|
165
|
+
font-size: 18px;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
padding: 0 4px;
|
|
168
|
+
opacity: 0.7;
|
|
169
|
+
transition: opacity 0.15s;
|
|
170
|
+
}
|
|
171
|
+
.upgrade-banner-close:hover { opacity: 1; }
|
|
172
|
+
.upgrade-banner-hint {
|
|
173
|
+
width: 100%;
|
|
174
|
+
margin-top: 6px;
|
|
175
|
+
padding: 6px 10px;
|
|
176
|
+
background: rgba(0,0,0,0.2);
|
|
177
|
+
border-radius: var(--radius-sm);
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
}
|
|
180
|
+
.upgrade-banner-hint code {
|
|
181
|
+
font-family: var(--font-mono);
|
|
182
|
+
font-size: 11px;
|
|
183
|
+
color: var(--text-primary);
|
|
184
|
+
}
|
|
185
|
+
.upgrade-banner + main {
|
|
186
|
+
margin-top: 38px;
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
/* ── Buttons ──────────────────────────────────────────────── */
|
|
122
190
|
|
|
123
191
|
.btn {
|
|
@@ -11,6 +11,8 @@ const { runDiagnostics } = require('../lib/core/doctor');
|
|
|
11
11
|
const { listBackups, getBackupFiles } = require('../lib/core/backups');
|
|
12
12
|
|
|
13
13
|
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
14
|
+
const PKG_PATH = path.resolve(__dirname, '..', '..', 'package.json');
|
|
15
|
+
const SERVER_VERSION = require('../../package.json').version;
|
|
14
16
|
const DEFAULT_PORT = 3120;
|
|
15
17
|
const MAX_PORT_RETRIES = 10;
|
|
16
18
|
const ALLOWED_HOSTS = /^(127\.0\.0\.1|localhost)(:\d+)?$/;
|
|
@@ -125,6 +127,19 @@ function serveStatic(reqUrl, res, serverToken) {
|
|
|
125
127
|
/* ── API routes ─────────────────────────────────────────────── */
|
|
126
128
|
|
|
127
129
|
function handleApi(pathname, query, registry, res) {
|
|
130
|
+
if (pathname === '/api/version') {
|
|
131
|
+
let installedVersion = SERVER_VERSION;
|
|
132
|
+
try {
|
|
133
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
|
|
134
|
+
installedVersion = pkg.version;
|
|
135
|
+
} catch { /* fallback to server version */ }
|
|
136
|
+
return json(res, {
|
|
137
|
+
serverVersion: SERVER_VERSION,
|
|
138
|
+
installedVersion,
|
|
139
|
+
updateAvailable: SERVER_VERSION !== installedVersion,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
if (pathname === '/api/projects') {
|
|
129
144
|
const list = [...registry.values()].map(({ id, name, pathLabel }) => ({ id, name, pathLabel }));
|
|
130
145
|
return json(res, list);
|