cursor-guard 2.1.0 → 3.0.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.
@@ -1,238 +1,46 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const {
6
- color, loadConfig, gitAvailable, git, isGitRepo, gitDir, gitVersion,
7
- walkDir, matchesAny, diskFreeGB,
8
- } = require('./utils');
9
-
10
- function runDoctor(projectDir) {
11
- let pass = 0, warn = 0, fail = 0;
12
-
13
- function check(name, status, detail) {
14
- const tag = `[${status}]`;
15
- const line = detail ? ` ${tag} ${name} — ${detail}` : ` ${tag} ${name}`;
16
- switch (status) {
17
- case 'PASS': pass++; console.log(color.green(line)); break;
18
- case 'WARN': warn++; console.log(color.yellow(line)); break;
19
- case 'FAIL': fail++; console.log(color.red(line)); break;
20
- default: console.log(line);
21
- }
22
- }
23
-
24
- console.log('');
25
- console.log(color.cyan('=== Cursor Guard Doctor ==='));
26
- console.log(color.cyan(` Target: ${projectDir}`));
27
- console.log('');
28
-
29
- // 1. Git availability
30
- const hasGit = gitAvailable();
31
- if (hasGit) {
32
- check('Git installed', 'PASS', `version ${gitVersion()}`);
33
- } else {
34
- check('Git installed', 'WARN', 'git not found in PATH; only shadow strategy available');
35
- }
36
-
37
- // 2. Git repo status
38
- let repo = false;
39
- let gDir = null;
40
- let isWorktree = false;
41
- if (hasGit) {
42
- repo = isGitRepo(projectDir);
43
- if (repo) {
44
- gDir = gitDir(projectDir);
45
- try {
46
- const commonDir = git(['rev-parse', '--git-common-dir'], { cwd: projectDir, allowFail: true });
47
- const currentDir = git(['rev-parse', '--git-dir'], { cwd: projectDir, allowFail: true });
48
- isWorktree = commonDir && currentDir && commonDir !== currentDir;
49
- } catch { /* ignore */ }
50
- if (isWorktree) {
51
- check('Git repository', 'PASS', `worktree detected (git-dir: ${gDir})`);
52
- } else {
53
- check('Git repository', 'PASS', 'standard repo');
54
- }
55
- } else {
56
- check('Git repository', 'WARN', 'not a Git repo; git/both strategies won\'t work');
57
- }
58
- }
59
-
60
- // 3. Config file
61
- const { cfg, loaded, error } = loadConfig(projectDir);
62
- if (loaded) {
63
- check('Config file', 'PASS', '.cursor-guard.json found and valid JSON');
64
- } else if (error) {
65
- check('Config file', 'FAIL', `JSON parse error: ${error}`);
66
- } else {
67
- check('Config file', 'WARN', 'no .cursor-guard.json found; using defaults (protect everything)');
68
- }
69
-
70
- // 4. Strategy vs environment
71
- const strategy = cfg.backup_strategy;
72
- if (strategy === 'git' || strategy === 'both') {
73
- if (!repo) {
74
- check('Strategy compatibility', 'FAIL', `backup_strategy='${strategy}' but directory is not a Git repo`);
75
- } else {
76
- check('Strategy compatibility', 'PASS', `backup_strategy='${strategy}' and Git repo exists`);
77
- }
78
- } else if (strategy === 'shadow') {
79
- check('Strategy compatibility', 'PASS', "backup_strategy='shadow' — no Git required");
80
- } else {
81
- check('Strategy compatibility', 'FAIL', `unknown backup_strategy='${strategy}' (must be git/shadow/both)`);
82
- }
83
-
84
- // 5. Backup ref
85
- if (repo) {
86
- const guardRef = 'refs/guard/auto-backup';
87
- const legacyRef = 'refs/heads/cursor-guard/auto-backup';
88
- const exists = git(['rev-parse', '--verify', guardRef], { cwd: projectDir, allowFail: true });
89
- const legacyExists = git(['rev-parse', '--verify', legacyRef], { cwd: projectDir, allowFail: true });
90
- if (exists) {
91
- const count = git(['rev-list', '--count', guardRef], { cwd: projectDir, allowFail: true }) || '?';
92
- check('Backup ref', 'PASS', `refs/guard/auto-backup exists (${count} commits)`);
93
- } else if (legacyExists) {
94
- const count = git(['rev-list', '--count', legacyRef], { cwd: projectDir, allowFail: true }) || '?';
95
- check('Backup ref', 'WARN', `legacy refs/heads/cursor-guard/auto-backup found (${count} commits) — run auto-backup once to migrate`);
96
- } else {
97
- check('Backup ref', 'WARN', 'refs/guard/auto-backup not created yet (will be created on first backup)');
98
- }
99
- }
100
-
101
- // 6. Guard refs
102
- if (repo) {
103
- const refs = git(['for-each-ref', 'refs/guard/', '--format=%(refname)'], { cwd: projectDir, allowFail: true });
104
- if (refs) {
105
- const refList = refs.split('\n').filter(Boolean);
106
- const preRestoreCount = refList.filter(r => r.includes('pre-restore/')).length;
107
- check('Guard refs', 'PASS', `${refList.length} ref(s) found (${preRestoreCount} pre-restore snapshots)`);
108
- } else {
109
- check('Guard refs', 'WARN', 'no guard refs yet (created on first snapshot or restore)');
110
- }
111
- }
112
-
113
- // 7. Shadow copy directory
114
- const backupDir = path.join(projectDir, '.cursor-guard-backup');
115
- if (fs.existsSync(backupDir)) {
116
- let snapCount = 0;
117
- let totalBytes = 0;
118
- try {
119
- const dirs = fs.readdirSync(backupDir, { withFileTypes: true })
120
- .filter(d => d.isDirectory() && (/^\d{8}_\d{6}$/.test(d.name) || d.name.startsWith('pre-restore-')));
121
- snapCount = dirs.length;
122
- } catch { /* ignore */ }
123
- try {
124
- const allFiles = walkDir(backupDir, backupDir);
125
- for (const f of allFiles) {
126
- try { totalBytes += fs.statSync(f.full).size; } catch { /* skip */ }
127
- }
128
- } catch { /* ignore */ }
129
- const totalMB = (totalBytes / (1024 * 1024)).toFixed(1);
130
- check('Shadow copies', 'PASS', `${snapCount} snapshot(s), ${totalMB} MB total`);
131
- } else {
132
- check('Shadow copies', 'WARN', '.cursor-guard-backup/ not found (will be created on first shadow backup)');
133
- }
134
-
135
- // 8. .gitignore / exclude coverage
136
- if (repo) {
137
- const ignored = git(['check-ignore', '.cursor-guard-backup/test'], { cwd: projectDir, allowFail: true });
138
- if (ignored) {
139
- check('Backup dir ignored', 'PASS', '.cursor-guard-backup/ is git-ignored');
140
- } else {
141
- check('Backup dir ignored', 'WARN', '.cursor-guard-backup/ may NOT be git-ignored — backup changes could trigger commits');
142
- }
143
- }
144
-
145
- // 9. Config field validation
146
- if (loaded) {
147
- const validStrategies = ['git', 'shadow', 'both'];
148
- if (cfg.backup_strategy && !validStrategies.includes(cfg.backup_strategy)) {
149
- check('Config: backup_strategy', 'FAIL', `invalid value '${cfg.backup_strategy}'`);
150
- }
151
- const validPreRestore = ['always', 'ask', 'never'];
152
- if (cfg.pre_restore_backup && !validPreRestore.includes(cfg.pre_restore_backup)) {
153
- check('Config: pre_restore_backup', 'FAIL', `invalid value '${cfg.pre_restore_backup}'`);
154
- } else if (cfg.pre_restore_backup === 'never') {
155
- check('Config: pre_restore_backup', 'WARN', "set to 'never' — restores won't auto-preserve current version");
156
- }
157
- if (cfg.auto_backup_interval_seconds && cfg.auto_backup_interval_seconds < 5) {
158
- check('Config: interval', 'WARN', `${cfg.auto_backup_interval_seconds}s is below minimum (5s), will be clamped`);
159
- }
160
- if (cfg.retention && cfg.retention.mode) {
161
- const validModes = ['days', 'count', 'size'];
162
- if (!validModes.includes(cfg.retention.mode)) {
163
- check('Config: retention.mode', 'FAIL', `invalid value '${cfg.retention.mode}'`);
164
- }
165
- }
166
- if (cfg.git_retention && cfg.git_retention.mode) {
167
- const validGitModes = ['days', 'count'];
168
- if (!validGitModes.includes(cfg.git_retention.mode)) {
169
- check('Config: git_retention.mode', 'FAIL', `invalid value '${cfg.git_retention.mode}'`);
170
- }
171
- }
172
- }
173
-
174
- // 10. Protect / Ignore effectiveness
175
- if (loaded && cfg.protect.length > 0) {
176
- const allFiles = walkDir(projectDir, projectDir);
177
- let protectedCount = 0;
178
- for (const f of allFiles) {
179
- if (matchesAny(cfg.protect, f.rel)) protectedCount++;
180
- }
181
- check('Protect patterns', 'PASS', `${protectedCount} / ${allFiles.length} files matched by protect patterns`);
182
- }
183
-
184
- // 11. Disk space
185
- const freeGB = diskFreeGB(projectDir);
186
- if (freeGB !== null) {
187
- const rounded = freeGB.toFixed(1);
188
- if (freeGB < 1) {
189
- check('Disk space', 'FAIL', `${rounded} GB free — critically low`);
190
- } else if (freeGB < 5) {
191
- check('Disk space', 'WARN', `${rounded} GB free`);
192
- } else {
193
- check('Disk space', 'PASS', `${rounded} GB free`);
194
- }
195
- } else {
196
- check('Disk space', 'WARN', 'could not determine free space');
197
- }
198
-
199
- // 12. Lock file
200
- const lockFile = gDir
201
- ? path.join(gDir, 'cursor-guard.lock')
202
- : path.join(backupDir, 'cursor-guard.lock');
203
- if (fs.existsSync(lockFile)) {
204
- let content = '';
205
- try { content = fs.readFileSync(lockFile, 'utf-8').trim(); } catch { /* ignore */ }
206
- check('Lock file', 'WARN', `lock file exists — another instance may be running. ${content}`);
207
- } else {
208
- check('Lock file', 'PASS', 'no lock file (no running instance)');
209
- }
210
-
211
- // 13. Node.js version
212
- const nodeVer = process.version;
213
- const major = parseInt(nodeVer.slice(1), 10);
214
- if (major >= 18) {
215
- check('Node.js', 'PASS', `${nodeVer}`);
216
- } else {
217
- check('Node.js', 'WARN', `${nodeVer} — recommended >=18`);
218
- }
219
-
220
- // Summary
221
- console.log('');
222
- console.log(color.cyan('=== Summary ==='));
223
- const summaryColor = fail > 0 ? 'red' : warn > 0 ? 'yellow' : 'green';
224
- console.log(color[summaryColor](` PASS: ${pass} | WARN: ${warn} | FAIL: ${fail}`));
225
- console.log('');
226
- if (fail > 0) {
227
- console.log(color.red(' Fix FAIL items before relying on Cursor Guard.'));
228
- } else if (warn > 0) {
229
- console.log(color.yellow(' Review WARN items to ensure everything works as expected.'));
230
- } else {
231
- console.log(color.green(' All checks passed. Cursor Guard is ready.'));
232
- }
233
- console.log('');
234
-
235
- return fail > 0 ? 1 : 0;
236
- }
237
-
238
- module.exports = { runDoctor };
1
+ 'use strict';
2
+
3
+ const { color } = require('./utils');
4
+ const { runDiagnostics } = require('./core/doctor');
5
+
6
+ /**
7
+ * Run doctor checks and print formatted output to console.
8
+ * Thin CLI wrapper over core/doctor.js.
9
+ */
10
+ function runDoctor(projectDir) {
11
+ const { checks, summary } = runDiagnostics(projectDir);
12
+
13
+ console.log('');
14
+ console.log(color.cyan('=== Cursor Guard Doctor ==='));
15
+ console.log(color.cyan(` Target: ${projectDir}`));
16
+ console.log('');
17
+
18
+ for (const c of checks) {
19
+ const tag = `[${c.status}]`;
20
+ const line = c.detail ? ` ${tag} ${c.name} — ${c.detail}` : ` ${tag} ${c.name}`;
21
+ switch (c.status) {
22
+ case 'PASS': console.log(color.green(line)); break;
23
+ case 'WARN': console.log(color.yellow(line)); break;
24
+ case 'FAIL': console.log(color.red(line)); break;
25
+ default: console.log(line);
26
+ }
27
+ }
28
+
29
+ console.log('');
30
+ console.log(color.cyan('=== Summary ==='));
31
+ const summaryColor = summary.fail > 0 ? 'red' : summary.warn > 0 ? 'yellow' : 'green';
32
+ console.log(color[summaryColor](` PASS: ${summary.pass} | WARN: ${summary.warn} | FAIL: ${summary.fail}`));
33
+ console.log('');
34
+ if (summary.fail > 0) {
35
+ console.log(color.red(' Fix FAIL items before relying on Cursor Guard.'));
36
+ } else if (summary.warn > 0) {
37
+ console.log(color.yellow(' Review WARN items to ensure everything works as expected.'));
38
+ } else {
39
+ console.log(color.green(' All checks passed. Cursor Guard is ready.'));
40
+ }
41
+ console.log('');
42
+
43
+ return summary.fail > 0 ? 1 : 0;
44
+ }
45
+
46
+ module.exports = { runDoctor };