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 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 };
@@ -184,7 +184,20 @@ function generateInit(config) {
184
184
  // 8. Append to .gitignore
185
185
  appendGitignore(dest, skipped);
186
186
 
187
- // 9. Copy and adapt .env.example if not present
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
  };
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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"
@@ -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