cursor-guard 3.4.0 → 4.1.0
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 +36 -23
- package/README.zh-CN.md +36 -23
- package/ROADMAP.md +309 -103
- package/SKILL.md +20 -6
- package/package.json +2 -1
- package/references/bin/cursor-guard-init.js +120 -0
- package/references/config-reference.md +40 -0
- package/references/config-reference.zh-CN.md +40 -0
- package/references/cursor-guard.example.json +6 -0
- package/references/cursor-guard.schema.json +30 -0
- package/references/lib/auto-backup.js +66 -8
- package/references/lib/core/anomaly.js +217 -0
- package/references/lib/core/backups.js +9 -9
- package/references/lib/core/core.test.js +600 -0
- package/references/lib/core/dashboard.js +208 -0
- package/references/lib/core/doctor-fix.js +1 -1
- package/references/lib/core/doctor.js +26 -8
- package/references/lib/core/restore.js +78 -23
- package/references/lib/core/snapshot.js +45 -19
- package/references/lib/core/status.js +4 -4
- package/references/lib/utils.js +72 -5
- package/references/mcp/mcp.test.js +98 -3
- package/references/mcp/server.js +74 -12
- package/references/quickstart.zh-CN.md +23 -1
package/SKILL.md
CHANGED
|
@@ -23,6 +23,8 @@ Use this skill when any of the following appear:
|
|
|
23
23
|
- **Health check**: e.g. "guard doctor", "检查备份配置", "自检", "诊断guard", "check guard setup", "MCP 能用吗". If MCP `doctor` tool is available, call `doctor { "path": "<project>" }` and format the result; otherwise run `guard-doctor.ps1` and report results. Doctor output includes an "MCP server" check (SDK installed + server.js present). If doctor reports FAIL items, suggest running `doctor_fix` (MCP) or guide the user through manual fixes.
|
|
24
24
|
- **Auto-fix**: e.g. "guard fix", "修复配置", "自动修复". If MCP `doctor_fix` tool is available, call `doctor_fix { "path": "<project>", "dry_run": true }` first to preview, then `doctor_fix { "path": "<project>" }` to apply. Without MCP, guide the user through manual steps based on doctor output.
|
|
25
25
|
- **Backup status**: e.g. "备份状态", "guard status", "watcher 在跑吗", "最近一次备份". If MCP `backup_status` tool is available, call `backup_status { "path": "<project>" }` and format the structured result for the user (watcher running/stale, last backup time, strategy, ref counts, disk). Without MCP, check lock file existence and `git log` manually.
|
|
26
|
+
- **Health dashboard**: e.g. "看板", "dashboard", "健康状态", "备份总览", "guard 概况". If MCP `dashboard` tool is available, call `dashboard { "path": "<project>" }` and present the structured dashboard (strategy, last backup, counts, disk usage, protection scope, health status, alerts). Format as a clear summary for the user.
|
|
27
|
+
- **Alert check**: e.g. "有告警吗", "alert status", "变更异常", "风险提示". If MCP `alert_status` tool is available, call `alert_status { "path": "<project>" }` to check for active change-velocity alerts. Report whether an alert is active and its details.
|
|
26
28
|
|
|
27
29
|
If none of the above, do not expand scope; answer normally.
|
|
28
30
|
|
|
@@ -58,7 +60,17 @@ On first trigger in a session, check if the workspace root contains `.cursor-gua
|
|
|
58
60
|
|
|
59
61
|
// Retention for Git auto-backup branch. Disabled by default.
|
|
60
62
|
// "count": keep N newest commits. "days": keep commits from last N days.
|
|
61
|
-
"git_retention": { "enabled": false, "mode": "count", "max_count": 200 }
|
|
63
|
+
"git_retention": { "enabled": false, "mode": "count", "max_count": 200 },
|
|
64
|
+
|
|
65
|
+
// V4: Proactive change-velocity detection (default: on).
|
|
66
|
+
// When enabled, the watcher monitors file change frequency and raises
|
|
67
|
+
// alerts when abnormal patterns are detected (e.g. 20+ files in 10s).
|
|
68
|
+
"proactive_alert": true,
|
|
69
|
+
"alert_thresholds": {
|
|
70
|
+
"files_per_window": 20, // trigger threshold
|
|
71
|
+
"window_seconds": 10, // sliding window
|
|
72
|
+
"cooldown_seconds": 60 // min gap between alerts
|
|
73
|
+
}
|
|
62
74
|
}
|
|
63
75
|
```
|
|
64
76
|
|
|
@@ -81,7 +93,7 @@ On first trigger in a session, check if the workspace root contains `.cursor-gua
|
|
|
81
93
|
|
|
82
94
|
cursor-guard provides an **MCP server** (`cursor-guard-mcp`) as an optional enhancement. When available, prefer MCP tool calls over shell commands — they are faster, return structured JSON, and consume fewer tokens.
|
|
83
95
|
|
|
84
|
-
**Detection**: at the start of a session, check if the following MCP tools are available in your tool list: `doctor`, `list_backups`, `snapshot_now`, `restore_file`, `restore_project`. If **any** of them exists, use MCP for that operation; otherwise, fall back to shell commands as described in the sections below.
|
|
96
|
+
**Detection**: at the start of a session, check if the following MCP tools are available in your tool list: `doctor`, `list_backups`, `snapshot_now`, `restore_file`, `restore_project`, `dashboard`, `alert_status`. If **any** of them exists, use MCP for that operation; otherwise, fall back to shell commands as described in the sections below.
|
|
85
97
|
|
|
86
98
|
**Routing table** (MCP tool → replaces which shell workflow):
|
|
87
99
|
|
|
@@ -95,6 +107,8 @@ cursor-guard provides an **MCP server** (`cursor-guard-mcp`) as an optional enha
|
|
|
95
107
|
| Restore single file | `restore_file` | §5a Step 5 git restore |
|
|
96
108
|
| Preview project restore | `restore_project` (preview=true) | §5a Step 5 git diff |
|
|
97
109
|
| Execute project restore | `restore_project` (preview=false) | §5a Step 5 git restore -- . |
|
|
110
|
+
| Backup health dashboard | `dashboard` | manual: combine backup_status + git/shadow stats |
|
|
111
|
+
| Change-velocity alerts | `alert_status` | manual: check alert file in .git/ or .cursor-guard-backup/ |
|
|
98
112
|
|
|
99
113
|
**Rules**:
|
|
100
114
|
- MCP results are JSON — parse `status`, `error`, and data fields; do not re-run shell to verify.
|
|
@@ -188,7 +202,7 @@ git update-ref refs/guard/snapshot $commit
|
|
|
188
202
|
|
|
189
203
|
1. **Quick git init** (preferred):
|
|
190
204
|
```
|
|
191
|
-
git init
|
|
205
|
+
git init; git add -A; git commit -m "guard: initial snapshot" --no-verify
|
|
192
206
|
```
|
|
193
207
|
2. **Shadow copy** (fallback if user declines git):
|
|
194
208
|
- Copy the target file(s) to `.cursor-guard-backup/<timestamp>/` via Shell.
|
|
@@ -570,8 +584,8 @@ Skip the block for unrelated turns.
|
|
|
570
584
|
- Recovery commands: [references/recovery.md](references/recovery.md)
|
|
571
585
|
- Auto-backup (Node.js core): [references/lib/auto-backup.js](references/lib/auto-backup.js)
|
|
572
586
|
- Guard doctor (Node.js core): [references/lib/guard-doctor.js](references/lib/guard-doctor.js)
|
|
573
|
-
- Core modules: [references/lib/core/](references/lib/core/) (doctor, doctor-fix, snapshot, backups, restore, status)
|
|
574
|
-
- MCP server: [references/mcp/server.js](references/mcp/server.js) (
|
|
587
|
+
- Core modules: [references/lib/core/](references/lib/core/) (doctor, doctor-fix, snapshot, backups, restore, status, anomaly, dashboard)
|
|
588
|
+
- MCP server: [references/mcp/server.js](references/mcp/server.js) (9 tools: doctor, doctor_fix, backup_status, list_backups, snapshot_now, restore_file, restore_project, dashboard, alert_status)
|
|
575
589
|
- Shared utilities: [references/lib/utils.js](references/lib/utils.js)
|
|
576
590
|
- Config JSON Schema: [references/cursor-guard.schema.json](references/cursor-guard.schema.json)
|
|
577
591
|
- Example config: [references/cursor-guard.example.json](references/cursor-guard.example.json)
|
|
@@ -614,4 +628,4 @@ If your Cursor config supports MCP, add `cursor-guard` as an MCP server for lowe
|
|
|
614
628
|
}
|
|
615
629
|
```
|
|
616
630
|
|
|
617
|
-
Once configured, the
|
|
631
|
+
Once configured, the 9 tools (`doctor`, `doctor_fix`, `backup_status`, `list_backups`, `snapshot_now`, `restore_file`, `restore_project`, `dashboard`, `alert_status`) are available as MCP tool calls. See §0a for routing logic.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"test": "node references/lib/utils.test.js && node references/lib/core/core.test.js && node references/mcp/mcp.test.js"
|
|
28
28
|
},
|
|
29
29
|
"bin": {
|
|
30
|
+
"cursor-guard-init": "references/bin/cursor-guard-init.js",
|
|
30
31
|
"cursor-guard-backup": "references/bin/cursor-guard-backup.js",
|
|
31
32
|
"cursor-guard-doctor": "references/bin/cursor-guard-doctor.js",
|
|
32
33
|
"cursor-guard-mcp": "references/mcp/server.js"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
11
|
+
console.log(`Usage: cursor-guard-init [options]
|
|
12
|
+
|
|
13
|
+
Installs cursor-guard skill into your Cursor skills directory, including
|
|
14
|
+
MCP dependencies and .gitignore entries.
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--global Install to ~/.cursor/skills/ (default: project-local)
|
|
18
|
+
--path <dir> Project directory (default: current dir)
|
|
19
|
+
--help, -h Show this help message
|
|
20
|
+
--version, -v Show version number`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
25
|
+
const pkg = require('../../package.json');
|
|
26
|
+
console.log(pkg.version);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isGlobal = args.includes('--global');
|
|
31
|
+
const pathIdx = args.indexOf('--path');
|
|
32
|
+
const projectDir = path.resolve(pathIdx >= 0 && args[pathIdx + 1] ? args[pathIdx + 1] : '.');
|
|
33
|
+
|
|
34
|
+
const skillSource = path.resolve(__dirname, '../..');
|
|
35
|
+
const skillTarget = isGlobal
|
|
36
|
+
? path.join(process.env.USERPROFILE || process.env.HOME || os.homedir(), '.cursor', 'skills', 'cursor-guard')
|
|
37
|
+
: path.join(projectDir, '.cursor', 'skills', 'cursor-guard');
|
|
38
|
+
|
|
39
|
+
function copyRecursive(src, dest) {
|
|
40
|
+
const stat = fs.statSync(src);
|
|
41
|
+
if (stat.isDirectory()) {
|
|
42
|
+
if (path.basename(src) === 'node_modules') return;
|
|
43
|
+
if (path.basename(src) === '.git') return;
|
|
44
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
45
|
+
for (const entry of fs.readdirSync(src)) {
|
|
46
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
50
|
+
fs.copyFileSync(src, dest);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`\n cursor-guard init\n`);
|
|
55
|
+
console.log(` Source: ${skillSource}`);
|
|
56
|
+
console.log(` Target: ${skillTarget}`);
|
|
57
|
+
console.log(` Mode: ${isGlobal ? 'global (~/.cursor/skills/)' : 'project-local (.cursor/skills/)'}\n`);
|
|
58
|
+
|
|
59
|
+
// Step 1: Copy skill files (excluding node_modules and .git)
|
|
60
|
+
console.log(' [1/4] Copying skill files...');
|
|
61
|
+
if (fs.existsSync(skillTarget)) {
|
|
62
|
+
fs.rmSync(skillTarget, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
copyRecursive(skillSource, skillTarget);
|
|
65
|
+
console.log(' Done.');
|
|
66
|
+
|
|
67
|
+
// Step 2: Install MCP dependencies in skill directory
|
|
68
|
+
console.log(' [2/4] Installing MCP dependencies...');
|
|
69
|
+
try {
|
|
70
|
+
execFileSync('npm', ['install', '--omit=dev', '--ignore-scripts'], {
|
|
71
|
+
cwd: skillTarget,
|
|
72
|
+
stdio: 'pipe',
|
|
73
|
+
shell: process.platform === 'win32',
|
|
74
|
+
});
|
|
75
|
+
console.log(' Done.');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(` Warning: npm install failed (${e.message}). MCP tools may not work.`);
|
|
78
|
+
console.log(' You can fix this later: cd "' + skillTarget + '" ; npm install');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Step 3: Add .gitignore entries for skill node_modules
|
|
82
|
+
console.log(' [3/4] Updating .gitignore...');
|
|
83
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
84
|
+
const entries = ['.cursor/skills/**/node_modules/'];
|
|
85
|
+
let gitignoreUpdated = false;
|
|
86
|
+
if (!isGlobal) {
|
|
87
|
+
let existing = '';
|
|
88
|
+
try { existing = fs.readFileSync(gitignorePath, 'utf-8'); } catch { /* doesn't exist */ }
|
|
89
|
+
const missing = entries.filter(e => !existing.includes(e));
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
const newline = existing.endsWith('\n') || !existing ? '' : '\n';
|
|
92
|
+
fs.appendFileSync(gitignorePath, `${newline}# cursor-guard skill dependencies\n${missing.join('\n')}\n`);
|
|
93
|
+
gitignoreUpdated = true;
|
|
94
|
+
console.log(' Added: ' + missing.join(', '));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(' Already configured.');
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
console.log(' Skipped (global install, not inside a project).');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Step 4: Summary
|
|
103
|
+
console.log(' [4/4] Verifying...');
|
|
104
|
+
const serverExists = fs.existsSync(path.join(skillTarget, 'references', 'mcp', 'server.js'));
|
|
105
|
+
const sdkExists = fs.existsSync(path.join(skillTarget, 'node_modules', '@modelcontextprotocol', 'sdk'));
|
|
106
|
+
const skillMdExists = fs.existsSync(path.join(skillTarget, 'SKILL.md'));
|
|
107
|
+
|
|
108
|
+
console.log(` SKILL.md: ${skillMdExists ? 'OK' : 'MISSING'}`);
|
|
109
|
+
console.log(` MCP server: ${serverExists ? 'OK' : 'MISSING'}`);
|
|
110
|
+
console.log(` MCP SDK: ${sdkExists ? 'OK' : 'MISSING — run npm install in skill dir'}`);
|
|
111
|
+
|
|
112
|
+
console.log(`\n Installation complete!\n`);
|
|
113
|
+
console.log(' Next steps:');
|
|
114
|
+
console.log(' 1. The skill activates automatically in Cursor Agent conversations.');
|
|
115
|
+
console.log(' 2. (Optional) Copy example config to project root:');
|
|
116
|
+
console.log(` cp "${path.join(skillTarget, 'references', 'cursor-guard.example.json')}" .cursor-guard.json`);
|
|
117
|
+
console.log(' 3. (Optional) Enable MCP — add to .cursor/mcp.json:');
|
|
118
|
+
console.log(` { "mcpServers": { "cursor-guard": { "command": "node", "args": ["${path.join(skillTarget, 'references', 'mcp', 'server.js').replace(/\\/g, '/')}"] } } }`);
|
|
119
|
+
console.log(' 4. (Optional) Start auto-backup:');
|
|
120
|
+
console.log(` npx cursor-guard-backup --path "${projectDir}"\n`);
|
|
@@ -173,3 +173,43 @@ Retention policy for the **`refs/guard/auto-backup` Git ref**. By default, auto-
|
|
|
173
173
|
"max_count": 200
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## `proactive_alert`
|
|
180
|
+
|
|
181
|
+
- **Type**: `boolean`
|
|
182
|
+
- **Default**: `true`
|
|
183
|
+
|
|
184
|
+
Enable V4 proactive change-velocity detection. When enabled, the auto-backup watcher monitors file change frequency and raises alerts when abnormal patterns are detected (e.g. 20+ files modified in 10 seconds). Alerts are persisted to a file so the MCP server can include them in tool responses.
|
|
185
|
+
|
|
186
|
+
Set to `false` to disable proactive monitoring entirely.
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
"proactive_alert": true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## `alert_thresholds`
|
|
195
|
+
|
|
196
|
+
- **Type**: `object`
|
|
197
|
+
- **Default**: `{ "files_per_window": 20, "window_seconds": 10, "cooldown_seconds": 60 }`
|
|
198
|
+
|
|
199
|
+
Thresholds for proactive change-velocity alerts. Only effective when `proactive_alert` is `true`.
|
|
200
|
+
|
|
201
|
+
### Sub-fields
|
|
202
|
+
|
|
203
|
+
| Field | Type | Default | Description |
|
|
204
|
+
|-------|------|---------|-------------|
|
|
205
|
+
| `files_per_window` | `integer` | `20` | Number of file changes within the time window that triggers an alert |
|
|
206
|
+
| `window_seconds` | `integer` | `10` | Sliding time window in seconds for counting file changes |
|
|
207
|
+
| `cooldown_seconds` | `integer` | `60` | Minimum seconds between consecutive alerts to avoid noise |
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
"alert_thresholds": {
|
|
211
|
+
"files_per_window": 20,
|
|
212
|
+
"window_seconds": 10,
|
|
213
|
+
"cooldown_seconds": 60
|
|
214
|
+
}
|
|
215
|
+
```
|
|
@@ -173,3 +173,43 @@
|
|
|
173
173
|
"max_count": 200
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## `proactive_alert`
|
|
180
|
+
|
|
181
|
+
- **类型**:`boolean`
|
|
182
|
+
- **默认值**:`true`
|
|
183
|
+
|
|
184
|
+
启用 V4 主动变更频率检测。开启后,自动备份 watcher 会监控文件变更频率,当检测到异常模式(如 10 秒内 20+ 文件被修改)时发出告警。告警会持久化到文件,以便 MCP 工具在响应中附加风险提示。
|
|
185
|
+
|
|
186
|
+
设为 `false` 可完全禁用主动监控。
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
"proactive_alert": true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## `alert_thresholds`
|
|
195
|
+
|
|
196
|
+
- **类型**:`object`
|
|
197
|
+
- **默认值**:`{ "files_per_window": 20, "window_seconds": 10, "cooldown_seconds": 60 }`
|
|
198
|
+
|
|
199
|
+
主动变更频率告警的阈值配置。仅在 `proactive_alert` 为 `true` 时生效。
|
|
200
|
+
|
|
201
|
+
### 子字段
|
|
202
|
+
|
|
203
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
204
|
+
|------|------|--------|------|
|
|
205
|
+
| `files_per_window` | `integer` | `20` | 时间窗口内触发告警的文件变更数量 |
|
|
206
|
+
| `window_seconds` | `integer` | `10` | 统计文件变更的滑动时间窗口(秒) |
|
|
207
|
+
| `cooldown_seconds` | `integer` | `60` | 连续告警之间的最小间隔(秒),避免噪声 |
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
"alert_thresholds": {
|
|
211
|
+
"files_per_window": 20,
|
|
212
|
+
"window_seconds": 10,
|
|
213
|
+
"cooldown_seconds": 60
|
|
214
|
+
}
|
|
215
|
+
```
|
|
@@ -102,6 +102,36 @@
|
|
|
102
102
|
}
|
|
103
103
|
},
|
|
104
104
|
"additionalProperties": false
|
|
105
|
+
},
|
|
106
|
+
"proactive_alert": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"default": true,
|
|
109
|
+
"description": "Enable V4 proactive change-velocity detection. When true, the auto-backup watcher monitors file change frequency and raises alerts when abnormal patterns are detected."
|
|
110
|
+
},
|
|
111
|
+
"alert_thresholds": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"description": "Thresholds for proactive change-velocity alerts (V4). Only effective when proactive_alert is true.",
|
|
114
|
+
"properties": {
|
|
115
|
+
"files_per_window": {
|
|
116
|
+
"type": "integer",
|
|
117
|
+
"minimum": 1,
|
|
118
|
+
"default": 20,
|
|
119
|
+
"description": "Number of file changes within the time window that triggers an alert."
|
|
120
|
+
},
|
|
121
|
+
"window_seconds": {
|
|
122
|
+
"type": "integer",
|
|
123
|
+
"minimum": 1,
|
|
124
|
+
"default": 10,
|
|
125
|
+
"description": "Sliding time window in seconds for counting file changes."
|
|
126
|
+
},
|
|
127
|
+
"cooldown_seconds": {
|
|
128
|
+
"type": "integer",
|
|
129
|
+
"minimum": 1,
|
|
130
|
+
"default": 60,
|
|
131
|
+
"description": "Minimum seconds between consecutive alerts to avoid noise."
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"additionalProperties": false
|
|
105
135
|
}
|
|
106
136
|
},
|
|
107
137
|
"additionalProperties": false
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
5
6
|
const {
|
|
6
7
|
color, loadConfig, gitAvailable, git, isGitRepo, gitDir: getGitDir,
|
|
7
8
|
walkDir, filterFiles, buildManifest, loadManifest, saveManifest,
|
|
8
|
-
manifestChanged, createLogger,
|
|
9
|
+
manifestChanged, createLogger, unquoteGitPath,
|
|
9
10
|
} = require('./utils');
|
|
10
11
|
const { createGitSnapshot, createShadowCopy } = require('./core/snapshot');
|
|
11
12
|
const { cleanShadowRetention, cleanGitRetention } = require('./core/backups');
|
|
13
|
+
const { createChangeTracker, recordChange, checkAnomaly, saveAlert, clearExpiredAlert } = require('./core/anomaly');
|
|
12
14
|
|
|
13
15
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
14
16
|
|
|
@@ -123,11 +125,7 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
123
125
|
git(['update-ref', '-d', legacyRef], { cwd: projectDir, allowFail: true });
|
|
124
126
|
console.log(color.green(`[guard] Migrated ${legacyRef} → ${branchRef}`));
|
|
125
127
|
} else {
|
|
126
|
-
|
|
127
|
-
if (head) {
|
|
128
|
-
git(['update-ref', branchRef, head], { cwd: projectDir, allowFail: true });
|
|
129
|
-
console.log(color.green(`[guard] Created ref: ${branchRef}`));
|
|
130
|
-
}
|
|
128
|
+
console.log(color.cyan(`[guard] Ref ${branchRef} does not exist yet — will be created on first snapshot.`));
|
|
131
129
|
}
|
|
132
130
|
}
|
|
133
131
|
|
|
@@ -153,6 +151,12 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
153
151
|
logger.error(`Unhandled rejection: ${reason}`);
|
|
154
152
|
});
|
|
155
153
|
|
|
154
|
+
// V4: Initialize change tracker for anomaly detection
|
|
155
|
+
let tracker = createChangeTracker(cfg);
|
|
156
|
+
if (cfg.proactive_alert) {
|
|
157
|
+
console.log(color.cyan(`[guard] Proactive alert: ON (threshold: ${cfg.alert_thresholds.files_per_window} files / ${cfg.alert_thresholds.window_seconds}s)`));
|
|
158
|
+
}
|
|
159
|
+
|
|
156
160
|
// Banner
|
|
157
161
|
console.log('');
|
|
158
162
|
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
@@ -175,6 +179,7 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
175
179
|
if (reload.loaded && !reload.error) {
|
|
176
180
|
cfg = reload.cfg;
|
|
177
181
|
cfgMtime = newMtime;
|
|
182
|
+
tracker = createChangeTracker(cfg);
|
|
178
183
|
logger.info('Config reloaded (file changed)');
|
|
179
184
|
}
|
|
180
185
|
}
|
|
@@ -184,6 +189,7 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
184
189
|
// Detect changes
|
|
185
190
|
let hasChanges = false;
|
|
186
191
|
let pendingManifest = null;
|
|
192
|
+
let lastManifest = null;
|
|
187
193
|
try {
|
|
188
194
|
if (repo) {
|
|
189
195
|
const dirty = git(['status', '--porcelain'], { cwd: projectDir, allowFail: true });
|
|
@@ -192,8 +198,8 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
192
198
|
const allFiles = walkDir(projectDir, projectDir);
|
|
193
199
|
const filtered = filterFiles(allFiles, cfg);
|
|
194
200
|
const newManifest = buildManifest(filtered);
|
|
195
|
-
|
|
196
|
-
hasChanges = manifestChanged(
|
|
201
|
+
lastManifest = loadManifest(backupDir);
|
|
202
|
+
hasChanges = manifestChanged(lastManifest, newManifest);
|
|
197
203
|
if (hasChanges) pendingManifest = newManifest;
|
|
198
204
|
}
|
|
199
205
|
} catch (e) {
|
|
@@ -202,6 +208,56 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
202
208
|
}
|
|
203
209
|
if (!hasChanges) continue;
|
|
204
210
|
|
|
211
|
+
// V4: Record change event and check for anomalies
|
|
212
|
+
let changedFileCount = 0;
|
|
213
|
+
if (repo) {
|
|
214
|
+
// Use execFileSync directly — git() helper's trim() strips leading spaces
|
|
215
|
+
// from porcelain output, corrupting the first line when it starts with ' '.
|
|
216
|
+
let porcelain = '';
|
|
217
|
+
try {
|
|
218
|
+
porcelain = execFileSync('git', ['status', '--porcelain'], {
|
|
219
|
+
cwd: projectDir, stdio: 'pipe', encoding: 'utf-8',
|
|
220
|
+
});
|
|
221
|
+
} catch { /* ignore */ }
|
|
222
|
+
if (porcelain) {
|
|
223
|
+
const lines = porcelain.split('\n').filter(Boolean);
|
|
224
|
+
if (cfg.protect.length === 0 && cfg.ignore.length === 0) {
|
|
225
|
+
changedFileCount = lines.length;
|
|
226
|
+
} else {
|
|
227
|
+
const changedPaths = lines.map(line => {
|
|
228
|
+
const filePart = line.substring(3);
|
|
229
|
+
const arrowIdx = filePart.indexOf(' -> ');
|
|
230
|
+
const raw = arrowIdx >= 0 ? filePart.substring(arrowIdx + 4) : filePart;
|
|
231
|
+
return unquoteGitPath(raw);
|
|
232
|
+
});
|
|
233
|
+
const fakeFiles = changedPaths.map(rel => ({ rel, full: path.join(projectDir, rel) }));
|
|
234
|
+
changedFileCount = filterFiles(fakeFiles, cfg).length;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else if (pendingManifest) {
|
|
238
|
+
if (!lastManifest) {
|
|
239
|
+
changedFileCount = Object.keys(pendingManifest).length;
|
|
240
|
+
} else {
|
|
241
|
+
const newKeys = new Set(Object.keys(pendingManifest));
|
|
242
|
+
const oldKeys = new Set(Object.keys(lastManifest));
|
|
243
|
+
let diffCount = 0;
|
|
244
|
+
for (const k of newKeys) {
|
|
245
|
+
if (!oldKeys.has(k) || lastManifest[k].mtimeMs !== pendingManifest[k].mtimeMs || lastManifest[k].size !== pendingManifest[k].size) diffCount++;
|
|
246
|
+
}
|
|
247
|
+
for (const k of oldKeys) {
|
|
248
|
+
if (!newKeys.has(k)) diffCount++;
|
|
249
|
+
}
|
|
250
|
+
changedFileCount = diffCount;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
recordChange(tracker, changedFileCount);
|
|
255
|
+
const anomalyResult = checkAnomaly(tracker);
|
|
256
|
+
if (anomalyResult.anomaly && anomalyResult.alert && !anomalyResult.suppressed) {
|
|
257
|
+
saveAlert(projectDir, anomalyResult.alert);
|
|
258
|
+
logger.warn(`ALERT: ${anomalyResult.alert.fileCount} files changed in ${anomalyResult.alert.windowSeconds}s (threshold: ${anomalyResult.alert.threshold})`);
|
|
259
|
+
}
|
|
260
|
+
|
|
205
261
|
// Git snapshot via Core
|
|
206
262
|
if ((cfg.backup_strategy === 'git' || cfg.backup_strategy === 'both') && repo) {
|
|
207
263
|
const snapResult = createGitSnapshot(projectDir, cfg, { branchRef });
|
|
@@ -250,6 +306,8 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
250
306
|
logger.log(`Git retention (${gitRetResult.mode}): rebuilt branch with ${gitRetResult.kept} newest snapshots, pruned ${gitRetResult.pruned}. Run 'git gc' to reclaim space.`, 'gray');
|
|
251
307
|
}
|
|
252
308
|
}
|
|
309
|
+
|
|
310
|
+
clearExpiredAlert(projectDir);
|
|
253
311
|
}
|
|
254
312
|
}
|
|
255
313
|
}
|