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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const {
|
|
8
|
+
globMatch, matchesAny, loadConfig, DEFAULT_CONFIG, DEFAULT_SECRETS,
|
|
9
|
+
filterFiles, buildManifest, manifestChanged, parseArgs, walkDir,
|
|
10
|
+
} = require('./utils');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
let failed = 0;
|
|
14
|
+
|
|
15
|
+
function test(name, fn) {
|
|
16
|
+
try {
|
|
17
|
+
fn();
|
|
18
|
+
passed++;
|
|
19
|
+
console.log(` \x1b[32m✓\x1b[0m ${name}`);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
failed++;
|
|
22
|
+
console.log(` \x1b[31m✗\x1b[0m ${name}`);
|
|
23
|
+
console.log(` ${e.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── globMatch ────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
console.log('\nglobMatch:');
|
|
30
|
+
|
|
31
|
+
test('exact filename match', () => {
|
|
32
|
+
assert.strictEqual(globMatch('.env', '.env'), true);
|
|
33
|
+
assert.strictEqual(globMatch('.env', '.envx'), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('* matches within a single segment', () => {
|
|
37
|
+
assert.strictEqual(globMatch('*.js', 'foo.js'), true);
|
|
38
|
+
assert.strictEqual(globMatch('*.js', 'bar.ts'), false);
|
|
39
|
+
assert.strictEqual(globMatch('*.js', 'dir/foo.js'), false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('** matches across directories', () => {
|
|
43
|
+
assert.strictEqual(globMatch('**/*.js', 'src/foo.js'), true);
|
|
44
|
+
assert.strictEqual(globMatch('**/*.js', 'a/b/c/foo.js'), true);
|
|
45
|
+
// **/*.js requires a slash — root-level 'foo.js' doesn't match (matchesAny checks leaf separately)
|
|
46
|
+
assert.strictEqual(globMatch('**/*.js', 'foo.js'), false);
|
|
47
|
+
assert.strictEqual(globMatch('**/*.js', 'foo.ts'), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('? matches single character', () => {
|
|
51
|
+
assert.strictEqual(globMatch('?.txt', 'a.txt'), true);
|
|
52
|
+
assert.strictEqual(globMatch('?.txt', 'ab.txt'), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('.env.* pattern', () => {
|
|
56
|
+
assert.strictEqual(globMatch('.env.*', '.env.local'), true);
|
|
57
|
+
assert.strictEqual(globMatch('.env.*', '.env.production'), true);
|
|
58
|
+
assert.strictEqual(globMatch('.env.*', '.env'), false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('credentials* pattern', () => {
|
|
62
|
+
assert.strictEqual(globMatch('credentials*', 'credentials'), true);
|
|
63
|
+
assert.strictEqual(globMatch('credentials*', 'credentials.json'), true);
|
|
64
|
+
assert.strictEqual(globMatch('credentials*', 'my-credentials'), false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('directory pattern src/**', () => {
|
|
68
|
+
assert.strictEqual(globMatch('src/**', 'src/foo.js'), true);
|
|
69
|
+
assert.strictEqual(globMatch('src/**', 'src/a/b.js'), true);
|
|
70
|
+
assert.strictEqual(globMatch('src/**', 'lib/foo.js'), false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('backslash normalization', () => {
|
|
74
|
+
assert.strictEqual(globMatch('src/**/*.ts', 'src\\components\\App.ts'), true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('regex special chars in pattern are escaped', () => {
|
|
78
|
+
assert.strictEqual(globMatch('file(1).txt', 'file(1).txt'), true);
|
|
79
|
+
assert.strictEqual(globMatch('file[0].txt', 'file[0].txt'), true); // [] escaped as literals
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── matchesAny ───────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
console.log('\nmatchesAny:');
|
|
85
|
+
|
|
86
|
+
test('matches when any pattern hits', () => {
|
|
87
|
+
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.js'), true);
|
|
88
|
+
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.ts'), true);
|
|
89
|
+
assert.strictEqual(matchesAny(['*.js', '*.ts'], 'foo.py'), false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('checks leaf filename for deep paths', () => {
|
|
93
|
+
assert.strictEqual(matchesAny(['*.key'], 'secrets/server.key'), true);
|
|
94
|
+
assert.strictEqual(matchesAny(['.env'], 'config/.env'), true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('empty patterns matches nothing', () => {
|
|
98
|
+
assert.strictEqual(matchesAny([], 'anything'), false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── loadConfig ───────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
console.log('\nloadConfig:');
|
|
104
|
+
|
|
105
|
+
test('returns defaults when no config file', () => {
|
|
106
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
107
|
+
try {
|
|
108
|
+
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
109
|
+
assert.strictEqual(loaded, false);
|
|
110
|
+
assert.strictEqual(error, null);
|
|
111
|
+
assert.deepStrictEqual(cfg.protect, []);
|
|
112
|
+
assert.deepStrictEqual(cfg.ignore, []);
|
|
113
|
+
assert.deepStrictEqual(cfg.secrets_patterns, DEFAULT_SECRETS);
|
|
114
|
+
assert.strictEqual(cfg.backup_strategy, 'git');
|
|
115
|
+
assert.strictEqual(cfg.git_retention.enabled, false);
|
|
116
|
+
} finally {
|
|
117
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('loads and merges custom config', () => {
|
|
122
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
125
|
+
protect: ['src/**'],
|
|
126
|
+
backup_strategy: 'both',
|
|
127
|
+
retention: { mode: 'count', max_count: 50 },
|
|
128
|
+
}));
|
|
129
|
+
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
130
|
+
assert.strictEqual(loaded, true);
|
|
131
|
+
assert.strictEqual(error, null);
|
|
132
|
+
assert.deepStrictEqual(cfg.protect, ['src/**']);
|
|
133
|
+
assert.strictEqual(cfg.backup_strategy, 'both');
|
|
134
|
+
assert.strictEqual(cfg.retention.mode, 'count');
|
|
135
|
+
assert.strictEqual(cfg.retention.max_count, 50);
|
|
136
|
+
assert.strictEqual(cfg.retention.days, 30); // default preserved
|
|
137
|
+
} finally {
|
|
138
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('handles malformed JSON gracefully', () => {
|
|
143
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
144
|
+
try {
|
|
145
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), '{ broken }');
|
|
146
|
+
const { cfg, loaded, error } = loadConfig(tmpDir);
|
|
147
|
+
assert.strictEqual(loaded, false);
|
|
148
|
+
assert.ok(error, 'should have an error message');
|
|
149
|
+
assert.deepStrictEqual(cfg.protect, []);
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('secrets_patterns override replaces defaults entirely', () => {
|
|
156
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
157
|
+
try {
|
|
158
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
159
|
+
secrets_patterns: ['my-secret'],
|
|
160
|
+
}));
|
|
161
|
+
const { cfg } = loadConfig(tmpDir);
|
|
162
|
+
assert.deepStrictEqual(cfg.secrets_patterns, ['my-secret']);
|
|
163
|
+
} finally {
|
|
164
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('secrets_patterns_extra appends to defaults', () => {
|
|
169
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
172
|
+
secrets_patterns_extra: ['*.secret', 'tokens.*'],
|
|
173
|
+
}));
|
|
174
|
+
const { cfg } = loadConfig(tmpDir);
|
|
175
|
+
assert.ok(cfg.secrets_patterns.includes('.env'), 'should keep default .env');
|
|
176
|
+
assert.ok(cfg.secrets_patterns.includes('*.p12'), 'should keep default *.p12');
|
|
177
|
+
assert.ok(cfg.secrets_patterns.includes('*.secret'), 'should include extra *.secret');
|
|
178
|
+
assert.ok(cfg.secrets_patterns.includes('tokens.*'), 'should include extra tokens.*');
|
|
179
|
+
} finally {
|
|
180
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('secrets_patterns_extra merges with custom secrets_patterns', () => {
|
|
185
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
186
|
+
try {
|
|
187
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
188
|
+
secrets_patterns: ['.env'],
|
|
189
|
+
secrets_patterns_extra: ['.env', '*.secret'],
|
|
190
|
+
}));
|
|
191
|
+
const { cfg } = loadConfig(tmpDir);
|
|
192
|
+
assert.deepStrictEqual(cfg.secrets_patterns, ['.env', '*.secret']);
|
|
193
|
+
} finally {
|
|
194
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('non-string backup_strategy is ignored', () => {
|
|
199
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-test-'));
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(path.join(tmpDir, '.cursor-guard.json'), JSON.stringify({
|
|
202
|
+
backup_strategy: 123,
|
|
203
|
+
auto_backup_interval_seconds: 'bad',
|
|
204
|
+
}));
|
|
205
|
+
const { cfg } = loadConfig(tmpDir);
|
|
206
|
+
assert.strictEqual(cfg.backup_strategy, 'git');
|
|
207
|
+
assert.strictEqual(cfg.auto_backup_interval_seconds, 60);
|
|
208
|
+
} finally {
|
|
209
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ── filterFiles ──────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
console.log('\nfilterFiles:');
|
|
216
|
+
|
|
217
|
+
const makeFiles = names => names.map(n => ({ full: `/fake/${n}`, rel: n, name: path.basename(n) }));
|
|
218
|
+
|
|
219
|
+
test('no protect/ignore returns all non-secret files', () => {
|
|
220
|
+
const files = makeFiles(['a.js', 'b.ts', '.env', 'credentials.json']);
|
|
221
|
+
const cfg = { ...DEFAULT_CONFIG };
|
|
222
|
+
const result = filterFiles(files, cfg);
|
|
223
|
+
const rels = result.map(f => f.rel);
|
|
224
|
+
assert.ok(!rels.includes('.env'));
|
|
225
|
+
assert.ok(!rels.includes('credentials.json'));
|
|
226
|
+
assert.ok(rels.includes('a.js'));
|
|
227
|
+
assert.ok(rels.includes('b.ts'));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('protect narrows scope', () => {
|
|
231
|
+
const files = makeFiles(['src/a.js', 'lib/b.js', 'README.md']);
|
|
232
|
+
const cfg = { ...DEFAULT_CONFIG, protect: ['src/**'] };
|
|
233
|
+
const result = filterFiles(files, cfg);
|
|
234
|
+
assert.strictEqual(result.length, 1);
|
|
235
|
+
assert.strictEqual(result[0].rel, 'src/a.js');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('ignore excludes files', () => {
|
|
239
|
+
const files = makeFiles(['src/a.js', 'src/a.test.js', 'src/b.js']);
|
|
240
|
+
const cfg = { ...DEFAULT_CONFIG, ignore: ['**/*.test.js'] };
|
|
241
|
+
const result = filterFiles(files, cfg);
|
|
242
|
+
const rels = result.map(f => f.rel);
|
|
243
|
+
assert.ok(!rels.includes('src/a.test.js'));
|
|
244
|
+
assert.ok(rels.includes('src/a.js'));
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ── manifestChanged ──────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
console.log('\nmanifestChanged:');
|
|
250
|
+
|
|
251
|
+
test('null old manifest means changed', () => {
|
|
252
|
+
assert.strictEqual(manifestChanged(null, { 'a.js': { mtimeMs: 1, size: 100 } }), true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('identical manifests are not changed', () => {
|
|
256
|
+
const m = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
257
|
+
assert.strictEqual(manifestChanged(m, { ...m }), false);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('different mtime means changed', () => {
|
|
261
|
+
const old = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
262
|
+
const nw = { 'a.js': { mtimeMs: 2, size: 100 } };
|
|
263
|
+
assert.strictEqual(manifestChanged(old, nw), true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('new file means changed', () => {
|
|
267
|
+
const old = { 'a.js': { mtimeMs: 1, size: 100 } };
|
|
268
|
+
const nw = { 'a.js': { mtimeMs: 1, size: 100 }, 'b.js': { mtimeMs: 2, size: 50 } };
|
|
269
|
+
assert.strictEqual(manifestChanged(old, nw), true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ── parseArgs ────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
console.log('\nparseArgs:');
|
|
275
|
+
|
|
276
|
+
test('parses --key value pairs', () => {
|
|
277
|
+
const args = parseArgs(['node', 'script', '--path', '/tmp', '--interval', '30']);
|
|
278
|
+
assert.strictEqual(args.path, '/tmp');
|
|
279
|
+
assert.strictEqual(args.interval, '30');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('parses boolean flags', () => {
|
|
283
|
+
const args = parseArgs(['node', 'script', '--verbose']);
|
|
284
|
+
assert.strictEqual(args.verbose, true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('empty args returns empty object', () => {
|
|
288
|
+
const args = parseArgs(['node', 'script']);
|
|
289
|
+
assert.deepStrictEqual(args, {});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ── walkDir ──────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
console.log('\nwalkDir:');
|
|
295
|
+
|
|
296
|
+
test('discovers files recursively', () => {
|
|
297
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
|
|
298
|
+
try {
|
|
299
|
+
fs.mkdirSync(path.join(tmpDir, 'sub'), { recursive: true });
|
|
300
|
+
fs.writeFileSync(path.join(tmpDir, 'a.txt'), 'a');
|
|
301
|
+
fs.writeFileSync(path.join(tmpDir, 'sub', 'b.txt'), 'b');
|
|
302
|
+
const files = walkDir(tmpDir, tmpDir);
|
|
303
|
+
const rels = files.map(f => f.rel).sort();
|
|
304
|
+
assert.deepStrictEqual(rels, ['a.txt', 'sub/b.txt']);
|
|
305
|
+
} finally {
|
|
306
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('skips .git and node_modules', () => {
|
|
311
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-walk-'));
|
|
312
|
+
try {
|
|
313
|
+
fs.mkdirSync(path.join(tmpDir, '.git'), { recursive: true });
|
|
314
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules'), { recursive: true });
|
|
315
|
+
fs.writeFileSync(path.join(tmpDir, '.git', 'HEAD'), 'ref');
|
|
316
|
+
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'x.js'), 'x');
|
|
317
|
+
fs.writeFileSync(path.join(tmpDir, 'real.js'), 'y');
|
|
318
|
+
const files = walkDir(tmpDir, tmpDir);
|
|
319
|
+
assert.strictEqual(files.length, 1);
|
|
320
|
+
assert.strictEqual(files[0].rel, 'real.js');
|
|
321
|
+
} finally {
|
|
322
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
console.log(`\n${passed + failed} tests: \x1b[32m${passed} passed\x1b[0m` + (failed ? `, \x1b[31m${failed} failed\x1b[0m` : ''));
|
|
329
|
+
process.exit(failed > 0 ? 1 : 0);
|
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
|