cursor-guard 4.3.3 → 4.3.4

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/README.md CHANGED
@@ -377,6 +377,13 @@ The skill activates on these signals:
377
377
 
378
378
  ## Changelog
379
379
 
380
+ ### v4.3.4
381
+
382
+ - **Improve**: Log rotation — `backup.log` now rotates at 1MB, keeping up to 3 old files (`backup.log.1`, `.2`, `.3`). Rotation runs on watcher startup and every 100 writes
383
+ - **Improve**: Watcher single-instance protection — lock file now includes startup timestamp; locks older than 24h are auto-cleaned even if PID check is unreliable on Windows
384
+ - **Improve**: `previewProjectRestore` output grouped — protected paths (`.cursor/`, `.gitignore`, `.cursor-guard.json`) summarized as `protectedPaths: { count: N }` instead of listing thousands of individual files, drastically reducing token cost
385
+ - **Improve**: SKILL.md Hard Rule #15 — agents must commit skill files after upgrade to ensure `restore_project` HEAD protection works correctly
386
+
380
387
  ### v4.3.3
381
388
 
382
389
  - **Feature**: Intent context for snapshots — `snapshot_now` now accepts `intent`, `agent`, and `session` parameters, stored as Git commit trailers to form an audit trail per operation
package/README.zh-CN.md CHANGED
@@ -377,6 +377,13 @@ node references\dashboard\server.js --path "D:\MyProject"
377
377
 
378
378
  ## 更新日志
379
379
 
380
+ ### v4.3.4
381
+
382
+ - **改进**:日志轮转——`backup.log` 超过 1MB 自动轮转,保留最近 3 个旧文件。watcher 启动时和每 100 次写入时检查
383
+ - **改进**:Watcher 单实例保护——锁文件新增启动时间戳;超过 24 小时的锁即使 PID 检查不可靠(Windows)也自动清理
384
+ - **改进**:`previewProjectRestore` 分组输出——受保护路径(`.cursor/`、`.gitignore`、`.cursor-guard.json`)汇总为 `protectedPaths: { count: N }`,不再逐一列出数千个文件,大幅降低 token 消耗
385
+ - **改进**:SKILL.md 硬规则 #15——升级后 Agent 必须提交 skill 文件,确保 `restore_project` 的 HEAD 保护机制生效
386
+
380
387
  ### v4.3.3
381
388
 
382
389
  - **功能**:快照意图上下文——`snapshot_now` 新增 `intent`(操作意图)、`agent`(AI 模型)、`session`(会话 ID)参数,作为 Git commit trailer 存储,形成按操作事件的审计链
package/ROADMAP.md CHANGED
@@ -3,8 +3,8 @@
3
3
  > 本文档描述 cursor-guard 从 V2 到 V7 的长期演进方向。
4
4
  > 每一代向下兼容,低版本功能永远不废弃。
5
5
  >
6
- > **当前版本**:`V4.0.0`
7
- > **文档状态**:`V2` ~ `V4.0` 已实施,`V5+` 规划中
6
+ > **当前版本**:`V4.3.3`
7
+ > **文档状态**:`V2` ~ `V4.3` 已实施(含 V5 intent 基础),`V5` 主体规划中
8
8
 
9
9
  ## 阅读导航
10
10
 
@@ -20,8 +20,8 @@
20
20
  |---|---|---|---|
21
21
  | `V2` | 能用 | Skill + Script | "AI 弄丢代码能恢复" |
22
22
  | `V3` | 更稳 | + Core 抽取 + 可选 MCP | "恢复操作更标准、更省 token" |
23
- | `V4` | 更聪明 | + 主动检测 + 可观测 | "cursor-guard 会主动提醒你" ✅ |
24
- | `V5` | 成闭环 | + 变更控制层 | "AI 代码变更可预防、可追溯、可按事件恢复" |
23
+ | `V4` | 更聪明 | + 主动检测 + 可观测 + Web 仪表盘 + Intent 基础 | "cursor-guard 会主动提醒你,还能看到为什么备份" ✅ |
24
+ | `V5` | 成闭环 | + 变更控制层 | "AI 代码变更可预防、可追溯、可按事件恢复"(intent 基础已在 V4.3 落地) |
25
25
  | `V6` | 成标准 | + 开放协议 + 团队工作流 | "把 AI 代码变更安全做成跨工具标准" |
