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.8`(V4 最终版)
7
- > **文档状态**:`V2` ~ `V4.5.8` 已完成交付(含 V5 intent/audit 基础),`V5` 主体规划中
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` 端点 | 新增无需 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 |
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.8",
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.restart': 'How to restart',
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.restart': '如何重启',
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-hint-btn" title="${t('upgrade.hint')}">${t('upgrade.restart')}</button>
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')}">&times;</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 { getDashboard } = require('../lib/core/dashboard');
10
- const { runDiagnostics } = require('../lib/core/doctor');
11
- const { listBackups, getBackupFiles } = require('../lib/core/backups');
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
- const SERVER_VERSION = require('../../package.json').version;
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) {