create-sdd-project 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -168,14 +168,62 @@ 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
+
171
218
  ### Preview Changes (--diff)
172
219
 
173
- See what `--init` or `--upgrade` would do without modifying anything:
220
+ See what `--init`, `--upgrade`, or `--eject` would do without modifying anything:
174
221
 
175
222
  ```bash
176
223
  cd your-existing-project
177
224
  npx create-sdd-project --init --diff # Preview init
178
225
  npx create-sdd-project --upgrade --diff # Preview upgrade
226
+ npx create-sdd-project --eject --diff # Preview eject
179
227
  ```
180
228
 
181
229
  Shows detected stack, files that would be created/replaced/preserved, and standards status — zero filesystem writes.
package/bin/cli.js CHANGED
@@ -11,6 +11,7 @@ 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');
14
15
  const isDiff = args.includes('--diff');
15
16
 
16
17
  async function main() {
@@ -20,6 +21,9 @@ async function main() {
20
21
  if (isDoctor) {
21
22
  return runDoctorCmd();
22
23
  }
24
+ if (isEject) {
25
+ return runEject();
26
+ }
23
27
  if (isUpgrade) {
24
28
  return runUpgrade();
25
29
  }
@@ -34,9 +38,9 @@ function runDoctorCmd() {
34
38
 
35
39
  const cwd = process.cwd();
36
40
 
37
- // Validate: must be in an existing project
38
- if (!fs.existsSync(path.join(cwd, 'package.json'))) {
39
- console.error('Error: No package.json found in current directory.');
41
+ // Validate: must be in a project with SDD installed
42
+ if (!fs.existsSync(path.join(cwd, 'package.json')) && !fs.existsSync(path.join(cwd, 'ai-specs'))) {
43
+ console.error('Error: No package.json or ai-specs/ found in current directory.');
40
44
  console.error('The --doctor flag must be run from inside an existing project.');
41
45
  process.exit(1);
42
46
  }
@@ -138,13 +142,6 @@ async function runUpgrade() {
138
142
 
139
143
  const cwd = process.cwd();
140
144
 
141
- // Validate: must be in an existing project
142
- if (!fs.existsSync(path.join(cwd, 'package.json'))) {
143
- console.error('Error: No package.json found in current directory.');
144
- console.error('The --upgrade flag must be run from inside an existing project.');
145
- process.exit(1);
146
- }
147
-
148
145
  // Validate: SDD must be installed
149
146
  if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
150
147
  console.error('Error: ai-specs/ directory not found.');
@@ -220,11 +217,57 @@ async function runUpgrade() {
220
217
  generateUpgrade(config);
221
218
  }
222
219
 
220
+ async function runEject() {
221
+ const readline = require('readline');
222
+ const {
223
+ collectEjectInventory,
224
+ buildEjectSummary,
225
+ generateEject,
226
+ } = require('../lib/eject-generator');
227
+
228
+ const cwd = process.cwd();
229
+
230
+ // Validate: SDD must be installed
231
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
232
+ console.error('Error: ai-specs/ directory not found.');
233
+ console.error('SDD DevFlow does not appear to be installed. Nothing to eject.');
234
+ process.exit(1);
235
+ }
236
+
237
+ // Validate: no project name with --eject
238
+ if (projectName) {
239
+ console.error('Error: Cannot specify a project name with --eject.');
240
+ console.error('Usage: create-sdd-project --eject');
241
+ process.exit(1);
242
+ }
243
+
244
+ const state = collectEjectInventory(cwd);
245
+
246
+ if (!useDefaults) {
247
+ console.log('\n' + buildEjectSummary(state));
248
+ console.log('');
249
+
250
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
251
+ const answer = await new Promise((resolve) => {
252
+ rl.question(' Proceed? (y/N) ', resolve);
253
+ });
254
+ rl.close();
255
+
256
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
257
+ console.log('\nEject cancelled.\n');
258
+ return;
259
+ }
260
+ }
261
+
262
+ generateEject(cwd);
263
+ }
264
+
223
265
  async function runDiff() {
224
- if (!isInit && !isUpgrade) {
225
- console.error('Error: --diff must be combined with --init or --upgrade.');
266
+ if (!isInit && !isUpgrade && !isEject) {
267
+ console.error('Error: --diff must be combined with --init, --upgrade, or --eject.');
226
268
  console.error('Usage: create-sdd-project --init --diff');
227
269
  console.error(' create-sdd-project --upgrade --diff');
270
+ console.error(' create-sdd-project --eject --diff');
228
271
  process.exit(1);
229
272
  }
230
273
 
@@ -235,9 +278,20 @@ async function runDiff() {
235
278
 
236
279
  const cwd = process.cwd();
237
280
 
238
- if (!fs.existsSync(path.join(cwd, 'package.json'))) {
239
- console.error('Error: No package.json found in current directory.');
240
- process.exit(1);
281
+ if (isEject) {
282
+ // Same validation as --eject
283
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
284
+ console.error('Error: ai-specs/ directory not found.');
285
+ console.error('SDD DevFlow does not appear to be installed. Nothing to eject.');
286
+ process.exit(1);
287
+ }
288
+
289
+ const { runEjectDiffReport } = require('../lib/diff-generator');
290
+ const { collectEjectInventory } = require('../lib/eject-generator');
291
+
292
+ const state = collectEjectInventory(cwd);
293
+ runEjectDiffReport(state);
294
+ return;
241
295
  }
242
296
 
243
297
  const { scan } = require('../lib/scanner');
@@ -246,6 +300,11 @@ async function runDiff() {
246
300
 
247
301
  if (isInit) {
248
302
  // Same validation as --init
303
+ if (!fs.existsSync(path.join(cwd, 'package.json'))) {
304
+ console.error('Error: No package.json found in current directory.');
305
+ console.error('The --init flag must be run from inside an existing project.');
306
+ process.exit(1);
307
+ }
249
308
  if (fs.existsSync(path.join(cwd, 'ai-specs'))) {
250
309
  console.error('Error: ai-specs/ directory already exists.');
251
310
  console.error('SDD DevFlow appears to already be installed. Use --upgrade --diff instead.');
@@ -271,4 +271,17 @@ function runUpgradeDiffReport(config, state) {
271
271
  console.log(lines.join('\n'));
272
272
  }
273
273
 
274
- module.exports = { runInitDiffReport, runUpgradeDiffReport };
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
+ };
package/lib/generator.js CHANGED
@@ -19,6 +19,16 @@ function generate(config) {
19
19
  step('Copying template files');
20
20
  fs.cpSync(templateDir, dest, { recursive: true });
21
21
 
22
+ // 1b. Rename gitignore → .gitignore (npm strips .gitignore during publish)
23
+ const gitignoreSrc = path.join(dest, 'gitignore');
24
+ if (fs.existsSync(gitignoreSrc)) {
25
+ fs.renameSync(gitignoreSrc, path.join(dest, '.gitignore'));
26
+ }
27
+
28
+ // 1c. Generate package.json
29
+ step('Generating package.json');
30
+ generatePackageJson(dest, config);
31
+
22
32
  // 2. Configure key_facts.md
23
33
  step(`Configuring project: ${config.projectName}`);
24
34
  updateKeyFacts(dest, config);
@@ -396,4 +406,23 @@ function adaptCiWorkflow(dest, config) {
396
406
  // Default (PostgreSQL) — template already has correct services
397
407
  }
398
408
 
409
+ function generatePackageJson(dest, config) {
410
+ const pkg = {
411
+ name: config.projectName,
412
+ version: '0.0.1',
413
+ private: true,
414
+ };
415
+ if (config.description) {
416
+ pkg.description = config.description;
417
+ }
418
+ pkg.scripts = {
419
+ test: 'echo "Error: no test specified" && exit 1',
420
+ };
421
+ fs.writeFileSync(
422
+ path.join(dest, 'package.json'),
423
+ JSON.stringify(pkg, null, 2) + '\n',
424
+ 'utf8'
425
+ );
426
+ }
427
+
399
428
  module.exports = { generate };
@@ -467,6 +467,33 @@ function generateUpgrade(config) {
467
467
 
468
468
  step('Adapted files for project type and stack');
469
469
 
470
+ // --- g2) Create package.json if missing (projects created with v0.8.0 bug) ---
471
+ const pkgJsonPath = path.join(dest, 'package.json');
472
+ if (!fs.existsSync(pkgJsonPath)) {
473
+ const pkg = {
474
+ name: config.projectName,
475
+ version: '0.0.1',
476
+ private: true,
477
+ scripts: {
478
+ test: 'echo "Error: no test specified" && exit 1',
479
+ },
480
+ };
481
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
482
+ step('Created missing package.json');
483
+ replaced++;
484
+ }
485
+
486
+ // --- g3) Create .gitignore if missing (projects created with v0.8.0 bug) ---
487
+ const gitignorePath = path.join(dest, '.gitignore');
488
+ if (!fs.existsSync(gitignorePath)) {
489
+ const gitignoreSrc = path.join(templateDir, 'gitignore');
490
+ if (fs.existsSync(gitignoreSrc)) {
491
+ fs.copyFileSync(gitignoreSrc, gitignorePath);
492
+ }
493
+ step('Created missing .gitignore');
494
+ replaced++;
495
+ }
496
+
470
497
  // --- g) Write version marker ---
471
498
  const newVersion = getPackageVersion();
472
499
  fs.writeFileSync(path.join(dest, '.sdd-version'), newVersion + '\n', 'utf8');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Create a new SDD DevFlow project with AI-assisted development workflow",
5
5
  "bin": {
6
6
  "create-sdd-project": "bin/cli.js"
@@ -0,0 +1,55 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npm test:*)",
5
+ "Bash(npm run lint:*)",
6
+ "Bash(npm run build:*)",
7
+ "Bash(git status:*)",
8
+ "Bash(git diff:*)",
9
+ "Bash(git log:*)",
10
+ "Bash(git branch:*)",
11
+ "Bash(git checkout -b:*)",
12
+ "Bash(git add:*)",
13
+ "Bash(git commit:*)",
14
+ "Bash(git push:*)",
15
+ "Bash(npx prisma:*)",
16
+ "Read",
17
+ "Glob",
18
+ "Grep"
19
+ ],
20
+ "deny": []
21
+ },
22
+ "hooks": {
23
+ "Notification": [
24
+ {
25
+ "matcher": "permission_prompt",
26
+ "hooks": [
27
+ {
28
+ "type": "command",
29
+ "command": "osascript -e 'display notification \"Permission needed\" with title \"Claude Code\" with subtitle \"Waiting for approval\"'"
30
+ }
31
+ ]
32
+ },
33
+ {
34
+ "matcher": "idle_prompt",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "osascript -e 'display notification \"Task finished, waiting for input\" with title \"Claude Code\"'"
39
+ }
40
+ ]
41
+ }
42
+ ],
43
+ "Stop": [
44
+ {
45
+ "matcher": "",
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "osascript -e 'display notification \"Response complete\" with title \"Claude Code\"'"
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ }
@@ -0,0 +1,48 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ .next/
7
+ out/
8
+ build/
9
+
10
+ # Environment — NEVER commit
11
+ .env
12
+ .env.local
13
+ .env.*.local
14
+ .env.development
15
+ .env.staging
16
+ .env.production
17
+ .npmrc
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # IDE
24
+ .idea/
25
+ .vscode/
26
+ *.swp
27
+ *.swo
28
+
29
+ # Testing
30
+ coverage/
31
+
32
+ # Prisma
33
+ backend/generated/
34
+
35
+ # Logs
36
+ *.log
37
+ npm-debug.log*
38
+
39
+ # Cache
40
+ .turbo/
41
+ .cache/
42
+
43
+ # Secrets and keys
44
+ *.pem
45
+ *.key
46
+
47
+ # Claude Code (local settings are personal — hooks, permissions, notifications)
48
+ .claude/settings.local.json