create-sdd-project 0.6.1 → 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 +12 -0
- package/bin/cli.js +98 -0
- package/lib/diff-generator.js +274 -0
- package/lib/init-wizard.js +1 -1
- package/lib/upgrade-generator.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -168,6 +168,18 @@ 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
|
+
|
|
171
183
|
### CI/CD (Auto-Generated)
|
|
172
184
|
|
|
173
185
|
Every project gets a GitHub Actions CI workflow at `.github/workflows/ci.yml`, adapted to your stack:
|
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/init-wizard.js
CHANGED
package/lib/upgrade-generator.js
CHANGED