cursor-guard 2.0.2 → 2.0.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursor-guard",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "Protects code from accidental AI overwrite or deletion in Cursor IDE — mandatory pre-write snapshots, review-before-apply, local Git safety net, and deterministic recovery. | 保护代码免受 Cursor AI 代理意外覆写或删除——强制写前快照、预览再执行、本地 Git 安全网、确定性恢复。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cursor",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": ">=18"
|
|
25
25
|
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node references/lib/utils.test.js"
|
|
28
|
+
},
|
|
26
29
|
"bin": {
|
|
27
30
|
"cursor-guard-backup": "references/bin/cursor-guard-backup.js",
|
|
28
31
|
"cursor-guard-doctor": "references/bin/cursor-guard-doctor.js"
|
|
@@ -32,7 +35,8 @@
|
|
|
32
35
|
"README.md",
|
|
33
36
|
"README.zh-CN.md",
|
|
34
37
|
"LICENSE",
|
|
35
|
-
"references/"
|
|
38
|
+
"references/",
|
|
39
|
+
"!references/lib/utils.test.js"
|
|
36
40
|
],
|
|
37
41
|
"main": "SKILL.md"
|
|
38
42
|
}
|
|
@@ -5,9 +5,26 @@ const path = require('path');
|
|
|
5
5
|
const { parseArgs } = require('../lib/utils');
|
|
6
6
|
|
|
7
7
|
const args = parseArgs(process.argv);
|
|
8
|
+
|
|
9
|
+
if (args.help || args.h) {
|
|
10
|
+
console.log(`Usage: cursor-guard-backup [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--path <dir> Project directory to watch (default: current dir)
|
|
14
|
+
--interval <sec> Override backup interval in seconds
|
|
15
|
+
--help, -h Show this help message
|
|
16
|
+
--version, -v Show version number`);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (args.version || args.v) {
|
|
21
|
+
const pkg = require('../../package.json');
|
|
22
|
+
console.log(pkg.version);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
const targetPath = args.path || '.';
|
|
9
27
|
const interval = parseInt(args.interval, 10) || 0;
|
|
10
|
-
|
|
11
28
|
const resolved = path.resolve(targetPath);
|
|
12
29
|
|
|
13
30
|
const { runBackup } = require('../lib/auto-backup');
|
|
@@ -5,6 +5,23 @@ const path = require('path');
|
|
|
5
5
|
const { parseArgs } = require('../lib/utils');
|
|
6
6
|
|
|
7
7
|
const args = parseArgs(process.argv);
|
|
8
|
+
|
|
9
|
+
if (args.help || args.h) {
|
|
10
|
+
console.log(`Usage: cursor-guard-doctor [options]
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--path <dir> Project directory to check (default: current dir)
|
|
14
|
+
--help, -h Show this help message
|
|
15
|
+
--version, -v Show version number`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (args.version || args.v) {
|
|
20
|
+
const pkg = require('../../package.json');
|
|
21
|
+
console.log(pkg.version);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
const targetPath = args.path || '.';
|
|
9
26
|
const resolved = path.resolve(targetPath);
|
|
10
27
|
|
|
@@ -312,8 +312,6 @@ function isProcessAlive(pid) {
|
|
|
312
312
|
// ── Main ────────────────────────────────────────────────────────
|
|
313
313
|
|
|
314
314
|
async function runBackup(projectDir, intervalOverride) {
|
|
315
|
-
process.chdir(projectDir);
|
|
316
|
-
|
|
317
315
|
const hasGit = gitAvailable();
|
|
318
316
|
const repo = hasGit && isGitRepo(projectDir);
|
|
319
317
|
const gDir = repo ? getGitDir(projectDir) : null;
|
|
@@ -326,7 +324,7 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
326
324
|
const guardIndex = gDir ? path.join(gDir, 'cursor-guard-index') : null;
|
|
327
325
|
|
|
328
326
|
// Load config
|
|
329
|
-
let { cfg, loaded, error } = loadConfig(projectDir);
|
|
327
|
+
let { cfg, loaded, error, warnings } = loadConfig(projectDir);
|
|
330
328
|
let interval = intervalOverride || cfg.auto_backup_interval_seconds || 60;
|
|
331
329
|
if (interval < 5) interval = 5;
|
|
332
330
|
let cfgMtime = 0;
|
|
@@ -337,6 +335,9 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
337
335
|
console.log(color.yellow(`[guard] WARNING: .cursor-guard.json parse error — using defaults. ${error}`));
|
|
338
336
|
} else if (loaded) {
|
|
339
337
|
console.log(color.cyan(`[guard] Config loaded protect=${cfg.protect.length} ignore=${cfg.ignore.length} strategy=${cfg.backup_strategy} git_retention=${cfg.git_retention.enabled ? 'on' : 'off'}`));
|
|
338
|
+
if (warnings && warnings.length > 0) {
|
|
339
|
+
for (const w of warnings) console.log(color.yellow(`[guard] WARNING: ${w}`));
|
|
340
|
+
}
|
|
340
341
|
}
|
|
341
342
|
|
|
342
343
|
// Strategy check
|
|
@@ -420,6 +421,16 @@ async function runBackup(projectDir, intervalOverride) {
|
|
|
420
421
|
|
|
421
422
|
const logger = createLogger(logFilePath);
|
|
422
423
|
|
|
424
|
+
// Global error handlers
|
|
425
|
+
process.on('uncaughtException', (err) => {
|
|
426
|
+
logger.error(`Uncaught exception: ${err.message}`);
|
|
427
|
+
cleanup();
|
|
428
|
+
process.exit(1);
|
|
429
|
+
});
|
|
430
|
+
process.on('unhandledRejection', (reason) => {
|
|
431
|
+
logger.error(`Unhandled rejection: ${reason}`);
|
|
432
|
+
});
|
|
433
|
+
|
|
423
434
|
// Banner
|
|
424
435
|
console.log('');
|
|
425
436
|
console.log(color.cyan(`[guard] Watching '${projectDir}' every ${interval}s (Ctrl+C to stop)`));
|
package/references/lib/utils.js
CHANGED
|
@@ -52,18 +52,22 @@ const ALWAYS_SKIP = /[/\\](\.git|\.cursor-guard-backup|node_modules)[/\\]/;
|
|
|
52
52
|
|
|
53
53
|
function walkDir(dir, rootDir) {
|
|
54
54
|
const results = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
const stack = [dir];
|
|
56
|
+
while (stack.length > 0) {
|
|
57
|
+
const current = stack.pop();
|
|
58
|
+
let entries;
|
|
59
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); }
|
|
60
|
+
catch { continue; }
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const full = path.join(current, entry.name);
|
|
63
|
+
const rel = path.relative(rootDir, full).replace(/\\/g, '/');
|
|
64
|
+
if (ALWAYS_SKIP.test('/' + rel + '/')) continue;
|
|
65
|
+
if (entry.isSymbolicLink()) continue;
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
stack.push(full);
|
|
68
|
+
} else if (entry.isFile()) {
|
|
69
|
+
results.push({ full, rel, name: entry.name });
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
return results;
|
|
@@ -73,6 +77,11 @@ function walkDir(dir, rootDir) {
|
|
|
73
77
|
|
|
74
78
|
const DEFAULT_SECRETS = ['.env', '.env.*', '*.key', '*.pem', '*.p12', '*.pfx', 'credentials*'];
|
|
75
79
|
|
|
80
|
+
const VALID_STRATEGIES = ['git', 'shadow', 'both'];
|
|
81
|
+
const VALID_PRE_RESTORE = ['always', 'ask', 'never'];
|
|
82
|
+
const VALID_RETENTION_MODES = ['days', 'count', 'size'];
|
|
83
|
+
const VALID_GIT_RETENTION_MODES = ['days', 'count'];
|
|
84
|
+
|
|
76
85
|
const DEFAULT_CONFIG = {
|
|
77
86
|
protect: [],
|
|
78
87
|
ignore: [],
|
|
@@ -101,22 +110,47 @@ function loadConfig(projectDir) {
|
|
|
101
110
|
const merged = [...new Set([...cfg.secrets_patterns, ...raw.secrets_patterns_extra])];
|
|
102
111
|
cfg.secrets_patterns = merged;
|
|
103
112
|
}
|
|
104
|
-
|
|
113
|
+
const warnings = [];
|
|
114
|
+
if (typeof raw.backup_strategy === 'string') {
|
|
115
|
+
if (VALID_STRATEGIES.includes(raw.backup_strategy)) {
|
|
116
|
+
cfg.backup_strategy = raw.backup_strategy;
|
|
117
|
+
} else {
|
|
118
|
+
warnings.push(`Unknown backup_strategy "${raw.backup_strategy}", using default "${cfg.backup_strategy}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
105
121
|
if (typeof raw.auto_backup_interval_seconds === 'number') cfg.auto_backup_interval_seconds = raw.auto_backup_interval_seconds;
|
|
106
|
-
if (typeof raw.pre_restore_backup === 'string')
|
|
122
|
+
if (typeof raw.pre_restore_backup === 'string') {
|
|
123
|
+
if (VALID_PRE_RESTORE.includes(raw.pre_restore_backup)) {
|
|
124
|
+
cfg.pre_restore_backup = raw.pre_restore_backup;
|
|
125
|
+
} else {
|
|
126
|
+
warnings.push(`Unknown pre_restore_backup "${raw.pre_restore_backup}", using default "${cfg.pre_restore_backup}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
107
129
|
if (raw.retention) {
|
|
108
|
-
if (raw.retention.mode)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
if (raw.retention.mode) {
|
|
131
|
+
if (VALID_RETENTION_MODES.includes(raw.retention.mode)) {
|
|
132
|
+
cfg.retention.mode = raw.retention.mode;
|
|
133
|
+
} else {
|
|
134
|
+
warnings.push(`Unknown retention.mode "${raw.retention.mode}", using default "${cfg.retention.mode}"`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (typeof raw.retention.days === 'number') cfg.retention.days = raw.retention.days;
|
|
138
|
+
if (typeof raw.retention.max_count === 'number') cfg.retention.max_count = raw.retention.max_count;
|
|
139
|
+
if (typeof raw.retention.max_size_mb === 'number') cfg.retention.max_size_mb = raw.retention.max_size_mb;
|
|
112
140
|
}
|
|
113
141
|
if (raw.git_retention) {
|
|
114
142
|
if (raw.git_retention.enabled === true) cfg.git_retention.enabled = true;
|
|
115
|
-
if (raw.git_retention.mode)
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
if (raw.git_retention.mode) {
|
|
144
|
+
if (VALID_GIT_RETENTION_MODES.includes(raw.git_retention.mode)) {
|
|
145
|
+
cfg.git_retention.mode = raw.git_retention.mode;
|
|
146
|
+
} else {
|
|
147
|
+
warnings.push(`Unknown git_retention.mode "${raw.git_retention.mode}", using default "${cfg.git_retention.mode}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (typeof raw.git_retention.days === 'number') cfg.git_retention.days = raw.git_retention.days;
|
|
151
|
+
if (typeof raw.git_retention.max_count === 'number') cfg.git_retention.max_count = raw.git_retention.max_count;
|
|
118
152
|
}
|
|
119
|
-
return { cfg, loaded: true, error: null };
|
|
153
|
+
return { cfg, loaded: true, error: null, warnings };
|
|
120
154
|
} catch (e) {
|
|
121
155
|
return { cfg, loaded: false, error: e.message };
|
|
122
156
|
}
|
|
@@ -249,11 +283,23 @@ function timestamp() {
|
|
|
249
283
|
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
250
284
|
}
|
|
251
285
|
|
|
252
|
-
function createLogger(logFilePath) {
|
|
286
|
+
function createLogger(logFilePath, maxSizeMB = 10) {
|
|
287
|
+
let writeCount = 0;
|
|
288
|
+
function rotateIfNeeded() {
|
|
289
|
+
if (++writeCount % 100 !== 0) return;
|
|
290
|
+
try {
|
|
291
|
+
const stat = fs.statSync(logFilePath);
|
|
292
|
+
if (stat.size > maxSizeMB * 1024 * 1024) {
|
|
293
|
+
const old = logFilePath + '.old';
|
|
294
|
+
try { fs.unlinkSync(old); } catch { /* ignore */ }
|
|
295
|
+
fs.renameSync(logFilePath, old);
|
|
296
|
+
}
|
|
297
|
+
} catch { /* ignore */ }
|
|
298
|
+
}
|
|
253
299
|
return {
|
|
254
300
|
log(msg, c = 'green') {
|
|
255
301
|
const line = `${timestamp()} ${msg}`;
|
|
256
|
-
try { fs.appendFileSync(logFilePath, line + '\n'); } catch { /* ignore */ }
|
|
302
|
+
try { fs.appendFileSync(logFilePath, line + '\n'); rotateIfNeeded(); } catch { /* ignore */ }
|
|
257
303
|
console.log(color[c] ? color[c](`[guard] ${line}`) : `[guard] ${line}`);
|
|
258
304
|
},
|
|
259
305
|
info(msg) { this.log(msg, 'cyan'); },
|
|
@@ -1,329 +0,0 @@
|
|
|
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);
|