26
26
  | `V7` | 可证明 | + 可验证信任 + 治理层 | "能证明安全流程被执行了" |
27
27
 
@@ -64,7 +64,9 @@ V2-V3 的目标是让用户"离不开",V6-V7 的目标是让行业"绕不开"
64
64
  | `Node Core` | `V3.0` ✅ 已完成 | 6 个模块:doctor / doctor-fix / snapshot / backups / restore / status | 否 |
65
65
  | `MCP Server` | `V3.1-V4.0` ✅ 已完成 | 9 个工具,Agent 标准调用入口,结构化 JSON 返回 | 否 |
66
66
  | `智能提醒 / 可观测` | `V4.0` ✅ 已完成 | 主动发现风险、汇总健康状态(anomaly + dashboard) | 否 |
67
- | `变更控制层` | `V5` 规划 | 覆盖编辑意图、冲突告警、影响半径、审计事件、按事件恢复 | 否 |
67
+ | `Web 仪表盘` | `V4.2` 已完成 | 本地只读 Web UI,备份/恢复点/诊断/保护范围,中英双语 | 否 |
68
+ | `备份上下文 + Intent` | `V4.3` ✅ 已完成 | 结构化 commit trailer(Files-Changed/Summary/Trigger/Intent/Agent/Session),仪表盘可追溯 | 否 |
69
+ | `变更控制层` | `V5` 规划(intent 基础已在 V4.3 落地) | 覆盖编辑意图、冲突告警、影响半径、审计事件、按事件恢复 | 否 |
68
70
  | `开放协议 / 团队工作流` | `V6` 规划 | 把变更控制能力提炼成跨工具协议、适配器和 CI 查询入口 | 否 |
69
71
  | `治理 / 可验证层` | `V7` 规划 | 让“是否走过安全流程”变成可以证明、可以审计、可以验证的事实 | 否 |
70
72
 
@@ -440,10 +442,25 @@ V4 经过 4 轮系统性代码审查,修复了以下关键问题:
440
442
  | 三审 | 4 项 | `doctor-fix.js` stale-lock PID 正则修正(`pid=` 格式);Git pre-restore ref 加毫秒防碰撞;非 Git pre-restore shadow 同步加碰撞检测;`doctor.js` shadow 目录正则支持毫秒后缀 |
441
443
  | 四审 | 3+2 项 | `createGitSnapshot` protect basename 语义对齐(`git add -A` + `matchesAny` 裁剪替代 `git add -- <pattern>`);`unquoteGitPath()` 反解 C-quoted 路径(支持 UTF-8 octal);pre-restore ref / shadow snapshot 碰撞重试循环(最多 1000 次);`previewProjectRestore` 正确解析 rename/copy 条目 |
442
444
 
445
+ ### V4.x 增量(全部已完成 ✅)
446
+
447
+ | 版本 | 新增 | 状态 |
448
+ |---|---|---|
449
+ | V4.1 | 用户反馈修复:fileCount 精度、安装流程、doctor MCP 检测、PowerShell 兼容 | ✅ |
450
+ | V4.2.0 | **Web 仪表盘**:本地只读 Web UI,健康总览、备份表格、恢复点抽屉、诊断、保护范围。中英双语、自动刷新、多项目支持 | ✅ |
451
+ | V4.2.1 | 代码审查修复:`t()` replaceAll、未用导入清理、过滤栏补全、跨平台 shell 兼容、去重 | ✅ |
452
+ | V4.2.2 | `restore_project` 保护 `.cursor-guard.json`;init 提示 `git commit` | ✅ |
453
+ | V4.3.0 | **备份上下文元数据**:Git commit 结构化 trailer(Files-Changed / Summary / Trigger),仪表盘"变更"列 | ✅ |
454
+ | V4.3.1 | `restore_project` 保护 `.gitignore`;`cursor-guard-index.lock` 清理;summary 按 protect/ignore 过滤 + 分类格式 | ✅ |
455
+ | V4.3.2 | `cursor-guard-init` 自动添加根目录 `node_modules/` 到 `.gitignore`;doctor MCP 版本提示含重载快捷键 | ✅ |
456
+ | V4.3.3 | **Intent 上下文**(V5 基础前置):`snapshot_now` 支持 `intent` / `agent` / `session` 参数,Git trailer 存储,仪表盘展示意图徽章和完整审计字段 | ✅ |
457
+
458
+ > **注**:V4.2 的 Web 仪表盘最初在 V4.0 规划中标记为"不做",但用户需求明确后实施。事实证明只读仪表盘投入产出比合理,且不违反安全原则。
459
+
443
460
  ### V4 不做的事
