cursor-guard 1.3.2 → 2.0.2
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 +54 -20
- package/README.zh-CN.md +54 -20
- package/SKILL.md +65 -21
- package/package.json +10 -2
- package/references/auto-backup.ps1 +10 -342
- package/references/auto-backup.sh +19 -0
- package/references/bin/cursor-guard-backup.js +14 -0
- package/references/bin/cursor-guard-doctor.js +13 -0
- package/references/config-reference.md +43 -2
- package/references/config-reference.zh-CN.md +43 -2
- package/references/cursor-guard.example.json +7 -0
- package/references/cursor-guard.schema.json +38 -3
- package/references/guard-doctor.ps1 +22 -0
- package/references/guard-doctor.sh +18 -0
- package/references/lib/auto-backup.js +508 -0
- package/references/lib/guard-doctor.js +233 -0
- package/references/lib/utils.js +325 -0
- package/references/lib/utils.test.js +329 -0
- package/references/recovery.md +32 -12
|
@@ -1,359 +1,27 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
|
|
4
|
-
Periodically snapshots protected files to a local Git branch using
|
|
5
|
-
plumbing commands — never switches branches or disturbs the main index.
|
|
6
|
-
Reads .cursor-guard.json for scope, secrets, and retention settings.
|
|
7
|
-
|
|
3
|
+
Thin wrapper — launches the Node.js auto-backup implementation.
|
|
8
4
|
.USAGE
|
|
9
|
-
# Run in a separate PowerShell window while working in Cursor:
|
|
10
5
|
.\auto-backup.ps1 -Path "D:\MyProject"
|
|
11
|
-
|
|
12
|
-
# Custom interval (default 60 seconds):
|
|
13
6
|
.\auto-backup.ps1 -Path "D:\MyProject" -IntervalSeconds 30
|
|
14
|
-
|
|
15
|
-
# Stop: Ctrl+C or close the PowerShell window.
|
|
16
|
-
|
|
17
7
|
.NOTES
|
|
18
|
-
|
|
19
|
-
- Never switches branches, never touches the main index.
|
|
20
|
-
- Does NOT push to any remote.
|
|
21
|
-
- Sensitive files matching secrets_patterns are auto-excluded.
|
|
22
|
-
- Shadow copies are cleaned per retention policy (default: keep 30 days).
|
|
23
|
-
- Log file: .cursor-guard-backup/backup.log
|
|
24
|
-
- IMPORTANT: Run this script in a SEPARATE PowerShell window, NOT inside
|
|
25
|
-
Cursor's integrated terminal. Cursor's terminal injects --trailer flags
|
|
26
|
-
into git commit commands, which corrupts plumbing calls like commit-tree.
|
|
8
|
+
Requires Node.js >= 18. Run in a SEPARATE terminal, not inside Cursor.
|
|
27
9
|
#>
|
|
28
|
-
|
|
29
10
|
param(
|
|
30
11
|
[Parameter(Mandatory)]
|
|
31
12
|
[string]$Path,
|
|
32
|
-
|
|
33
13
|
[int]$IntervalSeconds = 0
|
|
34
14
|
)
|
|
35
15
|
|
|
36
|
-
$
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# ── Paths (worktree-safe: uses git rev-parse instead of hard-coding .git) ──
|
|
41
|
-
$gitDir = (git rev-parse --git-dir 2>$null)
|
|
42
|
-
if (-not $gitDir) {
|
|
43
|
-
$gitDir = Join-Path $resolved ".git"
|
|
44
|
-
} else {
|
|
45
|
-
$gitDir = (Resolve-Path $gitDir -ErrorAction SilentlyContinue).Path
|
|
46
|
-
if (-not $gitDir) { $gitDir = Join-Path $resolved ".git" }
|
|
47
|
-
}
|
|
48
|
-
$lockFile = Join-Path $gitDir "cursor-guard.lock"
|
|
49
|
-
$guardIndex = Join-Path $gitDir "cursor-guard-index"
|
|
50
|
-
$backupDir = Join-Path $resolved ".cursor-guard-backup"
|
|
51
|
-
$logFilePath = Join-Path $backupDir "backup.log"
|
|
52
|
-
|
|
53
|
-
# ── Cleanup on exit ───────────────────────────────────────────────
|
|
54
|
-
function Invoke-Cleanup {
|
|
55
|
-
$env:GIT_INDEX_FILE = $null
|
|
56
|
-
Remove-Item $guardIndex -Force -ErrorAction SilentlyContinue
|
|
57
|
-
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
|
|
58
|
-
}
|
|
59
|
-
trap { Invoke-Cleanup; break }
|
|
60
|
-
|
|
61
|
-
# ── Defaults ──────────────────────────────────────────────────────
|
|
62
|
-
$protectPatterns = @()
|
|
63
|
-
$ignorePatterns = @()
|
|
64
|
-
$secretsPatterns = @(".env", ".env.*", "*.key", "*.pem", "*.p12", "*.pfx", "credentials*")
|
|
65
|
-
$backupStrategy = "git"
|
|
66
|
-
$retentionMode = "days"
|
|
67
|
-
$retentionDays = 30
|
|
68
|
-
$retentionMaxCnt = 100
|
|
69
|
-
$retentionMaxMB = 500
|
|
70
|
-
|
|
71
|
-
# ── Load .cursor-guard.json ──────────────────────────────────────
|
|
72
|
-
$cfgPath = Join-Path $resolved ".cursor-guard.json"
|
|
73
|
-
if (Test-Path $cfgPath) {
|
|
74
|
-
try {
|
|
75
|
-
$cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json
|
|
76
|
-
if ($cfg.protect) { $protectPatterns = @($cfg.protect) }
|
|
77
|
-
if ($cfg.ignore) { $ignorePatterns = @($cfg.ignore) }
|
|
78
|
-
if ($cfg.secrets_patterns) { $secretsPatterns = @($cfg.secrets_patterns) }
|
|
79
|
-
if ($cfg.backup_strategy) { $backupStrategy = $cfg.backup_strategy }
|
|
80
|
-
if ($cfg.auto_backup_interval_seconds -and $IntervalSeconds -eq 0) {
|
|
81
|
-
$IntervalSeconds = $cfg.auto_backup_interval_seconds
|
|
82
|
-
}
|
|
83
|
-
if ($cfg.retention) {
|
|
84
|
-
if ($cfg.retention.mode) { $retentionMode = $cfg.retention.mode }
|
|
85
|
-
if ($cfg.retention.days) { $retentionDays = $cfg.retention.days }
|
|
86
|
-
if ($cfg.retention.max_count) { $retentionMaxCnt = $cfg.retention.max_count }
|
|
87
|
-
if ($cfg.retention.max_size_mb) { $retentionMaxMB = $cfg.retention.max_size_mb }
|
|
88
|
-
}
|
|
89
|
-
Write-Host "[guard] Config loaded protect=$($protectPatterns.Count) ignore=$($ignorePatterns.Count) retention=$retentionMode" -ForegroundColor Cyan
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
Write-Host "[guard] WARNING: .cursor-guard.json parse error - using defaults." -ForegroundColor Yellow
|
|
93
|
-
Write-Host " $_" -ForegroundColor Yellow
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if ($IntervalSeconds -eq 0) { $IntervalSeconds = 60 }
|
|
97
|
-
|
|
98
|
-
# ── Git repo check ───────────────────────────────────────────────
|
|
99
|
-
$isRepo = git rev-parse --is-inside-work-tree 2>$null
|
|
100
|
-
if ($isRepo -ne "true") {
|
|
101
|
-
$ans = Read-Host "Directory is not a Git repo. Initialize? (y/n)"
|
|
102
|
-
if ($ans -eq 'y') {
|
|
103
|
-
git init
|
|
104
|
-
$gi = Join-Path $resolved ".gitignore"
|
|
105
|
-
$entry = ".cursor-guard-backup/"
|
|
106
|
-
if (Test-Path $gi) {
|
|
107
|
-
$content = Get-Content $gi -Raw
|
|
108
|
-
if ($content -notmatch [regex]::Escape($entry)) {
|
|
109
|
-
Add-Content $gi "`n$entry"
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
Set-Content $gi $entry
|
|
113
|
-
}
|
|
114
|
-
git add -A; git commit -m "guard: initial snapshot" --no-verify
|
|
115
|
-
Write-Host "[guard] Repo initialized with snapshot." -ForegroundColor Green
|
|
116
|
-
} else {
|
|
117
|
-
Write-Host "[guard] Git is required. Exiting." -ForegroundColor Red
|
|
118
|
-
exit 1
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# ── Lock file (prevent multiple instances) ───────────────────────
|
|
123
|
-
if (Test-Path $lockFile) {
|
|
124
|
-
Write-Host "[guard] ERROR: Lock file exists ($lockFile)." -ForegroundColor Red
|
|
125
|
-
Write-Host " If no other instance is running, delete it and retry." -ForegroundColor Red
|
|
16
|
+
$nodeCmd = if (Get-Command node -ErrorAction SilentlyContinue) { "node" } else { $null }
|
|
17
|
+
if (-not $nodeCmd) {
|
|
18
|
+
Write-Host "[guard] ERROR: Node.js not found. Install Node.js >= 18 first." -ForegroundColor Red
|
|
19
|
+
Write-Host " https://nodejs.org/" -ForegroundColor Yellow
|
|
126
20
|
exit 1
|
|
127
21
|
}
|
|
128
|
-
Set-Content $lockFile "pid=$PID`nstarted=$(Get-Date -Format 'o')"
|
|
129
|
-
|
|
130
|
-
# ── Backup branch ───────────────────────────────────────────────
|
|
131
|
-
$branch = "cursor-guard/auto-backup"
|
|
132
|
-
$branchRef = "refs/heads/$branch"
|
|
133
|
-
if (-not (git rev-parse --verify $branchRef 2>$null)) {
|
|
134
|
-
git branch $branch HEAD 2>$null
|
|
135
|
-
Write-Host "[guard] Created branch: $branch" -ForegroundColor Green
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
# ── Ensure .cursor-guard-backup/ is git-ignored ─────────────────
|
|
139
|
-
$excludeFile = Join-Path $gitDir "info/exclude"
|
|
140
|
-
$excludeDir = Split-Path $excludeFile
|
|
141
|
-
if (-not (Test-Path $excludeDir)) { New-Item -ItemType Directory -Force $excludeDir | Out-Null }
|
|
142
|
-
$excludeEntry = ".cursor-guard-backup/"
|
|
143
|
-
if (Test-Path $excludeFile) {
|
|
144
|
-
$content = Get-Content $excludeFile -Raw -ErrorAction SilentlyContinue
|
|
145
|
-
if (-not $content -or $content -notmatch [regex]::Escape($excludeEntry)) {
|
|
146
|
-
Add-Content $excludeFile "`n$excludeEntry"
|
|
147
|
-
}
|
|
148
|
-
} else {
|
|
149
|
-
Set-Content $excludeFile $excludeEntry
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
# ── Log directory & helpers ──────────────────────────────────────
|
|
153
|
-
if (-not (Test-Path $backupDir)) { New-Item -ItemType Directory -Force $backupDir | Out-Null }
|
|
154
|
-
|
|
155
|
-
function Write-Log {
|
|
156
|
-
param([string]$Msg, [ConsoleColor]$Color = "Green")
|
|
157
|
-
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Msg"
|
|
158
|
-
Add-Content -Path $logFilePath -Value $line -ErrorAction SilentlyContinue
|
|
159
|
-
Write-Host "[guard] $line" -ForegroundColor $Color
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
# ── Secrets filter ───────────────────────────────────────────────
|
|
163
|
-
function Remove-SecretsFromIndex {
|
|
164
|
-
$files = git ls-files --cached 2>$null
|
|
165
|
-
if (-not $files) { return }
|
|
166
|
-
$excluded = @()
|
|
167
|
-
foreach ($f in $files) {
|
|
168
|
-
$leaf = Split-Path $f -Leaf
|
|
169
|
-
foreach ($pat in $secretsPatterns) {
|
|
170
|
-
$re = '^' + [regex]::Escape($pat).Replace('\*','.*').Replace('\?','.') + '$'
|
|
171
|
-
if ($f -match $re -or $leaf -match $re) {
|
|
172
|
-
git rm --cached --ignore-unmatch -q -- $f 2>$null
|
|
173
|
-
$excluded += $f
|
|
174
|
-
break
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if ($excluded.Count -gt 0) {
|
|
179
|
-
Write-Log "Secrets auto-excluded: $($excluded -join ', ')" Yellow
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
# ── Retention cleanup ────────────────────────────────────────────
|
|
184
|
-
function Invoke-RetentionCleanup {
|
|
185
|
-
# Clean shadow-copy directories named yyyyMMdd_HHmmss
|
|
186
|
-
$dirs = Get-ChildItem $backupDir -Directory -ErrorAction SilentlyContinue |
|
|
187
|
-
Where-Object { $_.Name -match '^\d{8}_\d{6}$' } |
|
|
188
|
-
Sort-Object Name -Descending
|
|
189
|
-
if ($dirs -and $dirs.Count -gt 0) {
|
|
190
|
-
$removed = 0
|
|
191
|
-
switch ($retentionMode) {
|
|
192
|
-
"days" {
|
|
193
|
-
$cutoff = (Get-Date).AddDays(-$retentionDays)
|
|
194
|
-
foreach ($d in $dirs) {
|
|
195
|
-
try {
|
|
196
|
-
$dt = [datetime]::ParseExact($d.Name, "yyyyMMdd_HHmmss", $null)
|
|
197
|
-
if ($dt -lt $cutoff) { Remove-Item $d.FullName -Recurse -Force; $removed++ }
|
|
198
|
-
} catch {}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
"count" {
|
|
202
|
-
if ($dirs.Count -gt $retentionMaxCnt) {
|
|
203
|
-
$dirs | Select-Object -Skip $retentionMaxCnt |
|
|
204
|
-
ForEach-Object { Remove-Item $_.FullName -Recurse -Force; $removed++ }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
"size" {
|
|
208
|
-
$totalMB = (Get-ChildItem $backupDir -Recurse -File -ErrorAction SilentlyContinue |
|
|
209
|
-
Measure-Object Length -Sum).Sum / 1MB
|
|
210
|
-
$oldest = $dirs | Sort-Object Name
|
|
211
|
-
foreach ($d in $oldest) {
|
|
212
|
-
if ($totalMB -le $retentionMaxMB) { break }
|
|
213
|
-
$sz = (Get-ChildItem $d.FullName -Recurse -File |
|
|
214
|
-
Measure-Object Length -Sum).Sum / 1MB
|
|
215
|
-
Remove-Item $d.FullName -Recurse -Force
|
|
216
|
-
$totalMB -= $sz; $removed++
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if ($removed -gt 0) {
|
|
221
|
-
Write-Log "Retention ($retentionMode): cleaned $removed old snapshot(s)" DarkGray
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
# Disk-space warning
|
|
226
|
-
try {
|
|
227
|
-
$letter = (Split-Path $resolved -Qualifier) -replace ':$',''
|
|
228
|
-
$drv = Get-PSDrive $letter
|
|
229
|
-
$freeGB = [math]::Round($drv.Free / 1GB, 1)
|
|
230
|
-
if ($freeGB -lt 1) { Write-Log "WARNING: disk critically low - ${freeGB} GB free" Red }
|
|
231
|
-
elseif ($freeGB -lt 5) { Write-Log "Disk note: ${freeGB} GB free" Yellow }
|
|
232
|
-
} catch {}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
# ── Shadow copy helper ────────────────────────────────────────────
|
|
236
|
-
function Invoke-ShadowCopy {
|
|
237
|
-
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
238
|
-
$snapDir = Join-Path $backupDir $ts
|
|
239
|
-
New-Item -ItemType Directory -Force $snapDir | Out-Null
|
|
240
|
-
|
|
241
|
-
$files = if ($protectPatterns.Count -gt 0) {
|
|
242
|
-
$protectPatterns | ForEach-Object { Get-ChildItem $resolved -Recurse -File -Filter $_ -ErrorAction SilentlyContinue }
|
|
243
|
-
} else {
|
|
244
|
-
Get-ChildItem $resolved -Recurse -File -ErrorAction SilentlyContinue |
|
|
245
|
-
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.cursor-guard-backup|node_modules)[\\/]' }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
$copied = 0
|
|
249
|
-
foreach ($f in $files) {
|
|
250
|
-
$rel = $f.FullName.Substring($resolved.Length + 1)
|
|
251
|
-
$skip = $false
|
|
252
|
-
foreach ($ig in $ignorePatterns) {
|
|
253
|
-
$re = '^' + [regex]::Escape($ig).Replace('\*\*','.*').Replace('\*','[^/\\]*').Replace('\?','.') + '$'
|
|
254
|
-
if ($rel -match $re) { $skip = $true; break }
|
|
255
|
-
}
|
|
256
|
-
foreach ($pat in $secretsPatterns) {
|
|
257
|
-
$re = '^' + [regex]::Escape($pat).Replace('\*','.*').Replace('\?','.') + '$'
|
|
258
|
-
if ($rel -match $re -or $f.Name -match $re) { $skip = $true; break }
|
|
259
|
-
}
|
|
260
|
-
if ($skip) { continue }
|
|
261
|
-
$dest = Join-Path $snapDir $rel
|
|
262
|
-
$destDir = Split-Path $dest
|
|
263
|
-
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Force $destDir | Out-Null }
|
|
264
|
-
Copy-Item $f.FullName $dest -Force
|
|
265
|
-
$copied++
|
|
266
|
-
}
|
|
267
|
-
if ($copied -gt 0) {
|
|
268
|
-
Write-Log "Shadow copy $ts ($copied files)"
|
|
269
|
-
} else {
|
|
270
|
-
Remove-Item $snapDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
271
|
-
}
|
|
272
|
-
return $copied
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
# ── Banner ───────────────────────────────────────────────────────
|
|
276
|
-
Write-Host ""
|
|
277
|
-
Write-Host "[guard] Watching '$resolved' every ${IntervalSeconds}s (Ctrl+C to stop)" -ForegroundColor Cyan
|
|
278
|
-
Write-Host "[guard] Strategy: $backupStrategy | Branch: $branch | Retention: $retentionMode ($retentionDays days / $retentionMaxCnt count / ${retentionMaxMB} MB)" -ForegroundColor Cyan
|
|
279
|
-
Write-Host "[guard] Log: $logFilePath" -ForegroundColor Cyan
|
|
280
|
-
Write-Host ""
|
|
281
22
|
|
|
282
|
-
|
|
283
|
-
$
|
|
284
|
-
|
|
285
|
-
while ($true) {
|
|
286
|
-
Start-Sleep -Seconds $IntervalSeconds
|
|
287
|
-
$cycle++
|
|
23
|
+
$script = Join-Path (Join-Path $PSScriptRoot "bin") "cursor-guard-backup.js"
|
|
24
|
+
$args_ = @($script, "--path", $Path)
|
|
25
|
+
if ($IntervalSeconds -gt 0) { $args_ += @("--interval", $IntervalSeconds) }
|
|
288
26
|
|
|
289
|
-
|
|
290
|
-
if (-not $dirty) { continue }
|
|
291
|
-
|
|
292
|
-
# ── Git branch snapshot ──────────────────────────────────
|
|
293
|
-
if ($backupStrategy -eq "git" -or $backupStrategy -eq "both") {
|
|
294
|
-
try {
|
|
295
|
-
$env:GIT_INDEX_FILE = $guardIndex
|
|
296
|
-
|
|
297
|
-
$parentHash = git rev-parse --verify $branchRef 2>$null
|
|
298
|
-
if ($parentHash) { git read-tree $branchRef 2>$null }
|
|
299
|
-
|
|
300
|
-
if ($protectPatterns.Count -gt 0) {
|
|
301
|
-
foreach ($p in $protectPatterns) { git add -- $p 2>$null }
|
|
302
|
-
} else {
|
|
303
|
-
git add -A 2>$null
|
|
304
|
-
}
|
|
305
|
-
foreach ($ig in $ignorePatterns) {
|
|
306
|
-
git rm --cached --ignore-unmatch -rq -- $ig 2>$null
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
Remove-SecretsFromIndex
|
|
310
|
-
|
|
311
|
-
$newTree = git write-tree
|
|
312
|
-
$parentTree = if ($parentHash) { git rev-parse "${branchRef}^{tree}" 2>$null } else { $null }
|
|
313
|
-
|
|
314
|
-
if ($newTree -eq $parentTree) {
|
|
315
|
-
Write-Host "[guard] $(Get-Date -Format 'HH:mm:ss') tree unchanged, skipped." -ForegroundColor DarkGray
|
|
316
|
-
} else {
|
|
317
|
-
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
318
|
-
$msg = "guard: auto-backup $ts"
|
|
319
|
-
$commitHash = if ($parentHash) {
|
|
320
|
-
git commit-tree $newTree -p $parentHash -m $msg
|
|
321
|
-
} else {
|
|
322
|
-
git commit-tree $newTree -m $msg
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (-not $commitHash) {
|
|
326
|
-
Write-Log "ERROR: commit-tree failed, snapshot skipped" Red
|
|
327
|
-
} else {
|
|
328
|
-
git update-ref $branchRef $commitHash
|
|
329
|
-
$short = $commitHash.Substring(0, 7)
|
|
330
|
-
if ($parentTree) {
|
|
331
|
-
$diff = git diff-tree --no-commit-id --name-only -r $parentTree $newTree 2>$null
|
|
332
|
-
$count = if ($diff) { @($diff).Count } else { 0 }
|
|
333
|
-
} else {
|
|
334
|
-
$all = git ls-tree --name-only -r $newTree 2>$null
|
|
335
|
-
$count = if ($all) { @($all).Count } else { 0 }
|
|
336
|
-
}
|
|
337
|
-
Write-Log "Git snapshot $short ($count files)"
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
finally {
|
|
342
|
-
$env:GIT_INDEX_FILE = $null
|
|
343
|
-
Remove-Item $guardIndex -Force -ErrorAction SilentlyContinue
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
# ── Shadow copy ──────────────────────────────────────────
|
|
348
|
-
if ($backupStrategy -eq "shadow" -or $backupStrategy -eq "both") {
|
|
349
|
-
Invoke-ShadowCopy | Out-Null
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
# Periodic retention cleanup every 10 cycles
|
|
353
|
-
if ($cycle % 10 -eq 0) { Invoke-RetentionCleanup }
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
finally {
|
|
357
|
-
Invoke-Cleanup
|
|
358
|
-
Write-Host "`n[guard] Stopped." -ForegroundColor Cyan
|
|
359
|
-
}
|
|
27
|
+
& $nodeCmd @args_
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Thin wrapper — launches the Node.js auto-backup implementation.
|
|
3
|
+
# Usage: ./auto-backup.sh /path/to/project [interval_seconds]
|
|
4
|
+
# Requires: Node.js >= 18
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
if ! command -v node &>/dev/null; then
|
|
11
|
+
echo "[guard] ERROR: Node.js not found. Install Node.js >= 18 first."
|
|
12
|
+
echo " https://nodejs.org/"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
TARGET="${1:-.}"
|
|
17
|
+
INTERVAL="${2:-0}"
|
|
18
|
+
|
|
19
|
+
exec node "$SCRIPT_DIR/bin/cursor-guard-backup.js" --path "$TARGET" --interval "$INTERVAL"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseArgs } = require('../lib/utils');
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv);
|
|
8
|
+
const targetPath = args.path || '.';
|
|
9
|
+
const interval = parseInt(args.interval, 10) || 0;
|
|
10
|
+
|
|
11
|
+
const resolved = path.resolve(targetPath);
|
|
12
|
+
|
|
13
|
+
const { runBackup } = require('../lib/auto-backup');
|
|
14
|
+
runBackup(resolved, interval);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { parseArgs } = require('../lib/utils');
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv);
|
|
8
|
+
const targetPath = args.path || '.';
|
|
9
|
+
const resolved = path.resolve(targetPath);
|
|
10
|
+
|
|
11
|
+
const { runDoctor } = require('../lib/guard-doctor');
|
|
12
|
+
const exitCode = runDoctor(resolved);
|
|
13
|
+
process.exit(exitCode);
|
|
@@ -67,7 +67,7 @@ Blacklist glob patterns. Matching files are excluded from protection even if the
|
|
|
67
67
|
- **Minimum**: `5`
|
|
68
68
|
- **Default**: `60`
|
|
69
69
|
|
|
70
|
-
Interval in seconds for
|
|
70
|
+
Interval in seconds for the auto-backup script to check for changes and create snapshots.
|
|
71
71
|
|
|
72
72
|
```json
|
|
73
73
|
"auto_backup_interval_seconds": 60
|
|
@@ -80,7 +80,9 @@ Interval in seconds for `auto-backup.ps1` to check for changes and create snapsh
|
|
|
80
80
|
- **Type**: `string[]` (glob patterns)
|
|
81
81
|
- **Default**: built-in list (see below)
|
|
82
82
|
|
|
83
|
-
Glob patterns for sensitive files. Matching files are **auto-excluded** from backup, even if within `protect` scope. Built-in defaults
|
|
83
|
+
Glob patterns for sensitive files. Matching files are **auto-excluded** from backup, even if within `protect` scope. Built-in defaults: `.env`, `.env.*`, `*.key`, `*.pem`, `*.p12`, `*.pfx`, `credentials*`.
|
|
84
|
+
|
|
85
|
+
**Setting this field replaces the built-in defaults entirely.** If you only need to add patterns, use `secrets_patterns_extra` instead.
|
|
84
86
|
|
|
85
87
|
```json
|
|
86
88
|
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"]
|
|
@@ -88,6 +90,19 @@ Glob patterns for sensitive files. Matching files are **auto-excluded** from bac
|
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
93
|
+
## `secrets_patterns_extra`
|
|
94
|
+
|
|
95
|
+
- **Type**: `string[]` (glob patterns)
|
|
96
|
+
- **Default**: not set
|
|
97
|
+
|
|
98
|
+
Additional glob patterns **appended** to the current `secrets_patterns` (including defaults). Use this to add custom patterns without losing the built-in protection for `.p12`, `.pfx`, `credentials*`, etc.
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
"secrets_patterns_extra": ["*.secret", "tokens.*"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
91
106
|
## `pre_restore_backup`
|
|
92
107
|
|
|
93
108
|
- **Type**: `string`
|
|
@@ -132,3 +147,29 @@ Retention policy for **shadow copies** only. Git branch snapshots are not auto-c
|
|
|
132
147
|
"max_size_mb": 500
|
|
133
148
|
}
|
|
134
149
|
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## `git_retention`
|
|
154
|
+
|
|
155
|
+
- **Type**: `object`
|
|
156
|
+
- **Default**: `{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
157
|
+
|
|
158
|
+
Retention policy for the **`cursor-guard/auto-backup` Git branch**. By default, auto-backup commits accumulate indefinitely. Enable this to automatically prune old commits.
|
|
159
|
+
|
|
160
|
+
### Sub-fields
|
|
161
|
+
|
|
162
|
+
| Field | Type | Default | Description |
|
|
163
|
+
|-------|------|---------|-------------|
|
|
164
|
+
| `enabled` | `boolean` | `false` | Enable automatic pruning. When false, branch grows without limit. |
|
|
165
|
+
| `mode` | `"days"` \| `"count"` | `"count"` | Pruning strategy |
|
|
166
|
+
| `days` | `integer` | `30` | Keep commits from last N days (when mode=days) |
|
|
167
|
+
| `max_count` | `integer` | `200` | Keep N newest commits (when mode=count, minimum 10) |
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
"git_retention": {
|
|
171
|
+
"enabled": true,
|
|
172
|
+
"mode": "count",
|
|
173
|
+
"max_count": 200
|
|
174
|
+
}
|
|
175
|
+
```
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
- **最小值**:`5`
|
|
68
68
|
- **默认值**:`60`
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
自动备份脚本检查变更并创建快照的间隔秒数。
|
|
71
71
|
|
|
72
72
|
```json
|
|
73
73
|
"auto_backup_interval_seconds": 60
|
|
@@ -80,7 +80,9 @@
|
|
|
80
80
|
- **类型**:`string[]`(glob 模式)
|
|
81
81
|
- **默认值**:内置列表(见下)
|
|
82
82
|
|
|
83
|
-
敏感文件 glob 模式。匹配的文件**自动排除**备份,即使在 `protect`
|
|
83
|
+
敏感文件 glob 模式。匹配的文件**自动排除**备份,即使在 `protect` 范围内。内置默认值:`.env`、`.env.*`、`*.key`、`*.pem`、`*.p12`、`*.pfx`、`credentials*`。
|
|
84
|
+
|
|
85
|
+
**设置此字段会完全替换内置默认值。** 如果只想追加模式,请使用 `secrets_patterns_extra`。
|
|
84
86
|
|
|
85
87
|
```json
|
|
86
88
|
"secrets_patterns": [".env", ".env.*", "*.key", "*.pem"]
|
|
@@ -88,6 +90,19 @@
|
|
|
88
90
|
|
|
89
91
|
---
|
|
90
92
|
|
|
93
|
+
## `secrets_patterns_extra`
|
|
94
|
+
|
|
95
|
+
- **类型**:`string[]`(glob 模式)
|
|
96
|
+
- **默认值**:未设置
|
|
97
|
+
|
|
98
|
+
追加到当前 `secrets_patterns`(含默认值)的额外 glob 模式。使用此字段可在不丢失 `.p12`、`.pfx`、`credentials*` 等内置保护的情况下添加自定义模式。
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
"secrets_patterns_extra": ["*.secret", "tokens.*"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
91
106
|
## `pre_restore_backup`
|
|
92
107
|
|
|
93
108
|
- **类型**:`string`
|
|
@@ -132,3 +147,29 @@
|
|
|
132
147
|
"max_size_mb": 500
|
|
133
148
|
}
|
|
134
149
|
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## `git_retention`
|
|
154
|
+
|
|
155
|
+
- **类型**:`object`
|
|
156
|
+
- **默认值**:`{ "enabled": false, "mode": "count", "max_count": 200 }`
|
|
157
|
+
|
|
158
|
+
**`cursor-guard/auto-backup` Git 分支**的保留策略。默认情况下自动备份提交会无限累积。启用此项可自动裁剪旧提交。
|
|
159
|
+
|
|
160
|
+
### 子字段
|
|
161
|
+
|
|
162
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
163
|
+
|------|------|--------|------|
|
|
164
|
+
| `enabled` | `boolean` | `false` | 启用自动裁剪。关闭时分支无限增长。 |
|
|
165
|
+
| `mode` | `"days"` \| `"count"` | `"count"` | 裁剪策略 |
|
|
166
|
+
| `days` | `integer` | `30` | 保留最近 N 天的提交(mode=days 时生效) |
|
|
167
|
+
| `max_count` | `integer` | `200` | 保留最新 N 个提交(mode=count 时生效,最少 10) |
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
"git_retention": {
|
|
171
|
+
"enabled": true,
|
|
172
|
+
"mode": "count",
|
|
173
|
+
"max_count": 200
|
|
174
|
+
}
|
|
175
|
+
```
|
|
@@ -26,11 +26,18 @@
|
|
|
26
26
|
"*.key",
|
|
27
27
|
"*.pem"
|
|
28
28
|
],
|
|
29
|
+
"secrets_patterns_extra": [],
|
|
29
30
|
"pre_restore_backup": "always",
|
|
30
31
|
"retention": {
|
|
31
32
|
"mode": "days",
|
|
32
33
|
"days": 30,
|
|
33
34
|
"max_count": 100,
|
|
34
35
|
"max_size_mb": 500
|
|
36
|
+
},
|
|
37
|
+
"git_retention": {
|
|
38
|
+
"enabled": false,
|
|
39
|
+
"mode": "count",
|
|
40
|
+
"days": 30,
|
|
41
|
+
"max_count": 200
|
|
35
42
|
}
|
|
36
43
|
}
|
|
@@ -24,12 +24,17 @@
|
|
|
24
24
|
"type": "integer",
|
|
25
25
|
"minimum": 5,
|
|
26
26
|
"default": 60,
|
|
27
|
-
"description": "Interval in seconds for auto-backup
|
|
27
|
+
"description": "Interval in seconds for auto-backup script to check for changes."
|
|
28
28
|
},
|
|
29
29
|
"secrets_patterns": {
|
|
30
30
|
"type": "array",
|
|
31
31
|
"items": { "type": "string" },
|
|
32
|
-
"description": "Glob patterns for sensitive files auto-excluded from backups.
|
|
32
|
+
"description": "Glob patterns for sensitive files auto-excluded from backups. Overrides built-in defaults: .env, .env.*, *.key, *.pem, *.p12, *.pfx, credentials*. Use secrets_patterns_extra to add without replacing."
|
|
33
|
+
},
|
|
34
|
+
"secrets_patterns_extra": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"items": { "type": "string" },
|
|
37
|
+
"description": "Additional glob patterns appended to secrets_patterns (including defaults). Use this to add patterns without losing built-in protection."
|
|
33
38
|
},
|
|
34
39
|
"pre_restore_backup": {
|
|
35
40
|
"type": "string",
|
|
@@ -67,7 +72,37 @@
|
|
|
67
72
|
}
|
|
68
73
|
},
|
|
69
74
|
"additionalProperties": false
|
|
75
|
+
},
|
|
76
|
+
"git_retention": {
|
|
77
|
+
"type": "object",
|
|
78
|
+
"description": "Controls automatic cleanup of old commits on the cursor-guard/auto-backup branch.",
|
|
79
|
+
"properties": {
|
|
80
|
+
"enabled": {
|
|
81
|
+
"type": "boolean",
|
|
82
|
+
"default": false,
|
|
83
|
+
"description": "Enable automatic pruning of old auto-backup commits. Default false (manual cleanup)."
|
|
84
|
+
},
|
|
85
|
+
"mode": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"enum": ["days", "count"],
|
|
88
|
+
"default": "count",
|
|
89
|
+
"description": "'days': keep commits from the last N days. 'count': keep N newest commits."
|
|
90
|
+
},
|
|
91
|
+
"days": {
|
|
92
|
+
"type": "integer",
|
|
93
|
+
"minimum": 1,
|
|
94
|
+
"default": 30,
|
|
95
|
+
"description": "Number of days to keep auto-backup commits (when mode='days')."
|
|
96
|
+
},
|
|
97
|
+
"max_count": {
|
|
98
|
+
"type": "integer",
|
|
99
|
+
"minimum": 10,
|
|
100
|
+
"default": 200,
|
|
101
|
+
"description": "Maximum number of auto-backup commits to keep (when mode='count')."
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"additionalProperties": false
|
|
70
105
|
}
|
|
71
106
|
},
|
|
72
|
-
"additionalProperties":
|
|
107
|
+
"additionalProperties": false
|
|
73
108
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Thin wrapper — launches the Node.js guard-doctor implementation.
|
|
4
|
+
.USAGE
|
|
5
|
+
.\guard-doctor.ps1 -Path "D:\MyProject"
|
|
6
|
+
.NOTES
|
|
7
|
+
Requires Node.js >= 18.
|
|
8
|
+
#>
|
|
9
|
+
param(
|
|
10
|
+
[Parameter(Mandatory)]
|
|
11
|
+
[string]$Path
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
$nodeCmd = if (Get-Command node -ErrorAction SilentlyContinue) { "node" } else { $null }
|
|
15
|
+
if (-not $nodeCmd) {
|
|
16
|
+
Write-Host "[guard] ERROR: Node.js not found. Install Node.js >= 18 first." -ForegroundColor Red
|
|
17
|
+
Write-Host " https://nodejs.org/" -ForegroundColor Yellow
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$script = Join-Path (Join-Path $PSScriptRoot "bin") "cursor-guard-doctor.js"
|
|
22
|
+
& $nodeCmd $script --path $Path
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Thin wrapper — launches the Node.js guard-doctor implementation.
|
|
3
|
+
# Usage: ./guard-doctor.sh /path/to/project
|
|
4
|
+
# Requires: Node.js >= 18
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
|
|
10
|
+
if ! command -v node &>/dev/null; then
|
|
11
|
+
echo "[guard] ERROR: Node.js not found. Install Node.js >= 18 first."
|
|
12
|
+
echo " https://nodejs.org/"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
TARGET="${1:-.}"
|
|
17
|
+
|
|
18
|
+
exec node "$SCRIPT_DIR/bin/cursor-guard-doctor.js" --path "$TARGET"
|