create-sdd-project 0.5.0 → 0.6.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 +30 -0
- package/bin/cli.js +21 -0
- package/lib/doctor.js +492 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,6 +138,36 @@ npx create-sdd-project@latest --upgrade
|
|
|
138
138
|
|
|
139
139
|
For non-interactive upgrades (CI/scripts): `npx create-sdd-project@latest --upgrade --yes`
|
|
140
140
|
|
|
141
|
+
### Doctor (Diagnose Installation)
|
|
142
|
+
|
|
143
|
+
Check that your SDD installation is healthy:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
cd your-project
|
|
147
|
+
npx create-sdd-project --doctor
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
🩺 SDD DevFlow Doctor
|
|
152
|
+
|
|
153
|
+
✓ SDD installed (v0.5.0)
|
|
154
|
+
✓ Version up to date (0.5.0)
|
|
155
|
+
✓ AI tools: Claude Code + Gemini
|
|
156
|
+
✓ Top-level configs present (AGENTS.md, CLAUDE.md, GEMINI.md)
|
|
157
|
+
✓ Agents: 8/8 present
|
|
158
|
+
✓ Project type coherence: OK (fullstack)
|
|
159
|
+
✓ Cross-tool consistency: Claude and Gemini in sync
|
|
160
|
+
✓ Standards: 4/4 present
|
|
161
|
+
✓ Hooks: quick-scan.sh executable, jq installed, settings.json valid
|
|
162
|
+
✓ Project memory: 4/4 files present
|
|
163
|
+
|
|
164
|
+
Overall: HEALTHY
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**What it checks:** SDD files present, version, agents for your project type, no stray frontend agents in backend projects (and vice versa), Claude and Gemini agents in sync, standards files, hooks and dependencies (`jq`), settings.json integrity, project memory files.
|
|
168
|
+
|
|
169
|
+
Exit code 1 if errors found — useful for CI pipelines.
|
|
170
|
+
|
|
141
171
|
### After Setup
|
|
142
172
|
|
|
143
173
|
Open your project in Claude Code or Gemini and start building:
|
package/bin/cli.js
CHANGED
|
@@ -9,9 +9,13 @@ const projectName = args.find((a) => !a.startsWith('-'));
|
|
|
9
9
|
const useDefaults = args.includes('--yes') || args.includes('-y');
|
|
10
10
|
const isInit = args.includes('--init');
|
|
11
11
|
const isUpgrade = args.includes('--upgrade');
|
|
12
|
+
const isDoctor = args.includes('--doctor');
|
|
12
13
|
const isForce = args.includes('--force');
|
|
13
14
|
|
|
14
15
|
async function main() {
|
|
16
|
+
if (isDoctor) {
|
|
17
|
+
return runDoctorCmd();
|
|
18
|
+
}
|
|
15
19
|
if (isUpgrade) {
|
|
16
20
|
return runUpgrade();
|
|
17
21
|
}
|
|
@@ -21,6 +25,23 @@ async function main() {
|
|
|
21
25
|
return runCreate();
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function runDoctorCmd() {
|
|
29
|
+
const { runDoctor, printResults } = require('../lib/doctor');
|
|
30
|
+
|
|
31
|
+
const cwd = process.cwd();
|
|
32
|
+
|
|
33
|
+
// Validate: must be in an existing project
|
|
34
|
+
if (!fs.existsSync(path.join(cwd, 'package.json'))) {
|
|
35
|
+
console.error('Error: No package.json found in current directory.');
|
|
36
|
+
console.error('The --doctor flag must be run from inside an existing project.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const results = runDoctor(cwd);
|
|
41
|
+
const exitCode = printResults(results);
|
|
42
|
+
process.exit(exitCode);
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
async function runCreate() {
|
|
25
46
|
const { runWizard, buildDefaultConfig } = require('../lib/wizard');
|
|
26
47
|
const { generate } = require('../lib/generator');
|
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const {
|
|
7
|
+
FRONTEND_AGENTS,
|
|
8
|
+
BACKEND_AGENTS,
|
|
9
|
+
TEMPLATE_AGENTS,
|
|
10
|
+
} = require('./config');
|
|
11
|
+
const {
|
|
12
|
+
readInstalledVersion,
|
|
13
|
+
getPackageVersion,
|
|
14
|
+
detectAiTools,
|
|
15
|
+
detectProjectType,
|
|
16
|
+
} = require('./upgrade-generator');
|
|
17
|
+
|
|
18
|
+
const PASS = 'pass';
|
|
19
|
+
const WARN = 'warn';
|
|
20
|
+
const FAIL = 'fail';
|
|
21
|
+
|
|
22
|
+
const EXPECTED_MEMORY_FILES = [
|
|
23
|
+
'product-tracker.md',
|
|
24
|
+
'bugs.md',
|
|
25
|
+
'decisions.md',
|
|
26
|
+
'key_facts.md',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Run all doctor checks and return results.
|
|
31
|
+
*/
|
|
32
|
+
function runDoctor(cwd) {
|
|
33
|
+
const results = [];
|
|
34
|
+
|
|
35
|
+
// 1. SDD Installed
|
|
36
|
+
results.push(checkInstalled(cwd));
|
|
37
|
+
|
|
38
|
+
// 2. Version
|
|
39
|
+
results.push(checkVersion(cwd));
|
|
40
|
+
|
|
41
|
+
// Detect config for subsequent checks
|
|
42
|
+
const aiTools = detectAiTools(cwd);
|
|
43
|
+
const projectType = detectProjectType(cwd);
|
|
44
|
+
|
|
45
|
+
// 3. AI Tool Config
|
|
46
|
+
results.push(checkAiToolConfig(cwd, aiTools));
|
|
47
|
+
|
|
48
|
+
// 4. Top-level Configs
|
|
49
|
+
results.push(checkTopLevelConfigs(cwd, aiTools));
|
|
50
|
+
|
|
51
|
+
// 5. Agents Complete
|
|
52
|
+
results.push(checkAgentsComplete(cwd, aiTools, projectType));
|
|
53
|
+
|
|
54
|
+
// 6. Project Type Coherence
|
|
55
|
+
results.push(checkProjectTypeCoherence(cwd, aiTools, projectType));
|
|
56
|
+
|
|
57
|
+
// 7. Cross-tool Consistency
|
|
58
|
+
results.push(checkCrossToolConsistency(cwd, aiTools));
|
|
59
|
+
|
|
60
|
+
// 8. Standards Present
|
|
61
|
+
results.push(checkStandards(cwd, projectType));
|
|
62
|
+
|
|
63
|
+
// 9. Hooks & Dependencies
|
|
64
|
+
results.push(checkHooksAndDeps(cwd, aiTools));
|
|
65
|
+
|
|
66
|
+
// 10. Project Memory
|
|
67
|
+
results.push(checkProjectMemory(cwd));
|
|
68
|
+
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Print doctor results and return exit code.
|
|
74
|
+
*/
|
|
75
|
+
function printResults(results) {
|
|
76
|
+
console.log('\n🩺 SDD DevFlow Doctor\n');
|
|
77
|
+
|
|
78
|
+
for (const r of results) {
|
|
79
|
+
const icon = r.status === PASS ? '✓' : r.status === WARN ? '⚠' : '✗';
|
|
80
|
+
console.log(` ${icon} ${r.message}`);
|
|
81
|
+
if (r.details && r.details.length > 0) {
|
|
82
|
+
for (const d of r.details) {
|
|
83
|
+
console.log(` ${d}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fails = results.filter((r) => r.status === FAIL).length;
|
|
89
|
+
const warns = results.filter((r) => r.status === WARN).length;
|
|
90
|
+
|
|
91
|
+
let overall;
|
|
92
|
+
if (fails > 0) {
|
|
93
|
+
overall = 'UNHEALTHY';
|
|
94
|
+
} else if (warns > 0) {
|
|
95
|
+
overall = 'NEEDS ATTENTION';
|
|
96
|
+
} else {
|
|
97
|
+
overall = 'HEALTHY';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (fails > 0) parts.push(`${fails} error${fails > 1 ? 's' : ''}`);
|
|
102
|
+
if (warns > 0) parts.push(`${warns} warning${warns > 1 ? 's' : ''}`);
|
|
103
|
+
|
|
104
|
+
console.log(`\n Overall: ${overall}${parts.length > 0 ? ` (${parts.join(', ')})` : ''}\n`);
|
|
105
|
+
|
|
106
|
+
return fails > 0 ? 1 : 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- Individual checks ---
|
|
110
|
+
|
|
111
|
+
function checkInstalled(cwd) {
|
|
112
|
+
const missing = [];
|
|
113
|
+
if (!fs.existsSync(path.join(cwd, 'ai-specs'))) missing.push('ai-specs/');
|
|
114
|
+
if (!fs.existsSync(path.join(cwd, '.sdd-version'))) missing.push('.sdd-version');
|
|
115
|
+
if (!fs.existsSync(path.join(cwd, 'AGENTS.md'))) missing.push('AGENTS.md');
|
|
116
|
+
|
|
117
|
+
if (missing.length > 0) {
|
|
118
|
+
return {
|
|
119
|
+
status: FAIL,
|
|
120
|
+
message: 'SDD not installed — missing: ' + missing.join(', '),
|
|
121
|
+
details: ['Run: npx create-sdd-project --init'],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const version = readInstalledVersion(cwd);
|
|
126
|
+
return {
|
|
127
|
+
status: PASS,
|
|
128
|
+
message: `SDD installed (v${version})`,
|
|
129
|
+
details: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function checkVersion(cwd) {
|
|
134
|
+
const installed = readInstalledVersion(cwd);
|
|
135
|
+
const pkg = getPackageVersion();
|
|
136
|
+
|
|
137
|
+
if (installed === 'unknown') {
|
|
138
|
+
return {
|
|
139
|
+
status: WARN,
|
|
140
|
+
message: 'Version unknown — .sdd-version file missing or empty',
|
|
141
|
+
details: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (installed !== pkg) {
|
|
146
|
+
return {
|
|
147
|
+
status: WARN,
|
|
148
|
+
message: `Version mismatch: installed ${installed}, package ${pkg}`,
|
|
149
|
+
details: ['Run: npx create-sdd-project --upgrade'],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
status: PASS,
|
|
155
|
+
message: `Version up to date (${installed})`,
|
|
156
|
+
details: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function checkAiToolConfig(cwd, aiTools) {
|
|
161
|
+
const issues = [];
|
|
162
|
+
|
|
163
|
+
const checkToolDir = (dir, name) => {
|
|
164
|
+
const base = path.join(cwd, dir);
|
|
165
|
+
if (!fs.existsSync(base)) {
|
|
166
|
+
issues.push(`${dir}/ directory missing`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!fs.existsSync(path.join(base, 'agents'))) issues.push(`${dir}/agents/ missing`);
|
|
170
|
+
if (!fs.existsSync(path.join(base, 'skills'))) issues.push(`${dir}/skills/ missing`);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (aiTools !== 'gemini') checkToolDir('.claude', 'Claude Code');
|
|
174
|
+
if (aiTools !== 'claude') checkToolDir('.gemini', 'Gemini');
|
|
175
|
+
|
|
176
|
+
if (issues.length > 0) {
|
|
177
|
+
return {
|
|
178
|
+
status: FAIL,
|
|
179
|
+
message: 'AI tool config incomplete',
|
|
180
|
+
details: issues,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const label = aiTools === 'both' ? 'Claude Code + Gemini' : aiTools === 'claude' ? 'Claude Code' : 'Gemini';
|
|
185
|
+
return {
|
|
186
|
+
status: PASS,
|
|
187
|
+
message: `AI tools: ${label}`,
|
|
188
|
+
details: [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function checkTopLevelConfigs(cwd, aiTools) {
|
|
193
|
+
const missing = [];
|
|
194
|
+
|
|
195
|
+
if (aiTools !== 'gemini' && !fs.existsSync(path.join(cwd, 'CLAUDE.md'))) {
|
|
196
|
+
missing.push('CLAUDE.md');
|
|
197
|
+
}
|
|
198
|
+
if (aiTools !== 'claude' && !fs.existsSync(path.join(cwd, 'GEMINI.md'))) {
|
|
199
|
+
missing.push('GEMINI.md');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (missing.length > 0) {
|
|
203
|
+
return {
|
|
204
|
+
status: FAIL,
|
|
205
|
+
message: 'Missing top-level configs: ' + missing.join(', '),
|
|
206
|
+
details: ['Run: npx create-sdd-project --upgrade --force'],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
status: PASS,
|
|
212
|
+
message: 'Top-level configs present (AGENTS.md' +
|
|
213
|
+
(aiTools !== 'gemini' ? ', CLAUDE.md' : '') +
|
|
214
|
+
(aiTools !== 'claude' ? ', GEMINI.md' : '') + ')',
|
|
215
|
+
details: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function checkAgentsComplete(cwd, aiTools, projectType) {
|
|
220
|
+
let expectedAgents;
|
|
221
|
+
if (projectType === 'backend') {
|
|
222
|
+
expectedAgents = TEMPLATE_AGENTS.filter((a) => !FRONTEND_AGENTS.includes(a));
|
|
223
|
+
} else if (projectType === 'frontend') {
|
|
224
|
+
expectedAgents = TEMPLATE_AGENTS.filter((a) => !BACKEND_AGENTS.includes(a));
|
|
225
|
+
} else {
|
|
226
|
+
expectedAgents = [...TEMPLATE_AGENTS];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const toolDir = aiTools !== 'gemini' ? '.claude' : '.gemini';
|
|
230
|
+
const agentsDir = path.join(cwd, toolDir, 'agents');
|
|
231
|
+
if (!fs.existsSync(agentsDir)) {
|
|
232
|
+
return {
|
|
233
|
+
status: FAIL,
|
|
234
|
+
message: 'Agents directory missing',
|
|
235
|
+
details: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const present = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
|
|
240
|
+
const missing = expectedAgents.filter((a) => !present.includes(a));
|
|
241
|
+
const expectedCount = expectedAgents.length;
|
|
242
|
+
const templatePresent = expectedAgents.filter((a) => present.includes(a)).length;
|
|
243
|
+
|
|
244
|
+
if (missing.length > 0) {
|
|
245
|
+
return {
|
|
246
|
+
status: WARN,
|
|
247
|
+
message: `Agents: ${templatePresent}/${expectedCount} template agents present — missing ${missing.length}`,
|
|
248
|
+
details: missing.map((a) => `Missing: ${a}`),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const customCount = present.length - templatePresent;
|
|
253
|
+
const customNote = customCount > 0 ? ` + ${customCount} custom` : '';
|
|
254
|
+
return {
|
|
255
|
+
status: PASS,
|
|
256
|
+
message: `Agents: ${templatePresent}/${expectedCount} present${customNote}`,
|
|
257
|
+
details: [],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkProjectTypeCoherence(cwd, aiTools, projectType) {
|
|
262
|
+
const issues = [];
|
|
263
|
+
const toolDir = aiTools !== 'gemini' ? '.claude' : '.gemini';
|
|
264
|
+
const agentsDir = path.join(cwd, toolDir, 'agents');
|
|
265
|
+
|
|
266
|
+
if (!fs.existsSync(agentsDir)) {
|
|
267
|
+
return { status: PASS, message: 'Project type coherence: OK (no agents dir)', details: [] };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const agents = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.md'));
|
|
271
|
+
|
|
272
|
+
if (projectType === 'backend') {
|
|
273
|
+
const unexpected = agents.filter((a) => FRONTEND_AGENTS.includes(a));
|
|
274
|
+
if (unexpected.length > 0) {
|
|
275
|
+
issues.push(...unexpected.map((a) => `Frontend agent in backend project: ${a}`));
|
|
276
|
+
}
|
|
277
|
+
if (fs.existsSync(path.join(cwd, 'ai-specs', 'specs', 'frontend-standards.mdc'))) {
|
|
278
|
+
issues.push('frontend-standards.mdc present in backend-only project');
|
|
279
|
+
}
|
|
280
|
+
} else if (projectType === 'frontend') {
|
|
281
|
+
const unexpected = agents.filter((a) => BACKEND_AGENTS.includes(a));
|
|
282
|
+
if (unexpected.length > 0) {
|
|
283
|
+
issues.push(...unexpected.map((a) => `Backend agent in frontend project: ${a}`));
|
|
284
|
+
}
|
|
285
|
+
if (fs.existsSync(path.join(cwd, 'ai-specs', 'specs', 'backend-standards.mdc'))) {
|
|
286
|
+
issues.push('backend-standards.mdc present in frontend-only project');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (issues.length > 0) {
|
|
291
|
+
return {
|
|
292
|
+
status: WARN,
|
|
293
|
+
message: `Project type coherence: ${issues.length} issue${issues.length > 1 ? 's' : ''}`,
|
|
294
|
+
details: issues,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
status: PASS,
|
|
300
|
+
message: `Project type coherence: OK (${projectType})`,
|
|
301
|
+
details: [],
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function checkCrossToolConsistency(cwd, aiTools) {
|
|
306
|
+
if (aiTools !== 'both') {
|
|
307
|
+
return {
|
|
308
|
+
status: PASS,
|
|
309
|
+
message: 'Cross-tool consistency: N/A (single tool)',
|
|
310
|
+
details: [],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const issues = [];
|
|
315
|
+
|
|
316
|
+
// Compare agents
|
|
317
|
+
const claudeAgentsDir = path.join(cwd, '.claude', 'agents');
|
|
318
|
+
const geminiAgentsDir = path.join(cwd, '.gemini', 'agents');
|
|
319
|
+
|
|
320
|
+
if (fs.existsSync(claudeAgentsDir) && fs.existsSync(geminiAgentsDir)) {
|
|
321
|
+
const claudeAgents = new Set(fs.readdirSync(claudeAgentsDir).filter((f) => f.endsWith('.md')));
|
|
322
|
+
const geminiAgents = new Set(fs.readdirSync(geminiAgentsDir).filter((f) => f.endsWith('.md')));
|
|
323
|
+
|
|
324
|
+
for (const a of claudeAgents) {
|
|
325
|
+
if (!geminiAgents.has(a)) issues.push(`Agent in .claude but not .gemini: ${a}`);
|
|
326
|
+
}
|
|
327
|
+
for (const a of geminiAgents) {
|
|
328
|
+
if (!claudeAgents.has(a)) issues.push(`Agent in .gemini but not .claude: ${a}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Compare skills (directory names)
|
|
333
|
+
const claudeSkillsDir = path.join(cwd, '.claude', 'skills');
|
|
334
|
+
const geminiSkillsDir = path.join(cwd, '.gemini', 'skills');
|
|
335
|
+
|
|
336
|
+
if (fs.existsSync(claudeSkillsDir) && fs.existsSync(geminiSkillsDir)) {
|
|
337
|
+
const claudeSkills = new Set(fs.readdirSync(claudeSkillsDir).filter((f) =>
|
|
338
|
+
fs.statSync(path.join(claudeSkillsDir, f)).isDirectory()
|
|
339
|
+
));
|
|
340
|
+
const geminiSkills = new Set(fs.readdirSync(geminiSkillsDir).filter((f) =>
|
|
341
|
+
fs.statSync(path.join(geminiSkillsDir, f)).isDirectory()
|
|
342
|
+
));
|
|
343
|
+
|
|
344
|
+
for (const s of claudeSkills) {
|
|
345
|
+
if (!geminiSkills.has(s)) issues.push(`Skill in .claude but not .gemini: ${s}`);
|
|
346
|
+
}
|
|
347
|
+
for (const s of geminiSkills) {
|
|
348
|
+
if (!claudeSkills.has(s)) issues.push(`Skill in .gemini but not .claude: ${s}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (issues.length > 0) {
|
|
353
|
+
return {
|
|
354
|
+
status: WARN,
|
|
355
|
+
message: `Cross-tool consistency: ${issues.length} mismatch${issues.length > 1 ? 'es' : ''}`,
|
|
356
|
+
details: issues,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
status: PASS,
|
|
362
|
+
message: 'Cross-tool consistency: Claude and Gemini in sync',
|
|
363
|
+
details: [],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function checkStandards(cwd, projectType) {
|
|
368
|
+
const specsDir = path.join(cwd, 'ai-specs', 'specs');
|
|
369
|
+
if (!fs.existsSync(specsDir)) {
|
|
370
|
+
return {
|
|
371
|
+
status: FAIL,
|
|
372
|
+
message: 'Standards directory missing (ai-specs/specs/)',
|
|
373
|
+
details: [],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const expected = ['base-standards.mdc', 'documentation-standards.mdc'];
|
|
378
|
+
if (projectType !== 'frontend') expected.push('backend-standards.mdc');
|
|
379
|
+
if (projectType !== 'backend') expected.push('frontend-standards.mdc');
|
|
380
|
+
|
|
381
|
+
const missing = expected.filter((f) => !fs.existsSync(path.join(specsDir, f)));
|
|
382
|
+
|
|
383
|
+
if (missing.length > 0) {
|
|
384
|
+
return {
|
|
385
|
+
status: FAIL,
|
|
386
|
+
message: `Standards: ${expected.length - missing.length}/${expected.length} present`,
|
|
387
|
+
details: missing.map((f) => `Missing: ${f}`),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
status: PASS,
|
|
393
|
+
message: `Standards: ${expected.length}/${expected.length} present`,
|
|
394
|
+
details: [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function checkHooksAndDeps(cwd, aiTools) {
|
|
399
|
+
if (aiTools === 'gemini') {
|
|
400
|
+
return {
|
|
401
|
+
status: PASS,
|
|
402
|
+
message: 'Hooks: N/A (Gemini only)',
|
|
403
|
+
details: [],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const issues = [];
|
|
408
|
+
|
|
409
|
+
// Check quick-scan.sh
|
|
410
|
+
const hookPath = path.join(cwd, '.claude', 'hooks', 'quick-scan.sh');
|
|
411
|
+
if (!fs.existsSync(hookPath)) {
|
|
412
|
+
issues.push('quick-scan.sh missing');
|
|
413
|
+
} else {
|
|
414
|
+
try {
|
|
415
|
+
const stats = fs.statSync(hookPath);
|
|
416
|
+
if (!(stats.mode & 0o111)) {
|
|
417
|
+
issues.push('quick-scan.sh is not executable (run: chmod +x .claude/hooks/quick-scan.sh)');
|
|
418
|
+
}
|
|
419
|
+
} catch { /* ignore */ }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check jq
|
|
423
|
+
try {
|
|
424
|
+
execSync('which jq', { stdio: 'pipe' });
|
|
425
|
+
} catch {
|
|
426
|
+
issues.push('jq not installed — quick-scan hook needs it (brew install jq / apt install jq)');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check settings.json
|
|
430
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
431
|
+
if (!fs.existsSync(settingsPath)) {
|
|
432
|
+
issues.push('settings.json missing');
|
|
433
|
+
} else {
|
|
434
|
+
try {
|
|
435
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
436
|
+
if (!settings.hooks) {
|
|
437
|
+
issues.push('settings.json: no hooks configured');
|
|
438
|
+
}
|
|
439
|
+
} catch (e) {
|
|
440
|
+
issues.push(`settings.json: invalid JSON — ${e.message}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (issues.length > 0) {
|
|
445
|
+
const hasFail = issues.some((i) => i.includes('missing') || i.includes('invalid JSON'));
|
|
446
|
+
return {
|
|
447
|
+
status: hasFail ? FAIL : WARN,
|
|
448
|
+
message: `Hooks: ${issues.length} issue${issues.length > 1 ? 's' : ''}`,
|
|
449
|
+
details: issues,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
status: PASS,
|
|
455
|
+
message: 'Hooks: quick-scan.sh executable, jq installed, settings.json valid',
|
|
456
|
+
details: [],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function checkProjectMemory(cwd) {
|
|
461
|
+
const notesDir = path.join(cwd, 'docs', 'project_notes');
|
|
462
|
+
if (!fs.existsSync(notesDir)) {
|
|
463
|
+
return {
|
|
464
|
+
status: WARN,
|
|
465
|
+
message: 'Project memory: docs/project_notes/ missing',
|
|
466
|
+
details: [],
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const missing = EXPECTED_MEMORY_FILES.filter(
|
|
471
|
+
(f) => !fs.existsSync(path.join(notesDir, f))
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
if (missing.length > 0) {
|
|
475
|
+
return {
|
|
476
|
+
status: WARN,
|
|
477
|
+
message: `Project memory: ${EXPECTED_MEMORY_FILES.length - missing.length}/${EXPECTED_MEMORY_FILES.length} files`,
|
|
478
|
+
details: missing.map((f) => `Missing: ${f}`),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
status: PASS,
|
|
484
|
+
message: `Project memory: ${EXPECTED_MEMORY_FILES.length}/${EXPECTED_MEMORY_FILES.length} files present`,
|
|
485
|
+
details: [],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
runDoctor,
|
|
491
|
+
printResults,
|
|
492
|
+
};
|