cursor-guard 2.0.2 → 2.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +6 -6
- package/package.json +6 -2
- package/references/bin/cursor-guard-backup.js +18 -1
- package/references/bin/cursor-guard-doctor.js +17 -0
- package/references/config-reference.md +2 -2
- package/references/config-reference.zh-CN.md +2 -2
- package/references/cursor-guard.schema.json +1 -1
- package/references/lib/auto-backup.js +30 -8
- package/references/lib/guard-doctor.js +11 -6
- package/references/lib/utils.js +70 -24
- package/references/recovery.md +7 -7
- package/references/lib/utils.test.js +0 -329
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ npx cursor-guard-backup --path /my/project --interval 30
|
|
|
179
179
|
./references/auto-backup.sh /my/project
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
The script uses Git plumbing commands to snapshot to `
|
|
182
|
+
The script uses Git plumbing commands to snapshot to `refs/guard/auto-backup` — it never switches branches or touches your working index. The ref lives outside `refs/heads/` so `git push --all` won't push it. Supports `shadow` mode for non-Git directories.
|
|
183
183
|
|
|
184
184
|
### Health Check
|
|
185
185
|
|
|
@@ -229,7 +229,7 @@ If the pre-restore backup fails, the agent will **not** proceed — it will wait
|
|
|
229
229
|
### Recovery priority
|
|
230
230
|
|
|
231
231
|
1. **Git** — `git restore`, `git reset`, `git reflog`
|
|
232
|
-
2. **Auto-backup
|
|
232
|
+
2. **Auto-backup ref** — `refs/guard/auto-backup`
|
|
233
233
|
3. **Shadow copies** — `.cursor-guard-backup/<timestamp>/`
|
|
234
234
|
4. **Conversation context** — Original file content captured by agent Read calls
|
|
235
235
|
5. **Editor history** — VS Code/Cursor Timeline (auxiliary)
|
package/README.zh-CN.md
CHANGED
|
@@ -179,7 +179,7 @@ npx cursor-guard-backup --path /my/project --interval 30
|
|
|
179
179
|
./references/auto-backup.sh /my/project
|
|
180
180
|
```
|
|
181
181
|
|
|
182
|
-
脚本使用 Git 底层命令快照到 `
|
|
182
|
+
脚本使用 Git 底层命令快照到 `refs/guard/auto-backup`——不会切换分支,也不会影响你的工作索引。该引用位于 `refs/heads/` 之外,`git push --all` 不会推送它。支持 `shadow` 模式用于非 Git 目录。
|
|
183
183
|
|
|
184
184
|
### 健康检查
|
|
185
185
|
|
|
@@ -229,7 +229,7 @@ npx cursor-guard-doctor --path /my/project
|
|
|
229
229
|
### 恢复优先级
|
|
230
230
|
|
|
231
231
|
1. **Git** — `git restore`, `git reset`, `git reflog`
|
|
232
|
-
2.
|
|
232
|
+
2. **自动备份引用** — `refs/guard/auto-backup`
|
|
233
233
|
3. **影子拷贝** — `.cursor-guard-backup/<时间戳>/`
|
|
234
234
|
4. **对话上下文** — 代理 Read 调用捕获的原始文件内容
|
|
235
235
|
5. **编辑器历史** — VS Code/Cursor Timeline(辅助)
|
package/SKILL.md
CHANGED
|
@@ -204,16 +204,16 @@ There are two distinct backup mechanisms. Do not confuse them:
|
|
|
204
204
|
|
|
205
205
|
| | **Git branch snapshot** | **Shadow copy** |
|
|
206
206
|
|---|---|---|
|
|
207
|
-
| **What** | Commits to `
|
|
207
|
+
| **What** | Commits to `refs/guard/auto-backup` via plumbing | File copies to `.cursor-guard-backup/<timestamp>/` |
|
|
208
208
|
| **Who creates** | Auto-backup script (when `backup_strategy` = `git` or `both`) | Auto-backup script (when `backup_strategy` = `shadow` or `both`); or the agent manually (§2b) |
|
|
209
209
|
| **Who cleans up** | `git_retention` config (auto, opt-in); or manual `git branch -D` | `retention` config (auto); or manual |
|
|
210
|
-
| **Restore** | `git restore --source=
|
|
210
|
+
| **Restore** | `git restore --source=guard/auto-backup -- <file>` | Copy file from `.cursor-guard-backup/<ts>/<file>` to original path |
|
|
211
211
|
| **Requires Git** | Yes | No (fallback for non-git repos) |
|
|
212
212
|
|
|
213
213
|
**Priority order for the agent:**
|
|
214
214
|
|
|
215
215
|
1. **Guard ref snapshot** (`refs/guard/snapshot`) — agent creates before each high-risk edit using temp index (§2a). Does not pollute user's branch or staging area.
|
|
216
|
-
2. **Git
|
|
216
|
+
2. **Git auto-backup ref** (`refs/guard/auto-backup`) — periodic snapshots by auto-backup script. Lives outside `refs/heads/` so `git push --all` won't push it.
|
|
217
217
|
3. **Shadow copy** (`.cursor-guard-backup/`) — fallback for non-git repos, or as extra insurance when `backup_strategy = "both"`.
|
|
218
218
|
4. **Editor habits** — Ctrl+S frequently; optional extensions are user-configured, mention only if asked.
|
|
219
219
|
|
|
@@ -267,7 +267,7 @@ If unclear, ask: "你想恢复哪个文件?还是整个项目?" / "Which fil
|
|
|
267
267
|
git log --oneline --before="5 minutes ago" -5 -- <file>
|
|
268
268
|
|
|
269
269
|
# Auto-backup branch (if exists)
|
|
270
|
-
git log
|
|
270
|
+
git log guard/auto-backup --oneline --before="5 minutes ago" -5 -- <file>
|
|
271
271
|
|
|
272
272
|
# Reflog as fallback (shows all HEAD movements)
|
|
273
273
|
git reflog --before="5 minutes ago" -5
|
|
@@ -291,7 +291,7 @@ git log --oneline -<N+5> -- <file>
|
|
|
291
291
|
git show HEAD~<N>:<file>
|
|
292
292
|
|
|
293
293
|
# Auto-backup branch
|
|
294
|
-
git log
|
|
294
|
+
git log guard/auto-backup --oneline -<N+5> -- <file>
|
|
295
295
|
```
|
|
296
296
|
|
|
297
297
|
### Step 3: Present Candidates to User
|
|
@@ -314,7 +314,7 @@ Recommended: #1 (closest to target time). Restore this one? / 推荐 #1(最接
|
|
|
314
314
|
- If only ONE candidate is found, confirm with the user before restoring.
|
|
315
315
|
- If MULTIPLE candidates, pre-select #1 (closest before target) but let the user pick another.
|
|
316
316
|
- If NO candidates before the target time:
|
|
317
|
-
- Check auto-backup
|
|
317
|
+
- Check auto-backup ref: `git rev-parse --verify refs/guard/auto-backup`
|
|
318
318
|
- Check shadow copies: `Get-ChildItem .cursor-guard-backup/ -Directory | Sort-Object Name -Descending`
|
|
319
319
|
- If still nothing, report clearly: "No snapshot found before that time. The earliest available is [hash] at [time]. Do you want to use it?"
|
|
320
320
|
- **Never silently pick a version.** Always show and confirm.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.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",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node references/lib/utils.test.js"
|
|
28
|
+
},
|
|
26
29
|
"bin": {
|
|
27
30
|
"cursor-guard-backup": "references/bin/cursor-guard-backup.js",
|
|
28
31
|
"cursor-guard-doctor": "references/bin/cursor-guard-doctor.js"
|
|
@@ -32,7 +35,8 @@
|
|
|
32
35
|
"README.md",
|
|
33
36
|
"README.zh-CN.md",
|
|
34
37
|
"LICENSE",
|
|
35
|
-
"references/"
|
|
38
|
+
"references/",
|
|
39
|
+
"!references/lib/utils.test.js"
|
|
36
40
|
],
|
|
37
41
|
"main": "SKILL.md"
|
|
38
42
|
}
|
|
@@ -5,9 +5,26 @@ const path = require('path');
|
|
|
5
5
|
const { parseArgs } = require('../lib/utils');
|
|
6
6
|
|
|
7
7
|
const args = parseArgs(process.argv);
|
|
8
|
+
|
|
9
|
+
if (args.help || args.h) {
|
|
10
|
+
console.log(`Usage: cursor-guard-backup [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--path <dir> Project directory to watch (default: current dir)
|
|
14
|
+
--interval <sec> Override backup interval in seconds
|
|
15
|
+
--help, -h Show this help message
|
|
16
|
+
--version, -v Show version number`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (args.version || args.v) {
|
|
21
|
+
const pkg = require('../../package.json');
|
|
22
|
+
console.log(pkg.version);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
const targetPath = args.path || '.';
|
|
9
27
|
const interval = parseInt(args.interval, 10) || 0;
|
|
10
|
-
|
|
11
28
|
const resolved = path.resolve(targetPath);
|
|
12
29
|
|
|
13
30
|
const { runBackup } = require('../lib/auto-backup');
|
|
@@ -5,6 +5,23 @@ const path = require('path');
|
|
|
5
5
|
const { parseArgs } = require('../lib/utils');
|
|
6
6
|
|
|
7
7
|
const args = parseArgs(process.argv);
|
|
8
|
+
|
|
9
|
+
if (args.help || args.h) {
|
|
10
|
+
console.log(`Usage: cursor-guard-doctor [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--path <dir> Project directory to check (default: current dir)
|
|
14
|
+
--help, -h Show this help message
|
|
15
|
+
--version, -v Show version number`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (args.version || args.v) {
|
|
20
|
+
const pkg = require('../../package.json');
|
|
21
|
+
console.log(pkg.version);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
const targetPath = args.path || '.';
|
|
9
26
|
const resolved = path.resolve(targetPath);
|
|
10
27
|
|
|
@@ -51,7 +51,7 @@ Blacklist glob patterns. Matching files are excluded from protection even if the
|
|
|
51
51
|
|
|
52
52
|
| Value | Description |
|
|
53
53
|
|-------|-------------|
|
|
54
|
-
| `"git"` | Local commits to dedicated
|
|
54
|
+
| `"git"` | Local commits to dedicated ref `refs/guard/auto-backup` |
|
|
55
55
|
| `"shadow"` | File copies to `.cursor-guard-backup/<timestamp>/` |
|
|
56
56
|
| `"both"` | Git branch snapshot + shadow copies |
|
|
57
57
|
|
|
@@ -155,7 +155,7 @@ Retention policy for **shadow copies** only. Git branch snapshots are not auto-c
|
|
|
155
155
|
- **Type**: `object`
|
|
156
156
|
- **Default**: `{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
157
157
|
|
|
158
|
-
Retention policy for the **`
|
|
158
|
+
Retention policy for the **`refs/guard/auto-backup` Git ref**. By default, auto-backup commits accumulate indefinitely. Enable this to automatically prune old commits.
|
|
159
159
|
|
|
160
160
|
### Sub-fields
|
|
161
161
|
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
|
|
52
52
|
| 值 | 说明 |
|
|
53
53
|
|----|------|
|
|
54
|
-
| `"git"` |
|
|
54
|
+
| `"git"` | 提交到专用引用 `refs/guard/auto-backup` |
|
|
55
55
|
| `"shadow"` | 文件拷贝到 `.cursor-guard-backup/<timestamp>/` |
|
|
56
56
|
| `"both"` | Git 分支快照 + 影子拷贝同时进行 |
|
|
57
57
|
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
- **类型**:`object`
|
|
156
156
|
- **默认值**:`{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
157
157
|
|
|
158
|
-
**`
|
|
158
|
+
**`refs/guard/auto-backup` Git 引用**的保留策略。默认情况下自动备份提交会无限累积。启用此项可自动裁剪旧提交。
|
|
159
159
|
|
|
160
160
|
### 子字段
|
|
161
161
|
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"git_retention": {
|
|
77
77
|
"type": "object",
|
|
78
|
-
"description": "Controls automatic cleanup of old commits on
|
|
78
|
+
"description": "Controls automatic cleanup of old commits on refs/guard/auto-backup.",
|
|
79
79
|
"properties": {
|
|
80
80
|
"enabled": {
|
|
81
81
|
"type": "boolean",
|
|
@@ -312,8 +312,6 @@ function isProcessAlive(pid) {
|
|
|
312
312
|
// ── Main ────────────────────────────────────────────────────────
|
|
313
313
|
|
|
314
314
|
async function runBackup(projectDir, intervalOverride) {
|
|
315
|
-
process.chdir(projectDir);
|
|
316
|
-
|
|
317
315
|
const hasGit = gitAvailable();
|
|
318
316
|
const repo = hasGit && isGitRepo(projectDir);
|
|
319
317
|
const gDir = repo ? getGitDir(projectDir) : null;
|
|
@@ -326,7 +324,7 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
326
324
|
const guardIndex = gDir ? path.join(gDir, 'cursor-guard-index') : null;
|
|
327
325
|
|
|
328
326
|
// Load config
|
|
329
|
-
let { cfg, loaded, error } = loadConfig(projectDir);
|
|
327
|
+
let { cfg, loaded, error, warnings } = loadConfig(projectDir);
|
|
330
328
|
let interval = intervalOverride || cfg.auto_backup_interval_seconds || 60;
|
|
331
329
|
if (interval < 5) interval = 5;
|
|
332
330
|
let cfgMtime = 0;
|
|
@@ -337,6 +335,9 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
337
335
|
console.log(color.yellow(`[guard] WARNING: .cursor-guard.json parse error — using defaults. ${error}`));
|
|
338
336
|
} else if (loaded) {
|
|
339
337
|
console.log(color.cyan(`[guard] Config loaded protect=${cfg.protect.length} ignore=${cfg.ignore.length} strategy=${cfg.backup_strategy} git_retention=${cfg.git_retention.enabled ? 'on' : 'off'}`));
|
|
338
|
+
if (warnings && warnings.length > 0) {
|
|
339
|
+
for (const w of warnings) console.log(color.yellow(`[guard] WARNING: ${w}`));
|
|
340
|
+
}
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// Strategy check
|
|
@@ -399,13 +400,24 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
399
400
|
process.on('exit', cleanup);
|
|
400
401
|
|
|
401
402
|
// Git-specific setup
|
|
402
|
-
const
|
|
403
|
-
const
|
|
403
|
+
const branchRef = 'refs/guard/auto-backup';
|
|
404
|
+
const legacyRef = 'refs/heads/cursor-guard/auto-backup';
|
|
404
405
|
if (repo) {
|
|
405
406
|
const exists = git(['rev-parse', '--verify', branchRef], { cwd: projectDir, allowFail: true });
|
|
406
407
|
if (!exists) {
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
// Migrate from legacy refs/heads/ location if it exists
|
|
409
|
+
const legacyHash = git(['rev-parse', '--verify', legacyRef], { cwd: projectDir, allowFail: true });
|
|
410
|
+
if (legacyHash) {
|
|
411
|
+
git(['update-ref', branchRef, legacyHash], { cwd: projectDir, allowFail: true });
|
|
412
|
+
git(['update-ref', '-d', legacyRef], { cwd: projectDir, allowFail: true });
|
|
413
|
+
console.log(color.green(`[guard] Migrated ${legacyRef} → ${branchRef}`));
|
|
414
|
+
} else {
|
|
415
|
+
const head = git(['rev-parse', 'HEAD'], { cwd: projectDir, allowFail: true });
|
|
416
|
+
if (head) {
|
|
417
|
+
git(['update-ref', branchRef, head], { cwd: projectDir, allowFail: true });
|
|
418
|
+
console.log(color.green(`[guard] Created ref: ${branchRef}`));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
409
421
|
}
|
|
410
422
|
|
|
411
423
|
const excludeFile = path.join(gDir, 'info', 'exclude');
|
|
@@ -420,10 +432,20 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
420
432
|
|
|
421
433
|
const logger = createLogger(logFilePath);
|
|
422
434
|
|
|
435
|
+
// Global error handlers
|
|
436
|
+
process.on('uncaughtException', (err) => {
|
|
437
|
+
logger.error(`Uncaught exception: ${err.message}`);
|
|
438
|
+
cleanup();
|
|
439
|
+
process.exit(1);
|
|
440
|
+
});
|
|
441
|
+
process.on('unhandledRejection', (reason) => {
|
|
442
|
+
logger.error(`Unhandled rejection: ${reason}`);
|
|
443
|
+
});
|
|
444
|
+
|
|
423
445
|
// Banner
|
|
424
446
|
console.log('');
|
|
425
447
|
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
|
426
|
-
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} |
|
|
448
|
+
console.log(color.cyan(`[guard] Strategy: ${cfg.backup_strategy} | Ref: ${branchRef} | Retention: ${cfg.retention.mode}`));
|
|
427
449
|
console.log(color.cyan(`[guard] Log: ${logFilePath}`));
|
|
428
450
|
console.log('');
|
|
429
451
|
|
|
@@ -81,15 +81,20 @@ function runDoctor(projectDir) {
|
|
|
81
81
|
check('Strategy compatibility', 'FAIL', `unknown backup_strategy='${strategy}' (must be git/shadow/both)`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// 5. Backup
|
|
84
|
+
// 5. Backup ref
|
|
85
85
|
if (repo) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
86
|
+
const guardRef = 'refs/guard/auto-backup';
|
|
87
|
+
const legacyRef = 'refs/heads/cursor-guard/auto-backup';
|
|
88
|
+
const exists = git(['rev-parse', '--verify', guardRef], { cwd: projectDir, allowFail: true });
|
|
89
|
+
const legacyExists = git(['rev-parse', '--verify', legacyRef], { cwd: projectDir, allowFail: true });
|
|
88
90
|
if (exists) {
|
|
89
|
-
const count = git(['rev-list', '--count',
|
|
90
|
-
check('Backup
|
|
91
|
+
const count = git(['rev-list', '--count', guardRef], { cwd: projectDir, allowFail: true }) || '?';
|
|
92
|
+
check('Backup ref', 'PASS', `refs/guard/auto-backup exists (${count} commits)`);
|
|
93
|
+
} else if (legacyExists) {
|
|
94
|
+
const count = git(['rev-list', '--count', legacyRef], { cwd: projectDir, allowFail: true }) || '?';
|
|
95
|
+
check('Backup ref', 'WARN', `legacy refs/heads/cursor-guard/auto-backup found (${count} commits) — run auto-backup once to migrate`);
|
|
91
96
|
} else {
|
|
92
|
-
check('Backup
|
|
97
|
+
check('Backup ref', 'WARN', 'refs/guard/auto-backup not created yet (will be created on first backup)');
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
package/references/lib/utils.js
CHANGED
|
@@ -52,18 +52,22 @@ const ALWAYS_SKIP = /[/\\](\.git|\.cursor-guard-backup|node_modules)[/\\]/;
|
|
|
52
52
|
|
|
53
53
|
function walkDir(dir, rootDir) {
|
|
54
54
|
const results = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const stack = [dir];
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const current = stack.pop();
|
|
58
|
+
let entries;
|
|
59
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); }
|
|
60
|
+
catch { continue; }
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const full = path.join(current, entry.name);
|
|
63
|
+
const rel = path.relative(rootDir, full).replace(/\\/g, '/');
|
|
64
|
+
if (ALWAYS_SKIP.test('/' + rel + '/')) continue;
|
|
65
|
+
if (entry.isSymbolicLink()) continue;
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
stack.push(full);
|
|
68
|
+
} else if (entry.isFile()) {
|
|
69
|
+
results.push({ full, rel, name: entry.name });
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
return results;
|
|
@@ -73,6 +77,11 @@ function walkDir(dir, rootDir) {
|
|
|
73
77
|
|
|
74
78
|
const DEFAULT_SECRETS = ['.env', '.env.*', '*.key', '*.pem', '*.p12', '*.pfx', 'credentials*'];
|
|
75
79
|
|
|
80
|
+
const VALID_STRATEGIES = ['git', 'shadow', 'both'];
|
|
81
|
+
const VALID_PRE_RESTORE = ['always', 'ask', 'never'];
|
|
82
|
+
const VALID_RETENTION_MODES = ['days', 'count', 'size'];
|
|
83
|
+
const VALID_GIT_RETENTION_MODES = ['days', 'count'];
|
|
84
|
+
|
|
76
85
|
const DEFAULT_CONFIG = {
|
|
77
86
|
protect: [],
|
|
78
87
|
ignore: [],
|
|
@@ -101,22 +110,47 @@ function loadConfig(projectDir) {
|
|
|
101
110
|
const merged = [...new Set([...cfg.secrets_patterns, ...raw.secrets_patterns_extra])];
|
|
102
111
|
cfg.secrets_patterns = merged;
|
|
103
112
|
}
|
|
104
|
-
|
|
113
|
+
const warnings = [];
|
|
114
|
+
if (typeof raw.backup_strategy === 'string') {
|
|
115
|
+
if (VALID_STRATEGIES.includes(raw.backup_strategy)) {
|
|
116
|
+
cfg.backup_strategy = raw.backup_strategy;
|
|
117
|
+
} else {
|
|
118
|
+
warnings.push(`Unknown backup_strategy "${raw.backup_strategy}", using default "${cfg.backup_strategy}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
105
121
|
if (typeof raw.auto_backup_interval_seconds === 'number') cfg.auto_backup_interval_seconds = raw.auto_backup_interval_seconds;
|
|
106
|
-
if (typeof raw.pre_restore_backup === 'string')
|
|
122
|
+
if (typeof raw.pre_restore_backup === 'string') {
|
|
123
|
+
if (VALID_PRE_RESTORE.includes(raw.pre_restore_backup)) {
|
|
124
|
+
cfg.pre_restore_backup = raw.pre_restore_backup;
|
|
125
|
+
} else {
|
|
126
|
+
warnings.push(`Unknown pre_restore_backup "${raw.pre_restore_backup}", using default "${cfg.pre_restore_backup}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
107
129
|
if (raw.retention) {
|
|
108
|
-
if (raw.retention.mode)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
if (raw.retention.mode) {
|
|
131
|
+
if (VALID_RETENTION_MODES.includes(raw.retention.mode)) {
|
|
132
|
+
cfg.retention.mode = raw.retention.mode;
|
|
133
|
+
} else {
|
|
134
|
+
warnings.push(`Unknown retention.mode "${raw.retention.mode}", using default "${cfg.retention.mode}"`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (typeof raw.retention.days === 'number') cfg.retention.days = raw.retention.days;
|
|
138
|
+
if (typeof raw.retention.max_count === 'number') cfg.retention.max_count = raw.retention.max_count;
|
|
139
|
+
if (typeof raw.retention.max_size_mb === 'number') cfg.retention.max_size_mb = raw.retention.max_size_mb;
|
|
112
140
|
}
|
|
113
141
|
if (raw.git_retention) {
|
|
114
142
|
if (raw.git_retention.enabled === true) cfg.git_retention.enabled = true;
|
|
115
|
-
if (raw.git_retention.mode)
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
if (raw.git_retention.mode) {
|
|
144
|
+
if (VALID_GIT_RETENTION_MODES.includes(raw.git_retention.mode)) {
|
|
145
|
+
cfg.git_retention.mode = raw.git_retention.mode;
|
|
146
|
+
} else {
|
|
147
|
+
warnings.push(`Unknown git_retention.mode "${raw.git_retention.mode}", using default "${cfg.git_retention.mode}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (typeof raw.git_retention.days === 'number') cfg.git_retention.days = raw.git_retention.days;
|
|
151
|
+
if (typeof raw.git_retention.max_count === 'number') cfg.git_retention.max_count = raw.git_retention.max_count;
|
|
118
152
|
}
|
|
119
|
-
return { cfg, loaded: true, error: null };
|
|
153
|
+
return { cfg, loaded: true, error: null, warnings };
|
|
120
154
|
} catch (e) {
|
|
121
155
|
return { cfg, loaded: false, error: e.message };
|
|
122
156
|
}
|
|
@@ -249,11 +283,23 @@ function timestamp() {
|
|
|
249
283
|
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
250
284
|
}
|
|
251
285
|
|
|
252
|
-
function createLogger(logFilePath) {
|
|
286
|
+
function createLogger(logFilePath, maxSizeMB = 10) {
|
|
287
|
+
let writeCount = 0;
|
|
288
|
+
function rotateIfNeeded() {
|
|
289
|
+
if (++writeCount % 100 !== 0) return;
|
|
290
|
+
try {
|
|
291
|
+
const stat = fs.statSync(logFilePath);
|
|
292
|
+
if (stat.size > maxSizeMB * 1024 * 1024) {
|
|
293
|
+
const old = logFilePath + '.old';
|
|
294
|
+
try { fs.unlinkSync(old); } catch { /* ignore */ }
|
|
295
|
+
fs.renameSync(logFilePath, old);
|
|
296
|
+
}
|
|
297
|
+
} catch { /* ignore */ }
|
|
298
|
+
}
|
|
253
299
|
return {
|
|
254
300
|
log(msg, c = 'green') {
|
|
255
301
|
const line = `${timestamp()} ${msg}`;
|
|
256
|
-
try { fs.appendFileSync(logFilePath, line + '\n'); } catch { /* ignore */ }
|
|
302
|
+
try { fs.appendFileSync(logFilePath, line + '\n'); rotateIfNeeded(); } catch { /* ignore */ }
|
|
257
303
|
console.log(color[c] ? color[c](`[guard] ${line}`) : `[guard] ${line}`);
|
|
258
304
|
},
|
|
259
305
|
info(msg) { this.log(msg, 'cyan'); },
|
package/references/recovery.md
CHANGED
|
@@ -268,24 +268,24 @@ The `auto-backup.ps1` script stores periodic snapshots on a dedicated branch via
|
|
|
268
268
|
|
|
269
269
|
```bash
|
|
270
270
|
# List recent auto-backup snapshots
|
|
271
|
-
git log
|
|
271
|
+
git log guard/auto-backup --oneline -20
|
|
272
272
|
|
|
273
273
|
# Restore a file from the latest auto-backup
|
|
274
|
-
git restore --source=
|
|
274
|
+
git restore --source=guard/auto-backup -- <path/to/file>
|
|
275
275
|
|
|
276
276
|
# Restore from a specific auto-backup snapshot
|
|
277
277
|
git restore --source=<commit-hash> -- <path/to/file>
|
|
278
278
|
|
|
279
279
|
# Diff your working copy against the auto-backup version
|
|
280
|
-
git diff
|
|
280
|
+
git diff guard/auto-backup -- <path/to/file>
|
|
281
281
|
|
|
282
282
|
# Time-based: find auto-backup snapshot from before N minutes ago
|
|
283
283
|
# 按时间查找:N 分钟前之前最近的自动备份快照
|
|
284
|
-
git log
|
|
284
|
+
git log guard/auto-backup --oneline --before="5 minutes ago" -5 -- <path/to/file>
|
|
285
285
|
|
|
286
286
|
# Version-based: list recent N auto-backup snapshots
|
|
287
287
|
# 按版本查找:最近 N 个自动备份快照
|
|
288
|
-
git log
|
|
288
|
+
git log guard/auto-backup --oneline -10 -- <path/to/file>
|
|
289
289
|
```
|
|
290
290
|
|
|
291
291
|
## If not a Git repo yet
|
|
@@ -374,10 +374,10 @@ Get-ChildItem .cursor-guard-backup/ -Directory | Where-Object {
|
|
|
374
374
|
|
|
375
375
|
```bash
|
|
376
376
|
# View auto-backup history
|
|
377
|
-
git log
|
|
377
|
+
git log guard/auto-backup --oneline -30
|
|
378
378
|
|
|
379
379
|
# Delete the branch entirely (script will recreate it on next run)
|
|
380
|
-
git
|
|
380
|
+
git update-ref -d refs/guard/auto-backup
|
|
381
381
|
|
|
382
382
|
# Reclaim disk space after removing old branches
|
|
383
383
|
git gc --prune=now
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const assert = require('assert');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const {
|
|
8
|
-
globMatch, matchesAny, loadConfig, DEFAULT_CONFIG, DEFAULT_SECRETS,
|
|
9
|
-
filterFiles, buildManifest, manifestChanged, parseArgs, walkDir,
|
|
10
|
-
} = require('./utils');
|
|
11
|
-
|
|
12
|
-
let passed = 0;
|
|
13
|
-
let failed = 0;
|
|
14
|
-
|
|
15
|
-
function test(name, fn) {
|
|
16
|
-
try {
|
|
17
|
-
fn();
|
|
18
|
-
passed++;
|
|
19
|
-
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
20
|
-
} catch (e) {
|
|
21
|
-
failed++;
|
|
22
|
-
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
23
|
-
console.log(` ${e.message}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── globMatch ────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
console.log('\nglobMatch:');
|
|
30
|
-
|
|
31
|
-
test('exact filename match', () => {
|
|
32
|
-
assert.strictEqual(globMatch('.env', '.env'), true);
|
|
33
|
-
assert.strictEqual(globMatch('.env', '.envx'), false);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('* matches within a single segment', () => {
|
|
37
|
-
assert.strictEqual(globMatch('*.js', 'foo.js'), true);
|
|
38
|
-
assert.strictEqual(globMatch('*.js', 'bar.ts'), false);
|
|
39
|
-
assert.strictEqual(globMatch('*.js', 'dir/foo.js'), false);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('** matches across directories', () => {
|
|
43
|
-
assert.strictEqual(globMatch('**/*.js', 'src/foo.js'), true);
|
|
44
|
-
assert.strictEqual(globMatch('**/*.js', 'a/b/c/foo.js'), true);
|
|
45
|
-
// **/*.js requires a slash — root-level 'foo.js' doesn't match (matchesAny checks leaf separately)
|
|
46
|
-
assert.strictEqual(globMatch('**/*.js', 'foo.js'), false);
|
|
47
|
-
assert.strictEqual(globMatch('**/*.js', 'foo.ts'), false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('? matches single character', () => {
|
|
51
|
-
assert.strictEqual(globMatch('?.txt', 'a.txt'), true);
|
|
52
|
-
assert.strictEqual(globMatch('?.txt', 'ab.txt'), false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('.env.* pattern', () => {
|
|
56
|
-
assert.strictEqual(globMatch('.env.*', '.env.local'), true);
|
|
57
|
-
assert.strictEqual(globMatch('.env.*', '.env.production'), true);
|
|
58
|
-
assert.strictEqual(globMatch('.env.*', '.env'), false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('credentials* pattern', () => {
|
|
62
|
-
assert.strictEqual(globMatch('credentials*', 'credentials'), true);
|
|
63
|
-
assert.strictEqual(globMatch('credentials*', 'credentials.json'), true);
|
|
64
|
-
assert.strictEqual(globMatch('credentials*', 'my-credentials'), false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('directory pattern src/**', () => {
|
|
68
|
-
assert.strictEqual(globMatch('src/**', 'src/foo.js'), true);
|
|
69
|
-
assert.strictEqual(globMatch('src/**', 'src/a/b.js'), true);
|
|
70
|
-
assert.strictEqual(globMatch('src/**', 'lib/foo.js'), false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('backslash normalization', () => {
|
|
74
|
-
assert.strictEqual(globMatch('src/**/*.ts', 'src\\components\\App.ts'), true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('regex special chars in pattern are escaped', () => {
|
|
78
|
-
assert.strictEqual(globMatch('file(1).txt', 'file(1).txt'), true);
|
|
79
|
-
assert.strictEqual(globMatch('file[0].txt', 'file[0].txt'), true); // [] escaped as literals
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// ── matchesAny ───────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
console.log('\nmatchesAny:');
|
|
85
|
-
|
|
86
|
-
test('matches when any pattern hits', () => {
|
|
87
|
-
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.js'), true);
|
|
88
|
-
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.ts'), true);
|
|
89
|
-
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.py'), false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('checks leaf filename for deep paths', () => {
|
|
93
|
-
assert.strictEqual(matchesAny(['*.key'], 'secrets/server.key'), true);
|
|
94
|
-
assert.strictEqual(matchesAny(['.env'], 'config/.env'), true);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('empty patterns matches nothing', () => {
|
|
98
|
-
assert.strictEqual(matchesAny([], 'anything'), false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ── loadConfig ───────────────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
console.log('\nloadConfig:');
|
|
104
|
-
|
|
105
|
-
test('returns defaults when no config file', () => {
|
|
106
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
107
|
-
try {
|
|
108
|
-
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
109
|
-
assert.strictEqual(loaded, false);
|
|
110
|
-
assert.strictEqual(error, null);
|
|
111
|
-
assert.deepStrictEqual(cfg.protect, []);
|
|
112
|
-
assert.deepStrictEqual(cfg.ignore, []);
|
|
113
|
-
assert.deepStrictEqual(cfg.secrets_patterns, DEFAULT_SECRETS);
|
|
114
|
-
assert.strictEqual(cfg.backup_strategy, 'git');
|
|
115
|
-
assert.strictEqual(cfg.git_retention.enabled, false);
|
|
116
|
-
} finally {
|
|
117
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('loads and merges custom config', () => {
|
|
122
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
123
|
-
try {
|
|
124
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
125
|
-
protect: ['src/**'],
|
|
126
|
-
backup_strategy: 'both',
|
|
127
|
-
retention: { mode: 'count', max_count: 50 },
|
|
128
|
-
}));
|
|
129
|
-
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
130
|
-
assert.strictEqual(loaded, true);
|
|
131
|
-
assert.strictEqual(error, null);
|
|
132
|
-
assert.deepStrictEqual(cfg.protect, ['src/**']);
|
|
133
|
-
assert.strictEqual(cfg.backup_strategy, 'both');
|
|
134
|
-
assert.strictEqual(cfg.retention.mode, 'count');
|
|
135
|
-
assert.strictEqual(cfg.retention.max_count, 50);
|
|
136
|
-
assert.strictEqual(cfg.retention.days, 30); // default preserved
|
|
137
|
-
} finally {
|
|
138
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('handles malformed JSON gracefully', () => {
|
|
143
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
144
|
-
try {
|
|
145
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), '{ broken }');
|
|
146
|
-
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
147
|
-
assert.strictEqual(loaded, false);
|
|
148
|
-
assert.ok(error, 'should have an error message');
|
|
149
|
-
assert.deepStrictEqual(cfg.protect, []);
|
|
150
|
-
} finally {
|
|
151
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('secrets_patterns override replaces defaults entirely', () => {
|
|
156
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
157
|
-
try {
|
|
158
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
159
|
-
secrets_patterns: ['my-secret'],
|
|
160
|
-
}));
|
|
161
|
-
const { cfg } = loadConfig(tmpDir);
|
|
162
|
-
assert.deepStrictEqual(cfg.secrets_patterns, ['my-secret']);
|
|
163
|
-
} finally {
|
|
164
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('secrets_patterns_extra appends to defaults', () => {
|
|
169
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
170
|
-
try {
|
|
171
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
172
|
-
secrets_patterns_extra: ['*.secret', 'tokens.*'],
|
|
173
|
-
}));
|
|
174
|
-
const { cfg } = loadConfig(tmpDir);
|
|
175
|
-
assert.ok(cfg.secrets_patterns.includes('.env'), 'should keep default .env');
|
|
176
|
-
assert.ok(cfg.secrets_patterns.includes('*.p12'), 'should keep default *.p12');
|
|
177
|
-
assert.ok(cfg.secrets_patterns.includes('*.secret'), 'should include extra *.secret');
|
|
178
|
-
assert.ok(cfg.secrets_patterns.includes('tokens.*'), 'should include extra tokens.*');
|
|
179
|
-
} finally {
|
|
180
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('secrets_patterns_extra merges with custom secrets_patterns', () => {
|
|
185
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
186
|
-
try {
|
|
187
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
188
|
-
secrets_patterns: ['.env'],
|
|
189
|
-
secrets_patterns_extra: ['.env', '*.secret'],
|
|
190
|
-
}));
|
|
191
|
-
const { cfg } = loadConfig(tmpDir);
|
|
192
|
-
assert.deepStrictEqual(cfg.secrets_patterns, ['.env', '*.secret']);
|
|
193
|
-
} finally {
|
|
194
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('non-string backup_strategy is ignored', () => {
|
|
199
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
200
|
-
try {
|
|
201
|
-
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
202
|
-
backup_strategy: 123,
|
|
203
|
-
auto_backup_interval_seconds: 'bad',
|
|
204
|
-
}));
|
|
205
|
-
const { cfg } = loadConfig(tmpDir);
|
|
206
|
-
assert.strictEqual(cfg.backup_strategy, 'git');
|
|
207
|
-
assert.strictEqual(cfg.auto_backup_interval_seconds, 60);
|
|
208
|
-
} finally {
|
|
209
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ── filterFiles ──────────────────────────────────────────────────
|
|
214
|
-
|
|
215
|
-
console.log('\nfilterFiles:');
|
|
216
|
-
|
|
217
|
-
const makeFiles = names => names.map(n => ({ full: `/fake/${n}`, rel: n, name: path.basename(n) }));
|
|
218
|
-
|
|
219
|
-
test('no protect/ignore returns all non-secret files', () => {
|
|
220
|
-
const files = makeFiles(['a.js', 'b.ts', '.env', 'credentials.json']);
|
|
221
|
-
const cfg = { ...DEFAULT_CONFIG };
|
|
222
|
-
const result = filterFiles(files, cfg);
|
|
223
|
-
const rels = result.map(f => f.rel);
|
|
224
|
-
assert.ok(!rels.includes('.env'));
|
|
225
|
-
assert.ok(!rels.includes('credentials.json'));
|
|
226
|
-
assert.ok(rels.includes('a.js'));
|
|
227
|
-
assert.ok(rels.includes('b.ts'));
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('protect narrows scope', () => {
|
|
231
|
-
const files = makeFiles(['src/a.js', 'lib/b.js', 'README.md']);
|
|
232
|
-
const cfg = { ...DEFAULT_CONFIG, protect: ['src/**'] };
|
|
233
|
-
const result = filterFiles(files, cfg);
|
|
234
|
-
assert.strictEqual(result.length, 1);
|
|
235
|
-
assert.strictEqual(result[0].rel, 'src/a.js');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
test('ignore excludes files', () => {
|
|
239
|
-
const files = makeFiles(['src/a.js', 'src/a.test.js', 'src/b.js']);
|
|
240
|
-
const cfg = { ...DEFAULT_CONFIG, ignore: ['**/*.test.js'] };
|
|
241
|
-
const result = filterFiles(files, cfg);
|
|
242
|
-
const rels = result.map(f => f.rel);
|
|
243
|
-
assert.ok(!rels.includes('src/a.test.js'));
|
|
244
|
-
assert.ok(rels.includes('src/a.js'));
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ── manifestChanged ──────────────────────────────────────────────
|
|
248
|
-
|
|
249
|
-
console.log('\nmanifestChanged:');
|
|
250
|
-
|
|
251
|
-
test('null old manifest means changed', () => {
|
|
252
|
-
assert.strictEqual(manifestChanged(null, { 'a.js': { mtimeMs: 1, size: 100 } }), true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test('identical manifests are not changed', () => {
|
|
256
|
-
const m = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
257
|
-
assert.strictEqual(manifestChanged(m, { ...m }), false);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
test('different mtime means changed', () => {
|
|
261
|
-
const old = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
262
|
-
const nw = { 'a.js': { mtimeMs: 2, size: 100 } };
|
|
263
|
-
assert.strictEqual(manifestChanged(old, nw), true);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
test('new file means changed', () => {
|
|
267
|
-
const old = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
268
|
-
const nw = { 'a.js': { mtimeMs: 1, size: 100 }, 'b.js': { mtimeMs: 2, size: 50 } };
|
|
269
|
-
assert.strictEqual(manifestChanged(old, nw), true);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// ── parseArgs ────────────────────────────────────────────────────
|
|
273
|
-
|
|
274
|
-
console.log('\nparseArgs:');
|
|
275
|
-
|
|
276
|
-
test('parses --key value pairs', () => {
|
|
277
|
-
const args = parseArgs(['node', 'script', '--path', '/tmp', '--interval', '30']);
|
|
278
|
-
assert.strictEqual(args.path, '/tmp');
|
|
279
|
-
assert.strictEqual(args.interval, '30');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('parses boolean flags', () => {
|
|
283
|
-
const args = parseArgs(['node', 'script', '--verbose']);
|
|
284
|
-
assert.strictEqual(args.verbose, true);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test('empty args returns empty object', () => {
|
|
288
|
-
const args = parseArgs(['node', 'script']);
|
|
289
|
-
assert.deepStrictEqual(args, {});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// ── walkDir ──────────────────────────────────────────────────────
|
|
293
|
-
|
|
294
|
-
console.log('\nwalkDir:');
|
|
295
|
-
|
|
296
|
-
test('discovers files recursively', () => {
|
|
297
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
|
|
298
|
-
try {
|
|
299
|
-
fs.mkdirSync(path.join(tmpDir, 'sub'), { recursive: true });
|
|
300
|
-
fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'a');
|
|
301
|
-
fs.writeFileSync(path.join(tmpDir, 'sub', 'b.txt'), 'b');
|
|
302
|
-
const files = walkDir(tmpDir, tmpDir);
|
|
303
|
-
const rels = files.map(f => f.rel).sort();
|
|
304
|
-
assert.deepStrictEqual(rels, ['a.txt', 'sub/b.txt']);
|
|
305
|
-
} finally {
|
|
306
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test('skips .git and node_modules', () => {
|
|
311
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
|
|
312
|
-
try {
|
|
313
|
-
fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
|
|
314
|
-
fs.mkdirSync(path.join(tmpDir, 'node_modules'), { recursive: true });
|
|
315
|
-
fs.writeFileSync(path.join(tmpDir, '.git', 'HEAD'), 'ref');
|
|
316
|
-
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'x.js'), 'x');
|
|
317
|
-
fs.writeFileSync(path.join(tmpDir, 'real.js'), 'y');
|
|
318
|
-
const files = walkDir(tmpDir, tmpDir);
|
|
319
|
-
assert.strictEqual(files.length, 1);
|
|
320
|
-
assert.strictEqual(files[0].rel, 'real.js');
|
|
321
|
-
} finally {
|
|
322
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// ── Summary ──────────────────────────────────────────────────────
|
|
327
|
-
|
|
328
|
-
console.log(`\n${passed + failed} tests: \x1b[32m${passed} passed\x1b[0m` + (failed ? `, \x1b[31m${failed} failed\x1b[0m` : ''));
|
|
329
|
-
process.exit(failed > 0 ? 1 : 0);
|