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 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 };
@@ -283,4 +283,4 @@ function buildInitDefaultConfig(scanResult) {
283
283
  return config;
284
284
  }
285
285
 
286
- module.exports = { runInitWizard, buildInitDefaultConfig };
286
+ module.exports = { runInitWizard, buildInitDefaultConfig, formatScanSummary };
@@ -512,5 +512,6 @@ module.exports = {
512
512
  readAutonomyLevel,
513
513
  collectCustomAgents,
514
514
  collectCustomCommands,
515
+ isStandardModified,
515
516
  buildSummary,
516
517
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"