444
461
 
445
462
  - 不做自动恢复(恢复永远需要人确认,这是产品底线)
446
- - 不做 Web 仪表盘
463
+ - ~~不做 Web 仪表盘~~ → V4.2 已实施(只读、本地、零依赖)
447
464
  - 不做云端同步
448
465
 
449
466
  ### 进入 V5 的衡量标准
@@ -494,10 +511,10 @@ V5 不是"三个方向选一个",而是把下面这条链路做完整:
494
511
 
495
512
  | 模块 / 能力 | AI 要做什么 | 建议产物 | 完成标准 |
496
513
  |---|---|---|---|
497
- | `intent registry` | 在高风险写入前注册编辑意图,记录 agent、会话、工作区、分支、目标文件、风险级别 | `core/intent.*` 或同等模块 | 能列出活跃会话,能释放会话,能查到谁准备改哪个文件 |
514
+ | `intent registry` | 在高风险写入前注册编辑意图,记录 agent、会话、工作区、分支、目标文件、风险级别 | `core/intent.*` 或同等模块 | 能列出活跃会话,能释放会话,能查到谁准备改哪个文件。**基础版已在 V4.3.3 落地**:`snapshot_now` 支持 `intent` / `agent` / `session` 参数,存储为 Git commit trailer,仪表盘可展示 |
498
515
  | `pre-edit snapshot` | 在每次高风险 AI 写入前创建 `refs/guard/pre-edit/*` 恢复点 | `refs/guard/pre-edit/<session>/<seq>` | 任意一条 AI 编辑事件都能关联到写前快照 |
499
516
  | `conflict detection` | 先做文件路径级冲突检测,再预留符号级增强位 | `detectConflicts()` / `listConflicts()` | 两个会话同时改重叠文件时能给出 advisory warning |
500
- | `audit store` | 以 append-only 方式保存 AI 编辑事件 | 默认本地 `JSONL`;后续可升级 `SQLite` | 能按文件 / 会话 / agent / 时间 / 风险级别查询 |
517
+ | `audit store` | 以 append-only 方式保存 AI 编辑事件 | 默认本地 `JSONL`;后续可升级 `SQLite` | 能按文件 / 会话 / agent / 时间 / 风险级别查询。**雏形已在 V4.3.0-V4.3.3 落地**:审计元数据通过 Git commit trailer 持久化,`listBackups` 可按 trigger/intent/agent/session 解析 |
501
518
  | `restore by event` | 允许从一条审计事件直接跳转并执行恢复 | `restore_from_event` | 给定 `event_id` 可以定位 `before_ref` 或 `restore_ref` |
502
519
  | `impact set` | 为高风险编辑记录受影响文件 / 符号 / 测试集合 | `impact_set` 字段 | 查询事件时能看到"这次改动可能波及哪里" |
503
520
  | `MCP / CLI surface` | 暴露最小可用接口给 Agent 和终端 | `register_intent` / `list_active_intents` / `audit_query` / `get_event` / `restore_from_event` | AI 不需要拼复杂 shell,就能完成查询与恢复 |
@@ -935,8 +952,8 @@ V7.2 完整 attestation(安全操作证明链)
935
952
 
936
953
  | | V2 | V3 | V4 | V5 | V6 | V7 |
937
954
  |---|---|---|---|---|---|---|
