cursor-guard 4.5.6 → 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
|
|
|
@@ -463,6 +463,8 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
463
463
|
| V4.5.3 | **告警历史 UX 优化 + 备份结构化文件表格**:见下方详细说明 | ✅ |
|
|
464
464
|
| V4.5.4 | **Shadow 硬链接增量优化 + always_watch 强保护模式**:见下方详细说明 | ✅ |
|
|
465
465
|
| V4.5.6 | **Bug 修复 + 告警 UX + init 优化**:见下方详细说明 | ✅ |
|
|
466
|
+
| V4.5.7 | **文件详情 Modal 修复 + Dashboard 端口复用**:见下方详细说明 | ✅ |
|
|
467
|
+
| V4.5.8 | **Dashboard 版本更新检测**:见下方详细说明 | ✅ |
|
|
466
468
|
|
|
467
469
|
#### V4.4.1 详细内容
|
|
468
470
|
|
|
@@ -652,6 +654,35 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
|
|
|
652
654
|
| `cursor-guard-init` 自动创建配置 | init 流程新增 Step 4/5:若 `.cursor-guard.json` 不存在,自动从 `cursor-guard.example.json` 复制为项目根默认配置。升级场景下保留现有配置 |
|
|
653
655
|
| `backup_interval_seconds` 兼容别名 | `loadConfig` 支持 `backup_interval_seconds` 作为 `auto_backup_interval_seconds` 的别名(带 deprecation 警告) |
|
|
654
656
|
|
|
657
|
+
#### V4.5.7 详细内容
|
|
658
|
+
|
|
659
|
+
**Bug 修复**:
|
|
660
|
+
|
|
661
|
+
| 问题 | 根因 | 修复 |
|
|
662
|
+
|------|------|------|
|
|
663
|
+
| 告警"查看文件详情"Modal 点不开 | 事件处理中 `state.pageData?.alerts` 路径错误,告警数据实际存储在 `state.pageData.dashboard.alerts` | 修正为 `state.pageData?.dashboard?.alerts`,同时修正 projectPath 取值路径 |
|
|
664
|
+
|
|
665
|
+
**Dashboard 服务端口复用(单例模式)**:
|
|
666
|
+
|
|
667
|
+
| 改进 | 说明 |
|
|
668
|
+
|------|------|
|
|
669
|
+
| 模块级单例 `_instance` | `startDashboardServer` 首次调用时创建 HTTP 服务并缓存到 `_instance`(含 server/port/registry/token) |
|
|
670
|
+
| 热加载新项目 | 后续调用检测到 `_instance` 已存在时,不创建新服务,而是调用 `_mergeProjects` 将新路径合并到已有 registry 中 |
|
|
671
|
+
| 去重机制 | `_mergeProjects` 按 `_path.toLowerCase()` 去重,相同项目不重复注册 |
|
|
672
|
+
| 透明生效 | registry 是引用传递,已运行的 HTTP handler 闭包自动读取最新 registry,无需重启服务 |
|
|
673
|
+
| 导出 `getInstance()` | 外部可通过 `getInstance()` 获取当前运行实例的 server/port/registry 信息 |
|
|
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
|
+
|
|
655
686
|
#### V4.5.x 新增配置参考
|
|
656
687
|
|
|
657
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': '双重',
|
|
@@ -1540,10 +1550,10 @@ function setupEvents() {
|
|
|
1540
1550
|
}
|
|
1541
1551
|
const modalBtn = e.target.closest('[data-alert-files-modal]');
|
|
1542
1552
|
if (modalBtn) {
|
|
1543
|
-
const alerts = state.pageData?.alerts;
|
|
1553
|
+
const alerts = state.pageData?.dashboard?.alerts;
|
|
1544
1554
|
const files = alerts?.latest?.files || [];
|
|
1545
1555
|
if (files.length > 0) {
|
|
1546
|
-
const proj = state.pageData?.
|
|
1556
|
+
const proj = state.pageData?.dashboard?.watcher?.path || '';
|
|
1547
1557
|
openFileModal(t('modal.alertFiles'), files, proj, '');
|
|
1548
1558
|
}
|
|
1549
1559
|
return;
|
|
@@ -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);
|
|
@@ -186,19 +201,47 @@ function handleApi(pathname, query, registry, res) {
|
|
|
186
201
|
return notFound(res);
|
|
187
202
|
}
|
|
188
203
|
|
|
189
|
-
/* ── Server
|
|
204
|
+
/* ── Server (singleton) ─────────────────────────────────────── */
|
|
205
|
+
|
|
206
|
+
let _instance = null;
|
|
207
|
+
|
|
208
|
+
function _mergeProjects(registry, paths) {
|
|
209
|
+
const seen = new Set([...registry.values()].map(p => p._path.toLowerCase()));
|
|
210
|
+
let maxIdx = registry.size;
|
|
211
|
+
let added = 0;
|
|
212
|
+
for (const raw of paths) {
|
|
213
|
+
const resolved = path.resolve(raw);
|
|
214
|
+
if (seen.has(resolved.toLowerCase())) continue;
|
|
215
|
+
seen.add(resolved.toLowerCase());
|
|
216
|
+
const id = `p${maxIdx++}`;
|
|
217
|
+
const name = path.basename(resolved) || resolved;
|
|
218
|
+
const label = resolved.length > 50 ? '...' + resolved.slice(-47) : resolved;
|
|
219
|
+
registry.set(id, { id, name, pathLabel: label, _path: resolved });
|
|
220
|
+
added++;
|
|
221
|
+
}
|
|
222
|
+
return added;
|
|
223
|
+
}
|
|
190
224
|
|
|
191
225
|
/**
|
|
192
|
-
* Start the dashboard HTTP server.
|
|
193
|
-
*
|
|
226
|
+
* Start the dashboard HTTP server, or hot-add projects to an existing instance.
|
|
227
|
+
* Uses a module-level singleton: subsequent calls reuse the same port and server,
|
|
228
|
+
* merging new project paths into the live registry.
|
|
194
229
|
*
|
|
195
230
|
* @param {string[]} paths - Project directories to serve
|
|
196
231
|
* @param {object} [opts]
|
|
197
|
-
* @param {number} [opts.port=3120] - Starting port
|
|
232
|
+
* @param {number} [opts.port=3120] - Starting port (ignored if server already running)
|
|
198
233
|
* @param {boolean} [opts.silent=false] - Suppress banner output
|
|
199
234
|
* @returns {Promise<{server: http.Server, port: number, registry: Map}>}
|
|
200
235
|
*/
|
|
201
236
|
function startDashboardServer(paths, opts = {}) {
|
|
237
|
+
if (_instance) {
|
|
238
|
+
const added = _mergeProjects(_instance.registry, paths);
|
|
239
|
+
if (added > 0 && !opts.silent) {
|
|
240
|
+
console.log(` [dashboard] Hot-added ${added} project(s) — total: ${_instance.registry.size} on port ${_instance.port}`);
|
|
241
|
+
}
|
|
242
|
+
return Promise.resolve(_instance);
|
|
243
|
+
}
|
|
244
|
+
|
|
202
245
|
const port = opts.port || DEFAULT_PORT;
|
|
203
246
|
const silent = opts.silent || false;
|
|
204
247
|
const registry = buildRegistry(paths);
|
|
@@ -209,7 +252,6 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
209
252
|
let retries = 0;
|
|
210
253
|
|
|
211
254
|
const server = http.createServer((req, res) => {
|
|
212
|
-
// DNS rebinding protection: reject unexpected Host headers
|
|
213
255
|
const host = req.headers.host || '';
|
|
214
256
|
if (!ALLOWED_HOSTS.test(host)) {
|
|
215
257
|
res.writeHead(403);
|
|
@@ -224,7 +266,6 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
224
266
|
try { parsed = new URL(req.url, `http://${host}`); }
|
|
225
267
|
catch { return notFound(res); }
|
|
226
268
|
|
|
227
|
-
// API endpoints require per-process token
|
|
228
269
|
if (parsed.pathname.startsWith('/api/')) {
|
|
229
270
|
const reqToken = parsed.searchParams.get('token');
|
|
230
271
|
if (reqToken !== token) {
|
|
@@ -249,6 +290,7 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
249
290
|
|
|
250
291
|
server.on('listening', () => {
|
|
251
292
|
const addr = server.address();
|
|
293
|
+
_instance = { server, port: addr.port, registry, token };
|
|
252
294
|
if (!silent) {
|
|
253
295
|
console.log('');
|
|
254
296
|
console.log(' Cursor Guard Dashboard');
|
|
@@ -260,7 +302,7 @@ function startDashboardServer(paths, opts = {}) {
|
|
|
260
302
|
}
|
|
261
303
|
console.log('');
|
|
262
304
|
}
|
|
263
|
-
resolve(
|
|
305
|
+
resolve(_instance);
|
|
264
306
|
});
|
|
265
307
|
|
|
266
308
|
server.listen(currentPort, '127.0.0.1');
|
|
@@ -277,4 +319,4 @@ if (require.main === module) {
|
|
|
277
319
|
});
|
|
278
320
|
}
|
|
279
321
|
|
|
280
|
-
module.exports = { startDashboardServer };
|
|
322
|
+
module.exports = { startDashboardServer, getInstance: () => _instance };
|