cursor-guard 4.5.8 → 4.5.9
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.9`(V4 最终版)
|
|
7
|
+
> **文档状态**:`V2` ~ `V4.5.9` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
|
|
8
8
|
|
|
9
9
|
## 阅读导航
|
|
10
10
|
|
|
@@ -464,7 +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
|
+
| V4.5.8 | **Dashboard 版本更新检测 + 一键重启**:见下方详细说明 | ✅ |
|
|
468
468
|
|
|
469
469
|
#### V4.4.1 详细内容
|
|
470
470
|
|
|
@@ -674,14 +674,16 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
674
674
|
|
|
675
675
|
#### V4.5.8 详细内容
|
|
676
676
|
|
|
677
|
-
**Dashboard
|
|
677
|
+
**Dashboard 版本更新检测 + 一键重启**:
|
|
678
678
|
|
|
679
679
|
| 组件 | 说明 |
|
|
680
680
|
|------|------|
|
|
681
|
-
| `/api/version` 端点 |
|
|
682
|
-
|
|
|
683
|
-
|
|
|
684
|
-
|
|
|
681
|
+
| `/api/version` 端点 | 返回 `serverVersion`(启动时 require cache)和 `installedVersion`(实时从磁盘读取),以及 `updateAvailable` 布尔值 |
|
|
682
|
+
| `/api/restart` 端点 (POST) | 触发服务热重启:关闭旧 HTTP server → `clearGuardCache()` 清除所有 cursor-guard 模块的 require cache → 更新 `SERVER_VERSION` → 在同端口重启新 server |
|
|
683
|
+
| 懒加载 core deps | `handleApi` 中 `getDashboard`/`runDiagnostics`/`listBackups`/`getBackupFiles` 改为通过 `coreDeps()` 懒加载,重启后自动加载新代码 |
|
|
684
|
+
| 升级横幅 | `updateAvailable === true` 时显示黄色横幅。包含"一键重启"按钮(发送 POST → 轮询等待 → 自动 reload)、"手动重启"按钮(展开命令示例)、关闭按钮 |
|
|
685
|
+
| 重启流程 | 前端发 POST `/api/restart` → 显示"正在重启..." → 每 500ms 轮询 `/api/version`(最多 10s)→ 服务就绪后 `location.reload()` 自动刷新页面 |
|
|
686
|
+
| 双语支持 | 新增 `upgrade.*` 共 8 组 i18n key(banner/dismiss/restartNow/restart/hint/restarting/waiting/failed) |
|
|
685
687
|
|
|
686
688
|
#### V4.5.x 新增配置参考
|
|
687
689
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.9",
|
|
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",
|
|
@@ -142,8 +142,12 @@ const I18N = {
|
|
|
142
142
|
|
|
143
143
|
'upgrade.banner': 'New version {installed} available (current server: {server}). Please restart the Dashboard service to load the latest features.',
|
|
144
144
|
'upgrade.dismiss': 'Dismiss',
|
|
145
|
-
'upgrade.
|
|
145
|
+
'upgrade.restartNow': 'Restart Now',
|
|
146
|
+
'upgrade.restart': 'Manual restart',
|
|
146
147
|
'upgrade.hint': 'Stop the current process (Ctrl+C), then run: cursor-guard-backup --path <dir> --dashboard',
|
|
148
|
+
'upgrade.restarting': 'Restarting...',
|
|
149
|
+
'upgrade.waiting': 'Waiting for server...',
|
|
150
|
+
'upgrade.failed': 'Restart failed, try manually',
|
|
147
151
|
|
|
148
152
|
'strategy.git': 'Git',
|
|
149
153
|
'strategy.shadow': 'Shadow',
|
|
@@ -362,8 +366,12 @@ const I18N = {
|
|
|
362
366
|
|
|
363
367
|
'upgrade.banner': '检测到新版本 {installed}(当前服务: {server}),请重启 Dashboard 服务以加载最新功能',
|
|
364
368
|
'upgrade.dismiss': '关闭',
|
|
365
|
-
'upgrade.
|
|
369
|
+
'upgrade.restartNow': '一键重启',
|
|
370
|
+
'upgrade.restart': '手动重启',
|
|
366
371
|
'upgrade.hint': '停止当前进程 (Ctrl+C),然后运行: cursor-guard-backup --path <目录> --dashboard',
|
|
372
|
+
'upgrade.restarting': '正在重启...',
|
|
373
|
+
'upgrade.waiting': '等待服务就绪...',
|
|
374
|
+
'upgrade.failed': '重启失败,请手动重启',
|
|
367
375
|
|
|
368
376
|
'strategy.git': 'Git',
|
|
369
377
|
'strategy.shadow': '影子',
|
|
@@ -1606,7 +1614,8 @@ function showUpgradeBanner(data) {
|
|
|
1606
1614
|
banner.className = 'upgrade-banner';
|
|
1607
1615
|
banner.innerHTML = `
|
|
1608
1616
|
<span class="upgrade-banner-text">${t('upgrade.banner', { installed: esc(data.installedVersion), server: esc(data.serverVersion) })}</span>
|
|
1609
|
-
<button class="upgrade-banner-
|
|
1617
|
+
<button class="upgrade-banner-restart-btn">${t('upgrade.restartNow')}</button>
|
|
1618
|
+
<button class="upgrade-banner-hint-btn">${t('upgrade.restart')}</button>
|
|
1610
1619
|
<button class="upgrade-banner-close" aria-label="${t('upgrade.dismiss')}">×</button>
|
|
1611
1620
|
`;
|
|
1612
1621
|
const topbar = $('#topbar');
|
|
@@ -1620,6 +1629,42 @@ function showUpgradeBanner(data) {
|
|
|
1620
1629
|
el.innerHTML = `<code>${t('upgrade.hint')}</code>`;
|
|
1621
1630
|
banner.appendChild(el);
|
|
1622
1631
|
});
|
|
1632
|
+
banner.querySelector('.upgrade-banner-restart-btn').addEventListener('click', () => {
|
|
1633
|
+
restartServer(banner);
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
async function restartServer(banner) {
|
|
1638
|
+
const btn = banner.querySelector('.upgrade-banner-restart-btn');
|
|
1639
|
+
btn.disabled = true;
|
|
1640
|
+
btn.textContent = t('upgrade.restarting');
|
|
1641
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = 'none';
|
|
1642
|
+
banner.querySelector('.upgrade-banner-close').style.display = 'none';
|
|
1643
|
+
|
|
1644
|
+
try {
|
|
1645
|
+
const sep = '/api/restart'.includes('?') ? '&' : '?';
|
|
1646
|
+
const tokenParam = window.__GUARD_TOKEN__ ? `${sep}token=${window.__GUARD_TOKEN__}` : '';
|
|
1647
|
+
await fetch('/api/restart' + tokenParam, { method: 'POST' });
|
|
1648
|
+
} catch { /* server may close connection */ }
|
|
1649
|
+
|
|
1650
|
+
btn.textContent = t('upgrade.waiting');
|
|
1651
|
+
let ready = false;
|
|
1652
|
+
for (let i = 0; i < 20; i++) {
|
|
1653
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1654
|
+
try {
|
|
1655
|
+
const r = await fetch('/api/version' + (window.__GUARD_TOKEN__ ? '?token=' + window.__GUARD_TOKEN__ : ''));
|
|
1656
|
+
if (r.ok) { ready = true; break; }
|
|
1657
|
+
} catch { /* still restarting */ }
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
if (ready) {
|
|
1661
|
+
location.reload();
|
|
1662
|
+
} else {
|
|
1663
|
+
btn.textContent = t('upgrade.failed');
|
|
1664
|
+
btn.disabled = false;
|
|
1665
|
+
banner.querySelector('.upgrade-banner-hint-btn').style.display = '';
|
|
1666
|
+
banner.querySelector('.upgrade-banner-close').style.display = '';
|
|
1667
|
+
}
|
|
1623
1668
|
}
|
|
1624
1669
|
|
|
1625
1670
|
/* ── Init ─────────────────────────────────────────────────── */
|
|
@@ -143,6 +143,25 @@ body {
|
|
|
143
143
|
.upgrade-banner-text {
|
|
144
144
|
flex: 1;
|
|
145
145
|
}
|
|
146
|
+
.upgrade-banner-restart-btn {
|
|
147
|
+
padding: 4px 14px;
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
border: 1px solid var(--yellow);
|
|
151
|
+
border-radius: var(--radius-sm);
|
|
152
|
+
background: var(--yellow);
|
|
153
|
+
color: var(--bg);
|
|
154
|
+
cursor: pointer;
|
|
155
|
+
white-space: nowrap;
|
|
156
|
+
transition: all 0.15s;
|
|
157
|
+
}
|
|
158
|
+
.upgrade-banner-restart-btn:hover {
|
|
159
|
+
filter: brightness(1.1);
|
|
160
|
+
}
|
|
161
|
+
.upgrade-banner-restart-btn:disabled {
|
|
162
|
+
opacity: 0.7;
|
|
163
|
+
cursor: wait;
|
|
164
|
+
}
|
|
146
165
|
.upgrade-banner-hint-btn {
|
|
147
166
|
padding: 3px 10px;
|
|
148
167
|
font-size: 11px;
|
|
@@ -6,13 +6,26 @@ const crypto = require('crypto');
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const GUARD_ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
function coreDeps() {
|
|
12
|
+
return {
|
|
13
|
+
getDashboard: require('../lib/core/dashboard').getDashboard,
|
|
14
|
+
runDiagnostics: require('../lib/core/doctor').runDiagnostics,
|
|
15
|
+
listBackups: require('../lib/core/backups').listBackups,
|
|
16
|
+
getBackupFiles: require('../lib/core/backups').getBackupFiles,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearGuardCache() {
|
|
21
|
+
Object.keys(require.cache).forEach(key => {
|
|
22
|
+
if (key.startsWith(GUARD_ROOT)) delete require.cache[key];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
14
27
|
const PKG_PATH = path.resolve(__dirname, '..', '..', 'package.json');
|
|
15
|
-
|
|
28
|
+
let SERVER_VERSION = require('../../package.json').version;
|
|
16
29
|
const DEFAULT_PORT = 3120;
|
|
17
30
|
const MAX_PORT_RETRIES = 10;
|
|
18
31
|
const ALLOWED_HOSTS = /^(127\.0\.0\.1|localhost)(:\d+)?$/;
|
|
@@ -126,7 +139,7 @@ function serveStatic(reqUrl, res, serverToken) {
|
|
|
126
139
|
|
|
127
140
|
/* ── API routes ─────────────────────────────────────────────── */
|
|
128
141
|
|
|
129
|
-
function handleApi(pathname, query, registry, res) {
|
|
142
|
+
function handleApi(pathname, query, registry, res, req) {
|
|
130
143
|
if (pathname === '/api/version') {
|
|
131
144
|
let installedVersion = SERVER_VERSION;
|
|
132
145
|
try {
|
|
@@ -140,6 +153,17 @@ function handleApi(pathname, query, registry, res) {
|
|
|
140
153
|
});
|
|
141
154
|
}
|
|
142
155
|
|
|
156
|
+
if (pathname === '/api/restart') {
|
|
157
|
+
if (req.method !== 'POST') {
|
|
158
|
+
res.writeHead(405);
|
|
159
|
+
return res.end('Method Not Allowed');
|
|
160
|
+
}
|
|
161
|
+
if (!_instance) return json(res, { error: 'No running instance' }, 500);
|
|
162
|
+
json(res, { restarting: true });
|
|
163
|
+
setTimeout(() => restartDashboard(), 300);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
143
167
|
if (pathname === '/api/projects') {
|
|
144
168
|
const list = [...registry.values()].map(({ id, name, pathLabel }) => ({ id, name, pathLabel }));
|
|
145
169
|
return json(res, list);
|
|
@@ -154,6 +178,8 @@ function handleApi(pathname, query, registry, res) {
|
|
|
154
178
|
return json(res, { error: `Project directory not accessible: ${project.pathLabel}` }, 500);
|
|
155
179
|
}
|
|
156
180
|
|
|
181
|
+
const { getDashboard, runDiagnostics, listBackups, getBackupFiles } = coreDeps();
|
|
182
|
+
|
|
157
183
|
if (pathname === '/api/page-data') {
|
|
158
184
|
const scope = query.get('scope');
|
|
159
185
|
const result = { timestamp: new Date().toISOString() };
|
|
@@ -258,7 +284,7 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
258
284
|
return res.end('Forbidden: invalid host');
|
|
259
285
|
}
|
|
260
286
|
|
|
261
|
-
if (req.method !== 'GET') {
|
|
287
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
262
288
|
res.writeHead(405);
|
|
263
289
|
return res.end('Method Not Allowed');
|
|
264
290
|
}
|
|
@@ -272,8 +298,9 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
272
298
|
res.writeHead(403);
|
|
273
299
|
return res.end('Forbidden: invalid token');
|
|
274
300
|
}
|
|
275
|
-
handleApi(parsed.pathname, parsed.searchParams, registry, res);
|
|
301
|
+
handleApi(parsed.pathname, parsed.searchParams, registry, res, req);
|
|
276
302
|
} else {
|
|
303
|
+
if (req.method !== 'GET') { res.writeHead(405); return res.end('Method Not Allowed'); }
|
|
277
304
|
serveStatic(req.url, res, token);
|
|
278
305
|
}
|
|
279
306
|
});
|
|
@@ -309,6 +336,26 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
309
336
|
});
|
|
310
337
|
}
|
|
311
338
|
|
|
339
|
+
/* ── Hot Restart ───────────────────────────────────────────── */
|
|
340
|
+
|
|
341
|
+
async function restartDashboard() {
|
|
342
|
+
if (!_instance) return;
|
|
343
|
+
const paths = [..._instance.registry.values()].map(p => p._path);
|
|
344
|
+
const port = _instance.port;
|
|
345
|
+
|
|
346
|
+
_instance.server.close();
|
|
347
|
+
_instance = null;
|
|
348
|
+
|
|
349
|
+
clearGuardCache();
|
|
350
|
+
try {
|
|
351
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
|
|
352
|
+
SERVER_VERSION = pkg.version;
|
|
353
|
+
} catch { /* keep old version */ }
|
|
354
|
+
|
|
355
|
+
console.log(` [dashboard] Restarting on port ${port}...`);
|
|
356
|
+
await startDashboardServer(paths, { port, silent: false });
|
|
357
|
+
}
|
|
358
|
+
|
|
312
359
|
/* ── CLI entry ─────────────────────────────────────────────── */
|
|
313
360
|
|
|
314
361
|
if (require.main === module) {
|