cursor-guard 1.4.0 → 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 +35 -8
- package/package.json +10 -2
- package/references/auto-backup.ps1 +10 -462
- 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 +17 -2
- package/references/config-reference.zh-CN.md +17 -2
- package/references/cursor-guard.example.json +1 -0
- package/references/cursor-guard.schema.json +8 -3
- package/references/guard-doctor.ps1 +9 -213
- 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
|
@@ -1,479 +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
|
-
# ── 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
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# ── Paths ─────────────────────────────────────────────────────────
|
|
57
|
-
$backupDir = Join-Path $resolved ".cursor-guard-backup"
|
|
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 }
|
|
61
|
-
|
|
62
|
-
# ── Cleanup on exit ───────────────────────────────────────────────
|
|
63
|
-
function Invoke-Cleanup {
|
|
64
|
-
$env:GIT_INDEX_FILE = $null
|
|
65
|
-
Remove-Item $guardIndex -Force -ErrorAction SilentlyContinue
|
|
66
|
-
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
|
|
67
|
-
}
|
|
68
|
-
trap { Invoke-Cleanup; break }
|
|
69
|
-
|
|
70
|
-
# ── Defaults ──────────────────────────────────────────────────────
|
|
71
|
-
$protectPatterns = @()
|
|
72
|
-
$ignorePatterns = @()
|
|
73
|
-
$secretsPatterns = @(".env", ".env.*", "*.key", "*.pem", "*.p12", "*.pfx", "credentials*")
|
|
74
|
-
$backupStrategy = "git"
|
|
75
|
-
$retentionMode = "days"
|
|
76
|
-
$retentionDays = 30
|
|
77
|
-
$retentionMaxCnt = 100
|
|
78
|
-
$retentionMaxMB = 500
|
|
79
|
-
$gitRetEnabled = $false
|
|
80
|
-
$gitRetMode = "count"
|
|
81
|
-
$gitRetDays = 30
|
|
82
|
-
$gitRetMaxCnt = 200
|
|
83
|
-
|
|
84
|
-
# ── Load .cursor-guard.json ──────────────────────────────────────
|
|
85
|
-
$cfgPath = Join-Path $resolved ".cursor-guard.json"
|
|
86
|
-
if (Test-Path $cfgPath) {
|
|
87
|
-
try {
|
|
88
|
-
$cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json
|
|
89
|
-
if ($cfg.protect) { $protectPatterns = @($cfg.protect) }
|
|
90
|
-
if ($cfg.ignore) { $ignorePatterns = @($cfg.ignore) }
|
|
91
|
-
if ($cfg.secrets_patterns) { $secretsPatterns = @($cfg.secrets_patterns) }
|
|
92
|
-
if ($cfg.backup_strategy) { $backupStrategy = $cfg.backup_strategy }
|
|
93
|
-
if ($cfg.auto_backup_interval_seconds -and $IntervalSeconds -eq 0) {
|
|
94
|
-
$IntervalSeconds = $cfg.auto_backup_interval_seconds
|
|
95
|
-
}
|
|
96
|
-
if ($cfg.retention) {
|
|
97
|
-
if ($cfg.retention.mode) { $retentionMode = $cfg.retention.mode }
|
|
98
|
-
if ($cfg.retention.days) { $retentionDays = $cfg.retention.days }
|
|
99
|
-
if ($cfg.retention.max_count) { $retentionMaxCnt = $cfg.retention.max_count }
|
|
100
|
-
if ($cfg.retention.max_size_mb) { $retentionMaxMB = $cfg.retention.max_size_mb }
|
|
101
|
-
}
|
|
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
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
Write-Host "[guard] WARNING: .cursor-guard.json parse error - using defaults." -ForegroundColor Yellow
|
|
112
|
-
Write-Host " $_" -ForegroundColor Yellow
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if ($IntervalSeconds -eq 0) { $IntervalSeconds = 60 }
|
|
116
|
-
|
|
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
|
-
}
|
|
125
|
-
$ans = Read-Host "Directory is not a Git repo. Initialize? (y/n)"
|
|
126
|
-
if ($ans -eq 'y') {
|
|
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"
|
|
132
|
-
$gi = Join-Path $resolved ".gitignore"
|
|
133
|
-
$entry = ".cursor-guard-backup/"
|
|
134
|
-
if (Test-Path $gi) {
|
|
135
|
-
$content = Get-Content $gi -Raw
|
|
136
|
-
if ($content -notmatch [regex]::Escape($entry)) {
|
|
137
|
-
Add-Content $gi "`n$entry"
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
Set-Content $gi $entry
|
|
141
|
-
}
|
|
142
|
-
git add -A; git commit -m "guard: initial snapshot" --no-verify
|
|
143
|
-
Write-Host "[guard] Repo initialized with snapshot." -ForegroundColor Green
|
|
144
|
-
} else {
|
|
145
|
-
Write-Host "[guard] Git is required for '$backupStrategy' strategy. Exiting." -ForegroundColor Red
|
|
146
|
-
exit 1
|
|
147
|
-
}
|
|
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 }
|
|
155
|
-
|
|
156
|
-
# ── Lock file (prevent multiple instances) ───────────────────────
|
|
157
|
-
if (Test-Path $lockFile) {
|
|
158
|
-
Write-Host "[guard] ERROR: Lock file exists ($lockFile)." -ForegroundColor Red
|
|
159
|
-
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
|
|
160
20
|
exit 1
|
|
161
21
|
}
|
|
162
|
-
Set-Content $lockFile "pid=$PID`nstarted=$(Get-Date -Format 'o')"
|
|
163
|
-
|
|
164
|
-
# ── Git-specific setup (skip entirely for shadow-only) ───────────
|
|
165
|
-
$branch = "cursor-guard/auto-backup"
|
|
166
|
-
$branchRef = "refs/heads/$branch"
|
|
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
|
-
}
|
|
172
|
-
|
|
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
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
# ── Log & helpers ────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
function Write-Log {
|
|
190
|
-
param([string]$Msg, [ConsoleColor]$Color = "Green")
|
|
191
|
-
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Msg"
|
|
192
|
-
Add-Content -Path $logFilePath -Value $line -ErrorAction SilentlyContinue
|
|
193
|
-
Write-Host "[guard] $line" -ForegroundColor $Color
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# ── Secrets filter ───────────────────────────────────────────────
|
|
197
|
-
function Remove-SecretsFromIndex {
|
|
198
|
-
$files = git ls-files --cached 2>$null
|
|
199
|
-
if (-not $files) { return }
|
|
200
|
-
$excluded = @()
|
|
201
|
-
foreach ($f in $files) {
|
|
202
|
-
$leaf = Split-Path $f -Leaf
|
|
203
|
-
foreach ($pat in $secretsPatterns) {
|
|
204
|
-
$re = '^' + [regex]::Escape($pat).Replace('\*','.*').Replace('\?','.') + '$'
|
|
205
|
-
if ($f -match $re -or $leaf -match $re) {
|
|
206
|
-
git rm --cached --ignore-unmatch -q -- $f 2>$null
|
|
207
|
-
$excluded += $f
|
|
208
|
-
break
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
if ($excluded.Count -gt 0) {
|
|
213
|
-
Write-Log "Secrets auto-excluded: $($excluded -join ', ')" Yellow
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
# ── Retention cleanup ────────────────────────────────────────────
|
|
218
|
-
function Invoke-RetentionCleanup {
|
|
219
|
-
# Clean shadow-copy directories named yyyyMMdd_HHmmss
|
|
220
|
-
$dirs = Get-ChildItem $backupDir -Directory -ErrorAction SilentlyContinue |
|
|
221
|
-
Where-Object { $_.Name -match '^\d{8}_\d{6}$' } |
|
|
222
|
-
Sort-Object Name -Descending
|
|
223
|
-
if ($dirs -and $dirs.Count -gt 0) {
|
|
224
|
-
$removed = 0
|
|
225
|
-
switch ($retentionMode) {
|
|
226
|
-
"days" {
|
|
227
|
-
$cutoff = (Get-Date).AddDays(-$retentionDays)
|
|
228
|
-
foreach ($d in $dirs) {
|
|
229
|
-
try {
|
|
230
|
-
$dt = [datetime]::ParseExact($d.Name, "yyyyMMdd_HHmmss", $null)
|
|
231
|
-
if ($dt -lt $cutoff) { Remove-Item $d.FullName -Recurse -Force; $removed++ }
|
|
232
|
-
} catch {}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
"count" {
|
|
236
|
-
if ($dirs.Count -gt $retentionMaxCnt) {
|
|
237
|
-
$dirs | Select-Object -Skip $retentionMaxCnt |
|
|
238
|
-
ForEach-Object { Remove-Item $_.FullName -Recurse -Force; $removed++ }
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
"size" {
|
|
242
|
-
$totalMB = (Get-ChildItem $backupDir -Recurse -File -ErrorAction SilentlyContinue |
|
|
243
|
-
Measure-Object Length -Sum).Sum / 1MB
|
|
244
|
-
$oldest = $dirs | Sort-Object Name
|
|
245
|
-
foreach ($d in $oldest) {
|
|
246
|
-
if ($totalMB -le $retentionMaxMB) { break }
|
|
247
|
-
$sz = (Get-ChildItem $d.FullName -Recurse -File |
|
|
248
|
-
Measure-Object Length -Sum).Sum / 1MB
|
|
249
|
-
Remove-Item $d.FullName -Recurse -Force
|
|
250
|
-
$totalMB -= $sz; $removed++
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if ($removed -gt 0) {
|
|
255
|
-
Write-Log "Retention ($retentionMode): cleaned $removed old snapshot(s)" DarkGray
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
# Disk-space warning
|
|
260
|
-
try {
|
|
261
|
-
$letter = (Split-Path $resolved -Qualifier) -replace ':$',''
|
|
262
|
-
$drv = Get-PSDrive $letter
|
|
263
|
-
$freeGB = [math]::Round($drv.Free / 1GB, 1)
|
|
264
|
-
if ($freeGB -lt 1) { Write-Log "WARNING: disk critically low - ${freeGB} GB free" Red }
|
|
265
|
-
elseif ($freeGB -lt 5) { Write-Log "Disk note: ${freeGB} GB free" Yellow }
|
|
266
|
-
} catch {}
|
|
267
|
-
}
|
|
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
|
-
|
|
320
|
-
# ── Shadow copy helper ────────────────────────────────────────────
|
|
321
|
-
function Invoke-ShadowCopy {
|
|
322
|
-
$ts = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
323
|
-
$snapDir = Join-Path $backupDir $ts
|
|
324
|
-
New-Item -ItemType Directory -Force $snapDir | Out-Null
|
|
325
|
-
|
|
326
|
-
$allFiles = Get-ChildItem $resolved -Recurse -File -ErrorAction SilentlyContinue |
|
|
327
|
-
Where-Object { $_.FullName -notmatch '[\\/](\.git|\.cursor-guard-backup|node_modules)[\\/]' }
|
|
328
|
-
|
|
329
|
-
$files = if ($protectPatterns.Count -gt 0) {
|
|
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
|
-
}
|
|
339
|
-
} else {
|
|
340
|
-
$allFiles
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
$copied = 0
|
|
344
|
-
foreach ($f in $files) {
|
|
345
|
-
$rel = $f.FullName.Substring($resolved.Length + 1) -replace '\\','/'
|
|
346
|
-
$leaf = $f.Name
|
|
347
|
-
$skip = $false
|
|
348
|
-
foreach ($ig in $ignorePatterns) {
|
|
349
|
-
$p = $ig -replace '\\','/'
|
|
350
|
-
if ($rel -like $p -or $leaf -like $p) { $skip = $true; break }
|
|
351
|
-
}
|
|
352
|
-
if (-not $skip) {
|
|
353
|
-
foreach ($pat in $secretsPatterns) {
|
|
354
|
-
if ($rel -like $pat -or $leaf -like $pat) { $skip = $true; break }
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if ($skip) { continue }
|
|
358
|
-
$dest = Join-Path $snapDir $rel
|
|
359
|
-
$destDir = Split-Path $dest
|
|
360
|
-
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Force $destDir | Out-Null }
|
|
361
|
-
Copy-Item $f.FullName $dest -Force
|
|
362
|
-
$copied++
|
|
363
|
-
}
|
|
364
|
-
if ($copied -gt 0) {
|
|
365
|
-
Write-Log "Shadow copy $ts ($copied files)"
|
|
366
|
-
} else {
|
|
367
|
-
Remove-Item $snapDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
368
|
-
}
|
|
369
|
-
return $copied
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
# ── Banner ───────────────────────────────────────────────────────
|
|
373
|
-
Write-Host ""
|
|
374
|
-
Write-Host "[guard] Watching '$resolved' every ${IntervalSeconds}s (Ctrl+C to stop)" -ForegroundColor Cyan
|
|
375
|
-
Write-Host "[guard] Strategy: $backupStrategy | Branch: $branch | Retention: $retentionMode ($retentionDays days / $retentionMaxCnt count / ${retentionMaxMB} MB)" -ForegroundColor Cyan
|
|
376
|
-
Write-Host "[guard] Log: $logFilePath" -ForegroundColor Cyan
|
|
377
|
-
Write-Host ""
|
|
378
|
-
|
|
379
|
-
# ── Main loop ────────────────────────────────────────────────────
|
|
380
|
-
$cycle = 0
|
|
381
|
-
try {
|
|
382
|
-
while ($true) {
|
|
383
|
-
Start-Sleep -Seconds $IntervalSeconds
|
|
384
|
-
$cycle++
|
|
385
|
-
|
|
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 }
|
|
408
|
-
|
|
409
|
-
# ── Git branch snapshot ──────────────────────────────────
|
|
410
|
-
if ($backupStrategy -eq "git" -or $backupStrategy -eq "both") {
|
|
411
|
-
try {
|
|
412
|
-
$env:GIT_INDEX_FILE = $guardIndex
|
|
413
|
-
|
|
414
|
-
$parentHash = git rev-parse --verify $branchRef 2>$null
|
|
415
|
-
if ($parentHash) { git read-tree $branchRef 2>$null }
|
|
416
22
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
git add -A 2>$null
|
|
421
|
-
}
|
|
422
|
-
foreach ($ig in $ignorePatterns) {
|
|
423
|
-
git rm --cached --ignore-unmatch -rq -- $ig 2>$null
|
|
424
|
-
}
|
|
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) }
|
|
425
26
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
$newTree = git write-tree
|
|
429
|
-
$parentTree = if ($parentHash) { git rev-parse "${branchRef}^{tree}" 2>$null } else { $null }
|
|
430
|
-
|
|
431
|
-
if ($newTree -eq $parentTree) {
|
|
432
|
-
Write-Host "[guard] $(Get-Date -Format 'HH:mm:ss') tree unchanged, skipped." -ForegroundColor DarkGray
|
|
433
|
-
} else {
|
|
434
|
-
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
|
435
|
-
$msg = "guard: auto-backup $ts"
|
|
436
|
-
$commitHash = if ($parentHash) {
|
|
437
|
-
git commit-tree $newTree -p $parentHash -m $msg
|
|
438
|
-
} else {
|
|
439
|
-
git commit-tree $newTree -m $msg
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (-not $commitHash) {
|
|
443
|
-
Write-Log "ERROR: commit-tree failed, snapshot skipped" Red
|
|
444
|
-
} else {
|
|
445
|
-
git update-ref $branchRef $commitHash
|
|
446
|
-
$short = $commitHash.Substring(0, 7)
|
|
447
|
-
if ($parentTree) {
|
|
448
|
-
$diff = git diff-tree --no-commit-id --name-only -r $parentTree $newTree 2>$null
|
|
449
|
-
$count = if ($diff) { @($diff).Count } else { 0 }
|
|
450
|
-
} else {
|
|
451
|
-
$all = git ls-tree --name-only -r $newTree 2>$null
|
|
452
|
-
$count = if ($all) { @($all).Count } else { 0 }
|
|
453
|
-
}
|
|
454
|
-
Write-Log "Git snapshot $short ($count files)"
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
finally {
|
|
459
|
-
$env:GIT_INDEX_FILE = $null
|
|
460
|
-
Remove-Item $guardIndex -Force -ErrorAction SilentlyContinue
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
# ── Shadow copy ──────────────────────────────────────────
|
|
465
|
-
if ($backupStrategy -eq "shadow" -or $backupStrategy -eq "both") {
|
|
466
|
-
Invoke-ShadowCopy | Out-Null
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
# Periodic retention cleanup every 10 cycles
|
|
470
|
-
if ($cycle % 10 -eq 0) {
|
|
471
|
-
Invoke-RetentionCleanup
|
|
472
|
-
Invoke-GitRetention
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
finally {
|
|
477
|
-
Invoke-Cleanup
|
|
478
|
-
Write-Host "`n[guard] Stopped." -ForegroundColor Cyan
|
|
479
|
-
}
|
|
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`
|
|
@@ -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`
|
|
@@ -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",
|
|
@@ -99,5 +104,5 @@
|
|
|
99
104
|
"additionalProperties": false
|
|
100
105
|
}
|
|
101
106
|
},
|
|
102
|
-
"additionalProperties":
|
|
107
|
+
"additionalProperties": false
|
|
103
108
|
}
|