create-sdd-project 0.5.0 → 0.6.1
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 +44 -0
- package/bin/cli.js +21 -0
- package/lib/doctor.js +492 -0
- package/lib/generator.js +46 -0
- package/lib/init-generator.js +56 -1
- package/lib/upgrade-generator.js +12 -0
- package/package.json +1 -1
- package/template/.github/workflows/ci.yml +56 -0
package/README.md
CHANGED
|
@@ -138,6 +138,47 @@ 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
|
+
|
|
171
|
+
### CI/CD (Auto-Generated)
|
|
172
|
+
|
|
173
|
+
Every project gets a GitHub Actions CI workflow at `.github/workflows/ci.yml`, adapted to your stack:
|
|
174
|
+
|
|
175
|
+
- **PostgreSQL** projects get a `postgres:16` service with health checks
|
|
176
|
+
- **MongoDB** projects get a `mongo:7` service with health checks
|
|
177
|
+
- **Frontend-only** projects get a lightweight workflow without DB services
|
|
178
|
+
- **GitFlow** projects trigger on both `main` and `develop` branches
|
|
179
|
+
|
|
180
|
+
Customize the generated workflow as your project grows.
|
|
181
|
+
|
|
141
182
|
### After Setup
|
|
142
183
|
|
|
143
184
|
Open your project in Claude Code or Gemini and start building:
|
|
@@ -283,6 +324,9 @@ project/
|
|
|
283
324
|
├── GEMINI.md # Gemini config (autonomy)
|
|
284
325
|
├── .env.example # Environment variables template
|
|
285
326
|
│
|
|
327
|
+
├── .github/
|
|
328
|
+
│ └── workflows/ci.yml # CI pipeline (adapted to your stack)
|
|
329
|
+
│
|
|
286
330
|
├── .claude/
|
|
287
331
|
│ ├── agents/ # 9 specialized agents
|
|
288
332
|
│ ├── skills/
|
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/lib/generator.js
CHANGED
|
@@ -66,6 +66,10 @@ function generate(config) {
|
|
|
66
66
|
adaptAgentContent(dest, config);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// 7c. Adapt CI workflow for project type and stack
|
|
70
|
+
step('Configuring CI workflow');
|
|
71
|
+
adaptCiWorkflow(dest, config);
|
|
72
|
+
|
|
69
73
|
// 8. Remove AI tool config if single tool selected
|
|
70
74
|
if (config.aiTools === 'claude') {
|
|
71
75
|
step('Removing Gemini config (Claude only)');
|
|
@@ -350,4 +354,46 @@ function adaptAgentContent(dest, config) {
|
|
|
350
354
|
adaptAgentContentForProjectType(dest, config, replaceInFile);
|
|
351
355
|
}
|
|
352
356
|
|
|
357
|
+
function adaptCiWorkflow(dest, config) {
|
|
358
|
+
const ciPath = path.join(dest, '.github', 'workflows', 'ci.yml');
|
|
359
|
+
if (!fs.existsSync(ciPath)) return;
|
|
360
|
+
|
|
361
|
+
const bPreset = config.backendPreset || BACKEND_STACKS[0];
|
|
362
|
+
|
|
363
|
+
// Adapt branches for gitflow
|
|
364
|
+
if (config.branching === 'gitflow') {
|
|
365
|
+
replaceInFile(ciPath, [
|
|
366
|
+
['branches: [main]', 'branches: [main, develop]'],
|
|
367
|
+
]);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Adapt DB services based on stack
|
|
371
|
+
if (config.projectType === 'frontend') {
|
|
372
|
+
// Frontend-only: remove services and DATABASE_URL
|
|
373
|
+
replaceInFile(ciPath, [
|
|
374
|
+
[/\n # -- Database service.*?--health-retries=5\n/s, ''],
|
|
375
|
+
[/\n DATABASE_URL:.*\n/, '\n'],
|
|
376
|
+
]);
|
|
377
|
+
} else if (bPreset.db === 'MongoDB') {
|
|
378
|
+
// MongoDB: replace postgres service with mongo
|
|
379
|
+
replaceInFile(ciPath, [
|
|
380
|
+
[/ # -- Database service.*?--health-retries=5\n/s,
|
|
381
|
+
` # -- Database service (remove if not needed) --\n` +
|
|
382
|
+
` services:\n` +
|
|
383
|
+
` mongodb:\n` +
|
|
384
|
+
` image: mongo:7\n` +
|
|
385
|
+
` ports:\n` +
|
|
386
|
+
` - 27017:27017\n` +
|
|
387
|
+
` options: >-\n` +
|
|
388
|
+
` --health-cmd="mongosh --eval 'db.runCommand({ping:1})'"\n` +
|
|
389
|
+
` --health-interval=10s\n` +
|
|
390
|
+
` --health-timeout=5s\n` +
|
|
391
|
+
` --health-retries=5\n`],
|
|
392
|
+
['DATABASE_URL: postgresql://test:test@localhost:5432/testdb',
|
|
393
|
+
'MONGODB_URI: mongodb://localhost:27017/testdb'],
|
|
394
|
+
]);
|
|
395
|
+
}
|
|
396
|
+
// Default (PostgreSQL) — template already has correct services
|
|
397
|
+
}
|
|
398
|
+
|
|
353
399
|
module.exports = { generate };
|
package/lib/init-generator.js
CHANGED
|
@@ -184,7 +184,20 @@ function generateInit(config) {
|
|
|
184
184
|
// 8. Append to .gitignore
|
|
185
185
|
appendGitignore(dest, skipped);
|
|
186
186
|
|
|
187
|
-
// 9. Copy
|
|
187
|
+
// 9. Copy CI workflow if not present
|
|
188
|
+
const githubDir = path.join(dest, '.github', 'workflows');
|
|
189
|
+
const ciPath = path.join(githubDir, 'ci.yml');
|
|
190
|
+
if (!fs.existsSync(ciPath)) {
|
|
191
|
+
step('Creating .github/workflows/ci.yml');
|
|
192
|
+
ensureDir(githubDir);
|
|
193
|
+
let ciContent = fs.readFileSync(path.join(templateDir, '.github', 'workflows', 'ci.yml'), 'utf8');
|
|
194
|
+
ciContent = adaptCiWorkflowContent(ciContent, config, scan);
|
|
195
|
+
fs.writeFileSync(ciPath, ciContent, 'utf8');
|
|
196
|
+
} else {
|
|
197
|
+
skipped.push('.github/workflows/ci.yml');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 10. Copy and adapt .env.example if not present
|
|
188
201
|
const envExamplePath = path.join(dest, '.env.example');
|
|
189
202
|
if (!fs.existsSync(envExamplePath)) {
|
|
190
203
|
let envContent = fs.readFileSync(path.join(templateDir, '.env.example'), 'utf8');
|
|
@@ -950,6 +963,47 @@ function updateAutonomy(dest, config) {
|
|
|
950
963
|
}
|
|
951
964
|
}
|
|
952
965
|
|
|
966
|
+
// --- CI Workflow Adaptation ---
|
|
967
|
+
|
|
968
|
+
function adaptCiWorkflowContent(template, config, scan) {
|
|
969
|
+
let content = template;
|
|
970
|
+
|
|
971
|
+
// Adapt branches for gitflow
|
|
972
|
+
if (config.branching === 'gitflow') {
|
|
973
|
+
content = content.replaceAll('branches: [main]', 'branches: [main, develop]');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Adapt DB services based on detected stack
|
|
977
|
+
if (!scan.backend.detected) {
|
|
978
|
+
// Frontend-only: remove services block and DATABASE_URL
|
|
979
|
+
content = content.replace(/\n # -- Database service.*?--health-retries=5\n/s, '');
|
|
980
|
+
content = content.replace(/\n DATABASE_URL:.*\n/, '\n');
|
|
981
|
+
} else if (scan.backend.db === 'MongoDB') {
|
|
982
|
+
// MongoDB: replace postgres service with mongo
|
|
983
|
+
content = content.replace(
|
|
984
|
+
/ # -- Database service.*?--health-retries=5\n/s,
|
|
985
|
+
` # -- Database service (remove if not needed) --\n` +
|
|
986
|
+
` services:\n` +
|
|
987
|
+
` mongodb:\n` +
|
|
988
|
+
` image: mongo:7\n` +
|
|
989
|
+
` ports:\n` +
|
|
990
|
+
` - 27017:27017\n` +
|
|
991
|
+
` options: >-\n` +
|
|
992
|
+
` --health-cmd="mongosh --eval 'db.runCommand({ping:1})'"\n` +
|
|
993
|
+
` --health-interval=10s\n` +
|
|
994
|
+
` --health-timeout=5s\n` +
|
|
995
|
+
` --health-retries=5\n`
|
|
996
|
+
);
|
|
997
|
+
content = content.replace(
|
|
998
|
+
'DATABASE_URL: postgresql://test:test@localhost:5432/testdb',
|
|
999
|
+
'MONGODB_URI: mongodb://localhost:27017/testdb'
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
// Default (PostgreSQL) — template already has correct services
|
|
1003
|
+
|
|
1004
|
+
return content;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
953
1007
|
// --- .env.example Adaptation ---
|
|
954
1008
|
|
|
955
1009
|
function adaptEnvExample(template, config, scan) {
|
|
@@ -1008,6 +1062,7 @@ module.exports = {
|
|
|
1008
1062
|
adaptAgentsMd,
|
|
1009
1063
|
adaptCopiedFiles,
|
|
1010
1064
|
adaptEnvExample,
|
|
1065
|
+
adaptCiWorkflowContent,
|
|
1011
1066
|
updateAutonomy,
|
|
1012
1067
|
regexReplaceInFile,
|
|
1013
1068
|
};
|
package/lib/upgrade-generator.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
adaptAgentsMd,
|
|
17
17
|
adaptCopiedFiles,
|
|
18
18
|
adaptEnvExample,
|
|
19
|
+
adaptCiWorkflowContent,
|
|
19
20
|
updateAutonomy,
|
|
20
21
|
regexReplaceInFile,
|
|
21
22
|
} = require('./init-generator');
|
|
@@ -408,6 +409,17 @@ function generateUpgrade(config) {
|
|
|
408
409
|
|
|
409
410
|
step('Replaced AGENTS.md, CLAUDE.md/GEMINI.md, .env.example');
|
|
410
411
|
|
|
412
|
+
// --- e2) CI workflow — only add if not present (user may have customized) ---
|
|
413
|
+
const ciPath = path.join(dest, '.github', 'workflows', 'ci.yml');
|
|
414
|
+
if (!fs.existsSync(ciPath)) {
|
|
415
|
+
fs.mkdirSync(path.join(dest, '.github', 'workflows'), { recursive: true });
|
|
416
|
+
let ciContent = fs.readFileSync(path.join(templateDir, '.github', 'workflows', 'ci.yml'), 'utf8');
|
|
417
|
+
ciContent = adaptCiWorkflowContent(ciContent, config, scan);
|
|
418
|
+
fs.writeFileSync(ciPath, ciContent, 'utf8');
|
|
419
|
+
step('Added .github/workflows/ci.yml');
|
|
420
|
+
replaced++;
|
|
421
|
+
}
|
|
422
|
+
|
|
411
423
|
// --- f) Adapt for project type ---
|
|
412
424
|
// Remove agents for single-stack projects
|
|
413
425
|
if (projectType === 'backend') {
|
package/package.json
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ===========================================
|
|
2
|
+
# CI Pipeline — Generated by SDD DevFlow
|
|
3
|
+
# ===========================================
|
|
4
|
+
# Runs on pushes and PRs to main branch.
|
|
5
|
+
# Customize steps as your project grows.
|
|
6
|
+
|
|
7
|
+
name: CI
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
ci:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
# -- Database service (remove if not needed) --
|
|
20
|
+
services:
|
|
21
|
+
postgres:
|
|
22
|
+
image: postgres:16
|
|
23
|
+
env:
|
|
24
|
+
POSTGRES_USER: test
|
|
25
|
+
POSTGRES_PASSWORD: test
|
|
26
|
+
POSTGRES_DB: testdb
|
|
27
|
+
ports:
|
|
28
|
+
- 5432:5432
|
|
29
|
+
options: >-
|
|
30
|
+
--health-cmd="pg_isready"
|
|
31
|
+
--health-interval=10s
|
|
32
|
+
--health-timeout=5s
|
|
33
|
+
--health-retries=5
|
|
34
|
+
|
|
35
|
+
env:
|
|
36
|
+
NODE_ENV: test
|
|
37
|
+
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
|
|
42
|
+
- uses: actions/setup-node@v4
|
|
43
|
+
with:
|
|
44
|
+
node-version: 20
|
|
45
|
+
cache: npm
|
|
46
|
+
|
|
47
|
+
- run: npm ci
|
|
48
|
+
|
|
49
|
+
- name: Lint
|
|
50
|
+
run: npm run lint --if-present
|
|
51
|
+
|
|
52
|
+
- name: Test
|
|
53
|
+
run: npm test --if-present
|
|
54
|
+
|
|
55
|
+
- name: Build
|
|
56
|
+
run: npm run build --if-present
|