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.
@@ -1,479 +1,27 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Auto-backup script for Cursor Guard.
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
- - Snapshots go to branch `cursor-guard/auto-backup` via plumbing commands.
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
- $ErrorActionPreference = "Stop"
37
- $resolved = (Resolve-Path $Path).Path
38
- Set-Location $resolved
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
- if ($protectPatterns.Count -gt 0) {
418
- foreach ($p in $protectPatterns) { git add -- $p 2>$null }
419
- } else {
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
- Remove-SecretsFromIndex
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 `auto-backup.ps1` to check for changes and create snapshots.
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 (always active): `.env`, `.env.*`, `*.key`, `*.pem`, `*.p12`, `*.pfx`, `credentials*`. Set this field to override with your own patterns.
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
- 自动备份脚本 `auto-backup.ps1` 检查变更并创建快照的间隔秒数。
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` 范围内。内置默认值(始终生效):`.env`、`.env.*`、`*.key`、`*.pem`、`*.p12`、`*.pfx`、`credentials*`。设置此字段可覆盖为自定义模式。
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`
@@ -26,6 +26,7 @@
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",
@@ -24,12 +24,17 @@
24
24
  "type": "integer",
25
25
  "minimum": 5,
26
26
  "default": 60,
27
- "description": "Interval in seconds for auto-backup.ps1 to check for changes."
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. Built-in defaults: .env, .env.*, *.key, *.pem, *.p12, *.pfx, credentials*"
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": true
107
+ "additionalProperties": false
103
108
  }