938
- | **一句话** | 能恢复 | 更稳更省 | 主动提醒 | 变更闭环 | 跨工具标准 | 可证明 |
939
- | **核心架构** | Skill + Script | + Core + MCP | + 智能检测 | + 变更控制层 | + 开放协议 + 适配器 | + 治理层 |
955
+ | **一句话** | 能恢复 | 更稳更省 | 主动提醒 + 可观测 + Intent | 变更闭环 | 跨工具标准 | 可证明 |
956
+ | **核心架构** | Skill + Script | + Core + MCP | + 智能检测 + Web 仪表盘 + Intent 基础 | + 变更控制层 | + 开放协议 + 适配器 | + 治理层 |
940
957
  | **Agent 调用** | 拼 shell | 优先 MCP | MCP + 主动建议 | MCP + 意图 / 审计 / 恢复 | 标准接口 + 适配器 | 标准接口 + 审计 |
941
958
  | **安装门槛** | 最低 | 不变 | 不变 | 略增 | 看具体实现 | 看具体实现 |
942
959
  | **适合谁** | 所有人 | 所有人 | 所有人 | 重度 AI 用户 + 团队试点 | 工具开发者 + 团队 | 企业 + 合规场景 |
@@ -957,8 +974,11 @@ V3.4 ────── ✅ MCP 自检
957
974
 
958
975
  │ 前提:MCP 调用成功率 > 95%,token 消耗可观测下降
959
976
 
960
- V4.0 ────── ✅ 智能恢复建议 + 备份健康看板 + 4 轮代码审查加固(138 测试) ← 当前版本
961
- V4.x ────── 候选支线(完整性校验 / 多项目概览)
977
+ V4.0 ────── ✅ 智能恢复建议 + 备份健康看板 + 4 轮代码审查加固(138 测试)
978
+ V4.1 ────── 用户反馈修复
979
+ V4.2 ────── ✅ Web 仪表盘(只读、双语、多项目)+ 代码审查修复
980
+ V4.3 ────── ✅ 备份上下文元数据 + Intent 上下文 + restore 安全加固 ← 当前版本
981
+ V4.x ────── 候选支线(完整性校验 / 更丰富的审计查询)
962
982
 
963
983
  │ 前提:AI 编辑需要更强的追溯 / 恢复 / 查询闭环
964
984
  │ 前提:多 Agent / 多工具协作成为真实场景
@@ -1002,13 +1022,15 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
1002
1022
 
1003
1023
  ## 给用户说的话
1004
1024
 
1005
- ### 现在(V4)
1025
+ ### 现在(V4.3
1006
1026
 
1007
1027
  > cursor-guard 已经能保护你的代码,而且越来越聪明。
1008
1028
  > 自动备份、写前快照、确定性恢复——开箱即用。
1009
1029
  > V3 新增:MCP 工具调用(可选)让 AI 操作更稳、更快、更省 token。
1010
- > V4 新增:系统会主动监测异常变更并提醒你,一个 `dashboard` 就能看全局健康状态。
1011
- > 经过 4 轮代码审查,138 个测试覆盖所有核心路径。
1030
+ > V4.0 新增:系统会主动监测异常变更并提醒你,一个 `dashboard` 就能看全局健康状态。
1031
+ > V4.2 新增:本地 Web 仪表盘——健康、备份、恢复点、诊断一页可见,中英双语自动刷新。
1032
+ > V4.3 新增:每次备份带上下文(改了什么、为什么备份、哪个 AI 在操作),在仪表盘可追溯。
1033
+ > 经过 4 轮代码审查,138+ 个测试覆盖所有核心路径。
1012
1034
 
1013
1035
  ### 未来
1014
1036
 
@@ -1027,7 +1049,7 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
1027
1049
  | 自动恢复(无人确认) | 恢复操作必须有人确认,这是产品底线 |
1028
1050
  | 自动 push 到远程 | 本地优先,push 需用户明确指令 |
1029
1051
  | 云端备份服务 | cursor-guard 的定位是本地安全网 |
1030
- | Web 仪表盘 | 投入产出比不合理 |
1052
+ | ~~Web 仪表盘~~ | ~~投入产出比不合理~~ → V4.2 已实施只读仪表盘(本地、零依赖、不执行写操作) |
1031
1053
  | 取代 Git | cursor-guard 是 Git 的增强,不是替代 |
1032
1054
  | AI 行为限制 | cursor-guard 是安全网,不是笼子 |
1033
1055
  | 多 IDE 全面适配 | 聚焦 Cursor,其他 IDE 由社区或 V6 协议解决 |
