create-sdd-project 0.6.1 → 0.8.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,66 @@ npx create-sdd-project --doctor
168
168
 
169
169
  Exit code 1 if errors found — useful for CI pipelines.
170
170
 
171
+ ### Eject (Uninstall SDD)
172
+
173
+ Cleanly remove all SDD DevFlow files from your project:
174
+
175
+ ```bash
176
+ cd your-project
177
+ npx create-sdd-project --eject
178
+ ```
179
+
180
+ ```
181
+ šŸ—‘ļø SDD DevFlow Eject
182
+
183
+ Installed version: 0.7.0
184
+ AI tools: Claude Code + Gemini
185
+ Project type: backend
186
+
187
+ Will remove:
188
+ āœ— .claude/agents/ template agents
189
+ āœ— .claude/skills/
190
+ āœ— .claude/hooks/
191
+ āœ— .claude/settings.json
192
+ āœ— .gemini/ entire directory
193
+ āœ— ai-specs/ standards
194
+ āœ— AGENTS.md
195
+ āœ— CLAUDE.md / GEMINI.md
196
+ āœ— .env.example
197
+ āœ— .sdd-version
198
+ āœ— .github/workflows/ci.yml (SDD-generated)
199
+
200
+ Will preserve:
201
+ ⊘ .claude/agents/my-agent.md (custom agent)
202
+ ⊘ .claude/settings.local.json (personal settings)
203
+ ⊘ docs/ (project notes, specs, tickets)
204
+
205
+ Proceed? (y/N)
206
+ ```
207
+
208
+ **What gets removed:** All SDD-generated files (agents, skills, hooks, standards, configs, CI workflow).
209
+
210
+ **What is always preserved:** Custom agents, custom commands, personal settings, project documentation (`docs/`), customized CI workflows (without SDD marker).
211
+
212
+ For non-interactive eject: `npx create-sdd-project --eject --yes`
213
+
214
+ Preview what would be removed: `npx create-sdd-project --eject --diff`
215
+
216
+ To re-install later: `npx create-sdd-project --init`
217
+
218
+ ### Preview Changes (--diff)
219
+
220
+ See what `--init`, `--upgrade`, or `--eject` would do without modifying anything:
221
+
222
+ ```bash
223
+ cd your-existing-project
224
+ npx create-sdd-project --init --diff # Preview init
225
+ npx create-sdd-project --upgrade --diff # Preview upgrade
226
+ npx create-sdd-project --eject --diff # Preview eject
227
+ ```
228
+
229
+ Shows detected stack, files that would be created/replaced/preserved, and standards status — zero filesystem writes.
230
+
171
231
  ### CI/CD (Auto-Generated)
172
232
 
173
233
  Every project gets a GitHub Actions CI workflow at `.github/workflows/ci.yml`, adapted to your stack:
package/bin/cli.js CHANGED
@@ -11,11 +11,19 @@ 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 isEject = args.includes('--eject');
15
+ const isDiff = args.includes('--diff');
14
16
 
