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 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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"