@@ -1036,5 +1058,5 @@ V7 的"可验证治理"是这条产品线的逻辑终点——该保护的都保
1036
1058
 
1037
1059
  ---
1038
1060
 
1039
- *最后更新:2026-03-21*
1040
- *版本:v1.2(V4.0 最终交付,含 4 轮代码审查加固)*
1061
+ *最后更新:2026-03-22*
1062
+ *版本:v1.3(V4.3.3 交付,含 Web 仪表盘、备份上下文元数据、Intent 基础)*
package/SKILL.md CHANGED
@@ -583,6 +583,7 @@ Skip the block for unrelated turns.
583
583
  12. **Concurrent agents**: if multiple Agent threads are active, warn the user to avoid simultaneous writes to the same file. Snapshots cannot prevent race conditions between parallel agents.
584
584
  13. **Preservation must not pollute** — all pre-restore backups use temp index + dedicated ref (`refs/guard/pre-restore`). The user's staging area, working tree, and commit history on their branch are never modified by the preservation process.
585
585
  14. **Do not restore `.cursor/` directory** — `.cursor/skills/`, `.cursor/rules/`, and `.cursor/mcp.json` are tool infrastructure, not project code. `restore_project` automatically protects this directory. Never include `.cursor/` in manual restore commands unless the user explicitly requests it. Restoring `.cursor/skills/cursor-guard/` would downgrade the protection tool itself.
586
+ 15. **Commit skill files after upgrade** — after running `cursor-guard-init` or upgrading the skill, immediately commit `.cursor/skills/cursor-guard/` and `.cursor-guard.json` to HEAD (`git add .cursor/ .cursor-guard.json && git commit -m "chore: upgrade cursor-guard"`). This ensures `restore_project`'s HEAD protection mechanism can preserve the current skill version. Without this commit, a restore could revert the skill to an older version.
586
587
 
587
588
  ---
