cursor-guard 1.3.0 → 1.4.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 +13 -0
- package/README.zh-CN.md +13 -0
- package/SKILL.md +54 -22
- package/package.json +1 -1
- package/references/auto-backup.ps1 +164 -44
- package/references/config-reference.md +160 -0
- package/references/config-reference.zh-CN.md +160 -0
- package/references/cursor-guard.example.json +7 -10
- package/references/cursor-guard.schema.json +36 -0
- package/references/guard-doctor.ps1 +226 -0
- package/references/recovery.md +32 -12
package/README.md
CHANGED
|
@@ -132,10 +132,23 @@ Edit `.cursor-guard.json` to define which files to protect:
|
|
|
132
132
|
"ignore": ["node_modules/**", "dist/**"],
|
|
133
133
|
"auto_backup_interval_seconds": 60,
|
|
134
134
|
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"],
|
|
135
|
+
"pre_restore_backup": "always",
|
|
135
136
|
"retention": { "mode": "days", "days": 30 }
|
|
136
137
|
}
|
|
137
138
|
```
|
|
138
139
|
|
|
140
|
+
#### `pre_restore_backup` — restore behavior control
|
|
141
|
+
|
|
142
|
+
| Value | Behavior |
|
|
143
|
+
|-------|----------|
|
|
144
|
+
| `"always"` (default) | Automatically preserve current version before every restore. No prompt. |
|
|
145
|
+
| `"ask"` | Prompt you each time: "Preserve current version before restore? (Y/n)" — you decide per restore. |
|
|
146
|
+
| `"never"` | Never preserve current version before restore (not recommended). |
|
|
147
|
+
|
|
148
|
+
Regardless of config, you can always override per-request:
|
|
149
|
+
- Say "don't preserve current version" to skip even when config is `"always"`
|
|
150
|
+
- Say "preserve current first" to force even when config is `"never"`
|
|
151
|
+
|
|
139
152
|
---
|
|
140
153
|
|
|
141
154
|
## Auto-Backup Script
|
package/README.zh-CN.md
CHANGED
|
@@ -132,10 +132,23 @@ cp .cursor/skills/cursor-guard/references/cursor-guard.example.json .cursor-guar
|
|
|
132
132
|
"ignore": ["node_modules/**", "dist/**"],
|
|
133
133
|
"auto_backup_interval_seconds": 60,
|
|
134
134
|
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"],
|
|
135
|
+
"pre_restore_backup": "always",
|
|
135
136
|
"retention": { "mode": "days", "days": 30 }
|
|
136
137
|
}
|
|
137
138
|
```
|
|
138
139
|
|
|
140
|
+
#### `pre_restore_backup` — 恢复前保留行为控制
|
|
141
|
+
|
|
142
|
+
| 值 | 行为 |
|
|
143
|
+
|----|------|
|
|
144
|
+
| `"always"`(默认) | 每次恢复前自动保留当前版本,无需确认。 |
|
|
145
|
+
| `"ask"` | 每次恢复前询问你:"恢复前是否保留当前版本?(Y/n)"——由你逐次决定。 |
|
|
146
|
+
| `"never"` | 恢复前不保留当前版本(不推荐)。 |
|
|
147
|
+
|
|
148
|
+
无论配置如何,你始终可以在单次请求中覆盖:
|
|
149
|
+
- 说"不保留当前版本"可跳过保留(即使配置为 `"always"`)
|
|
150
|
+
- 说"先保留当前版本"可强制保留(即使配置为 `"never"`)
|
|
151
|
+
|
|
139
152
|
---
|
|
140
153
|
|
|
141
154
|
## 自动备份脚本
|
package/SKILL.md
CHANGED
|
@@ -20,6 +20,7 @@ Use this skill when any of the following appear:
|
|
|
20
20
|
- **Parallel context**: Multiple repos or branches; unclear which folder is the workspace root.
|
|
21
21
|
- **Recovery asks**: e.g. "改不回来", "丢版本", "回滚", "reflog", "误删", or English equivalents.
|
|
22
22
|
- **Time/version recovery**: e.g. "恢复到5分钟前", "恢复到前3个版本", "回到上一个版本", "restore to 10 minutes ago", "go back 2 versions", "恢复到下午3点的状态".
|
|
23
|
+
- **Health check**: e.g. "guard doctor", "检查备份配置", "自检", "诊断guard", "check guard setup". Run `guard-doctor.ps1` and report results.
|
|
23
24
|
|
|
24
25
|
If none of the above, do not expand scope; answer normally.
|
|
25
26
|
|
|
@@ -44,6 +45,12 @@ On first trigger in a session, check if the workspace root contains `.cursor-gua
|
|
|
44
45
|
// Built-in defaults: .env, .env.*, *.key, *.pem, *.p12, *.pfx, credentials*
|
|
45
46
|
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"],
|
|
46
47
|
|
|
48
|
+
// Controls behavior before restore operations.
|
|
49
|
+
// "always" (default): automatically preserve current version before every restore.
|
|
50
|
+
// "ask": prompt the user each time to decide.
|
|
51
|
+
// "never": skip preservation entirely (not recommended).
|
|
52
|
+
"pre_restore_backup": "always",
|
|
53
|
+
|
|
47
54
|
// Retention for shadow copies. mode: "days" | "count" | "size"
|
|
48
55
|
"retention": { "mode": "days", "days": 30, "max_count": 100, "max_size_mb": 500 }
|
|
49
56
|
}
|
|
@@ -94,19 +101,22 @@ When the target file of an edit **falls outside the protected scope**, the agent
|
|
|
94
101
|
Use a **temporary index and dedicated ref** so the user's staged/unstaged state is never touched:
|
|
95
102
|
|
|
96
103
|
```bash
|
|
104
|
+
GIT_DIR=$(git rev-parse --git-dir)
|
|
105
|
+
GUARD_IDX="$GIT_DIR/guard-snapshot-index"
|
|
106
|
+
|
|
97
107
|
# 1. Create temp index from HEAD
|
|
98
|
-
GIT_INDEX_FILE
|
|
108
|
+
GIT_INDEX_FILE="$GUARD_IDX" git read-tree HEAD
|
|
99
109
|
|
|
100
110
|
# 2. Stage working-tree files into temp index
|
|
101
|
-
GIT_INDEX_FILE
|
|
111
|
+
GIT_INDEX_FILE="$GUARD_IDX" git add -A
|
|
102
112
|
|
|
103
113
|
# 3. Write tree and create commit on a guard ref (not on the user's branch)
|
|
104
|
-
TREE=$(GIT_INDEX_FILE
|
|
105
|
-
COMMIT=$(git commit-tree $TREE -p HEAD -m "guard: snapshot before ai edit")
|
|
106
|
-
git update-ref refs/guard/snapshot $COMMIT
|
|
114
|
+
TREE=$(GIT_INDEX_FILE="$GUARD_IDX" git write-tree)
|
|
115
|
+
COMMIT=$(git commit-tree "$TREE" -p HEAD -m "guard: snapshot before ai edit")
|
|
116
|
+
git update-ref refs/guard/snapshot "$COMMIT"
|
|
107
117
|
|
|
108
118
|
# 4. Cleanup
|
|
109
|
-
rm
|
|
119
|
+
rm -f "$GUARD_IDX"
|
|
110
120
|
```
|
|
111
121
|
|
|
112
122
|
**PowerShell equivalent** (for agent Shell calls):
|
|
@@ -302,19 +312,26 @@ Recommended: #1 (closest to target time). Restore this one? / 推荐 #1(最接
|
|
|
302
312
|
- If still nothing, report clearly: "No snapshot found before that time. The earliest available is [hash] at [time]. Do you want to use it?"
|
|
303
313
|
- **Never silently pick a version.** Always show and confirm.
|
|
304
314
|
|
|
305
|
-
### Step 4: Preserve Current Version Before Restore
|
|
315
|
+
### Step 4: Preserve Current Version Before Restore
|
|
306
316
|
|
|
307
317
|
> **Rule: `restore_requires_preserve_current_by_default`**
|
|
308
318
|
>
|
|
309
|
-
>
|
|
319
|
+
> The behavior is controlled by `pre_restore_backup` in `.cursor-guard.json` (default: `"always"`).
|
|
320
|
+
|
|
321
|
+
**4a. Determine preservation mode**
|
|
310
322
|
|
|
311
|
-
|
|
323
|
+
Read `pre_restore_backup` from config (§0). Three modes:
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
| Config value | Behavior |
|
|
326
|
+
|-------------|----------|
|
|
327
|
+
| `"always"` (default) | Automatically preserve current version. No prompt. Jump to 4b. |
|
|
328
|
+
| `"ask"` | Prompt the user: "恢复前是否保留当前版本?(Y/n)" / "Preserve current version before restore? (Y/n)". If user answers yes/default → jump to 4b. If user answers no → inform and jump to Step 5. |
|
|
329
|
+
| `"never"` | Skip preservation entirely. Inform: "配置已设为不保留当前版本 (pre_restore_backup=never),直接恢复。" and jump to Step 5. |
|
|
316
330
|
|
|
317
|
-
|
|
331
|
+
**Override rules** (apply regardless of config):
|
|
332
|
+
- If the user **explicitly** says "不保留当前版本" / "skip backup before restore" in the current message → skip, even if config is `"always"`.
|
|
333
|
+
- If the user **explicitly** says "先保留当前版本" / "preserve current first" → preserve, even if config is `"never"`.
|
|
334
|
+
- User's explicit instruction in the current message always takes priority over config.
|
|
318
335
|
|
|
319
336
|
**4b. Determine preservation scope**
|
|
320
337
|
|
|
@@ -346,9 +363,12 @@ Then jump to Step 5.
|
|
|
346
363
|
|
|
347
364
|
Use the same temp-index plumbing as §2a to avoid polluting the user's staging area:
|
|
348
365
|
|
|
349
|
-
**Git repo (preferred):**
|
|
366
|
+
**Git repo (preferred) — timestamped ref stack:**
|
|
367
|
+
|
|
368
|
+
Each pre-restore snapshot writes to a unique ref `refs/guard/pre-restore/<yyyyMMdd_HHmmss>` so consecutive restores never overwrite each other:
|
|
350
369
|
|
|
351
370
|
```powershell
|
|
371
|
+
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
352
372
|
$guardIdx = Join-Path (git rev-parse --git-dir) "guard-pre-restore-index"
|
|
353
373
|
$env:GIT_INDEX_FILE = $guardIdx
|
|
354
374
|
|
|
@@ -365,10 +385,16 @@ $env:GIT_INDEX_FILE = $null
|
|
|
365
385
|
Remove-Item $guardIdx -Force -ErrorAction SilentlyContinue
|
|
366
386
|
|
|
367
387
|
$commit = git commit-tree $tree -p HEAD -m "guard: preserve current before restore to <target>"
|
|
388
|
+
git update-ref "refs/guard/pre-restore/$ts" $commit
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Record the short hash and the ref path. Also update `refs/guard/pre-restore` as an alias pointing to the latest:
|
|
392
|
+
|
|
393
|
+
```powershell
|
|
368
394
|
git update-ref refs/guard/pre-restore $commit
|
|
369
395
|
```
|
|
370
396
|
|
|
371
|
-
|
|
397
|
+
To list all pre-restore snapshots: `git for-each-ref refs/guard/pre-restore/ --sort=-creatordate --format="%(refname:short) %(creatordate:short) %(objectname:short)"`
|
|
372
398
|
|
|
373
399
|
**Non-Git fallback (shadow copy):**
|
|
374
400
|
|
|
@@ -391,12 +417,14 @@ If the snapshot fails (e.g. disk full, permission error):
|
|
|
391
417
|
Before executing restore, tell the user:
|
|
392
418
|
```
|
|
393
419
|
在恢复前,我已保留当前版本:
|
|
394
|
-
- 备份引用: refs/guard/pre-restore (abc1234)
|
|
395
|
-
- 恢复方式: git restore --source=refs/guard/pre-restore -- <file>
|
|
420
|
+
- 备份引用: refs/guard/pre-restore/20260321_163005 (abc1234)
|
|
421
|
+
- 恢复方式: git restore --source=refs/guard/pre-restore/20260321_163005 -- <file>
|
|
422
|
+
- 历史栈: git for-each-ref refs/guard/pre-restore/ --sort=-creatordate
|
|
396
423
|
|
|
397
424
|
Current version preserved before restore:
|
|
398
|
-
- Backup ref: refs/guard/pre-restore (abc1234)
|
|
399
|
-
- To undo: git restore --source=refs/guard/pre-restore -- <file>
|
|
425
|
+
- Backup ref: refs/guard/pre-restore/20260321_163005 (abc1234)
|
|
426
|
+
- To undo: git restore --source=refs/guard/pre-restore/20260321_163005 -- <file>
|
|
427
|
+
- History: git for-each-ref refs/guard/pre-restore/ --sort=-creatordate
|
|
400
428
|
```
|
|
401
429
|
|
|
402
430
|
### Step 5: Execute Recovery
|
|
@@ -438,11 +466,12 @@ After restoring, always:
|
|
|
438
466
|
|
|
439
467
|
```markdown
|
|
440
468
|
**Cursor Guard — restore status**
|
|
441
|
-
- **Pre-restore backup**: `refs/guard/pre-restore
|
|
469
|
+
- **Pre-restore backup**: `refs/guard/pre-restore/<ts>` (`<short-hash>`) or `shadow copy at .cursor-guard-backup/pre-restore-<ts>/` or `skipped (user opted out)` or `skipped (no changes)`
|
|
442
470
|
- **Restored to**: `<target-hash>` / `<target description>`
|
|
443
471
|
- **Scope**: single file `<path>` / N files / entire project
|
|
444
472
|
- **Result**: success / failed
|
|
445
|
-
- **To undo restore**: `git restore --source=refs/guard/pre-restore -- <file>`
|
|
473
|
+
- **To undo restore**: `git restore --source=refs/guard/pre-restore/<ts> -- <file>`
|
|
474
|
+
- **All pre-restore snapshots**: `git for-each-ref refs/guard/pre-restore/ --sort=-creatordate`
|
|
446
475
|
```
|
|
447
476
|
|
|
448
477
|
---
|
|
@@ -486,7 +515,7 @@ Skip the block for unrelated turns.
|
|
|
486
515
|
|
|
487
516
|
- If the workspace has `.cursor-guard.json`, the agent MUST read and follow it (see §0).
|
|
488
517
|
- If `.cursor-guard-backup/` folder exists, align shadow copy paths with it.
|
|
489
|
-
- Template config: [references/cursor-guard.example.json](references/cursor-guard.example.json) — copy to workspace root and customize.
|
|
518
|
+
- Template config: [references/cursor-guard.example.json](references/cursor-guard.example.json) — copy to workspace root and customize. Field docs: [references/config-reference.md](references/config-reference.md).
|
|
490
519
|
|
|
491
520
|
---
|
|
492
521
|
|
|
@@ -496,3 +525,6 @@ Skip the block for unrelated turns.
|
|
|
496
525
|
- Auto-backup script: [references/auto-backup.ps1](references/auto-backup.ps1)
|
|
497
526
|
- Config JSON Schema: [references/cursor-guard.schema.json](references/cursor-guard.schema.json)
|
|
498
527
|
- Example config: [references/cursor-guard.example.json](references/cursor-guard.example.json)
|
|
528
|
+
- Config field reference (EN): [references/config-reference.md](references/config-reference.md)
|
|
529
|
+
- 配置参数说明(中文): [references/config-reference.zh-CN.md](references/config-reference.zh-CN.md)
|
|
530
|
+
- Health check: [references/guard-doctor.ps1](references/guard-doctor.ps1) — run `.\guard-doctor.ps1 -Path .` to verify setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -37,18 +37,27 @@ $ErrorActionPreference = "Stop"
|
|
|
37
37
|
$resolved = (Resolve-Path $Path).Path
|
|
38
38
|
Set-Location $resolved
|
|
39
39
|
|
|
40
|
-
# ──
|
|
41
|
-
$
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
$
|
|
46
|
-
|
|
40
|
+
# ── Git availability check ────────────────────────────────────────
|
|
41
|
+
$hasGit = $false
|
|
42
|
+
$gitDir = $null
|
|
43
|
+
$isRepo = $false
|
|
44
|
+
if (Get-Command git -ErrorAction SilentlyContinue) {
|
|
45
|
+
$hasGit = $true
|
|
46
|
+
$isRepo = (git rev-parse --is-inside-work-tree 2>$null) -eq "true"
|
|
47
|
+
if ($isRepo) {
|
|
48
|
+
$gitDir = (git rev-parse --git-dir 2>$null)
|
|
49
|
+
if ($gitDir) {
|
|
50
|
+
$gitDir = (Resolve-Path $gitDir -ErrorAction SilentlyContinue).Path
|
|
51
|
+
}
|
|
52
|
+
if (-not $gitDir) { $gitDir = Join-Path $resolved ".git" }
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
|
|
56
|
+
# ── Paths ─────────────────────────────────────────────────────────
|
|
50
57
|
$backupDir = Join-Path $resolved ".cursor-guard-backup"
|
|
51
58
|
$logFilePath = Join-Path $backupDir "backup.log"
|
|
59
|
+
$lockFile = if ($gitDir) { Join-Path $gitDir "cursor-guard.lock" } else { Join-Path $backupDir "cursor-guard.lock" }
|
|
60
|
+
$guardIndex = if ($gitDir) { Join-Path $gitDir "cursor-guard-index" } else { $null }
|
|
52
61
|
|
|
53
62
|
# ── Cleanup on exit ───────────────────────────────────────────────
|
|
54
63
|
function Invoke-Cleanup {
|
|
@@ -67,6 +76,10 @@ $retentionMode = "days"
|
|
|
67
76
|
$retentionDays = 30
|
|
68
77
|
$retentionMaxCnt = 100
|
|
69
78
|
$retentionMaxMB = 500
|
|
79
|
+
$gitRetEnabled = $false
|
|
80
|
+
$gitRetMode = "count"
|
|
81
|
+
$gitRetDays = 30
|
|
82
|
+
$gitRetMaxCnt = 200
|
|
70
83
|
|
|
71
84
|
# ── Load .cursor-guard.json ──────────────────────────────────────
|
|
72
85
|
$cfgPath = Join-Path $resolved ".cursor-guard.json"
|
|
@@ -86,7 +99,13 @@ if (Test-Path $cfgPath) {
|
|
|
86
99
|
if ($cfg.retention.max_count) { $retentionMaxCnt = $cfg.retention.max_count }
|
|
87
100
|
if ($cfg.retention.max_size_mb) { $retentionMaxMB = $cfg.retention.max_size_mb }
|
|
88
101
|
}
|
|
89
|
-
|
|
102
|
+
if ($cfg.git_retention) {
|
|
103
|
+
if ($cfg.git_retention.enabled -eq $true) { $gitRetEnabled = $true }
|
|
104
|
+
if ($cfg.git_retention.mode) { $gitRetMode = $cfg.git_retention.mode }
|
|
105
|
+
if ($cfg.git_retention.days) { $gitRetDays = $cfg.git_retention.days }
|
|
106
|
+
if ($cfg.git_retention.max_count) { $gitRetMaxCnt = $cfg.git_retention.max_count }
|
|
107
|
+
}
|
|
108
|
+
Write-Host "[guard] Config loaded protect=$($protectPatterns.Count) ignore=$($ignorePatterns.Count) retention=$retentionMode git_retention=$(if($gitRetEnabled){'on'}else{'off'})" -ForegroundColor Cyan
|
|
90
109
|
}
|
|
91
110
|
catch {
|
|
92
111
|
Write-Host "[guard] WARNING: .cursor-guard.json parse error - using defaults." -ForegroundColor Yellow
|
|
@@ -95,12 +114,21 @@ if (Test-Path $cfgPath) {
|
|
|
95
114
|
}
|
|
96
115
|
if ($IntervalSeconds -eq 0) { $IntervalSeconds = 60 }
|
|
97
116
|
|
|
98
|
-
# ── Git repo check
|
|
99
|
-
$
|
|
100
|
-
if ($
|
|
117
|
+
# ── Git repo check (only required for git/both strategy) ─────────
|
|
118
|
+
$needsGit = ($backupStrategy -eq "git" -or $backupStrategy -eq "both")
|
|
119
|
+
if ($needsGit -and -not $isRepo) {
|
|
120
|
+
if (-not $hasGit) {
|
|
121
|
+
Write-Host "[guard] ERROR: backup_strategy='$backupStrategy' requires Git, but git is not installed." -ForegroundColor Red
|
|
122
|
+
Write-Host " Either install Git or set backup_strategy to 'shadow' in .cursor-guard.json." -ForegroundColor Yellow
|
|
123
|
+
exit 1
|
|
124
|
+
}
|
|
101
125
|
$ans = Read-Host "Directory is not a Git repo. Initialize? (y/n)"
|
|
102
126
|
if ($ans -eq 'y') {
|
|
103
127
|
git init
|
|
128
|
+
$isRepo = $true
|
|
129
|
+
$gitDir = (Resolve-Path (git rev-parse --git-dir)).Path
|
|
130
|
+
$lockFile = Join-Path $gitDir "cursor-guard.lock"
|
|
131
|
+
$guardIndex = Join-Path $gitDir "cursor-guard-index"
|
|
104
132
|
$gi = Join-Path $resolved ".gitignore"
|
|
105
133
|
$entry = ".cursor-guard-backup/"
|
|
106
134
|
if (Test-Path $gi) {
|
|
@@ -114,10 +142,16 @@ if ($isRepo -ne "true") {
|
|
|
114
142
|
git add -A; git commit -m "guard: initial snapshot" --no-verify
|
|
115
143
|
Write-Host "[guard] Repo initialized with snapshot." -ForegroundColor Green
|
|
116
144
|
} else {
|
|
117
|
-
Write-Host "[guard] Git is required. Exiting." -ForegroundColor Red
|
|
145
|
+
Write-Host "[guard] Git is required for '$backupStrategy' strategy. Exiting." -ForegroundColor Red
|
|
118
146
|
exit 1
|
|
119
147
|
}
|
|
120
148
|
}
|
|
149
|
+
if (-not $isRepo -and $backupStrategy -eq "shadow") {
|
|
150
|
+
Write-Host "[guard] Non-Git directory detected. Running in shadow-only mode." -ForegroundColor Cyan
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# ── Ensure backup dir exists ──────────────────────────────────────
|
|
154
|
+
if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Force $backupDir | Out-Null }
|
|
121
155
|
|
|
122
156
|
# ── Lock file (prevent multiple instances) ───────────────────────
|
|
123
157
|
if (Test-Path $lockFile) {
|
|
@@ -127,30 +161,30 @@ if (Test-Path $lockFile) {
|
|
|
127
161
|
}
|
|
128
162
|
Set-Content $lockFile "pid=$PID`nstarted=$(Get-Date -Format 'o')"
|
|
129
163
|
|
|
130
|
-
# ──
|
|
164
|
+
# ── Git-specific setup (skip entirely for shadow-only) ───────────
|
|
131
165
|
$branch = "cursor-guard/auto-backup"
|
|
132
166
|
$branchRef = "refs/heads/$branch"
|
|
133
|
-
if (
|
|
134
|
-
git
|
|
135
|
-
|
|
136
|
-
|
|
167
|
+
if ($isRepo) {
|
|
168
|
+
if (-not (git rev-parse --verify $branchRef 2>$null)) {
|
|
169
|
+
git branch $branch HEAD 2>$null
|
|
170
|
+
Write-Host "[guard] Created branch: $branch" -ForegroundColor Green
|
|
171
|
+
}
|
|
137
172
|
|
|
138
|
-
|
|
139
|
-
$
|
|
140
|
-
$excludeDir
|
|
141
|
-
|
|
142
|
-
$
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
$excludeFile = Join-Path $gitDir "info/exclude"
|
|
174
|
+
$excludeDir = Split-Path $excludeFile
|
|
175
|
+
if (-not (Test-Path $excludeDir)) { New-Item -ItemType Directory -Force $excludeDir | Out-Null }
|
|
176
|
+
$excludeEntry = ".cursor-guard-backup/"
|
|
177
|
+
if (Test-Path $excludeFile) {
|
|
178
|
+
$content = Get-Content $excludeFile -Raw -ErrorAction SilentlyContinue
|
|
179
|
+
if (-not $content -or $content -notmatch [regex]::Escape($excludeEntry)) {
|
|
180
|
+
Add-Content $excludeFile "`n$excludeEntry"
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
Set-Content $excludeFile $excludeEntry
|
|
147
184
|
}
|
|
148
|
-
} else {
|
|
149
|
-
Set-Content $excludeFile $excludeEntry
|
|
150
185
|
}
|
|
151
186
|
|
|
152
|
-
# ── Log
|
|
153
|
-
if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Force $backupDir | Out-Null }
|
|
187
|
+
# ── Log & helpers ────────────────────────────────────────────────
|
|
154
188
|
|
|
155
189
|
function Write-Log {
|
|
156
190
|
param([string]$Msg, [ConsoleColor]$Color = "Green")
|
|
@@ -232,30 +266,93 @@ function Invoke-RetentionCleanup {
|
|
|
232
266
|
} catch {}
|
|
233
267
|
}
|
|
234
268
|
|
|
269
|
+
# ── Git branch retention ─────────────────────────────────────────
|
|
270
|
+
function Invoke-GitRetention {
|
|
271
|
+
if (-not $gitRetEnabled -or -not $isRepo) { return }
|
|
272
|
+
$commits = git rev-list $branchRef 2>$null
|
|
273
|
+
if (-not $commits) { return }
|
|
274
|
+
$commitList = @($commits)
|
|
275
|
+
$total = $commitList.Count
|
|
276
|
+
|
|
277
|
+
$keepCount = $total
|
|
278
|
+
switch ($gitRetMode) {
|
|
279
|
+
"count" {
|
|
280
|
+
$keepCount = [math]::Min($total, $gitRetMaxCnt)
|
|
281
|
+
}
|
|
282
|
+
"days" {
|
|
283
|
+
$cutoff = (Get-Date).AddDays(-$gitRetDays).ToString("yyyy-MM-ddTHH:mm:ss")
|
|
284
|
+
$keepCommits = git rev-list $branchRef --after=$cutoff 2>$null
|
|
285
|
+
$keepCount = if ($keepCommits) { @($keepCommits).Count } else { 0 }
|
|
286
|
+
$keepCount = [math]::Max($keepCount, 10)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if ($keepCount -ge $total) { return }
|
|
291
|
+
|
|
292
|
+
$newTip = $commitList[$keepCount - 1]
|
|
293
|
+
$orphanTree = git rev-parse "${newTip}^{tree}" 2>$null
|
|
294
|
+
if (-not $orphanTree) { return }
|
|
295
|
+
|
|
296
|
+
$orphanCommit = git commit-tree $orphanTree -m "guard: retention truncation point"
|
|
297
|
+
if (-not $orphanCommit) { return }
|
|
298
|
+
|
|
299
|
+
$keptCommits = $commitList[0..($keepCount - 2)]
|
|
300
|
+
$grafts = @()
|
|
301
|
+
foreach ($i in 0..($keptCommits.Count - 1)) {
|
|
302
|
+
if ($i -lt ($keptCommits.Count - 1)) {
|
|
303
|
+
$grafts += "$($keptCommits[$i]) $($commitList[$i + 1])"
|
|
304
|
+
} else {
|
|
305
|
+
$grafts += "$($keptCommits[$i]) $orphanCommit"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
$infoDir = Join-Path $gitDir "info"
|
|
310
|
+
if (-not (Test-Path $infoDir)) { New-Item -ItemType Directory -Force $infoDir | Out-Null }
|
|
311
|
+
$graftsFile = Join-Path $infoDir "grafts"
|
|
312
|
+
$grafts | Set-Content $graftsFile
|
|
313
|
+
git filter-branch -f --tag-name-filter cat -- $branchRef 2>$null
|
|
314
|
+
Remove-Item $graftsFile -Force -ErrorAction SilentlyContinue
|
|
315
|
+
|
|
316
|
+
$pruned = $total - $keepCount
|
|
317
|
+
Write-Log "Git retention ($gitRetMode): pruned $pruned old commit(s), kept $keepCount" DarkGray
|
|
318
|
+
}
|
|
319
|
+
|
|
235
320
|
# ── Shadow copy helper ────────────────────────────────────────────
|
|
236
321
|
function Invoke-ShadowCopy {
|
|
237
322
|
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
238
323
|
$snapDir = Join-Path $backupDir $ts
|
|
239
324
|
New-Item -ItemType Directory -Force $snapDir | Out-Null
|
|
240
325
|
|
|
326
|
+
$allFiles = Get-ChildItem $resolved -Recurse -File -ErrorAction SilentlyContinue |
|
|
327
|
+
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.cursor-guard-backup|node_modules)[\\/]' }
|
|
328
|
+
|
|
241
329
|
$files = if ($protectPatterns.Count -gt 0) {
|
|
242
|
-
$
|
|
330
|
+
$allFiles | Where-Object {
|
|
331
|
+
$rel = $_.FullName.Substring($resolved.Length + 1) -replace '\\','/'
|
|
332
|
+
$matched = $false
|
|
333
|
+
foreach ($pat in $protectPatterns) {
|
|
334
|
+
$p = $pat -replace '\\','/'
|
|
335
|
+
if ($rel -like $p -or (Split-Path $rel -Leaf) -like $p) { $matched = $true; break }
|
|
336
|
+
}
|
|
337
|
+
$matched
|
|
338
|
+
}
|
|
243
339
|
} else {
|
|
244
|
-
|
|
245
|
-
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.cursor-guard-backup|node_modules)[\\/]' }
|
|
340
|
+
$allFiles
|
|
246
341
|
}
|
|
247
342
|
|
|
248
343
|
$copied = 0
|
|
249
344
|
foreach ($f in $files) {
|
|
250
|
-
$rel = $f.FullName.Substring($resolved.Length + 1)
|
|
345
|
+
$rel = $f.FullName.Substring($resolved.Length + 1) -replace '\\','/'
|
|
346
|
+
$leaf = $f.Name
|
|
251
347
|
$skip = $false
|
|
252
348
|
foreach ($ig in $ignorePatterns) {
|
|
253
|
-
$
|
|
254
|
-
if ($rel -
|
|
349
|
+
$p = $ig -replace '\\','/'
|
|
350
|
+
if ($rel -like $p -or $leaf -like $p) { $skip = $true; break }
|
|
255
351
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
352
|
+
if (-not $skip) {
|
|
353
|
+
foreach ($pat in $secretsPatterns) {
|
|
354
|
+
if ($rel -like $pat -or $leaf -like $pat) { $skip = $true; break }
|
|
355
|
+
}
|
|
259
356
|
}
|
|
260
357
|
if ($skip) { continue }
|
|
261
358
|
$dest = Join-Path $snapDir $rel
|
|
@@ -286,8 +383,28 @@ try {
|
|
|
286
383
|
Start-Sleep -Seconds $IntervalSeconds
|
|
287
384
|
$cycle++
|
|
288
385
|
|
|
289
|
-
|
|
290
|
-
|
|
386
|
+
# ── Detect changes ────────────────────────────────────────
|
|
387
|
+
$hasChanges = $false
|
|
388
|
+
if ($isRepo) {
|
|
389
|
+
$dirty = git status --porcelain 2>$null
|
|
390
|
+
$hasChanges = [bool]$dirty
|
|
391
|
+
} else {
|
|
392
|
+
# Non-Git: compare file timestamps against last snapshot
|
|
393
|
+
$lastSnap = Get-ChildItem $backupDir -Directory -ErrorAction SilentlyContinue |
|
|
394
|
+
Where-Object { $_.Name -match '^\d{8}_\d{6}$' } |
|
|
395
|
+
Sort-Object Name -Descending | Select-Object -First 1
|
|
396
|
+
if (-not $lastSnap) {
|
|
397
|
+
$hasChanges = $true
|
|
398
|
+
} else {
|
|
399
|
+
$latest = Get-ChildItem $resolved -Recurse -File -ErrorAction SilentlyContinue |
|
|
400
|
+
Where-Object { $_.FullName -notmatch '[\\/](\.cursor-guard-backup)[\\/]' } |
|
|
401
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
402
|
+
if ($latest -and $latest.LastWriteTime -gt $lastSnap.CreationTime) {
|
|
403
|
+
$hasChanges = $true
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (-not $hasChanges) { continue }
|
|
291
408
|
|
|
292
409
|
# ── Git branch snapshot ──────────────────────────────────
|
|
293
410
|
if ($backupStrategy -eq "git" -or $backupStrategy -eq "both") {
|
|
@@ -350,7 +467,10 @@ try {
|
|
|
350
467
|
}
|
|
351
468
|
|
|
352
469
|
# Periodic retention cleanup every 10 cycles
|
|
353
|
-
if ($cycle % 10 -eq 0) {
|
|
470
|
+
if ($cycle % 10 -eq 0) {
|
|
471
|
+
Invoke-RetentionCleanup
|
|
472
|
+
Invoke-GitRetention
|
|
473
|
+
}
|
|
354
474
|
}
|
|
355
475
|
}
|
|
356
476
|
finally {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Configuration Reference
|
|
2
|
+
|
|
3
|
+
This document explains every field in `.cursor-guard.json`.
|
|
4
|
+
|
|
5
|
+
> Example file: [cursor-guard.example.json](cursor-guard.example.json)
|
|
6
|
+
>
|
|
7
|
+
> JSON Schema: [cursor-guard.schema.json](cursor-guard.schema.json)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## `protect`
|
|
12
|
+
|
|
13
|
+
- **Type**: `string[]` (glob patterns)
|
|
14
|
+
- **Default**: not set (all files protected)
|
|
15
|
+
|
|
16
|
+
Whitelist glob patterns relative to workspace root. Only matching files get backup protection. If empty or missing, all files are protected.
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
"protect": ["src/**", "lib/**", "package.json"]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## `ignore`
|
|
25
|
+
|
|
26
|
+
- **Type**: `string[]` (glob patterns)
|
|
27
|
+
- **Default**: not set
|
|
28
|
+
|
|
29
|
+
Blacklist glob patterns. Matching files are excluded from protection even if they match `protect`. Applied on top of `.gitignore`.
|
|
30
|
+
|
|
31
|
+
**Resolution rules**:
|
|
32
|
+
|
|
33
|
+
| Scenario | Behavior |
|
|
34
|
+
|----------|----------|
|
|
35
|
+
| Both `protect` and `ignore` set | File must match `protect` AND not match `ignore` |
|
|
36
|
+
| Only `protect` set | Only matching files are protected |
|
|
37
|
+
| Only `ignore` set | Everything protected except matches |
|
|
38
|
+
| Neither set | Protect everything |
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"ignore": ["node_modules/**", "dist/**", "*.log"]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## `backup_strategy`
|
|
47
|
+
|
|
48
|
+
- **Type**: `string`
|
|
49
|
+
- **Allowed**: `"git"` | `"shadow"` | `"both"`
|
|
50
|
+
- **Default**: `"git"`
|
|
51
|
+
|
|
52
|
+
| Value | Description |
|
|
53
|
+
|-------|-------------|
|
|
54
|
+
| `"git"` | Local commits to dedicated branch `cursor-guard/auto-backup` |
|
|
55
|
+
| `"shadow"` | File copies to `.cursor-guard-backup/<timestamp>/` |
|
|
56
|
+
| `"both"` | Git branch snapshot + shadow copies |
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"backup_strategy": "git"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## `auto_backup_interval_seconds`
|
|
65
|
+
|
|
66
|
+
- **Type**: `integer`
|
|
67
|
+
- **Minimum**: `5`
|
|
68
|
+
- **Default**: `60`
|
|
69
|
+
|
|
70
|
+
Interval in seconds for `auto-backup.ps1` to check for changes and create snapshots.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"auto_backup_interval_seconds": 60
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## `secrets_patterns`
|
|
79
|
+
|
|
80
|
+
- **Type**: `string[]` (glob patterns)
|
|
81
|
+
- **Default**: built-in list (see below)
|
|
82
|
+
|
|
83
|
+
Glob patterns for sensitive files. Matching files are **auto-excluded** from backup, even if within `protect` scope. Built-in defaults (always active): `.env`, `.env.*`, `*.key`, `*.pem`, `*.p12`, `*.pfx`, `credentials*`. Set this field to override with your own patterns.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## `pre_restore_backup`
|
|
92
|
+
|
|
93
|
+
- **Type**: `string`
|
|
94
|
+
- **Allowed**: `"always"` | `"ask"` | `"never"`
|
|
95
|
+
- **Default**: `"always"`
|
|
96
|
+
|
|
97
|
+
| Value | Description |
|
|
98
|
+
|-------|-------------|
|
|
99
|
+
| `"always"` | Auto-preserve current version before every restore. No prompt. |
|
|
100
|
+
| `"ask"` | Prompt user each time: "Preserve current version?" |
|
|
101
|
+
| `"never"` | Skip preservation entirely (not recommended). |
|
|
102
|
+
|
|
103
|
+
Regardless of this config, the user's explicit instruction in the current message always takes priority. Say "don't preserve" to skip, or "preserve first" to force.
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
"pre_restore_backup": "always"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `retention`
|
|
112
|
+
|
|
113
|
+
- **Type**: `object`
|
|
114
|
+
- **Default**: `{ "mode": "days", "days": 30 }`
|
|
115
|
+
|
|
116
|
+
Retention policy for **shadow copies** only. Git branch snapshots are not auto-cleaned — manage them manually. Controls automatic cleanup of old `.cursor-guard-backup/` directories.
|
|
117
|
+
|
|
118
|
+
### Sub-fields
|
|
119
|
+
|
|
120
|
+
| Field | Type | Default | Description |
|
|
121
|
+
|-------|------|---------|-------------|
|
|
122
|
+
| `mode` | `"days"` \| `"count"` \| `"size"` | `"days"` | Cleanup strategy |
|
|
123
|
+
| `days` | `integer` | `30` | Keep snapshots from last N days (when mode=days) |
|
|
124
|
+
| `max_count` | `integer` | `100` | Keep N newest snapshots (when mode=count) |
|
|
125
|
+
| `max_size_mb` | `integer` | `500` | Keep total size under N MB (when mode=size) |
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"retention": {
|
|
129
|
+
"mode": "days",
|
|
130
|
+
"days": 30,
|
|
131
|
+
"max_count": 100,
|
|
132
|
+
"max_size_mb": 500
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## `git_retention`
|
|
139
|
+
|
|
140
|
+
- **Type**: `object`
|
|
141
|
+
- **Default**: `{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
142
|
+
|
|
143
|
+
Retention policy for the **`cursor-guard/auto-backup` Git branch**. By default, auto-backup commits accumulate indefinitely. Enable this to automatically prune old commits.
|
|
144
|
+
|
|
145
|
+
### Sub-fields
|
|
146
|
+
|
|
147
|
+
| Field | Type | Default | Description |
|
|
148
|
+
|-------|------|---------|-------------|
|
|
149
|
+
| `enabled` | `boolean` | `false` | Enable automatic pruning. When false, branch grows without limit. |
|
|
150
|
+
| `mode` | `"days"` \| `"count"` | `"count"` | Pruning strategy |
|
|
151
|
+
| `days` | `integer` | `30` | Keep commits from last N days (when mode=days) |
|
|
152
|
+
| `max_count` | `integer` | `200` | Keep N newest commits (when mode=count, minimum 10) |
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
"git_retention": {
|
|
156
|
+
"enabled": true,
|
|
157
|
+
"mode": "count",
|
|
158
|
+
"max_count": 200
|
|
159
|
+
}
|
|
160
|
+
```
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# 配置参数说明
|
|
2
|
+
|
|
3
|
+
本文档说明 `.cursor-guard.json` 中的每个配置项。
|
|
4
|
+
|
|
5
|
+
> 示例文件:[cursor-guard.example.json](cursor-guard.example.json)
|
|
6
|
+
>
|
|
7
|
+
> JSON Schema:[cursor-guard.schema.json](cursor-guard.schema.json)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## `protect`
|
|
12
|
+
|
|
13
|
+
- **类型**:`string[]`(glob 模式)
|
|
14
|
+
- **默认值**:未设置(保护所有文件)
|
|
15
|
+
|
|
16
|
+
白名单 glob 模式,相对于工作区根目录。只有匹配的文件才会被保护。留空或不设置则保护所有文件。
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
"protect": ["src/**", "lib/**", "package.json"]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## `ignore`
|
|
25
|
+
|
|
26
|
+
- **类型**:`string[]`(glob 模式)
|
|
27
|
+
- **默认值**:未设置
|
|
28
|
+
|
|
29
|
+
黑名单 glob 模式。匹配的文件即使被 `protect` 包含也会排除。在 `.gitignore` 基础上额外生效。
|
|
30
|
+
|
|
31
|
+
**解析规则**:
|
|
32
|
+
|
|
33
|
+
| 场景 | 行为 |
|
|
34
|
+
|------|------|
|
|
35
|
+
| 同时设置 `protect` 和 `ignore` | 文件须匹配 protect 且不匹配 ignore |
|
|
36
|
+
| 仅设置 `protect` | 仅匹配文件被保护 |
|
|
37
|
+
| 仅设置 `ignore` | 除匹配文件外全部保护 |
|
|
38
|
+
| 都不设置 | 保护全部文件 |
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"ignore": ["node_modules/**", "dist/**", "*.log"]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## `backup_strategy`
|
|
47
|
+
|
|
48
|
+
- **类型**:`string`
|
|
49
|
+
- **可选值**:`"git"` | `"shadow"` | `"both"`
|
|
50
|
+
- **默认值**:`"git"`
|
|
51
|
+
|
|
52
|
+
| 值 | 说明 |
|
|
53
|
+
|----|------|
|
|
54
|
+
| `"git"` | 提交到专用分支 `cursor-guard/auto-backup` |
|
|
55
|
+
| `"shadow"` | 文件拷贝到 `.cursor-guard-backup/<timestamp>/` |
|
|
56
|
+
| `"both"` | Git 分支快照 + 影子拷贝同时进行 |
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
"backup_strategy": "git"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## `auto_backup_interval_seconds`
|
|
65
|
+
|
|
66
|
+
- **类型**:`integer`
|
|
67
|
+
- **最小值**:`5`
|
|
68
|
+
- **默认值**:`60`
|
|
69
|
+
|
|
70
|
+
自动备份脚本 `auto-backup.ps1` 检查变更并创建快照的间隔秒数。
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
"auto_backup_interval_seconds": 60
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## `secrets_patterns`
|
|
79
|
+
|
|
80
|
+
- **类型**:`string[]`(glob 模式)
|
|
81
|
+
- **默认值**:内置列表(见下)
|
|
82
|
+
|
|
83
|
+
敏感文件 glob 模式。匹配的文件**自动排除**备份,即使在 `protect` 范围内。内置默认值(始终生效):`.env`、`.env.*`、`*.key`、`*.pem`、`*.p12`、`*.pfx`、`credentials*`。设置此字段可覆盖为自定义模式。
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## `pre_restore_backup`
|
|
92
|
+
|
|
93
|
+
- **类型**:`string`
|
|
94
|
+
- **可选值**:`"always"` | `"ask"` | `"never"`
|
|
95
|
+
- **默认值**:`"always"`
|
|
96
|
+
|
|
97
|
+
| 值 | 说明 |
|
|
98
|
+
|----|------|
|
|
99
|
+
| `"always"` | 恢复前自动保留当前版本,无需确认。 |
|
|
100
|
+
| `"ask"` | 每次恢复前询问用户是否保留当前版本。 |
|
|
101
|
+
| `"never"` | 不保留当前版本(不推荐)。 |
|
|
102
|
+
|
|
103
|
+
无论此配置如何,用户在当前消息中的明确指令始终优先。说"不保留当前版本"可跳过,说"先保留当前版本"可强制保留。
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
"pre_restore_backup": "always"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## `retention`
|
|
112
|
+
|
|
113
|
+
- **类型**:`object`
|
|
114
|
+
- **默认值**:`{ "mode": "days", "days": 30 }`
|
|
115
|
+
|
|
116
|
+
仅针对**影子拷贝**的保留策略。Git 分支快照不会自动清理,需手动管理。控制 `.cursor-guard-backup/` 旧目录的自动清理。
|
|
117
|
+
|
|
118
|
+
### 子字段
|
|
119
|
+
|
|
120
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
121
|
+
|------|------|--------|------|
|
|
122
|
+
| `mode` | `"days"` \| `"count"` \| `"size"` | `"days"` | 清理策略 |
|
|
123
|
+
| `days` | `integer` | `30` | 保留最近 N 天的快照(mode=days 时生效) |
|
|
124
|
+
| `max_count` | `integer` | `100` | 保留最新 N 份快照(mode=count 时生效) |
|
|
125
|
+
| `max_size_mb` | `integer` | `500` | 总大小不超过 N MB(mode=size 时生效) |
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
"retention": {
|
|
129
|
+
"mode": "days",
|
|
130
|
+
"days": 30,
|
|
131
|
+
"max_count": 100,
|
|
132
|
+
"max_size_mb": 500
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## `git_retention`
|
|
139
|
+
|
|
140
|
+
- **类型**:`object`
|
|
141
|
+
- **默认值**:`{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
142
|
+
|
|
143
|
+
**`cursor-guard/auto-backup` Git 分支**的保留策略。默认情况下自动备份提交会无限累积。启用此项可自动裁剪旧提交。
|
|
144
|
+
|
|
145
|
+
### 子字段
|
|
146
|
+
|
|
147
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
148
|
+
|------|------|--------|------|
|
|
149
|
+
| `enabled` | `boolean` | `false` | 启用自动裁剪。关闭时分支无限增长。 |
|
|
150
|
+
| `mode` | `"days"` \| `"count"` | `"count"` | 裁剪策略 |
|
|
151
|
+
| `days` | `integer` | `30` | 保留最近 N 天的提交(mode=days 时生效) |
|
|
152
|
+
| `max_count` | `integer` | `200` | 保留最新 N 个提交(mode=count 时生效,最少 10) |
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
"git_retention": {
|
|
156
|
+
"enabled": true,
|
|
157
|
+
"mode": "count",
|
|
158
|
+
"max_count": 200
|
|
159
|
+
}
|
|
160
|
+
```
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment_schema": "Optional: add \"$schema\" pointing to cursor-guard.schema.json for IDE validation. Adjust the path to where you placed the schema file.",
|
|
3
|
-
|
|
4
|
-
"_comment_mode": "Two modes: whitelist (set 'protect') or blacklist (set 'ignore'). If both are set, 'protect' is checked first, then 'ignore' excludes from it.",
|
|
5
|
-
|
|
6
2
|
"protect": [
|
|
7
3
|
"src/**",
|
|
8
4
|
"lib/**",
|
|
@@ -12,7 +8,6 @@
|
|
|
12
8
|
"tsconfig.json",
|
|
13
9
|
".env.example"
|
|
14
10
|
],
|
|
15
|
-
|
|
16
11
|
"ignore": [
|
|
17
12
|
"node_modules/**",
|
|
18
13
|
"dist/**",
|
|
@@ -23,23 +18,25 @@
|
|
|
23
18
|
"*.tmp",
|
|
24
19
|
".cursor-guard-backup/**"
|
|
25
20
|
],
|
|
26
|
-
|
|
27
21
|
"backup_strategy": "git",
|
|
28
22
|
"auto_backup_interval_seconds": 60,
|
|
29
|
-
|
|
30
|
-
"_comment_secrets": "Glob patterns for sensitive files — auto-excluded from backup even if matched by 'protect'. Built-in defaults: .env, .env.*, *.key, *.pem, *.p12, *.pfx, credentials*",
|
|
31
23
|
"secrets_patterns": [
|
|
32
24
|
".env",
|
|
33
25
|
".env.*",
|
|
34
26
|
"*.key",
|
|
35
27
|
"*.pem"
|
|
36
28
|
],
|
|
37
|
-
|
|
38
|
-
"_comment_retention": "Retention policy for shadow copies. mode: 'days' (default, keep N days), 'count' (keep N newest snapshots), 'size' (keep total under N MB).",
|
|
29
|
+
"pre_restore_backup": "always",
|
|
39
30
|
"retention": {
|
|
40
31
|
"mode": "days",
|
|
41
32
|
"days": 30,
|
|
42
33
|
"max_count": 100,
|
|
43
34
|
"max_size_mb": 500
|
|
35
|
+
},
|
|
36
|
+
"git_retention": {
|
|
37
|
+
"enabled": false,
|
|
38
|
+
"mode": "count",
|
|
39
|
+
"days": 30,
|
|
40
|
+
"max_count": 200
|
|
44
41
|
}
|
|
45
42
|
}
|
|
@@ -31,6 +31,12 @@
|
|
|
31
31
|
"items": { "type": "string" },
|
|
32
32
|
"description": "Glob patterns for sensitive files auto-excluded from backups. Built-in defaults: .env, .env.*, *.key, *.pem, *.p12, *.pfx, credentials*"
|
|
33
33
|
},
|
|
34
|
+
"pre_restore_backup": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["always", "ask", "never"],
|
|
37
|
+
"default": "always",
|
|
38
|
+
"description": "'always' (default): automatically preserve current version before every restore. 'ask': prompt the user each time to choose whether to preserve. 'never': skip preservation entirely (not recommended)."
|
|
39
|
+
},
|
|
34
40
|
"retention": {
|
|
35
41
|
"type": "object",
|
|
36
42
|
"description": "Controls automatic cleanup of old shadow-copy snapshots.",
|
|
@@ -61,6 +67,36 @@
|
|
|
61
67
|
}
|
|
62
68
|
},
|
|
63
69
|
"additionalProperties": false
|
|
70
|
+
},
|
|
71
|
+
"git_retention": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"description": "Controls automatic cleanup of old commits on the cursor-guard/auto-backup branch.",
|
|
74
|
+
"properties": {
|
|
75
|
+
"enabled": {
|
|
76
|
+
"type": "boolean",
|
|
77
|
+
"default": false,
|
|
78
|
+
"description": "Enable automatic pruning of old auto-backup commits. Default false (manual cleanup)."
|
|
79
|
+
},
|
|
80
|
+
"mode": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"enum": ["days", "count"],
|
|
83
|
+
"default": "count",
|
|
84
|
+
"description": "'days': keep commits from the last N days. 'count': keep N newest commits."
|
|
85
|
+
},
|
|
86
|
+
"days": {
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"minimum": 1,
|
|
89
|
+
"default": 30,
|
|
90
|
+
"description": "Number of days to keep auto-backup commits (when mode='days')."
|
|
91
|
+
},
|
|
92
|
+
"max_count": {
|
|
93
|
+
"type": "integer",
|
|
94
|
+
"minimum": 10,
|
|
95
|
+
"default": 200,
|
|
96
|
+
"description": "Maximum number of auto-backup commits to keep (when mode='count')."
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"additionalProperties": false
|
|
64
100
|
}
|
|
65
101
|
},
|
|
66
102
|
"additionalProperties": true
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Cursor Guard Doctor — one-command health check for your guard setup.
|
|
4
|
+
|
|
5
|
+
.USAGE
|
|
6
|
+
.\guard-doctor.ps1 -Path "D:\MyProject"
|
|
7
|
+
|
|
8
|
+
.NOTES
|
|
9
|
+
Checks: Git availability, worktree layout, .cursor-guard.json validity,
|
|
10
|
+
backup strategy vs environment compatibility, ignore effectiveness,
|
|
11
|
+
pre-restore refs, shadow copy directory, disk space, and more.
|
|
12
|
+
#>
|
|
13
|
+
|
|
14
|
+
param(
|
|
15
|
+
[Parameter(Mandatory)]
|
|
16
|
+
[string]$Path
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
$ErrorActionPreference = "Continue"
|
|
20
|
+
$resolved = (Resolve-Path $Path -ErrorAction Stop).Path
|
|
21
|
+
Set-Location $resolved
|
|
22
|
+
|
|
23
|
+
$pass = 0; $warn = 0; $fail = 0
|
|
24
|
+
|
|
25
|
+
function Write-Check {
|
|
26
|
+
param([string]$Name, [string]$Status, [string]$Detail = "")
|
|
27
|
+
switch ($Status) {
|
|
28
|
+
"PASS" { $color = "Green"; $script:pass++ }
|
|
29
|
+
"WARN" { $color = "Yellow"; $script:warn++ }
|
|
30
|
+
"FAIL" { $color = "Red"; $script:fail++ }
|
|
31
|
+
default { $color = "Gray" }
|
|
32
|
+
}
|
|
33
|
+
$line = " [$Status] $Name"
|
|
34
|
+
if ($Detail) { $line += " — $Detail" }
|
|
35
|
+
Write-Host $line -ForegroundColor $color
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Write-Host ""
|
|
39
|
+
Write-Host "=== Cursor Guard Doctor ===" -ForegroundColor Cyan
|
|
40
|
+
Write-Host " Target: $resolved" -ForegroundColor Cyan
|
|
41
|
+
Write-Host ""
|
|
42
|
+
|
|
43
|
+
# ── 1. Git availability ──────────────────────────────────────────
|
|
44
|
+
$hasGit = [bool](Get-Command git -ErrorAction SilentlyContinue)
|
|
45
|
+
if ($hasGit) {
|
|
46
|
+
$gitVer = (git --version 2>$null) -replace 'git version ',''
|
|
47
|
+
Write-Check "Git installed" "PASS" "version $gitVer"
|
|
48
|
+
} else {
|
|
49
|
+
Write-Check "Git installed" "WARN" "git not found in PATH; only shadow strategy available"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ── 2. Git repo status ───────────────────────────────────────────
|
|
53
|
+
$isRepo = $false
|
|
54
|
+
$gitDir = $null
|
|
55
|
+
if ($hasGit) {
|
|
56
|
+
$isRepo = (git rev-parse --is-inside-work-tree 2>$null) -eq "true"
|
|
57
|
+
if ($isRepo) {
|
|
58
|
+
$gitDir = (Resolve-Path (git rev-parse --git-dir 2>$null) -ErrorAction SilentlyContinue).Path
|
|
59
|
+
$isWorktree = (git rev-parse --is-inside-work-tree 2>$null) -eq "true" -and
|
|
60
|
+
(git rev-parse --git-common-dir 2>$null) -ne (git rev-parse --git-dir 2>$null)
|
|
61
|
+
if ($isWorktree) {
|
|
62
|
+
Write-Check "Git repository" "PASS" "worktree detected (git-dir: $gitDir)"
|
|
63
|
+
} else {
|
|
64
|
+
Write-Check "Git repository" "PASS" "standard repo"
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
Write-Check "Git repository" "WARN" "not a Git repo; git/both strategies won't work"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ── 3. .cursor-guard.json ────────────────────────────────────────
|
|
72
|
+
$cfgPath = Join-Path $resolved ".cursor-guard.json"
|
|
73
|
+
$cfg = $null
|
|
74
|
+
if (Test-Path $cfgPath) {
|
|
75
|
+
try {
|
|
76
|
+
$cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json
|
|
77
|
+
Write-Check "Config file" "PASS" ".cursor-guard.json found and valid JSON"
|
|
78
|
+
} catch {
|
|
79
|
+
Write-Check "Config file" "FAIL" "JSON parse error: $_"
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
Write-Check "Config file" "WARN" "no .cursor-guard.json found; using defaults (protect everything)"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ── 4. Strategy vs environment ────────────────────────────────────
|
|
86
|
+
$strategy = "git"
|
|
87
|
+
if ($cfg -and $cfg.backup_strategy) { $strategy = $cfg.backup_strategy }
|
|
88
|
+
if ($strategy -eq "git" -or $strategy -eq "both") {
|
|
89
|
+
if (-not $isRepo) {
|
|
90
|
+
Write-Check "Strategy compatibility" "FAIL" "backup_strategy='$strategy' but directory is not a Git repo"
|
|
91
|
+
} else {
|
|
92
|
+
Write-Check "Strategy compatibility" "PASS" "backup_strategy='$strategy' and Git repo exists"
|
|
93
|
+
}
|
|
94
|
+
} elseif ($strategy -eq "shadow") {
|
|
95
|
+
Write-Check "Strategy compatibility" "PASS" "backup_strategy='shadow' — no Git required"
|
|
96
|
+
} else {
|
|
97
|
+
Write-Check "Strategy compatibility" "FAIL" "unknown backup_strategy='$strategy' (must be git/shadow/both)"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ── 5. Backup branch ─────────────────────────────────────────────
|
|
101
|
+
if ($isRepo) {
|
|
102
|
+
$branchRef = "refs/heads/cursor-guard/auto-backup"
|
|
103
|
+
$branchExists = git rev-parse --verify $branchRef 2>$null
|
|
104
|
+
if ($branchExists) {
|
|
105
|
+
$commitCount = (git rev-list --count $branchRef 2>$null)
|
|
106
|
+
Write-Check "Backup branch" "PASS" "cursor-guard/auto-backup exists ($commitCount commits)"
|
|
107
|
+
} else {
|
|
108
|
+
Write-Check "Backup branch" "WARN" "cursor-guard/auto-backup not created yet (will be created on first backup)"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# ── 6. Guard refs ────────────────────────────────────────────────
|
|
113
|
+
if ($isRepo) {
|
|
114
|
+
$guardRefs = git for-each-ref refs/guard/ --format="%(refname)" 2>$null
|
|
115
|
+
if ($guardRefs) {
|
|
116
|
+
$refCount = @($guardRefs).Count
|
|
117
|
+
$preRestoreRefs = @($guardRefs | Where-Object { $_ -match 'pre-restore/' })
|
|
118
|
+
Write-Check "Guard refs" "PASS" "$refCount ref(s) found ($($preRestoreRefs.Count) pre-restore snapshots)"
|
|
119
|
+
} else {
|
|
120
|
+
Write-Check "Guard refs" "WARN" "no guard refs yet (created on first snapshot or restore)"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ── 7. Shadow copy directory ─────────────────────────────────────
|
|
125
|
+
$backupDir = Join-Path $resolved ".cursor-guard-backup"
|
|
126
|
+
if (Test-Path $backupDir) {
|
|
127
|
+
$snapDirs = Get-ChildItem $backupDir -Directory -ErrorAction SilentlyContinue |
|
|
128
|
+
Where-Object { $_.Name -match '^\d{8}_\d{6}$' -or $_.Name -match '^pre-restore-' }
|
|
129
|
+
$snapCount = if ($snapDirs) { @($snapDirs).Count } else { 0 }
|
|
130
|
+
$totalMB = [math]::Round(((Get-ChildItem $backupDir -Recurse -File -ErrorAction SilentlyContinue |
|
|
131
|
+
Measure-Object Length -Sum).Sum / 1MB), 1)
|
|
132
|
+
Write-Check "Shadow copies" "PASS" "$snapCount snapshot(s), ${totalMB} MB total"
|
|
133
|
+
} else {
|
|
134
|
+
Write-Check "Shadow copies" "WARN" ".cursor-guard-backup/ not found (will be created on first shadow backup)"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# ── 8. .gitignore / exclude coverage ────────────────────────────
|
|
138
|
+
if ($isRepo) {
|
|
139
|
+
$checkIgnored = git check-ignore ".cursor-guard-backup/test" 2>$null
|
|
140
|
+
if ($checkIgnored) {
|
|
141
|
+
Write-Check "Backup dir ignored" "PASS" ".cursor-guard-backup/ is git-ignored"
|
|
142
|
+
} else {
|
|
143
|
+
Write-Check "Backup dir ignored" "WARN" ".cursor-guard-backup/ may NOT be git-ignored — backup changes could trigger commits"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# ── 9. Config field validation ────────────────────────────────────
|
|
148
|
+
if ($cfg) {
|
|
149
|
+
$validStrategies = @("git", "shadow", "both")
|
|
150
|
+
if ($cfg.backup_strategy -and $cfg.backup_strategy -notin $validStrategies) {
|
|
151
|
+
Write-Check "Config: backup_strategy" "FAIL" "invalid value '$($cfg.backup_strategy)'"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
$validPreRestore = @("always", "ask", "never")
|
|
155
|
+
if ($cfg.pre_restore_backup -and $cfg.pre_restore_backup -notin $validPreRestore) {
|
|
156
|
+
Write-Check "Config: pre_restore_backup" "FAIL" "invalid value '$($cfg.pre_restore_backup)'"
|
|
157
|
+
} elseif ($cfg.pre_restore_backup -eq "never") {
|
|
158
|
+
Write-Check "Config: pre_restore_backup" "WARN" "set to 'never' — restores won't auto-preserve current version"
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if ($cfg.auto_backup_interval_seconds -and $cfg.auto_backup_interval_seconds -lt 5) {
|
|
162
|
+
Write-Check "Config: interval" "WARN" "$($cfg.auto_backup_interval_seconds)s is below minimum (5s), will be clamped"
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if ($cfg.retention -and $cfg.retention.mode) {
|
|
166
|
+
$validModes = @("days", "count", "size")
|
|
167
|
+
if ($cfg.retention.mode -notin $validModes) {
|
|
168
|
+
Write-Check "Config: retention.mode" "FAIL" "invalid value '$($cfg.retention.mode)'"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# ── 10. Protect / Ignore effectiveness ───────────────────────────
|
|
174
|
+
if ($cfg -and $cfg.protect) {
|
|
175
|
+
$allFiles = Get-ChildItem $resolved -Recurse -File -ErrorAction SilentlyContinue |
|
|
176
|
+
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.cursor-guard-backup|node_modules)[\\/]' }
|
|
177
|
+
$protectedCount = 0
|
|
178
|
+
foreach ($f in $allFiles) {
|
|
179
|
+
$rel = $f.FullName.Substring($resolved.Length + 1) -replace '\\','/'
|
|
180
|
+
foreach ($pat in @($cfg.protect)) {
|
|
181
|
+
$p = $pat -replace '\\','/'
|
|
182
|
+
if ($rel -like $p -or (Split-Path $rel -Leaf) -like $p) { $protectedCount++; break }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
$totalCount = if ($allFiles) { @($allFiles).Count } else { 0 }
|
|
186
|
+
Write-Check "Protect patterns" "PASS" "$protectedCount / $totalCount files matched by protect patterns"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ── 11. Disk space ────────────────────────────────────────────────
|
|
190
|
+
try {
|
|
191
|
+
$letter = (Split-Path $resolved -Qualifier) -replace ':$',''
|
|
192
|
+
$drv = Get-PSDrive $letter -ErrorAction Stop
|
|
193
|
+
$freeGB = [math]::Round($drv.Free / 1GB, 1)
|
|
194
|
+
if ($freeGB -lt 1) {
|
|
195
|
+
Write-Check "Disk space" "FAIL" "${freeGB} GB free — critically low"
|
|
196
|
+
} elseif ($freeGB -lt 5) {
|
|
197
|
+
Write-Check "Disk space" "WARN" "${freeGB} GB free"
|
|
198
|
+
} else {
|
|
199
|
+
Write-Check "Disk space" "PASS" "${freeGB} GB free"
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
Write-Check "Disk space" "WARN" "could not determine free space"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# ── 12. Lock file ────────────────────────────────────────────────
|
|
206
|
+
$lockFile = if ($gitDir) { Join-Path $gitDir "cursor-guard.lock" } else { Join-Path $backupDir "cursor-guard.lock" }
|
|
207
|
+
if (Test-Path $lockFile) {
|
|
208
|
+
$lockContent = Get-Content $lockFile -Raw -ErrorAction SilentlyContinue
|
|
209
|
+
Write-Check "Lock file" "WARN" "lock file exists — another instance may be running. Content: $lockContent"
|
|
210
|
+
} else {
|
|
211
|
+
Write-Check "Lock file" "PASS" "no lock file (no running instance)"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ── Summary ──────────────────────────────────────────────────────
|
|
215
|
+
Write-Host ""
|
|
216
|
+
Write-Host "=== Summary ===" -ForegroundColor Cyan
|
|
217
|
+
Write-Host " PASS: $pass | WARN: $warn | FAIL: $fail" -ForegroundColor $(if ($fail -gt 0) { "Red" } elseif ($warn -gt 0) { "Yellow" } else { "Green" })
|
|
218
|
+
Write-Host ""
|
|
219
|
+
if ($fail -gt 0) {
|
|
220
|
+
Write-Host " Fix FAIL items before relying on Cursor Guard." -ForegroundColor Red
|
|
221
|
+
} elseif ($warn -gt 0) {
|
|
222
|
+
Write-Host " Review WARN items to ensure everything works as expected." -ForegroundColor Yellow
|
|
223
|
+
} else {
|
|
224
|
+
Write-Host " All checks passed. Cursor Guard is ready." -ForegroundColor Green
|
|
225
|
+
}
|
|
226
|
+
Write-Host ""
|
package/references/recovery.md
CHANGED
|
@@ -13,8 +13,7 @@ Replace `<path>` / `<file>` with real paths. Run from repository root. **Review
|
|
|
13
13
|
### Single file / 单文件
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
|
|
17
|
-
# 通过临时索引保留当前文件(不影响暂存区)
|
|
16
|
+
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
18
17
|
$guardIdx = Join-Path (git rev-parse --git-dir) "guard-pre-restore-index"
|
|
19
18
|
$env:GIT_INDEX_FILE = $guardIdx
|
|
20
19
|
git read-tree HEAD
|
|
@@ -23,14 +22,15 @@ $tree = git write-tree
|
|
|
23
22
|
$env:GIT_INDEX_FILE = $null
|
|
24
23
|
Remove-Item $guardIdx -Force -ErrorAction SilentlyContinue
|
|
25
24
|
$commit = git commit-tree $tree -p HEAD -m "guard: preserve current before restore"
|
|
26
|
-
git update-ref refs/guard/pre-restore $commit
|
|
27
|
-
|
|
25
|
+
git update-ref "refs/guard/pre-restore/$ts" $commit
|
|
26
|
+
git update-ref refs/guard/pre-restore $commit # alias to latest
|
|
27
|
+
Write-Host "Pre-restore backup: refs/guard/pre-restore/$ts ($($commit.Substring(0,7)))"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
### Entire project / 整个项目
|
|
31
31
|
|
|
32
32
|
```powershell
|
|
33
|
-
|
|
33
|
+
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
34
34
|
$guardIdx = Join-Path (git rev-parse --git-dir) "guard-pre-restore-index"
|
|
35
35
|
$env:GIT_INDEX_FILE = $guardIdx
|
|
36
36
|
git read-tree HEAD
|
|
@@ -39,8 +39,9 @@ $tree = git write-tree
|
|
|
39
39
|
$env:GIT_INDEX_FILE = $null
|
|
40
40
|
Remove-Item $guardIdx -Force -ErrorAction SilentlyContinue
|
|
41
41
|
$commit = git commit-tree $tree -p HEAD -m "guard: preserve current before restore"
|
|
42
|
-
git update-ref refs/guard/pre-restore $commit
|
|
43
|
-
|
|
42
|
+
git update-ref "refs/guard/pre-restore/$ts" $commit
|
|
43
|
+
git update-ref refs/guard/pre-restore $commit # alias to latest
|
|
44
|
+
Write-Host "Pre-restore backup: refs/guard/pre-restore/$ts ($($commit.Substring(0,7)))"
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
### Non-Git fallback (shadow copy) / 非 Git 备选方案
|
|
@@ -53,21 +54,40 @@ Copy-Item "<file>" "$dir/<filename>"
|
|
|
53
54
|
Write-Host "Pre-restore shadow copy: $dir"
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
### List all pre-restore snapshots / 列出所有恢复前快照
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
git for-each-ref refs/guard/pre-restore/ --sort=-creatordate \
|
|
61
|
+
--format="%(refname:short) %(creatordate:short) %(objectname:short)"
|
|
62
|
+
```
|
|
63
|
+
|
|
56
64
|
### Undo a restore (recover pre-restore state) / 撤销恢复(回到恢复前的状态)
|
|
57
65
|
|
|
58
66
|
```bash
|
|
59
|
-
#
|
|
60
|
-
#
|
|
67
|
+
# Undo using a specific timestamped snapshot
|
|
68
|
+
# 使用特定时间戳快照撤销恢复
|
|
69
|
+
git restore --source=refs/guard/pre-restore/<yyyyMMdd_HHmmss> -- <file>
|
|
70
|
+
|
|
71
|
+
# Undo using the latest pre-restore snapshot (alias)
|
|
72
|
+
# 使用最近一次的恢复前快照撤销
|
|
61
73
|
git restore --source=refs/guard/pre-restore -- <file>
|
|
62
74
|
|
|
63
|
-
# Restore entire project
|
|
64
|
-
|
|
65
|
-
git restore --source=refs/guard/pre-restore -- .
|
|
75
|
+
# Restore entire project / 恢复整个项目
|
|
76
|
+
git restore --source=refs/guard/pre-restore/<yyyyMMdd_HHmmss> -- .
|
|
66
77
|
|
|
67
78
|
# From shadow copy / 从影子拷贝恢复
|
|
68
79
|
Copy-Item ".cursor-guard-backup/pre-restore-<ts>/<file>" "<original-path>"
|
|
69
80
|
```
|
|
70
81
|
|
|
82
|
+
### Clean up old pre-restore refs / 清理旧的恢复前快照
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Delete all pre-restore refs (keep only the latest alias)
|
|
86
|
+
# 删除所有时间戳快照(仅保留 latest alias)
|
|
87
|
+
git for-each-ref refs/guard/pre-restore/ --format="%(refname)" |
|
|
88
|
+
xargs -n1 git update-ref -d
|
|
89
|
+
```
|
|
90
|
+
|
|
71
91
|
---
|
|
72
92
|
|
|
73
93
|
## Inspect current state
|