15
17
  async function main() {
18
+ if (isDiff) {
19
+ return runDiff();
20
+ }
16
21
  if (isDoctor) {
17
22
  return runDoctorCmd();
18
23
  }
24
+ if (isEject) {
25
+ return runEject();
26
+ }
19
27
  if (isUpgrade) {
20
28
  return runUpgrade();
21
29
  }
@@ -216,6 +224,169 @@ async function runUpgrade() {
216
224
  generateUpgrade(config);
217
225
  }
218
226
 
227
+ async function runEject() {
228
+ const readline = require('readline');
229
+ const {
230
+ collectEjectInventory,
231
+ buildEjectSummary,
232
+ generateEject,
233
+ } = require('../lib/eject-generator');
234
+
235
+ const cwd = process.cwd();
236
+
237
+ // Validate: must be in an existing project
238
+ if (!fs.existsSync(path.join(cwd, 'package.json'))) {
239
+ console.error('Error: No package.json found in current directory.');
240
+ console.error('The --eject flag must be run from inside an existing project.');
241
+ process.exit(1);
242
+ }
243
+
244
+ // Validate: SDD must be installed
245
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
246
+ console.error('Error: ai-specs/ directory not found.');
247
+ console.error('SDD DevFlow does not appear to be installed. Nothing to eject.');
248
+ process.exit(1);
249
+ }
250
+
251
+ // Validate: no project name with --eject
252
+ if (projectName) {
253
+ console.error('Error: Cannot specify a project name with --eject.');
254
+ console.error('Usage: create-sdd-project --eject');
255
+ process.exit(1);
256
+ }
257
+
258
+ const state = collectEjectInventory(cwd);
259
+
260
+ if (!useDefaults) {
261
+ console.log('\n' + buildEjectSummary(state));
262
+ console.log('');
263
+
264
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
265
+ const answer = await new Promise((resolve) => {
266
+ rl.question(' Proceed? (y/N) ', resolve);
267
+ });
268
+ rl.close();
269
+
270
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
271
+ console.log('\nEject cancelled.\n');
272
+ return;
273
+ }
274
+ }
275
+
276
+ generateEject(cwd);
277
+ }
278
+
279
+ async function runDiff() {
280
+ if (!isInit && !isUpgrade && !isEject) {
281
+ console.error('Error: --diff must be combined with --init, --upgrade, or --eject.');
282
+ console.error('Usage: create-sdd-project --init --diff');
283
+ console.error(' create-sdd-project --upgrade --diff');
284
+ console.error(' create-sdd-project --eject --diff');
285
+ process.exit(1);
286
+ }
287
+
288
+ if (projectName) {
289
+ console.error('Error: Cannot specify a project name with --diff.');
290
+ process.exit(1);
291
+ }
292
+
293
+ const cwd = process.cwd();
294
+
295
+ if (!fs.existsSync(path.join(cwd, 'package.json'))) {
296
+ console.error('Error: No package.json found in current directory.');
297
+ process.exit(1);
298
+ }
299
+
300
+ if (isEject) {
301
+ // Same validation as --eject
302
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
303
+ console.error('Error: ai-specs/ directory not found.');
304
+ console.error('SDD DevFlow does not appear to be installed. Nothing to eject.');
305
+ process.exit(1);
306
+ }
307
+
308
+ const { runEjectDiffReport } = require('../lib/diff-generator');
309
+ const { collectEjectInventory } = require('../lib/eject-generator');
310
+
311
+ const state = collectEjectInventory(cwd);
312
+ runEjectDiffReport(state);
313
+ return;
314
+ }
315
+
316
+ const { scan } = require('../lib/scanner');
317
+ const { buildInitDefaultConfig } = require('../lib/init-wizard');
318
+ const { runInitDiffReport, runUpgradeDiffReport } = require('../lib/diff-generator');
319
+
320
+ if (isInit) {
321
+ // Same validation as --init
322
+ if (fs.existsSync(path.join(cwd, 'ai-specs'))) {
323
+ console.error('Error: ai-specs/ directory already exists.');
324
+ console.error('SDD DevFlow appears to already be installed. Use --upgrade --diff instead.');
325
+ process.exit(1);
326
+ }
327
+
328
+ const scanResult = scan(cwd);
329
+ const config = buildInitDefaultConfig(scanResult);
330
+ runInitDiffReport(config);
331
+ return;
332
+ }
333
+
334
+ if (isUpgrade) {
335
+ // Same validation as --upgrade
336
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
337
+ console.error('Error: ai-specs/ directory not found.');
338
+ console.error('SDD DevFlow does not appear to be installed. Use --init --diff instead.');
339
+ process.exit(1);
340
+ }
341
+
342
+ const {
343
+ readInstalledVersion,
344
+ getPackageVersion,
345
+ detectAiTools,
346
+ detectProjectType,
347
+ readAutonomyLevel,
348
+ collectCustomAgents,
349
+ collectCustomCommands,
350
+ } = require('../lib/upgrade-generator');
351
+
352
+ const installedVersion = readInstalledVersion(cwd);
353
+ const packageVersion = getPackageVersion();
354
+
355
+ if (installedVersion === packageVersion && !isForce) {
356
+ console.log(`\nSDD DevFlow is already at version ${packageVersion}.`);
357
+ console.log('Use --force --diff to preview a re-install.\n');
358
+ return;
359
+ }
360
+
361
+ const scanResult = scan(cwd);
362
+ const aiTools = detectAiTools(cwd);
363
+ const projectType = detectProjectType(cwd);
364
+ const autonomy = readAutonomyLevel(cwd);
365
+ const customAgents = collectCustomAgents(cwd);
366
+ const customCommands = collectCustomCommands(cwd);
367
+ const settingsLocal = fs.existsSync(path.join(cwd, '.claude', 'settings.local.json'));
368
+
369
+ const config = buildInitDefaultConfig(scanResult);
370
+ config.aiTools = aiTools;
371
+ config.projectType = projectType;
372
+ config.autonomyLevel = autonomy.level;
373
+ config.autonomyName = autonomy.name;
374
+ config.installedVersion = installedVersion;
375
+
376
+ const state = {
377
+ installedVersion,
378
+ packageVersion,
379
+ aiTools,
380
+ projectType,
381
+ customAgents,
382
+ customCommands,
383
+ settingsLocal,
384
+ };
385
+
386
+ runUpgradeDiffReport(config, state);
387
+ }
388
+ }
389
+
219
390
  main().catch((err) => {
220
391
  console.error('\nError:', err.message);
221
392
  process.exit(1);
@@ -0,0 +1,287 @@
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
+ // ── Eject Diff ──────────────────────────────────────────────
275
+
276
+ function runEjectDiffReport(state) {
277
+ const { buildEjectSummary } = require('./eject-generator');
278
+
279
+ const lines = [];
280
+ lines.push('\nšŸ” SDD DevFlow — Preview (--eject)\n');
281
+ lines.push(buildEjectSummary(state).split('\n').slice(1).join('\n')); // skip the emoji header (already have our own)
282
+ lines.push('\n No files removed. Run without --diff to apply.\n');
283
+
284
+ console.log(lines.join('\n'));
285
+ }
286
+
287
+ module.exports = { runInitDiffReport, runUpgradeDiffReport, runEjectDiffReport };
@@ -0,0 +1,352 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { TEMPLATE_AGENTS } = require('./config');
6
+ const {
7
+ readInstalledVersion,
8
+ detectAiTools,
9
+ detectProjectType,
10
+ collectCustomAgents,
11
+ collectCustomCommands,
12
+ } = require('./upgrade-generator');
13
+
14
+ /**
15
+ * Collect inventory of what eject would remove vs preserve.
16
+ */
17
+ function collectEjectInventory(cwd) {
18
+ const installedVersion = readInstalledVersion(cwd);
19
+ const aiTools = detectAiTools(cwd);
20
+ const projectType = detectProjectType(cwd);
21
+ const customAgents = collectCustomAgents(cwd);
22
+ const customCommands = collectCustomCommands(cwd);
23
+
24
+ const hasSettingsLocal = fs.existsSync(path.join(cwd, '.claude', 'settings.local.json'));
25
+ const hasDocsDir = fs.existsSync(path.join(cwd, 'docs'));
26
+
27
+ // Detect SDD-generated CI workflow
28
+ let ciIsSddGenerated = false;
29
+ const ciPath = path.join(cwd, '.github', 'workflows', 'ci.yml');
30
+ if (fs.existsSync(ciPath)) {
31
+ const ciContent = fs.readFileSync(ciPath, 'utf8');
32
+ ciIsSddGenerated = ciContent.includes('Generated by SDD DevFlow');
33
+ }
34
+
35
+ // Detect SDD entries in .gitignore
36
+ let gitignoreHasSddEntries = false;
37
+ const gitignorePath = path.join(cwd, '.gitignore');
38
+ if (fs.existsSync(gitignorePath)) {
39
+ const content = fs.readFileSync(gitignorePath, 'utf8');
40
+ gitignoreHasSddEntries = content.includes('# SDD DevFlow');
41
+ }
42
+
43
+ return {
44
+ installedVersion,
45
+ aiTools,
46
+ projectType,
47
+ customAgents,
48
+ customCommands,
49
+ hasSettingsLocal,
50
+ hasDocsDir,
51
+ ciPath: fs.existsSync(ciPath) ? ciPath : null,
52
+ ciIsSddGenerated,
53
+ gitignoreHasSddEntries,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Build the eject summary for display.
59
+ */
60
+ function buildEjectSummary(state) {
61
+ const lines = [];
62
+ lines.push('šŸ—‘ļø SDD DevFlow Eject\n');
63
+ lines.push(` Installed version: ${state.installedVersion}`);
64
+ lines.push(` AI tools: ${state.aiTools === 'both' ? 'Claude Code + Gemini' : state.aiTools === 'claude' ? 'Claude Code' : 'Gemini'}`);
65
+ lines.push(` Project type: ${state.projectType}\n`);
66
+
67
+ // Will remove
68
+ lines.push(' Will remove:');
69
+ if (state.aiTools !== 'gemini') {
70
+ lines.push(' āœ— .claude/agents/ template agents');
71
+ lines.push(' āœ— .claude/skills/');
72
+ lines.push(' āœ— .claude/hooks/');
73
+ lines.push(' āœ— .claude/settings.json');
74
+ }
75
+ if (state.aiTools !== 'claude') {
76
+ lines.push(' āœ— .gemini/ entire directory');
77
+ }
78
+ lines.push(' āœ— ai-specs/ standards');
79
+ lines.push(' āœ— AGENTS.md');
80
+ if (state.aiTools !== 'gemini') lines.push(' āœ— CLAUDE.md');
81
+ if (state.aiTools !== 'claude') lines.push(' āœ— GEMINI.md');
82
+ lines.push(' āœ— .env.example');
83
+ lines.push(' āœ— .sdd-version');
84
+ if (state.ciIsSddGenerated) {
85
+ lines.push(' āœ— .github/workflows/ci.yml (SDD-generated)');
86
+ }
87
+
88
+ // Will preserve
89
+ const preserveItems = [];
90
+ if (state.customAgents.length > 0) {
91
+ for (const a of state.customAgents) {
92
+ preserveItems.push(`${a.relativePath} (custom agent)`);
93
+ }
94
+ }
95
+ if (state.customCommands.length > 0) {
96
+ for (const c of state.customCommands) {
97
+ preserveItems.push(`${c.relativePath} (custom command)`);
98
+ }
99
+ }
100
+ if (state.hasSettingsLocal) {
101
+ preserveItems.push('.claude/settings.local.json (personal settings)');
102
+ }
103
+ if (state.hasDocsDir) {
104
+ preserveItems.push('docs/ (project notes, specs, tickets)');
105
+ }
106
+ if (state.ciPath && !state.ciIsSddGenerated) {
107
+ preserveItems.push('.github/workflows/ci.yml (customized — not SDD-generated)');
108
+ }
109
+
110
+ if (preserveItems.length > 0) {
111
+ lines.push('\n Will preserve:');
112
+ for (const item of preserveItems) {
113
+ lines.push(` ⊘ ${item}`);
114
+ }
115
+ }
116
+
117
+ // Will modify
118
+ if (state.gitignoreHasSddEntries) {
119
+ lines.push('\n Will modify:');
120
+ lines.push(' ~ .gitignore remove SDD DevFlow section');
121
+ }
122
+
123
+ return lines.join('\n');
124
+ }
125
+
126
+ /**
127
+ * Execute the eject: remove all SDD-owned files, preserve user work.
128
+ */
129
+ function generateEject(cwd) {
130
+ const state = collectEjectInventory(cwd);
131
+ let removed = 0;
132
+ let preserved = 0;
133
+
134
+ console.log(`\nEjecting SDD DevFlow from ${path.basename(cwd)}...\n`);
135
+
136
+ // --- 1. Remove template agents (preserve custom) ---
137
+ for (const toolDir of ['.claude', '.gemini']) {
138
+ const agentsDir = path.join(cwd, toolDir, 'agents');
139
+ if (!fs.existsSync(agentsDir)) continue;
140
+
141
+ const files = fs.readdirSync(agentsDir);
142
+ for (const file of files) {
143
+ if (TEMPLATE_AGENTS.includes(file)) {
144
+ fs.unlinkSync(path.join(agentsDir, file));
145
+ removed++;
146
+ }
147
+ }
148
+
149
+ // Remove agents dir if empty
150
+ if (fs.existsSync(agentsDir) && fs.readdirSync(agentsDir).length === 0) {
151
+ fs.rmSync(agentsDir, { recursive: true });
152
+ }
153
+ }
154
+ step('Removed template agents');
155
+
156
+ // --- 2. Remove skills ---
157
+ for (const toolDir of ['.claude', '.gemini']) {
158
+ const skillsDir = path.join(cwd, toolDir, 'skills');
159
+ if (fs.existsSync(skillsDir)) {
160
+ removed += countFilesRecursive(skillsDir);
161
+ fs.rmSync(skillsDir, { recursive: true });
162
+ }
163
+ }
164
+ step('Removed skills');
165
+
166
+ // --- 3. Remove hooks ---
167
+ const hooksDir = path.join(cwd, '.claude', 'hooks');
168
+ if (fs.existsSync(hooksDir)) {
169
+ removed += countFilesRecursive(hooksDir);
170
+ fs.rmSync(hooksDir, { recursive: true });
171
+ }
172
+ step('Removed hooks');
173
+
174
+ // --- 4. Handle commands ---
175
+ // .claude/commands: only remove .gitkeep, keep custom commands
176
+ const claudeCommandsDir = path.join(cwd, '.claude', 'commands');
177
+ if (fs.existsSync(claudeCommandsDir)) {
178
+ const gitkeepPath = path.join(claudeCommandsDir, '.gitkeep');
179
+ if (fs.existsSync(gitkeepPath)) {
180
+ fs.unlinkSync(gitkeepPath);
181
+ removed++;
182
+ }
183
+ // Remove dir if empty
184
+ if (fs.readdirSync(claudeCommandsDir).length === 0) {
185
+ fs.rmSync(claudeCommandsDir, { recursive: true });
186
+ }
187
+ }
188
+
189
+ // .gemini/commands: remove entirely (SDD-owned TOML commands)
190
+ const geminiCommandsDir = path.join(cwd, '.gemini', 'commands');
191
+ if (fs.existsSync(geminiCommandsDir)) {
192
+ removed += countFilesRecursive(geminiCommandsDir);
193
+ fs.rmSync(geminiCommandsDir, { recursive: true });
194
+ }
195
+
196
+ // --- 5. Handle .claude/settings.json ---
197
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
198
+ if (fs.existsSync(settingsPath)) {
199
+ try {
200
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
201
+ delete settings.hooks;
202
+
203
+ // If nothing meaningful remains, delete the file
204
+ const remainingKeys = Object.keys(settings);
205
+ if (remainingKeys.length === 0) {
206
+ fs.unlinkSync(settingsPath);
207
+ removed++;
208
+ } else {
209
+ // Keep permissions and other user settings
210
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
211
+ preserved++;
212
+ }
213
+ } catch {
214
+ // Corrupted JSON — remove it
215
+ fs.unlinkSync(settingsPath);
216
+ removed++;
217
+ }
218
+ }
219
+ step('Handled settings.json');
220
+
221
+ // --- 6. Remove .gemini/ entirely (styles, settings) ---
222
+ // styles
223
+ const geminiStylesDir = path.join(cwd, '.gemini', 'styles');
224
+ if (fs.existsSync(geminiStylesDir)) {
225
+ removed += countFilesRecursive(geminiStylesDir);
226
+ fs.rmSync(geminiStylesDir, { recursive: true });
227
+ }
228
+ // settings.json
229
+ const geminiSettingsPath = path.join(cwd, '.gemini', 'settings.json');
230
+ if (fs.existsSync(geminiSettingsPath)) {
231
+ fs.unlinkSync(geminiSettingsPath);
232
+ removed++;
233
+ }
234
+
235
+ // --- 7. Remove ai-specs/ entirely ---
236
+ const aiSpecsDir = path.join(cwd, 'ai-specs');
237
+ if (fs.existsSync(aiSpecsDir)) {
238
+ removed += countFilesRecursive(aiSpecsDir);
239
+ fs.rmSync(aiSpecsDir, { recursive: true });
240
+ }
241
+ step('Removed ai-specs/');
242
+
243
+ // --- 8. Remove top-level SDD files ---
244
+ const topLevelFiles = ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md', '.env.example', '.sdd-version'];
245
+ for (const file of topLevelFiles) {
246
+ const filePath = path.join(cwd, file);
247
+ if (fs.existsSync(filePath)) {
248
+ fs.unlinkSync(filePath);
249
+ removed++;
250
+ }
251
+ }
252
+ step('Removed AGENTS.md, CLAUDE.md, GEMINI.md, .env.example, .sdd-version');
253
+
254
+ // --- 9. Handle CI workflow ---
255
+ if (state.ciIsSddGenerated) {
256
+ fs.unlinkSync(state.ciPath);
257
+ removed++;
258
+ step('Removed .github/workflows/ci.yml (SDD-generated)');
259
+
260
+ // Clean empty dirs
261
+ const workflowsDir = path.join(cwd, '.github', 'workflows');
262
+ if (fs.existsSync(workflowsDir) && fs.readdirSync(workflowsDir).length === 0) {
263
+ fs.rmSync(workflowsDir, { recursive: true });
264
+ }
265
+ const githubDir = path.join(cwd, '.github');
266
+ if (fs.existsSync(githubDir) && fs.readdirSync(githubDir).length === 0) {
267
+ fs.rmSync(githubDir, { recursive: true });
268
+ }
269
+ } else if (state.ciPath) {
270
+ preserved++;
271
+ step('Preserved .github/workflows/ci.yml (customized)');
272
+ }
273
+
274
+ // --- 10. Clean .gitignore ---
275
+ if (state.gitignoreHasSddEntries) {
276
+ const gitignorePath = path.join(cwd, '.gitignore');
277
+ let content = fs.readFileSync(gitignorePath, 'utf8');
278
+ // Remove the SDD DevFlow section (tolerant of leading/trailing newlines)
279
+ content = content.replace(/\n?# SDD DevFlow\ndocs\/tickets\/\*\.md\n!docs\/tickets\/\.gitkeep\n?/, '');
280
+ fs.writeFileSync(gitignorePath, content, 'utf8');
281
+ step('Cleaned .gitignore');
282
+ }
283
+
284
+ // --- 11. Clean empty parent directories ---
285
+ for (const dir of ['.claude', '.gemini']) {
286
+ const dirPath = path.join(cwd, dir);
287
+ if (fs.existsSync(dirPath)) {
288
+ cleanEmptyDirs(dirPath);
289
+ }
290
+ }
291
+
292
+ // Count preserved items
293
+ preserved += state.customAgents.length + state.customCommands.length;
294
+ if (state.hasSettingsLocal) preserved++;
295
+ if (state.hasDocsDir) preserved++;
296
+
297
+ // --- Show result ---
298
+ console.log(`\nāœ… SDD DevFlow ejected (was v${state.installedVersion})\n`);
299
+ console.log(` Removed: ${removed} SDD files`);
300
+ if (preserved > 0) {
301
+ const items = [];
302
+ if (state.customAgents.length > 0) items.push(`${state.customAgents.length} custom agent(s)`);
303
+ if (state.customCommands.length > 0) items.push(`${state.customCommands.length} custom command(s)`);
304
+ if (state.hasSettingsLocal) items.push('personal settings');
305
+ if (state.hasDocsDir) items.push('docs/');
306
+ if (state.ciPath && !state.ciIsSddGenerated) items.push('CI workflow (customized)');
307
+ console.log(` Preserved: ${items.join(', ')}`);
308
+ }
309
+ console.log('\n To re-install later: npx create-sdd-project --init\n');
310
+ }
311
+
312
+ function step(msg) {
313
+ console.log(` āœ“ ${msg}`);
314
+ }
315
+
316
+ function countFilesRecursive(dir) {
317
+ let count = 0;
318
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
319
+ if (entry.isDirectory()) {
320
+ count += countFilesRecursive(path.join(dir, entry.name));
321
+ } else {
322
+ count++;
323
+ }
324
+ }
325
+ return count;
326
+ }
327
+
328
+ /**
329
+ * Remove a directory if empty, then check parent recursively.
330
+ */
331
+ function cleanEmptyDirs(dirPath) {
332
+ if (!fs.existsSync(dirPath)) return;
333
+
334
+ // First clean subdirectories
335
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
336
+ for (const entry of entries) {
337
+ if (entry.isDirectory()) {
338
+ cleanEmptyDirs(path.join(dirPath, entry.name));
339
+ }
340
+ }
341
+
342
+ // Then remove if empty
343
+ if (fs.existsSync(dirPath) && fs.readdirSync(dirPath).length === 0) {
344
+ fs.rmSync(dirPath, { recursive: true });
345
+ }
346
+ }
347
+
348
+ module.exports = {
349
+ collectEjectInventory,
350
+ buildEjectSummary,
351
+ generateEject,
352
+ };
@@ -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.8.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"