588
589
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cursor-guard",
3
- "version": "4.3.3",
3
+ "version": "4.3.4",
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",
@@ -72,24 +72,39 @@ async function runBackup(projectDir, intervalOverride) {
72
72
  // Ensure backup dir
73
73
  fs.mkdirSync(backupDir, { recursive: true });
74
74
 
75
- // Lock file with stale detection
75
+ // Lock file with stale detection (PID + age)
76
+ const LOCK_MAX_AGE_MS = 24 * 60 * 60 * 1000;
76
77
  if (fs.existsSync(lockFile)) {
77
78
  let stale = false;
79
+ let reason = '';
78
80
  try {
79
81
  const content = fs.readFileSync(lockFile, 'utf-8');
80
82
  const pidMatch = content.match(/pid=(\d+)/);
81
- if (pidMatch) {
82
- const oldPid = parseInt(pidMatch[1], 10);
83
- if (!isProcessAlive(oldPid)) {
83
+ const startedMatch = content.match(/started=(.+)/);
84
+ const oldPid = pidMatch ? parseInt(pidMatch[1], 10) : null;
85
+ const startedAt = startedMatch ? Date.parse(startedMatch[1]) : NaN;
86
+
87
+ if (oldPid && !isProcessAlive(oldPid)) {
88
+ stale = true;
89
+ reason = `pid ${oldPid} not running`;
90
+ } else if (!isNaN(startedAt) && Date.now() - startedAt > LOCK_MAX_AGE_MS) {
91
+ stale = true;
92
+ reason = `lock age > 24h (started ${startedMatch[1]})`;
93
+ } else if (!oldPid) {
94
+ const stat = fs.statSync(lockFile);
95
+ if (Date.now() - stat.mtimeMs > LOCK_MAX_AGE_MS) {
84
96
  stale = true;
85
- console.log(color.yellow(`[guard] Stale lock detected (pid ${oldPid} not running). Cleaning up.`));
86
- fs.unlinkSync(lockFile);
97
+ reason = 'lock file too old (no PID, mtime > 24h)';
87
98
  }
88
99
  }
89
100
  } catch { /* ignore */ }
90
- if (!stale) {
101
+ if (stale) {
102
+ console.log(color.yellow(`[guard] Stale lock detected (${reason}). Cleaning up.`));
103
+ try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
104
+ } else {
91
105
  console.log(color.red(`[guard] ERROR: Lock file exists (${lockFile}).`));
92
- console.log(color.red(' If no other instance is running, delete it and retry.'));
106
+ console.log(color.red(' Another watcher instance may be running.'));
107
+ console.log(color.red(' If no other instance is running, delete the lock file and retry.'));
93
108
  process.exit(1);
94
109
  }
95
110
  }
@@ -170,7 +170,7 @@ function restoreFile(projectDir, file, source, opts = {}) {
170
170
  *
171
171
  * @param {string} projectDir
172
172
  * @param {string} source - Commit hash or ref
173
- * @returns {{ status: 'ok'|'error', files?: Array<{path: string, change: 'modified'|'added'|'deleted'}>, totalChanged?: number, error?: string }}
173
+ * @returns {{ status: 'ok'|'error', files?: Array<{path: string, change: string}>, protectedPaths?: {count: number, note?: string}, totalChanged?: number, error?: string }}
174
174
  */
175
175
  function previewProjectRestore(projectDir, source) {
176
176
  if (!isGitRepo(projectDir)) {
@@ -218,13 +218,27 @@ function previewProjectRestore(projectDir, source) {
218
218
  }
219
219
  }
220
220
 
221
+ const protectedFiles = [];
222
+ const projectFiles = [];
221
223
  for (const f of files) {
222
224
  if (isToolPath(f.path)) {
223
- f.warning = 'protected path — will be preserved from HEAD to prevent tool/config downgrade';
225
+ protectedFiles.push(f);
226
+ } else {
227
+ projectFiles.push(f);
224
228
  }
225
229
  }
226
230
 
227
- return { status: 'ok', files, totalChanged: files.length };
231
+ return {
232
+ status: 'ok',
233
+ files: projectFiles,
234
+ protectedPaths: {
235
+ count: protectedFiles.length,
236
+ note: protectedFiles.length > 0
237
+ ? 'these paths (.cursor/, .cursor-guard.json, .gitignore) will be preserved from HEAD'
238
+ : undefined,
239
+ },
240
+ totalChanged: files.length,
241
+ };
228
242
  } catch (e) {
229
243
  return { status: 'error', error: e.message };
230
244
  }
@@ -354,19 +354,31 @@ function timestamp() {
354
354
  return new Date().toISOString().replace('T', ' ').substring(0, 19);
355
355
  }
356
356
 
357
- function createLogger(logFilePath, maxSizeMB = 10) {
357
+ function createLogger(logFilePath, { maxSizeMB = 1, maxFiles = 3 } = {}) {
358
358
  let writeCount = 0;
359
- function rotateIfNeeded() {
360
- if (++writeCount % 100 !== 0) return;
359
+ const maxBytes = maxSizeMB * 1024 * 1024;
360
+
361
+ function rotate() {
361
362
  try {
362
363
  const stat = fs.statSync(logFilePath);
363
- if (stat.size > maxSizeMB * 1024 * 1024) {
364
- const old = logFilePath + '.old';
365
- try { fs.unlinkSync(old); } catch { /* ignore */ }
366
- fs.renameSync(logFilePath, old);
367
- }
368
- } catch { /* ignore */ }
364
+ if (stat.size <= maxBytes) return;
365
+ } catch { return; }
366
+ for (let i = maxFiles - 1; i >= 1; i--) {
367
+ const from = i === 1 ? logFilePath + '.1' : logFilePath + '.' + i;
368
+ const to = logFilePath + '.' + (i + 1);
369
+ try { fs.renameSync(from, to); } catch { /* ignore */ }
370
+ }
371
+ try { fs.renameSync(logFilePath, logFilePath + '.1'); } catch { /* ignore */ }
372
+ try { fs.unlinkSync(logFilePath + '.' + (maxFiles + 1)); } catch { /* ignore */ }
369
373
  }
374
+
375
+ rotate();
376
+
377
+ function rotateIfNeeded() {
378
+ if (++writeCount % 100 !== 0) return;
379
+ rotate();
380
+ }
381
+
370
382
  return {
371
383
  log(msg, c = 'green') {
372
384
  const line = `${timestamp()} ${msg}`;