cursor-guard 4.5.6 → 4.5.7

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