cursor-guard 2.1.1 → 3.1.0
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 +63 -11
- package/README.zh-CN.md +345 -293
- package/ROADMAP.md +834 -0
- package/SKILL.md +617 -557
- package/package.json +14 -5
- package/references/config-reference.md +175 -175
- package/references/config-reference.zh-CN.md +175 -175
- package/references/cursor-guard.example.json +0 -6
- package/references/lib/auto-backup.js +257 -530
- package/references/lib/core/backups.js +357 -0
- package/references/lib/core/core.test.js +859 -0
- package/references/lib/core/doctor-fix.js +237 -0
- package/references/lib/core/doctor.js +248 -0
- package/references/lib/core/restore.js +305 -0
- package/references/lib/core/snapshot.js +173 -0
- package/references/lib/core/status.js +163 -0
- package/references/lib/guard-doctor.js +46 -238
- package/references/lib/utils.js +371 -371
- package/references/mcp/mcp.test.js +279 -0
- package/references/mcp/server.js +198 -0
- package/references/quickstart.zh-CN.md +342 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const {
|
|
7
|
+
loadConfig, gitAvailable, git, isGitRepo, gitDir,
|
|
8
|
+
} = require('../utils');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Auto-fix common configuration and environment issues detected by doctor.
|
|
12
|
+
*
|
|
13
|
+
* Each fix is idempotent — running it when nothing is wrong is a no-op.
|
|
14
|
+
* Returns a list of actions taken (or skipped).
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectDir
|
|
17
|
+
* @param {object} [opts]
|
|
18
|
+
* @param {boolean} [opts.dryRun=false] - If true, report what would be fixed without modifying anything.
|
|
19
|
+
* @returns {{ actions: Array<{name: string, status: 'fixed'|'skipped'|'error', detail: string}>, totalFixed: number }}
|
|
20
|
+
*/
|
|
21
|
+
function runFixes(projectDir, opts = {}) {
|
|
22
|
+
const dryRun = !!opts.dryRun;
|
|
23
|
+
const actions = [];
|
|
24
|
+
let totalFixed = 0;
|
|
25
|
+
|
|
26
|
+
function action(name, status, detail) {
|
|
27
|
+
actions.push({ name, status, detail });
|
|
28
|
+
if (status === 'fixed') totalFixed++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hasGit = gitAvailable();
|
|
32
|
+
const repo = hasGit && isGitRepo(projectDir);
|
|
33
|
+
|
|
34
|
+
// Fix 1: Create .cursor-guard.json with defaults if missing
|
|
35
|
+
const configPath = path.join(projectDir, '.cursor-guard.json');
|
|
36
|
+
if (!fs.existsSync(configPath)) {
|
|
37
|
+
const examplePath = path.resolve(__dirname, '../../cursor-guard.example.json');
|
|
38
|
+
if (fs.existsSync(examplePath)) {
|
|
39
|
+
if (dryRun) {
|
|
40
|
+
action('Create config', 'skipped', 'would copy cursor-guard.example.json → .cursor-guard.json (dry-run)');
|
|
41
|
+
} else {
|
|
42
|
+
try {
|
|
43
|
+
fs.copyFileSync(examplePath, configPath);
|
|
44
|
+
action('Create config', 'fixed', 'copied cursor-guard.example.json → .cursor-guard.json');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
action('Create config', 'error', e.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
const defaultConfig = {
|
|
51
|
+
protect: [],
|
|
52
|
+
ignore: ["node_modules/**", "dist/**", "*.log"],
|
|
53
|
+
backup_strategy: "git",
|
|
54
|
+
auto_backup_interval_seconds: 60,
|
|
55
|
+
pre_restore_backup: "always",
|
|
56
|
+
retention: { mode: "days", days: 30 },
|
|
57
|
+
};
|
|
58
|
+
if (dryRun) {
|
|
59
|
+
action('Create config', 'skipped', 'would create .cursor-guard.json with defaults (dry-run)');
|
|
60
|
+
} else {
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
63
|
+
action('Create config', 'fixed', 'created .cursor-guard.json with defaults');
|
|
64
|
+
} catch (e) {
|
|
65
|
+
action('Create config', 'error', e.message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
action('Create config', 'skipped', '.cursor-guard.json already exists');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Fix 2: Init git repo if missing
|
|
74
|
+
if (hasGit && !repo) {
|
|
75
|
+
if (dryRun) {
|
|
76
|
+
action('Init Git repo', 'skipped', 'would run git init (dry-run)');
|
|
77
|
+
} else {
|
|
78
|
+
try {
|
|
79
|
+
execFileSync('git', ['init'], { cwd: projectDir, stdio: 'pipe' });
|
|
80
|
+
// Ensure git commit works even without global user config
|
|
81
|
+
try { execFileSync('git', ['config', 'user.email'], { cwd: projectDir, stdio: 'pipe' }); }
|
|
82
|
+
catch { execFileSync('git', ['config', 'user.email', 'cursor-guard@local'], { cwd: projectDir, stdio: 'pipe' }); }
|
|
83
|
+
try { execFileSync('git', ['config', 'user.name'], { cwd: projectDir, stdio: 'pipe' }); }
|
|
84
|
+
catch { execFileSync('git', ['config', 'user.name', 'cursor-guard'], { cwd: projectDir, stdio: 'pipe' }); }
|
|
85
|
+
// Ensure .gitignore contains backup dir + secrets patterns BEFORE git add
|
|
86
|
+
const { cfg: initCfg } = loadConfig(projectDir);
|
|
87
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
88
|
+
const existingIgnore = fs.existsSync(gitignorePath)
|
|
89
|
+
? fs.readFileSync(gitignorePath, 'utf-8')
|
|
90
|
+
: '';
|
|
91
|
+
const missingPatterns = [];
|
|
92
|
+
if (!existingIgnore.includes('.cursor-guard-backup')) {
|
|
93
|
+
missingPatterns.push('# cursor-guard shadow copies', '.cursor-guard-backup/', '');
|
|
94
|
+
}
|
|
95
|
+
const missingSecrets = initCfg.secrets_patterns.filter(p => !existingIgnore.includes(p));
|
|
96
|
+
if (missingSecrets.length > 0) {
|
|
97
|
+
missingPatterns.push('# Secrets (cursor-guard defaults)', ...missingSecrets, '');
|
|
98
|
+
}
|
|
99
|
+
if (missingPatterns.length > 0) {
|
|
100
|
+
const separator = existingIgnore && !existingIgnore.endsWith('\n') ? '\n' : '';
|
|
101
|
+
const block = separator + missingPatterns.join('\n');
|
|
102
|
+
if (existingIgnore) {
|
|
103
|
+
fs.appendFileSync(gitignorePath, block);
|
|
104
|
+
} else {
|
|
105
|
+
fs.writeFileSync(gitignorePath, block);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
execFileSync('git', ['add', '-A'], { cwd: projectDir, stdio: 'pipe' });
|
|
109
|
+
execFileSync('git', ['commit', '-m', 'guard: initial snapshot', '--no-verify', '--allow-empty'], {
|
|
110
|
+
cwd: projectDir, stdio: 'pipe',
|
|
111
|
+
});
|
|
112
|
+
action('Init Git repo', 'fixed', 'initialized git repo with .gitignore and initial commit');
|
|
113
|
+
} catch (e) {
|
|
114
|
+
action('Init Git repo', 'error', e.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (repo) {
|
|
118
|
+
action('Init Git repo', 'skipped', 'already a git repo');
|
|
119
|
+
} else {
|
|
120
|
+
action('Init Git repo', 'skipped', 'git not available');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fix 3: Add .cursor-guard-backup/ to .gitignore
|
|
124
|
+
const effectiveRepo = hasGit && isGitRepo(projectDir);
|
|
125
|
+
if (effectiveRepo) {
|
|
126
|
+
const ignored = git(['check-ignore', '.cursor-guard-backup/test'], { cwd: projectDir, allowFail: true });
|
|
127
|
+
if (!ignored) {
|
|
128
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
129
|
+
const entry = '\n# cursor-guard shadow copies\n.cursor-guard-backup/\n';
|
|
130
|
+
if (dryRun) {
|
|
131
|
+
action('Gitignore backup dir', 'skipped', 'would add .cursor-guard-backup/ to .gitignore (dry-run)');
|
|
132
|
+
} else {
|
|
133
|
+
try {
|
|
134
|
+
if (fs.existsSync(gitignorePath)) {
|
|
135
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
136
|
+
if (!content.includes('.cursor-guard-backup')) {
|
|
137
|
+
fs.appendFileSync(gitignorePath, entry);
|
|
138
|
+
action('Gitignore backup dir', 'fixed', 'appended .cursor-guard-backup/ to .gitignore');
|
|
139
|
+
} else {
|
|
140
|
+
action('Gitignore backup dir', 'skipped', 'entry exists but git check-ignore failed — may need manual fix');
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
fs.writeFileSync(gitignorePath, entry.trimStart());
|
|
144
|
+
action('Gitignore backup dir', 'fixed', 'created .gitignore with .cursor-guard-backup/ entry');
|
|
145
|
+
}
|
|
146
|
+
} catch (e) {
|
|
147
|
+
action('Gitignore backup dir', 'error', e.message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
action('Gitignore backup dir', 'skipped', '.cursor-guard-backup/ already git-ignored');
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
action('Gitignore backup dir', 'skipped', 'not a git repo');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fix 4: Create .cursor-guard-backup/ directory if shadow strategy
|
|
158
|
+
const { cfg } = loadConfig(projectDir);
|
|
159
|
+
if (cfg.backup_strategy === 'shadow' || cfg.backup_strategy === 'both') {
|
|
160
|
+
const backupDir = path.join(projectDir, '.cursor-guard-backup');
|
|
161
|
+
if (!fs.existsSync(backupDir)) {
|
|
162
|
+
if (dryRun) {
|
|
163
|
+
action('Create backup dir', 'skipped', 'would create .cursor-guard-backup/ (dry-run)');
|
|
164
|
+
} else {
|
|
165
|
+
try {
|
|
166
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
167
|
+
action('Create backup dir', 'fixed', 'created .cursor-guard-backup/');
|
|
168
|
+
} catch (e) {
|
|
169
|
+
action('Create backup dir', 'error', e.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
action('Create backup dir', 'skipped', '.cursor-guard-backup/ already exists');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Fix 5: Remove stale lock file
|
|
178
|
+
const gDir = effectiveRepo ? gitDir(projectDir) : null;
|
|
179
|
+
const lockFile = gDir
|
|
180
|
+
? path.join(gDir, 'cursor-guard.lock')
|
|
181
|
+
: path.join(projectDir, '.cursor-guard-backup', 'cursor-guard.lock');
|
|
182
|
+
if (fs.existsSync(lockFile)) {
|
|
183
|
+
let stale = false;
|
|
184
|
+
try {
|
|
185
|
+
const content = fs.readFileSync(lockFile, 'utf-8').trim();
|
|
186
|
+
const pidMatch = content.match(/pid[:\s]+(\d+)/i);
|
|
187
|
+
if (pidMatch) {
|
|
188
|
+
const pid = parseInt(pidMatch[1], 10);
|
|
189
|
+
try { process.kill(pid, 0); } catch { stale = true; }
|
|
190
|
+
} else {
|
|
191
|
+
const stat = fs.statSync(lockFile);
|
|
192
|
+
if (Date.now() - stat.mtimeMs > 300000) stale = true;
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
stale = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (stale) {
|
|
199
|
+
if (dryRun) {
|
|
200
|
+
action('Remove stale lock', 'skipped', 'would remove stale cursor-guard.lock (dry-run)');
|
|
201
|
+
} else {
|
|
202
|
+
try {
|
|
203
|
+
fs.unlinkSync(lockFile);
|
|
204
|
+
action('Remove stale lock', 'fixed', 'removed stale cursor-guard.lock');
|
|
205
|
+
} catch (e) {
|
|
206
|
+
action('Remove stale lock', 'error', e.message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
action('Remove stale lock', 'skipped', 'lock file is active — another instance is running');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Fix 6: Fix config strategy mismatch — if strategy is git/both but no repo
|
|
215
|
+
// (Already handled by Fix 2 above — this reports if the combo is still wrong)
|
|
216
|
+
const { cfg: freshCfg } = loadConfig(projectDir);
|
|
217
|
+
const freshRepo = hasGit && isGitRepo(projectDir);
|
|
218
|
+
if ((freshCfg.backup_strategy === 'git' || freshCfg.backup_strategy === 'both') && !freshRepo) {
|
|
219
|
+
if (dryRun) {
|
|
220
|
+
action('Fix strategy mismatch', 'skipped', "would switch backup_strategy to 'shadow' since no git repo (dry-run)");
|
|
221
|
+
} else {
|
|
222
|
+
try {
|
|
223
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
224
|
+
const parsed = JSON.parse(raw);
|
|
225
|
+
parsed.backup_strategy = 'shadow';
|
|
226
|
+
fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
227
|
+
action('Fix strategy mismatch', 'fixed', "changed backup_strategy to 'shadow' (no git repo available)");
|
|
228
|
+
} catch (e) {
|
|
229
|
+
action('Fix strategy mismatch', 'error', e.message);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { actions, totalFixed };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { runFixes };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
loadConfig, gitAvailable, git, isGitRepo, gitDir, gitVersion,
|
|
7
|
+
walkDir, matchesAny, diskFreeGB,
|
|
8
|
+
} = require('../utils');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Run all diagnostic checks for a project directory.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} projectDir - Absolute path to the project root.
|
|
14
|
+
* @returns {{ checks: Array<{name: string, status: 'PASS'|'WARN'|'FAIL', detail?: string}>, summary: {pass: number, warn: number, fail: number} }}
|
|
15
|
+
*/
|
|
16
|
+
function runDiagnostics(projectDir) {
|
|
17
|
+
const checks = [];
|
|
18
|
+
|
|
19
|
+
function check(name, status, detail) {
|
|
20
|
+
checks.push({ name, status, detail: detail || null });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 1. Git availability
|
|
24
|
+
const hasGit = gitAvailable();
|
|
25
|
+
if (hasGit) {
|
|
26
|
+
check('Git installed', 'PASS', `version ${gitVersion()}`);
|
|
27
|
+
} else {
|
|
28
|
+
check('Git installed', 'WARN', 'git not found in PATH; only shadow strategy available');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 2. Git repo status
|
|
32
|
+
let repo = false;
|
|
33
|
+
let gDir = null;
|
|
34
|
+
let isWorktree = false;
|
|
35
|
+
if (hasGit) {
|
|
36
|
+
repo = isGitRepo(projectDir);
|
|
37
|
+
if (repo) {
|
|
38
|
+
gDir = gitDir(projectDir);
|
|
39
|
+
try {
|
|
40
|
+
const commonDir = git(['rev-parse', '--git-common-dir'], { cwd: projectDir, allowFail: true });
|
|
41
|
+
const currentDir = git(['rev-parse', '--git-dir'], { cwd: projectDir, allowFail: true });
|
|
42
|
+
isWorktree = commonDir && currentDir && commonDir !== currentDir;
|
|
43
|
+
} catch { /* ignore */ }
|
|
44
|
+
if (isWorktree) {
|
|
45
|
+
check('Git repository', 'PASS', `worktree detected (git-dir: ${gDir})`);
|
|
46
|
+
} else {
|
|
47
|
+
check('Git repository', 'PASS', 'standard repo');
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
check('Git repository', 'WARN', 'not a Git repo; git/both strategies won\'t work');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Config file
|
|
55
|
+
const { cfg, loaded, error } = loadConfig(projectDir);
|
|
56
|
+
if (loaded) {
|
|
57
|
+
check('Config file', 'PASS', '.cursor-guard.json found and valid JSON');
|
|
58
|
+
} else if (error) {
|
|
59
|
+
check('Config file', 'FAIL', `JSON parse error: ${error}`);
|
|
60
|
+
} else {
|
|
61
|
+
check('Config file', 'WARN', 'no .cursor-guard.json found; using defaults (protect everything)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. Strategy vs environment
|
|
65
|
+
const strategy = cfg.backup_strategy;
|
|
66
|
+
if (strategy === 'git' || strategy === 'both') {
|
|
67
|
+
if (!repo) {
|
|
68
|
+
check('Strategy compatibility', 'FAIL', `backup_strategy='${strategy}' but directory is not a Git repo`);
|
|
69
|
+
} else {
|
|
70
|
+
check('Strategy compatibility', 'PASS', `backup_strategy='${strategy}' and Git repo exists`);
|
|
71
|
+
}
|
|
72
|
+
} else if (strategy === 'shadow') {
|
|
73
|
+
check('Strategy compatibility', 'PASS', "backup_strategy='shadow' — no Git required");
|
|
74
|
+
} else {
|
|
75
|
+
check('Strategy compatibility', 'FAIL', `unknown backup_strategy='${strategy}' (must be git/shadow/both)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 5. Backup ref
|
|
79
|
+
if (repo) {
|
|
80
|
+
const guardRef = 'refs/guard/auto-backup';
|
|
81
|
+
const legacyRef = 'refs/heads/cursor-guard/auto-backup';
|
|
82
|
+
const exists = git(['rev-parse', '--verify', guardRef], { cwd: projectDir, allowFail: true });
|
|
83
|
+
const legacyExists = git(['rev-parse', '--verify', legacyRef], { cwd: projectDir, allowFail: true });
|
|
84
|
+
if (exists) {
|
|
85
|
+
const count = git(['rev-list', '--count', guardRef], { cwd: projectDir, allowFail: true }) || '?';
|
|
86
|
+
check('Backup ref', 'PASS', `refs/guard/auto-backup exists (${count} commits)`);
|
|
87
|
+
} else if (legacyExists) {
|
|
88
|
+
const count = git(['rev-list', '--count', legacyRef], { cwd: projectDir, allowFail: true }) || '?';
|
|
89
|
+
check('Backup ref', 'WARN', `legacy refs/heads/cursor-guard/auto-backup found (${count} commits) — run auto-backup once to migrate`);
|
|
90
|
+
} else {
|
|
91
|
+
check('Backup ref', 'WARN', 'refs/guard/auto-backup not created yet (will be created on first backup)');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 6. Guard refs
|
|
96
|
+
if (repo) {
|
|
97
|
+
const refs = git(['for-each-ref', 'refs/guard/', '--format=%(refname)'], { cwd: projectDir, allowFail: true });
|
|
98
|
+
if (refs) {
|
|
99
|
+
const refList = refs.split('\n').filter(Boolean);
|
|
100
|
+
const preRestoreCount = refList.filter(r => r.includes('pre-restore/')).length;
|
|
101
|
+
check('Guard refs', 'PASS', `${refList.length} ref(s) found (${preRestoreCount} pre-restore snapshots)`);
|
|
102
|
+
} else {
|
|
103
|
+
check('Guard refs', 'WARN', 'no guard refs yet (created on first snapshot or restore)');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 7. Shadow copy directory
|
|
108
|
+
const backupDir = path.join(projectDir, '.cursor-guard-backup');
|
|
109
|
+
if (fs.existsSync(backupDir)) {
|
|
110
|
+
let snapCount = 0;
|
|
111
|
+
let totalBytes = 0;
|
|
112
|
+
try {
|
|
113
|
+
const dirs = fs.readdirSync(backupDir, { withFileTypes: true })
|
|
114
|
+
.filter(d => d.isDirectory() && (/^\d{8}_\d{6}$/.test(d.name) || d.name.startsWith('pre-restore-')));
|
|
115
|
+
snapCount = dirs.length;
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
try {
|
|
118
|
+
const allFiles = walkDir(backupDir, backupDir);
|
|
119
|
+
for (const f of allFiles) {
|
|
120
|
+
try { totalBytes += fs.statSync(f.full).size; } catch { /* skip */ }
|
|
121
|
+
}
|
|
122
|
+
} catch { /* ignore */ }
|
|
123
|
+
const totalMB = (totalBytes / (1024 * 1024)).toFixed(1);
|
|
124
|
+
check('Shadow copies', 'PASS', `${snapCount} snapshot(s), ${totalMB} MB total`);
|
|
125
|
+
} else {
|
|
126
|
+
check('Shadow copies', 'WARN', '.cursor-guard-backup/ not found (will be created on first shadow backup)');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 8. .gitignore / exclude coverage
|
|
130
|
+
if (repo) {
|
|
131
|
+
const ignored = git(['check-ignore', '.cursor-guard-backup/test'], { cwd: projectDir, allowFail: true });
|
|
132
|
+
if (ignored) {
|
|
133
|
+
check('Backup dir ignored', 'PASS', '.cursor-guard-backup/ is git-ignored');
|
|
134
|
+
} else {
|
|
135
|
+
check('Backup dir ignored', 'WARN', '.cursor-guard-backup/ may NOT be git-ignored — backup changes could trigger commits');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 9. Config field validation
|
|
140
|
+
if (loaded) {
|
|
141
|
+
const validStrategies = ['git', 'shadow', 'both'];
|
|
142
|
+
if (cfg.backup_strategy && !validStrategies.includes(cfg.backup_strategy)) {
|
|
143
|
+
check('Config: backup_strategy', 'FAIL', `invalid value '${cfg.backup_strategy}'`);
|
|
144
|
+
}
|
|
145
|
+
const validPreRestore = ['always', 'ask', 'never'];
|
|
146
|
+
if (cfg.pre_restore_backup && !validPreRestore.includes(cfg.pre_restore_backup)) {
|
|
147
|
+
check('Config: pre_restore_backup', 'FAIL', `invalid value '${cfg.pre_restore_backup}'`);
|
|
148
|
+
} else if (cfg.pre_restore_backup === 'never') {
|
|
149
|
+
check('Config: pre_restore_backup', 'WARN', "set to 'never' — restores won't auto-preserve current version");
|
|
150
|
+
}
|
|
151
|
+
if (cfg.auto_backup_interval_seconds && cfg.auto_backup_interval_seconds < 5) {
|
|
152
|
+
check('Config: interval', 'WARN', `${cfg.auto_backup_interval_seconds}s is below minimum (5s), will be clamped`);
|
|
153
|
+
}
|
|
154
|
+
if (cfg.retention && cfg.retention.mode) {
|
|
155
|
+
const validModes = ['days', 'count', 'size'];
|
|
156
|
+
if (!validModes.includes(cfg.retention.mode)) {
|
|
157
|
+
check('Config: retention.mode', 'FAIL', `invalid value '${cfg.retention.mode}'`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (cfg.git_retention && cfg.git_retention.mode) {
|
|
161
|
+
const validGitModes = ['days', 'count'];
|
|
162
|
+
if (!validGitModes.includes(cfg.git_retention.mode)) {
|
|
163
|
+
check('Config: git_retention.mode', 'FAIL', `invalid value '${cfg.git_retention.mode}'`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 10. Protect / Ignore effectiveness
|
|
169
|
+
if (loaded && cfg.protect.length > 0) {
|
|
170
|
+
const allFiles = walkDir(projectDir, projectDir);
|
|
171
|
+
let protectedCount = 0;
|
|
172
|
+
for (const f of allFiles) {
|
|
173
|
+
if (matchesAny(cfg.protect, f.rel)) protectedCount++;
|
|
174
|
+
}
|
|
175
|
+
check('Protect patterns', 'PASS', `${protectedCount} / ${allFiles.length} files matched by protect patterns`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 11. Disk space
|
|
179
|
+
const freeGB = diskFreeGB(projectDir);
|
|
180
|
+
if (freeGB !== null) {
|
|
181
|
+
const rounded = freeGB.toFixed(1);
|
|
182
|
+
if (freeGB < 1) {
|
|
183
|
+
check('Disk space', 'FAIL', `${rounded} GB free — critically low`);
|
|
184
|
+
} else if (freeGB < 5) {
|
|
185
|
+
check('Disk space', 'WARN', `${rounded} GB free`);
|
|
186
|
+
} else {
|
|
187
|
+
check('Disk space', 'PASS', `${rounded} GB free`);
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
check('Disk space', 'WARN', 'could not determine free space');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 12. Lock file
|
|
194
|
+
const lockFile = gDir
|
|
195
|
+
? path.join(gDir, 'cursor-guard.lock')
|
|
196
|
+
: path.join(backupDir, 'cursor-guard.lock');
|
|
197
|
+
if (fs.existsSync(lockFile)) {
|
|
198
|
+
let content = '';
|
|
199
|
+
try { content = fs.readFileSync(lockFile, 'utf-8').trim(); } catch { /* ignore */ }
|
|
200
|
+
check('Lock file', 'WARN', `lock file exists — another instance may be running. ${content}`);
|
|
201
|
+
} else {
|
|
202
|
+
check('Lock file', 'PASS', 'no lock file (no running instance)');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 13. Node.js version
|
|
206
|
+
const nodeVer = process.version;
|
|
207
|
+
const major = parseInt(nodeVer.slice(1), 10);
|
|
208
|
+
if (major >= 18) {
|
|
209
|
+
check('Node.js', 'PASS', `${nodeVer}`);
|
|
210
|
+
} else {
|
|
211
|
+
check('Node.js', 'WARN', `${nodeVer} — recommended >=18`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 14. MCP server status
|
|
215
|
+
const mcpServerPath = path.resolve(__dirname, '../../mcp/server.js');
|
|
216
|
+
const mcpServerExists = fs.existsSync(mcpServerPath);
|
|
217
|
+
|
|
218
|
+
let mcpSdkAvailable = false;
|
|
219
|
+
let mcpSdkVersion = null;
|
|
220
|
+
try {
|
|
221
|
+
const mcpPkgPath = require.resolve('@modelcontextprotocol/sdk/package.json');
|
|
222
|
+
const mcpPkg = JSON.parse(fs.readFileSync(mcpPkgPath, 'utf-8'));
|
|
223
|
+
mcpSdkAvailable = true;
|
|
224
|
+
mcpSdkVersion = mcpPkg.version;
|
|
225
|
+
} catch { /* not installed */ }
|
|
226
|
+
|
|
227
|
+
if (mcpServerExists && mcpSdkAvailable) {
|
|
228
|
+
check('MCP server', 'PASS', `server.js found, SDK ${mcpSdkVersion}`);
|
|
229
|
+
} else if (mcpServerExists && !mcpSdkAvailable) {
|
|
230
|
+
check('MCP server', 'WARN', 'server.js found but @modelcontextprotocol/sdk not installed — run npm install');
|
|
231
|
+
} else if (!mcpServerExists && mcpSdkAvailable) {
|
|
232
|
+
check('MCP server', 'WARN', `SDK installed (${mcpSdkVersion}) but server.js not found at expected path`);
|
|
233
|
+
} else {
|
|
234
|
+
check('MCP server', 'WARN', 'MCP not configured (optional — cursor-guard works without it)');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Build summary
|
|
238
|
+
let pass = 0, warn = 0, fail = 0;
|
|
239
|
+
for (const c of checks) {
|
|
240
|
+
if (c.status === 'PASS') pass++;
|
|
241
|
+
else if (c.status === 'WARN') warn++;
|
|
242
|
+
else if (c.status === 'FAIL') fail++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { checks, summary: { pass, warn, fail } };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = { runDiagnostics };
|