create-sdd-project 0.6.0 → 0.7.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 +26 -0
- package/bin/cli.js +98 -0
- package/lib/diff-generator.js +274 -0
- package/lib/generator.js +46 -0
- package/lib/init-generator.js +56 -1
- package/lib/init-wizard.js +1 -1
- package/lib/upgrade-generator.js +13 -0
- package/package.json +1 -1
- package/template/.github/workflows/ci.yml +56 -0
package/README.md
CHANGED
|
@@ -168,6 +168,29 @@ npx create-sdd-project --doctor
|
|
|
168
168
|
|
|
169
169
|
Exit code 1 if errors found — useful for CI pipelines.
|
|
170
170
|
|
|
171
|
+
### Preview Changes (--diff)
|
|
172
|
+
|
|
173
|
+
See what `--init` or `--upgrade` would do without modifying anything:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
cd your-existing-project
|
|
177
|
+
npx create-sdd-project --init --diff # Preview init
|
|
178
|
+
npx create-sdd-project --upgrade --diff # Preview upgrade
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Shows detected stack, files that would be created/replaced/preserved, and standards status — zero filesystem writes.
|
|
182
|
+
|
|
183
|
+
### CI/CD (Auto-Generated)
|
|
184
|
+
|
|
185
|
+
Every project gets a GitHub Actions CI workflow at `.github/workflows/ci.yml`, adapted to your stack:
|
|
186
|
+
|
|
187
|
+
- **PostgreSQL** projects get a `postgres:16` service with health checks
|
|
188
|
+
- **MongoDB** projects get a `mongo:7` service with health checks
|
|
189
|
+
- **Frontend-only** projects get a lightweight workflow without DB services
|
|
190
|
+
- **GitFlow** projects trigger on both `main` and `develop` branches
|
|
191
|
+
|
|
192
|
+
Customize the generated workflow as your project grows.
|
|
193
|
+
|
|
171
194
|
### After Setup
|
|
172
195
|
|
|
173
196
|
Open your project in Claude Code or Gemini and start building:
|
|
@@ -313,6 +336,9 @@ project/
|
|
|
313
336
|
├── GEMINI.md # Gemini config (autonomy)
|
|
314
337
|
├── .env.example # Environment variables template
|
|
315
338
|
│
|
|
339
|
+
├── .github/
|
|
340
|
+
│ └── workflows/ci.yml # CI pipeline (adapted to your stack)
|
|
341
|
+
│
|
|
316
342
|
├── .claude/
|
|
317
343
|
│ ├── agents/ # 9 specialized agents
|
|
318
344
|
│ ├── skills/
|
package/bin/cli.js
CHANGED
|
@@ -11,8 +11,12 @@ const isInit = args.includes('--init');
|
|
|
11
11
|
const isUpgrade = args.includes('--upgrade');
|
|
12
12
|
const isDoctor = args.includes('--doctor');
|
|
13
13
|
const isForce = args.includes('--force');
|
|
14
|
+
const isDiff = args.includes('--diff');
|
|
14
15
|
|
|
15
16
|
async function main() {
|
|
17
|
+
if (isDiff) {
|
|
18
|
+
return runDiff();
|
|
19
|
+
}
|
|
16
20
|
if (isDoctor) {
|
|
17
21
|
return runDoctorCmd();
|
|
18
22
|
}
|
|
@@ -216,6 +220,100 @@ async function runUpgrade() {
|
|
|
216
220
|
generateUpgrade(config);
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
async function runDiff() {
|
|
224
|
+
if (!isInit && !isUpgrade) {
|
|
225
|
+
console.error('Error: --diff must be combined with --init or --upgrade.');
|
|
226
|
+
console.error('Usage: create-sdd-project --init --diff');
|
|
227
|
+
console.error(' create-sdd-project --upgrade --diff');
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (projectName) {
|
|
232
|
+
console.error('Error: Cannot specify a project name with --diff.');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const cwd = process.cwd();
|
|
237
|
+
|
|
238
|
+
if (!fs.existsSync(path.join(cwd, 'package.json'))) {
|
|
239
|
+
console.error('Error: No package.json found in current directory.');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const { scan } = require('../lib/scanner');
|
|
244
|
+
const { buildInitDefaultConfig } = require('../lib/init-wizard');
|
|
245
|
+
const { runInitDiffReport, runUpgradeDiffReport } = require('../lib/diff-generator');
|
|
246
|
+
|
|
247
|
+
if (isInit) {
|
|
248
|
+
// Same validation as --init
|
|
249
|
+
if (fs.existsSync(path.join(cwd, 'ai-specs'))) {
|
|
250
|
+
console.error('Error: ai-specs/ directory already exists.');
|
|
251
|
+
console.error('SDD DevFlow appears to already be installed. Use --upgrade --diff instead.');
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const scanResult = scan(cwd);
|
|
256
|
+
const config = buildInitDefaultConfig(scanResult);
|
|
257
|
+
runInitDiffReport(config);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isUpgrade) {
|
|
262
|
+
// Same validation as --upgrade
|
|
263
|
+
if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
|
|
264
|
+
console.error('Error: ai-specs/ directory not found.');
|
|
265
|
+
console.error('SDD DevFlow does not appear to be installed. Use --init --diff instead.');
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const {
|
|
270
|
+
readInstalledVersion,
|
|
271
|
+
getPackageVersion,
|
|
272
|
+
detectAiTools,
|
|
273
|
+
detectProjectType,
|
|
274
|
+
readAutonomyLevel,
|
|
275
|
+
collectCustomAgents,
|
|
276
|
+
collectCustomCommands,
|
|
277
|
+
} = require('../lib/upgrade-generator');
|
|
278
|
+
|
|
279
|
+
const installedVersion = readInstalledVersion(cwd);
|
|
280
|
+
const packageVersion = getPackageVersion();
|
|
281
|
+
|
|
282
|
+
if (installedVersion === packageVersion && !isForce) {
|
|
283
|
+
console.log(`\nSDD DevFlow is already at version ${packageVersion}.`);
|
|
284
|
+
console.log('Use --force --diff to preview a re-install.\n');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const scanResult = scan(cwd);
|
|
289
|
+
const aiTools = detectAiTools(cwd);
|
|
290
|
+
const projectType = detectProjectType(cwd);
|
|
291
|
+
const autonomy = readAutonomyLevel(cwd);
|
|
292
|
+
const customAgents = collectCustomAgents(cwd);
|
|
293
|
+
const customCommands = collectCustomCommands(cwd);
|
|
294
|
+
const settingsLocal = fs.existsSync(path.join(cwd, '.claude', 'settings.local.json'));
|
|
295
|
+
|
|
296
|
+
const config = buildInitDefaultConfig(scanResult);
|
|
297
|
+
config.aiTools = aiTools;
|
|
298
|
+
config.projectType = projectType;
|
|
299
|
+
config.autonomyLevel = autonomy.level;
|
|
300
|
+
config.autonomyName = autonomy.name;
|
|
301
|
+
config.installedVersion = installedVersion;
|
|
302
|
+
|
|
303
|
+
const state = {
|
|
304
|
+
installedVersion,
|
|
305
|
+
packageVersion,
|
|
306
|
+
aiTools,
|
|
307
|
+
projectType,
|
|
308
|
+
customAgents,
|
|
309
|
+
customCommands,
|
|
310
|
+
settingsLocal,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
runUpgradeDiffReport(config, state);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
219
317
|
main().catch((err) => {
|
|
220
318
|
console.error('\nError:', err.message);
|
|
221
319
|
process.exit(1);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const {
|
|
6
|
+
FRONTEND_AGENTS,
|
|
7
|
+
BACKEND_AGENTS,
|
|
8
|
+
TEMPLATE_AGENTS,
|
|
9
|
+
} = require('./config');
|
|
10
|
+
const {
|
|
11
|
+
adaptBaseStandards,
|
|
12
|
+
adaptBackendStandards,
|
|
13
|
+
adaptFrontendStandards,
|
|
14
|
+
} = require('./init-generator');
|
|
15
|
+
const {
|
|
16
|
+
isStandardModified,
|
|
17
|
+
getPackageVersion,
|
|
18
|
+
} = require('./upgrade-generator');
|
|
19
|
+
const { formatScanSummary } = require('./init-wizard');
|
|
20
|
+
|
|
21
|
+
const templateDir = path.join(__dirname, '..', 'template');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Count files recursively in a directory.
|
|
25
|
+
*/
|
|
26
|
+
function countFiles(dir) {
|
|
27
|
+
if (!fs.existsSync(dir)) return 0;
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
count += countFiles(path.join(dir, entry.name));
|
|
32
|
+
} else {
|
|
33
|
+
count++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return count;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute how many agents would be kept after project-type filtering.
|
|
41
|
+
*/
|
|
42
|
+
function countAgents(projectType) {
|
|
43
|
+
const all = TEMPLATE_AGENTS.length; // 9
|
|
44
|
+
if (projectType === 'backend') return all - FRONTEND_AGENTS.length;
|
|
45
|
+
if (projectType === 'frontend') return all - BACKEND_AGENTS.length;
|
|
46
|
+
return all;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Compute how many standards would be created for a project type.
|
|
51
|
+
* base + documentation + backend (if not frontend) + frontend (if not backend)
|
|
52
|
+
*/
|
|
53
|
+
function countStandards(projectType) {
|
|
54
|
+
let count = 2; // base + documentation
|
|
55
|
+
if (projectType !== 'frontend') count++; // backend
|
|
56
|
+
if (projectType !== 'backend') count++; // frontend
|
|
57
|
+
return count;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Init Diff ────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function runInitDiffReport(config) {
|
|
63
|
+
const dest = config.projectDir;
|
|
64
|
+
const scan = config.scanResult;
|
|
65
|
+
const projectType = config.projectType;
|
|
66
|
+
const aiTools = config.aiTools;
|
|
67
|
+
|
|
68
|
+
const lines = [];
|
|
69
|
+
lines.push('\n🔍 SDD DevFlow — Preview (--init)\n');
|
|
70
|
+
|
|
71
|
+
// Detected stack
|
|
72
|
+
lines.push(' Detected stack:');
|
|
73
|
+
lines.push(formatScanSummary(scan));
|
|
74
|
+
lines.push('');
|
|
75
|
+
|
|
76
|
+
// Compute what would be created
|
|
77
|
+
const wouldCreate = [];
|
|
78
|
+
|
|
79
|
+
// AI tool directories
|
|
80
|
+
const agentCount = countAgents(projectType);
|
|
81
|
+
const removedAgents = TEMPLATE_AGENTS.length - agentCount;
|
|
82
|
+
|
|
83
|
+
if (aiTools !== 'gemini') {
|
|
84
|
+
const claudeSkills = countFiles(path.join(templateDir, '.claude', 'skills'));
|
|
85
|
+
wouldCreate.push(` .claude/agents/ ${agentCount} agents${removedAgents > 0 ? ` (${removedAgents} removed: ${projectType}-only)` : ''}`);
|
|
86
|
+
wouldCreate.push(` .claude/skills/ ${claudeSkills} files (4 skills)`);
|
|
87
|
+
wouldCreate.push(' .claude/hooks/ quick-scan.sh');
|
|
88
|
+
wouldCreate.push(' .claude/settings.json');
|
|
89
|
+
wouldCreate.push(' .claude/commands/ .gitkeep');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (aiTools !== 'claude') {
|
|
93
|
+
const geminiSkills = countFiles(path.join(templateDir, '.gemini', 'skills'));
|
|
94
|
+
const geminiCmds = countFiles(path.join(templateDir, '.gemini', 'commands'));
|
|
95
|
+
wouldCreate.push(` .gemini/agents/ ${agentCount} agents${removedAgents > 0 ? ` (${removedAgents} removed: ${projectType}-only)` : ''}`);
|
|
96
|
+
wouldCreate.push(` .gemini/skills/ ${geminiSkills} files (4 skills)`);
|
|
97
|
+
wouldCreate.push(` .gemini/commands/ ${geminiCmds} commands`);
|
|
98
|
+
wouldCreate.push(' .gemini/styles/ default.md');
|
|
99
|
+
wouldCreate.push(' .gemini/settings.json');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Standards
|
|
103
|
+
const stdCount = countStandards(projectType);
|
|
104
|
+
const stdDetails = ['base-standards'];
|
|
105
|
+
if (projectType !== 'frontend') stdDetails.push('backend-standards');
|
|
106
|
+
if (projectType !== 'backend') stdDetails.push('frontend-standards');
|
|
107
|
+
stdDetails.push('documentation-standards');
|
|
108
|
+
wouldCreate.push(` ai-specs/specs/ ${stdCount} standards (${stdDetails.join(', ')})`);
|
|
109
|
+
|
|
110
|
+
// Docs
|
|
111
|
+
wouldCreate.push(' docs/project_notes/ 4 files (bugs, decisions, key_facts, product-tracker)');
|
|
112
|
+
const specFiles = [];
|
|
113
|
+
if (projectType !== 'frontend') specFiles.push('api-spec.yaml');
|
|
114
|
+
if (projectType !== 'backend') specFiles.push('ui-components.md');
|
|
115
|
+
if (specFiles.length > 0) {
|
|
116
|
+
wouldCreate.push(` docs/specs/ ${specFiles.join(', ')}`);
|
|
117
|
+
}
|
|
118
|
+
wouldCreate.push(' docs/tickets/ .gitkeep');
|
|
119
|
+
|
|
120
|
+
// Top-level files
|
|
121
|
+
wouldCreate.push(' AGENTS.md');
|
|
122
|
+
if (aiTools !== 'gemini') wouldCreate.push(' CLAUDE.md');
|
|
123
|
+
if (aiTools !== 'claude') wouldCreate.push(' GEMINI.md');
|
|
124
|
+
wouldCreate.push(' .env.example');
|
|
125
|
+
wouldCreate.push(' .github/workflows/ci.yml');
|
|
126
|
+
wouldCreate.push(' .sdd-version');
|
|
127
|
+
|
|
128
|
+
lines.push(` Would create (${wouldCreate.length} items):`);
|
|
129
|
+
for (const item of wouldCreate) {
|
|
130
|
+
lines.push(` + ${item.trim()}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// .gitignore handling
|
|
134
|
+
const gitignorePath = path.join(dest, '.gitignore');
|
|
135
|
+
if (fs.existsSync(gitignorePath)) {
|
|
136
|
+
lines.push('\n Would modify:');
|
|
137
|
+
lines.push(' ~ .gitignore append SDD entries');
|
|
138
|
+
} else {
|
|
139
|
+
lines.push('\n Would create:');
|
|
140
|
+
lines.push(' + .gitignore with SDD entries');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
lines.push('\n No files deleted. Run without --diff to apply.\n');
|
|
144
|
+
|
|
145
|
+
console.log(lines.join('\n'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Upgrade Diff ─────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function runUpgradeDiffReport(config, state) {
|
|
151
|
+
const dest = config.projectDir;
|
|
152
|
+
const scan = config.scanResult;
|
|
153
|
+
const projectType = config.projectType;
|
|
154
|
+
const aiTools = config.aiTools;
|
|
155
|
+
|
|
156
|
+
const lines = [];
|
|
157
|
+
lines.push('\n🔍 SDD DevFlow — Preview (--upgrade)\n');
|
|
158
|
+
lines.push(` ${state.installedVersion} → ${state.packageVersion}\n`);
|
|
159
|
+
|
|
160
|
+
// Would replace
|
|
161
|
+
const replaceItems = [];
|
|
162
|
+
|
|
163
|
+
if (aiTools !== 'gemini') {
|
|
164
|
+
const agentCount = countAgents(projectType);
|
|
165
|
+
replaceItems.push(`.claude/agents/ ${agentCount} agents`);
|
|
166
|
+
replaceItems.push('.claude/skills/ 4 skills');
|
|
167
|
+
replaceItems.push('.claude/hooks/ quick-scan.sh');
|
|
168
|
+
replaceItems.push('.claude/settings.json hooks updated, permissions preserved');
|
|
169
|
+
}
|
|
170
|
+
if (aiTools !== 'claude') {
|
|
171
|
+
const agentCount = countAgents(projectType);
|
|
172
|
+
replaceItems.push(`.gemini/agents/ ${agentCount} agents`);
|
|
173
|
+
replaceItems.push('.gemini/skills/ 4 skills');
|
|
174
|
+
replaceItems.push('.gemini/commands/ TOML commands');
|
|
175
|
+
replaceItems.push('.gemini/styles/ default.md');
|
|
176
|
+
replaceItems.push('.gemini/settings.json');
|
|
177
|
+
}
|
|
178
|
+
replaceItems.push('AGENTS.md');
|
|
179
|
+
if (aiTools !== 'gemini') replaceItems.push('CLAUDE.md');
|
|
180
|
+
if (aiTools !== 'claude') replaceItems.push('GEMINI.md');
|
|
181
|
+
replaceItems.push('.env.example custom vars preserved');
|
|
182
|
+
replaceItems.push('.sdd-version');
|
|
183
|
+
|
|
184
|
+
lines.push(` Would replace (${replaceItems.length} items):`);
|
|
185
|
+
for (const item of replaceItems) {
|
|
186
|
+
lines.push(` ✓ ${item}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Would preserve
|
|
190
|
+
const preserveItems = [];
|
|
191
|
+
if (state.settingsLocal) preserveItems.push('.claude/settings.local.json personal settings');
|
|
192
|
+
for (const c of state.customCommands) preserveItems.push(`${c.relativePath} custom command`);
|
|
193
|
+
for (const a of state.customAgents) preserveItems.push(`${a.relativePath} custom agent`);
|
|
194
|
+
preserveItems.push('docs/project_notes/* project memory');
|
|
195
|
+
preserveItems.push('docs/specs/* your specs');
|
|
196
|
+
preserveItems.push('docs/tickets/* your tickets');
|
|
197
|
+
preserveItems.push('.gitignore');
|
|
198
|
+
|
|
199
|
+
lines.push(`\n Would preserve (${preserveItems.length} items):`);
|
|
200
|
+
for (const item of preserveItems) {
|
|
201
|
+
lines.push(` ⊘ ${item}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Standards diff
|
|
205
|
+
const standardsStatus = [];
|
|
206
|
+
|
|
207
|
+
const baseStdPath = path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc');
|
|
208
|
+
if (fs.existsSync(baseStdPath)) {
|
|
209
|
+
const existing = fs.readFileSync(baseStdPath, 'utf8');
|
|
210
|
+
const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'base-standards.mdc'), 'utf8');
|
|
211
|
+
const fresh = adaptBaseStandards(template, scan, config);
|
|
212
|
+
standardsStatus.push({
|
|
213
|
+
name: 'base-standards.mdc',
|
|
214
|
+
modified: isStandardModified(existing, fresh),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (projectType !== 'frontend') {
|
|
219
|
+
const backendStdPath = path.join(dest, 'ai-specs', 'specs', 'backend-standards.mdc');
|
|
220
|
+
if (fs.existsSync(backendStdPath)) {
|
|
221
|
+
const existing = fs.readFileSync(backendStdPath, 'utf8');
|
|
222
|
+
const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'backend-standards.mdc'), 'utf8');
|
|
223
|
+
const fresh = adaptBackendStandards(template, scan);
|
|
224
|
+
standardsStatus.push({
|
|
225
|
+
name: 'backend-standards.mdc',
|
|
226
|
+
modified: isStandardModified(existing, fresh),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (projectType !== 'backend') {
|
|
232
|
+
const frontendStdPath = path.join(dest, 'ai-specs', 'specs', 'frontend-standards.mdc');
|
|
233
|
+
if (fs.existsSync(frontendStdPath)) {
|
|
234
|
+
const existing = fs.readFileSync(frontendStdPath, 'utf8');
|
|
235
|
+
const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'frontend-standards.mdc'), 'utf8');
|
|
236
|
+
const fresh = adaptFrontendStandards(template, scan);
|
|
237
|
+
standardsStatus.push({
|
|
238
|
+
name: 'frontend-standards.mdc',
|
|
239
|
+
modified: isStandardModified(existing, fresh),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
standardsStatus.push({ name: 'documentation-standards.mdc', modified: false });
|
|
245
|
+
|
|
246
|
+
lines.push('\n Standards:');
|
|
247
|
+
for (const s of standardsStatus) {
|
|
248
|
+
if (s.modified) {
|
|
249
|
+
lines.push(` ⚠ ${s.name} customized → preserve`);
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(` ✓ ${s.name} unchanged → update`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Would add (new files)
|
|
256
|
+
const addItems = [];
|
|
257
|
+
const ciPath = path.join(dest, '.github', 'workflows', 'ci.yml');
|
|
258
|
+
if (!fs.existsSync(ciPath)) {
|
|
259
|
+
addItems.push('.github/workflows/ci.yml');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (addItems.length > 0) {
|
|
263
|
+
lines.push(`\n Would add (${addItems.length} new):`);
|
|
264
|
+
for (const item of addItems) {
|
|
265
|
+
lines.push(` + ${item}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
lines.push('\n Run without --diff to apply.\n');
|
|
270
|
+
|
|
271
|
+
console.log(lines.join('\n'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = { runInitDiffReport, runUpgradeDiffReport };
|
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/init-wizard.js
CHANGED
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') {
|
|
@@ -500,5 +512,6 @@ module.exports = {
|
|
|
500
512
|
readAutonomyLevel,
|
|
501
513
|
collectCustomAgents,
|
|
502
514
|
collectCustomCommands,
|
|
515
|
+
isStandardModified,
|
|
503
516
|
buildSummary,
|
|
504
517
|
};
|
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
|