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 +49 -1
- package/bin/cli.js +74 -15
- package/lib/diff-generator.js +14 -1
- package/lib/eject-generator.js +352 -0
- package/lib/generator.js +29 -0
- package/lib/upgrade-generator.js +27 -0
- package/package.json +1 -1
- package/template/.claude/settings.local.json +55 -0
- package/template/gitignore +48 -0
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
|
|
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
|
|
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 --
|
|
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 (
|
|
239
|
-
|
|
240
|
-
|
|
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.');
|
package/lib/diff-generator.js
CHANGED
|
@@ -271,4 +271,17 @@ function runUpgradeDiffReport(config, state) {
|
|
|
271
271
|
console.log(lines.join('\n'));
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
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 };
|
package/lib/upgrade-generator.js
CHANGED
|
@@ -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
|
@@ -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
|