create-sdd-project 0.4.2 → 0.5.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
@@ -1,6 +1,7 @@
1
1
  # SDD DevFlow
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/create-sdd-project)](https://www.npmjs.com/package/create-sdd-project)
4
+ [![npm downloads](https://img.shields.io/npm/dm/create-sdd-project)](https://www.npmjs.com/package/create-sdd-project)
4
5
  [![license](https://img.shields.io/npm/l/create-sdd-project)](LICENSE)
5
6
  [![node](https://img.shields.io/node/v/create-sdd-project)](package.json)
6
7
 
@@ -16,7 +17,34 @@ A complete development methodology for Claude Code and Gemini that combines spec
16
17
  npx create-sdd-project my-app
17
18
  ```
18
19
 
19
- The interactive wizard asks about your stack, AI tools, and autonomy level. For defaults (fullstack Express+Next.js):
20
+ The interactive wizard asks about your stack, AI tools, and autonomy level:
21
+
22
+ ```
23
+ šŸš€ Create SDD DevFlow Project
24
+
25
+ ── Step 1: Project Basics ──────────────────────
26
+ Project name: my-app
27
+ Brief project description: Task management API
28
+
29
+ ── Step 3: Project Type & Tech Stack ──────────
30
+ 1) Backend + Frontend (monorepo) ← default
31
+ 2) Backend only
32
+ 3) Frontend only
33
+
34
+ ── Step 4: AI Tools ────────────────────────────
35
+ 1) Claude Code + Gemini ← default
36
+ 2) Claude Code only
37
+ 3) Gemini only
38
+
39
+ ── Step 5: Workflow Configuration ──────────────
40
+ Autonomy level:
41
+ 1) L1 Full Control — Human approves every checkpoint
42
+ 2) L2 Trusted — Human reviews plans + merges only ← default
43
+ 3) L3 Autopilot — Human only approves merges
44
+ 4) L4 Full Auto — No human checkpoints, CI/CD gates only
45
+ ```
46
+
47
+ For defaults (fullstack Express+Next.js, L2 autonomy):
20
48
 
21
49
  ```bash
22
50
  npx create-sdd-project my-app --yes
@@ -29,18 +57,108 @@ cd your-existing-project
29
57
  npx create-sdd-project --init
30
58
  ```
31
59
 
32
- Scans your project, detects your stack and architecture, and installs SDD files adapted to your project. Never modifies existing code or overwrites existing files.
60
+ The scanner detects your stack and installs adapted SDD files:
61
+
62
+ ```
63
+ šŸ” Scanning project...
64
+
65
+ Project: my-api
66
+ Language: TypeScript
67
+ Backend: Express + Mongoose + MongoDB
68
+ Frontend: Not detected
69
+ Architecture: Layered (controllers + handlers + managers)
70
+ Tests: Jest (3 test files)
71
+ Monorepo: No
72
+
73
+ Adding SDD DevFlow to my-api...
74
+
75
+ āœ“ Installing Claude Code config (agents, skills, commands, hooks)
76
+ āœ“ Installing Gemini config (agents, skills, commands)
77
+ āœ“ Creating ai-specs/specs/ (4 standards files)
78
+ āœ“ Creating docs/project_notes/ (product tracker, memory)
79
+ āœ“ Creating AGENTS.md
80
+ āœ“ Setting autonomy level: L2 (Trusted)
81
+
82
+ Done! Next steps:
83
+ git add -A && git commit -m "chore: add SDD DevFlow to existing project"
84
+ # Open in your AI coding tool and run: add feature "your first feature"
85
+ ```
86
+
87
+ Never modifies existing code or overwrites existing files.
88
+
89
+ ### Upgrade
90
+
91
+ When a new version of SDD DevFlow is released, upgrade your project's template files:
92
+
93
+ ```bash
94
+ cd your-existing-project
95
+ npx create-sdd-project@latest --upgrade
96
+ ```
97
+
98
+ ```
99
+ šŸ”„ SDD DevFlow Upgrade
100
+
101
+ Current version: 0.4.2
102
+ New version: 0.5.0
103
+ AI tools: Claude Code + Gemini
104
+ Project type: backend
105
+
106
+ Will replace:
107
+ āœ“ .claude/ (agents, skills, hooks)
108
+ āœ“ .gemini/ (agents, skills, commands)
109
+ āœ“ AGENTS.md, CLAUDE.md / GEMINI.md
110
+ āœ“ .env.example
111
+
112
+ Will preserve:
113
+ ⊘ .claude/settings.local.json (personal settings)
114
+ ⊘ docs/project_notes/* (project memory)
115
+ ⊘ docs/specs/* (your specs)
116
+ ⊘ docs/tickets/* (your tickets)
117
+ ⊘ .gitignore
118
+
119
+ Standards:
120
+ āœ“ ai-specs/specs/base-standards.mdc — unchanged, will update
121
+ ⚠ ai-specs/specs/backend-standards.mdc — modified by you, preserved
122
+
123
+ Proceed? (y/N)
124
+ ```
125
+
126
+ **What gets upgraded:**
127
+ - Agent definitions, skills, hooks, and commands
128
+ - `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.env.example`
129
+ - Standards files (`.mdc`) — only if you haven't customized them
130
+
131
+ **What is always preserved:**
132
+ - Your project documentation (`docs/project_notes/`, `docs/specs/`, `docs/tickets/`)
133
+ - Custom agents you added to `.claude/agents/` or `.gemini/agents/`
134
+ - Custom commands in `.claude/commands/`
135
+ - Personal settings (`.claude/settings.local.json`)
136
+ - Your `.gitignore`
137
+ - Autonomy level setting
138
+
139
+ For non-interactive upgrades (CI/scripts): `npx create-sdd-project@latest --upgrade --yes`
33
140
 
34
141
  ### After Setup
35
142
 
36
- Open in your AI coding tool and run:
143
+ Open your project in Claude Code or Gemini and start building:
144
+
145
+ **Claude Code:**
146
+ ```
147
+ /add-feature "user authentication with JWT"
148
+ /start-task F001
149
+ /show-progress
150
+ /next-task
151
+ ```
37
152
 
153
+ **Gemini:**
38
154
  ```
39
- add feature "your first feature"
40
- start task F001
155
+ /add-feature "user authentication with JWT"
156
+ /start-task F001
157
+ /show-progress
158
+ /next-task
41
159
  ```
42
160
 
43
- The workflow skill guides you through each step with checkpoints based on your autonomy level.
161
+ The workflow skill guides you through each step — from spec writing to implementation to code review — with checkpoints based on your autonomy level.
44
162
 
45
163
  ---
46
164
 
@@ -149,7 +267,7 @@ When running `--init` on an existing project, the scanner automatically detects:
149
267
  | **Component libraries** | Radix UI, Headless UI, Material UI, Chakra UI, Ant Design |
150
268
  | **State management** | Zustand, Redux, Jotai, TanStack Query, Recoil, Pinia, MobX |
151
269
  | **Testing** | Jest, Vitest, Mocha (unit) + Playwright, Cypress (e2e) |
152
- | **Architecture** | MVC, DDD, feature-based, handler-based, flat |
270
+ | **Architecture** | MVC, DDD, feature-based, handler-based, layered, flat |
153
271
  | **Project type** | Monorepo (workspaces, Lerna, Turbo, pnpm) or single-package |
154
272
 
155
273
  Standards files are adapted to match your actual architecture — not generic defaults.
@@ -203,7 +321,7 @@ Configurable via the wizard or `<!-- CONFIG -->` comments in template files:
203
321
 
204
322
  - **Backend**: Node.js + Express + Prisma + PostgreSQL
205
323
  - **Frontend**: Next.js (App Router) + Tailwind CSS + Radix UI + Zustand
206
- - **Shared Types**: Zod schemas with `z.infer<>` for TypeScript types
324
+ - **Shared Types**: Validation schemas with TypeScript type inference
207
325
  - **Testing**: Jest (unit) + Playwright (e2e)
208
326
  - **Methodology**: TDD + DDD + Spec-Driven Development
209
327
 
@@ -214,7 +332,7 @@ These 6 principles apply to ALL tasks, ALL agents, ALL complexity levels:
214
332
  1. **Spec First** — No implementation without an approved specification
215
333
  2. **Small Tasks** — Work in baby steps, one at a time
216
334
  3. **Test-Driven Development** — Write tests before implementation
217
- 4. **Type Safety** — Strict TypeScript, no `any`, runtime validation with Zod
335
+ 4. **Type Safety** — Strict TypeScript, no `any`, runtime validation at boundaries
218
336
  5. **English Only** — All code, comments, docs, commits, and tickets in English
219
337
  6. **Reuse Over Recreate** — Always check existing code before proposing new files
220
338
 
@@ -224,21 +342,25 @@ These 6 principles apply to ALL tasks, ALL agents, ALL complexity levels:
224
342
  - Node.js 18+
225
343
  - `jq` (for quick-scan hook): `brew install jq` (macOS) or `apt install jq` (Linux)
226
344
 
227
- ## Manual Setup (Alternative)
345
+ ## Manual Setup
228
346
 
229
- If you prefer manual configuration over the CLI wizard:
347
+ If you prefer manual configuration over the CLI wizard, copy the template directory and look for `<!-- CONFIG: ... -->` comments in the files to customize:
230
348
 
231
349
  ```bash
232
- cp -r template/ /path/to/your-project/
350
+ cp -r node_modules/create-sdd-project/template/ /path/to/your-project/
233
351
  ```
234
352
 
235
- Then look for `<!-- CONFIG: ... -->` comments in the files to customize.
236
-
237
353
  ## Roadmap
238
354
 
239
- - **Agent Teams**: Parallel execution of independent tasks (waiting for Claude Code Agent Teams to stabilize)
240
- - **PM Agent + L5 Autonomous**: AI-driven feature orchestration with human review at milestone boundaries
355
+ - **Monorepo improvements**: Better support for pnpm workspaces and Turbo
356
+ - **SDD Upgrade/Migration**: Version bumps for projects already using SDD
241
357
  - **Retrofit Testing**: Automated test generation for existing projects with low coverage
358
+ - **Agent Teams**: Parallel execution of independent tasks
359
+ - **PM Agent + L5 Autonomous**: AI-driven feature orchestration with human review at milestone boundaries
360
+
361
+ ## Contributing
362
+
363
+ Found a bug or have a suggestion? [Open an issue on GitHub](https://github.com/pbojeda/sdd-devflow/issues).
242
364
 
243
365
  ## License
244
366
 
package/bin/cli.js CHANGED
@@ -8,8 +8,13 @@ const args = process.argv.slice(2);
8
8
  const projectName = args.find((a) => !a.startsWith('-'));
9
9
  const useDefaults = args.includes('--yes') || args.includes('-y');
10
10
  const isInit = args.includes('--init');
11
+ const isUpgrade = args.includes('--upgrade');
12
+ const isForce = args.includes('--force');
11
13
 
12
14
  async function main() {
15
+ if (isUpgrade) {
16
+ return runUpgrade();
17
+ }
13
18
  if (isInit) {
14
19
  return runInit();
15
20
  }
@@ -90,6 +95,106 @@ async function runInit() {
90
95
  generateInit(config);
91
96
  }
92
97
 
98
+ async function runUpgrade() {
99
+ const { scan } = require('../lib/scanner');
100
+ const { buildInitDefaultConfig } = require('../lib/init-wizard');
101
+ const readline = require('readline');
102
+ const {
103
+ generateUpgrade,
104
+ readInstalledVersion,
105
+ getPackageVersion,
106
+ detectAiTools,
107
+ detectProjectType,
108
+ readAutonomyLevel,
109
+ collectCustomAgents,
110
+ collectCustomCommands,
111
+ buildSummary,
112
+ } = require('../lib/upgrade-generator');
113
+
114
+ const cwd = process.cwd();
115
+
116
+ // Validate: must be in an existing project
117
+ if (!fs.existsSync(path.join(cwd, 'package.json'))) {
118
+ console.error('Error: No package.json found in current directory.');
119
+ console.error('The --upgrade flag must be run from inside an existing project.');
120
+ process.exit(1);
121
+ }
122
+
123
+ // Validate: SDD must be installed
124
+ if (!fs.existsSync(path.join(cwd, 'ai-specs'))) {
125
+ console.error('Error: ai-specs/ directory not found.');
126
+ console.error('SDD DevFlow does not appear to be installed. Use --init first.');
127
+ process.exit(1);
128
+ }
129
+
130
+ // Validate: no project name with --upgrade
131
+ if (projectName) {
132
+ console.error('Error: Cannot specify a project name with --upgrade.');
133
+ console.error('Usage: create-sdd-project --upgrade');
134
+ process.exit(1);
135
+ }
136
+
137
+ // Read current state
138
+ const installedVersion = readInstalledVersion(cwd);
139
+ const packageVersion = getPackageVersion();
140
+
141
+ // Same version check
142
+ if (installedVersion === packageVersion && !isForce) {
143
+ console.log(`\nSDD DevFlow is already at version ${packageVersion}.`);
144
+ console.log('Use --force to re-install the same version.\n');
145
+ return;
146
+ }
147
+
148
+ // Scan project
149
+ const scanResult = scan(cwd);
150
+
151
+ // Detect current config from installed files
152
+ const aiTools = detectAiTools(cwd);
153
+ const projectType = detectProjectType(cwd);
154
+ const autonomy = readAutonomyLevel(cwd);
155
+ const customAgents = collectCustomAgents(cwd);
156
+ const customCommands = collectCustomCommands(cwd);
157
+ const settingsLocal = fs.existsSync(path.join(cwd, '.claude', 'settings.local.json'));
158
+
159
+ // Build config (reuse init defaults as base)
160
+ const config = buildInitDefaultConfig(scanResult);
161
+ config.aiTools = aiTools;
162
+ config.projectType = projectType;
163
+ config.autonomyLevel = autonomy.level;
164
+ config.autonomyName = autonomy.name;
165
+ config.installedVersion = installedVersion;
166
+
167
+ // Build and show summary
168
+ const state = {
169
+ installedVersion,
170
+ packageVersion,
171
+ aiTools,
172
+ projectType,
173
+ customAgents,
174
+ customCommands,
175
+ settingsLocal,
176
+ standardsStatus: [], // computed during generation
177
+ };
178
+
179
+ if (!useDefaults) {
180
+ console.log('\n' + buildSummary(state));
181
+ console.log('');
182
+
183
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
184
+ const answer = await new Promise((resolve) => {
185
+ rl.question(' Proceed? (y/N) ', resolve);
186
+ });
187
+ rl.close();
188
+
189
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
190
+ console.log('\nUpgrade cancelled.\n');
191
+ return;
192
+ }
193
+ }
194
+
195
+ generateUpgrade(config);
196
+ }
197
+
93
198
  main().catch((err) => {
94
199
  console.error('\nError:', err.message);
95
200
  process.exit(1);
package/lib/config.js CHANGED
@@ -93,6 +93,16 @@ const BRANCHING_STRATEGIES = [
93
93
  const FRONTEND_AGENTS = ['frontend-developer.md', 'frontend-planner.md'];
94
94
  const BACKEND_AGENTS = ['backend-developer.md', 'backend-planner.md', 'database-architect.md'];
95
95
 
96
+ // All template-provided agent files (for upgrade: detect custom agents)
97
+ const TEMPLATE_AGENTS = [
98
+ ...FRONTEND_AGENTS,
99
+ ...BACKEND_AGENTS,
100
+ 'spec-creator.md',
101
+ 'code-review-specialist.md',
102
+ 'production-code-validator.md',
103
+ 'qa-engineer.md',
104
+ ];
105
+
96
106
  module.exports = {
97
107
  DEFAULTS,
98
108
  PROJECT_TYPES,
@@ -103,4 +113,5 @@ module.exports = {
103
113
  BRANCHING_STRATEGIES,
104
114
  FRONTEND_AGENTS,
105
115
  BACKEND_AGENTS,
116
+ TEMPLATE_AGENTS,
106
117
  };
package/lib/generator.js CHANGED
@@ -77,6 +77,10 @@ function generate(config) {
77
77
  safeDelete(path.join(dest, 'CLAUDE.md'));
78
78
  }
79
79
 
80
+ // Write version marker
81
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
82
+ fs.writeFileSync(path.join(dest, '.sdd-version'), pkg.version + '\n', 'utf8');
83
+
80
84
  // Show notes
81
85
  const notes = collectNotes(config);
82
86
  if (notes.length > 0) {
@@ -230,6 +230,10 @@ function generateInit(config) {
230
230
  console.log(' Referenced in docs/project_notes/key_facts.md');
231
231
  }
232
232
 
233
+ // Write version marker
234
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
235
+ fs.writeFileSync(path.join(dest, '.sdd-version'), pkg.version + '\n', 'utf8');
236
+
233
237
  // Done
234
238
  console.log(`\nDone! Next steps:`);
235
239
  console.log(` git add -A && git commit -m "chore: add SDD DevFlow to existing project"`);
@@ -996,4 +1000,14 @@ function appendGitignore(dest, skipped) {
996
1000
  }
997
1001
  }
998
1002
 
999
- module.exports = { generateInit };
1003
+ module.exports = {
1004
+ generateInit,
1005
+ adaptBaseStandards,
1006
+ adaptBackendStandards,
1007
+ adaptFrontendStandards,
1008
+ adaptAgentsMd,
1009
+ adaptCopiedFiles,
1010
+ adaptEnvExample,
1011
+ updateAutonomy,
1012
+ regexReplaceInFile,
1013
+ };
@@ -0,0 +1,504 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ AUTONOMY_LEVELS,
7
+ FRONTEND_AGENTS,
8
+ BACKEND_AGENTS,
9
+ TEMPLATE_AGENTS,
10
+ } = require('./config');
11
+ const { adaptAgentContentForProjectType } = require('./adapt-agents');
12
+ const {
13
+ adaptBaseStandards,
14
+ adaptBackendStandards,
15
+ adaptFrontendStandards,
16
+ adaptAgentsMd,
17
+ adaptCopiedFiles,
18
+ adaptEnvExample,
19
+ updateAutonomy,
20
+ regexReplaceInFile,
21
+ } = require('./init-generator');
22
+
23
+ /**
24
+ * Read the installed SDD version from .sdd-version file.
25
+ */
26
+ function readInstalledVersion(dest) {
27
+ const versionFile = path.join(dest, '.sdd-version');
28
+ if (fs.existsSync(versionFile)) {
29
+ return fs.readFileSync(versionFile, 'utf8').trim();
30
+ }
31
+ return 'unknown';
32
+ }
33
+
34
+ /**
35
+ * Read the current package version.
36
+ */
37
+ function getPackageVersion() {
38
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
39
+ return pkg.version;
40
+ }
41
+
42
+ /**
43
+ * Detect which AI tools are installed by checking existing directories.
44
+ */
45
+ function detectAiTools(dest) {
46
+ const hasClaude = fs.existsSync(path.join(dest, '.claude'));
47
+ const hasGemini = fs.existsSync(path.join(dest, '.gemini'));
48
+ if (hasClaude && hasGemini) return 'both';
49
+ if (hasClaude) return 'claude';
50
+ if (hasGemini) return 'gemini';
51
+ return 'both'; // fallback: install both
52
+ }
53
+
54
+ /**
55
+ * Detect project type from existing agent files.
56
+ */
57
+ function detectProjectType(dest) {
58
+ const toolDir = fs.existsSync(path.join(dest, '.claude')) ? '.claude' : '.gemini';
59
+ const agentsDir = path.join(dest, toolDir, 'agents');
60
+ if (!fs.existsSync(agentsDir)) return 'fullstack';
61
+
62
+ const agents = fs.readdirSync(agentsDir);
63
+ const hasFrontend = agents.some((a) => FRONTEND_AGENTS.includes(a));
64
+ const hasBackend = agents.some((a) => BACKEND_AGENTS.includes(a));
65
+
66
+ if (hasFrontend && hasBackend) return 'fullstack';
67
+ if (hasBackend) return 'backend';
68
+ if (hasFrontend) return 'frontend';
69
+ return 'fullstack';
70
+ }
71
+
72
+ /**
73
+ * Read autonomy level from CLAUDE.md or GEMINI.md.
74
+ */
75
+ function readAutonomyLevel(dest) {
76
+ for (const file of ['CLAUDE.md', 'GEMINI.md']) {
77
+ const filePath = path.join(dest, file);
78
+ if (fs.existsSync(filePath)) {
79
+ const content = fs.readFileSync(filePath, 'utf8');
80
+ const match = content.match(/\*\*Autonomy Level: (\d+) \(([^)]+)\)\*\*/);
81
+ if (match) {
82
+ return { level: parseInt(match[1], 10), name: match[2] };
83
+ }
84
+ }
85
+ }
86
+ return { level: 2, name: 'Trusted' }; // default
87
+ }
88
+
89
+ /**
90
+ * Find custom agent files (not in template list).
91
+ * Returns array of { name, content } objects.
92
+ */
93
+ function collectCustomAgents(dest) {
94
+ const customs = [];
95
+ for (const toolDir of ['.claude', '.gemini']) {
96
+ const agentsDir = path.join(dest, toolDir, 'agents');
97
+ if (!fs.existsSync(agentsDir)) continue;
98
+ const files = fs.readdirSync(agentsDir);
99
+ for (const file of files) {
100
+ if (!TEMPLATE_AGENTS.includes(file) && file.endsWith('.md')) {
101
+ customs.push({
102
+ relativePath: path.join(toolDir, 'agents', file),
103
+ content: fs.readFileSync(path.join(agentsDir, file), 'utf8'),
104
+ });
105
+ }
106
+ }
107
+ }
108
+ return customs;
109
+ }
110
+
111
+ /**
112
+ * Find custom command files (not .gitkeep).
113
+ * Returns array of { relativePath, content } objects.
114
+ */
115
+ function collectCustomCommands(dest) {
116
+ const customs = [];
117
+ const commandsDir = path.join(dest, '.claude', 'commands');
118
+ if (!fs.existsSync(commandsDir)) return customs;
119
+ const files = fs.readdirSync(commandsDir);
120
+ for (const file of files) {
121
+ if (file === '.gitkeep') continue;
122
+ customs.push({
123
+ relativePath: path.join('.claude', 'commands', file),
124
+ content: fs.readFileSync(path.join(commandsDir, file), 'utf8'),
125
+ });
126
+ }
127
+ return customs;
128
+ }
129
+
130
+ /**
131
+ * Check if a standard file has been modified by the user.
132
+ * Compares existing file against freshly generated version.
133
+ */
134
+ function isStandardModified(existingContent, freshContent) {
135
+ return existingContent.trim() !== freshContent.trim();
136
+ }
137
+
138
+ /**
139
+ * Build the upgrade summary for display.
140
+ */
141
+ function buildSummary(state) {
142
+ const lines = [];
143
+ lines.push('šŸ”„ SDD DevFlow Upgrade\n');
144
+ lines.push(` Current version: ${state.installedVersion}`);
145
+ lines.push(` New version: ${state.packageVersion}`);
146
+ lines.push(` AI tools: ${state.aiTools === 'both' ? 'Claude Code + Gemini' : state.aiTools === 'claude' ? 'Claude Code' : 'Gemini'}`);
147
+ lines.push(` Project type: ${state.projectType}\n`);
148
+
149
+ lines.push(' Will replace:');
150
+ if (state.aiTools !== 'gemini') {
151
+ const customNote = state.customAgents.filter((a) => a.relativePath.startsWith('.claude')).length > 0
152
+ ? ` — custom agents preserved` : '';
153
+ lines.push(` āœ“ .claude/ (agents, skills, hooks)${customNote}`);
154
+ }
155
+ if (state.aiTools !== 'claude') {
156
+ lines.push(` āœ“ .gemini/ (agents, skills, commands)`);
157
+ }
158
+ lines.push(' āœ“ AGENTS.md, CLAUDE.md / GEMINI.md');
159
+ lines.push(' āœ“ .env.example\n');
160
+
161
+ lines.push(' Will preserve:');
162
+ if (state.settingsLocal) lines.push(' ⊘ .claude/settings.local.json (personal settings)');
163
+ lines.push(' ⊘ .claude/settings.json permissions (if any)');
164
+ for (const c of state.customCommands) lines.push(` ⊘ ${c.relativePath} (custom command)`);
165
+ for (const a of state.customAgents) lines.push(` ⊘ ${a.relativePath} (custom agent)`);
166
+ lines.push(' ⊘ docs/project_notes/* (project memory)');
167
+ lines.push(' ⊘ docs/specs/* (your specs)');
168
+ lines.push(' ⊘ docs/tickets/* (your tickets)');
169
+ lines.push(' ⊘ .gitignore\n');
170
+
171
+ if (state.standardsStatus.length > 0) {
172
+ lines.push(' Standards:');
173
+ for (const s of state.standardsStatus) {
174
+ if (s.modified) {
175
+ lines.push(` ⚠ ${s.name} — modified by you, will be preserved`);
176
+ } else {
177
+ lines.push(` āœ“ ${s.name} — unchanged, will update`);
178
+ }
179
+ }
180
+ }
181
+
182
+ return lines.join('\n');
183
+ }
184
+
185
+ /**
186
+ * Main upgrade function.
187
+ */
188
+ function generateUpgrade(config) {
189
+ const templateDir = path.join(__dirname, '..', 'template');
190
+ const dest = config.projectDir;
191
+ const scan = config.scanResult;
192
+ const aiTools = config.aiTools;
193
+ const projectType = config.projectType;
194
+
195
+ console.log(`\nUpgrading SDD DevFlow in ${config.projectName}...\n`);
196
+
197
+ // --- a) Preserve user items ---
198
+ const autonomy = readAutonomyLevel(dest);
199
+ const customAgents = collectCustomAgents(dest);
200
+ const customCommands = collectCustomCommands(dest);
201
+
202
+ // Preserve settings.local.json
203
+ let settingsLocal = null;
204
+ const settingsLocalPath = path.join(dest, '.claude', 'settings.local.json');
205
+ if (fs.existsSync(settingsLocalPath)) {
206
+ settingsLocal = fs.readFileSync(settingsLocalPath, 'utf8');
207
+ }
208
+
209
+ let replaced = 0;
210
+ let preserved = 0;
211
+
212
+ // --- b) Replace SDD-owned directories ---
213
+ const toolDirs = [];
214
+ if (aiTools !== 'gemini') toolDirs.push('.claude');
215
+ if (aiTools !== 'claude') toolDirs.push('.gemini');
216
+
217
+ for (const dir of toolDirs) {
218
+ const base = path.join(dest, dir);
219
+ // Delete specific SDD-owned subdirectories (NOT commands for .claude)
220
+ for (const sub of ['agents', 'skills', 'hooks', 'styles']) {
221
+ const subDir = path.join(base, sub);
222
+ if (fs.existsSync(subDir)) {
223
+ fs.rmSync(subDir, { recursive: true, force: true });
224
+ }
225
+ }
226
+ // For .gemini, also remove commands (SDD-owned — Gemini TOML commands)
227
+ if (dir === '.gemini') {
228
+ const cmdDir = path.join(base, 'commands');
229
+ if (fs.existsSync(cmdDir)) {
230
+ fs.rmSync(cmdDir, { recursive: true, force: true });
231
+ }
232
+ }
233
+
234
+ // Copy fresh from template
235
+ const templateToolDir = path.join(templateDir, dir);
236
+ if (fs.existsSync(templateToolDir)) {
237
+ for (const sub of ['agents', 'skills', 'hooks', 'styles', 'commands']) {
238
+ const srcSub = path.join(templateToolDir, sub);
239
+ const destSub = path.join(base, sub);
240
+ if (fs.existsSync(srcSub)) {
241
+ // For .claude/commands, merge (don't overwrite user's custom commands)
242
+ if (dir === '.claude' && sub === 'commands') {
243
+ // Just ensure directory exists — template only has .gitkeep
244
+ fs.mkdirSync(destSub, { recursive: true });
245
+ } else {
246
+ fs.cpSync(srcSub, destSub, { recursive: true });
247
+ }
248
+ replaced++;
249
+ }
250
+ }
251
+
252
+ // Merge settings.json — update hooks from template, preserve user's permissions/additionalDirectories
253
+ const settingsSrc = path.join(templateToolDir, 'settings.json');
254
+ const settingsDest = path.join(base, 'settings.json');
255
+ if (fs.existsSync(settingsSrc)) {
256
+ const templateSettings = JSON.parse(fs.readFileSync(settingsSrc, 'utf8'));
257
+ if (fs.existsSync(settingsDest)) {
258
+ const userSettings = JSON.parse(fs.readFileSync(settingsDest, 'utf8'));
259
+ // Keep user's permissions and additionalDirectories, take template's hooks
260
+ const merged = { ...templateSettings };
261
+ if (userSettings.permissions) merged.permissions = userSettings.permissions;
262
+ if (userSettings.additionalDirectories) merged.additionalDirectories = userSettings.additionalDirectories;
263
+ fs.writeFileSync(settingsDest, JSON.stringify(merged, null, 2) + '\n', 'utf8');
264
+ } else {
265
+ fs.copyFileSync(settingsSrc, settingsDest);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ step('Replaced agents, skills, hooks, and settings');
271
+
272
+ // --- c) Restore preserved items ---
273
+ for (const agent of customAgents) {
274
+ const agentPath = path.join(dest, agent.relativePath);
275
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
276
+ fs.writeFileSync(agentPath, agent.content, 'utf8');
277
+ preserved++;
278
+ }
279
+ if (customAgents.length > 0) {
280
+ step(`Restored ${customAgents.length} custom agent(s)`);
281
+ }
282
+
283
+ // Restore settings.local.json
284
+ if (settingsLocal) {
285
+ fs.writeFileSync(settingsLocalPath, settingsLocal, 'utf8');
286
+ preserved++;
287
+ }
288
+
289
+ // --- d) Handle standards (smart diff) ---
290
+ const standardsResults = [];
291
+
292
+ // base-standards.mdc
293
+ const baseStdPath = path.join(dest, 'ai-specs', 'specs', 'base-standards.mdc');
294
+ if (fs.existsSync(baseStdPath)) {
295
+ const existing = fs.readFileSync(baseStdPath, 'utf8');
296
+ const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'base-standards.mdc'), 'utf8');
297
+ const fresh = adaptBaseStandards(template, scan, config);
298
+ if (isStandardModified(existing, fresh)) {
299
+ standardsResults.push({ name: 'ai-specs/specs/base-standards.mdc', modified: true });
300
+ preserved++;
301
+ } else {
302
+ fs.writeFileSync(baseStdPath, fresh, 'utf8');
303
+ standardsResults.push({ name: 'ai-specs/specs/base-standards.mdc', modified: false });
304
+ replaced++;
305
+ }
306
+ }
307
+
308
+ // backend-standards.mdc
309
+ if (projectType !== 'frontend') {
310
+ const backendStdPath = path.join(dest, 'ai-specs', 'specs', 'backend-standards.mdc');
311
+ if (fs.existsSync(backendStdPath)) {
312
+ const existing = fs.readFileSync(backendStdPath, 'utf8');
313
+ const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'backend-standards.mdc'), 'utf8');
314
+ const fresh = adaptBackendStandards(template, scan);
315
+ if (isStandardModified(existing, fresh)) {
316
+ standardsResults.push({ name: 'ai-specs/specs/backend-standards.mdc', modified: true });
317
+ preserved++;
318
+ } else {
319
+ fs.writeFileSync(backendStdPath, fresh, 'utf8');
320
+ standardsResults.push({ name: 'ai-specs/specs/backend-standards.mdc', modified: false });
321
+ replaced++;
322
+ }
323
+ }
324
+ }
325
+
326
+ // frontend-standards.mdc
327
+ if (projectType !== 'backend') {
328
+ const frontendStdPath = path.join(dest, 'ai-specs', 'specs', 'frontend-standards.mdc');
329
+ if (fs.existsSync(frontendStdPath)) {
330
+ const existing = fs.readFileSync(frontendStdPath, 'utf8');
331
+ const template = fs.readFileSync(path.join(templateDir, 'ai-specs', 'specs', 'frontend-standards.mdc'), 'utf8');
332
+ const fresh = adaptFrontendStandards(template, scan);
333
+ if (isStandardModified(existing, fresh)) {
334
+ standardsResults.push({ name: 'ai-specs/specs/frontend-standards.mdc', modified: true });
335
+ preserved++;
336
+ } else {
337
+ fs.writeFileSync(frontendStdPath, fresh, 'utf8');
338
+ standardsResults.push({ name: 'ai-specs/specs/frontend-standards.mdc', modified: false });
339
+ replaced++;
340
+ }
341
+ }
342
+ }
343
+
344
+ // documentation-standards.mdc — always replace (unlikely customized, no adaptation)
345
+ const docStdSrc = path.join(templateDir, 'ai-specs', 'specs', 'documentation-standards.mdc');
346
+ const docStdDest = path.join(dest, 'ai-specs', 'specs', 'documentation-standards.mdc');
347
+ if (fs.existsSync(docStdSrc)) {
348
+ fs.copyFileSync(docStdSrc, docStdDest);
349
+ replaced++;
350
+ }
351
+
352
+ step('Updated standards files');
353
+ const modifiedStandards = standardsResults.filter((s) => s.modified);
354
+ if (modifiedStandards.length > 0) {
355
+ for (const s of modifiedStandards) {
356
+ console.log(` ⚠ ${s.name} — customized, preserved`);
357
+ }
358
+ }
359
+
360
+ // --- e) Replace top-level configs ---
361
+ // AGENTS.md
362
+ const agentsMdTemplate = fs.readFileSync(path.join(templateDir, 'AGENTS.md'), 'utf8');
363
+ const adaptedAgentsMd = adaptAgentsMd(agentsMdTemplate, config, scan);
364
+ fs.writeFileSync(path.join(dest, 'AGENTS.md'), adaptedAgentsMd, 'utf8');
365
+ replaced++;
366
+
367
+ // CLAUDE.md / GEMINI.md
368
+ if (aiTools !== 'gemini') {
369
+ fs.copyFileSync(path.join(templateDir, 'CLAUDE.md'), path.join(dest, 'CLAUDE.md'));
370
+ replaced++;
371
+ }
372
+ if (aiTools !== 'claude') {
373
+ fs.copyFileSync(path.join(templateDir, 'GEMINI.md'), path.join(dest, 'GEMINI.md'));
374
+ replaced++;
375
+ }
376
+
377
+ // Restore autonomy level
378
+ const autonomyConfig = {
379
+ autonomyLevel: autonomy.level,
380
+ autonomyName: autonomy.name,
381
+ };
382
+ // Ensure autonomy name matches expected values
383
+ const levelInfo = AUTONOMY_LEVELS.find((l) => l.level === autonomy.level);
384
+ if (levelInfo) {
385
+ autonomyConfig.autonomyName = levelInfo.name;
386
+ }
387
+ updateAutonomy(dest, autonomyConfig);
388
+ step(`Restored autonomy level: L${autonomy.level} (${autonomyConfig.autonomyName})`);
389
+
390
+ // .env.example — generate fresh, then append any custom lines from existing
391
+ let envContent = fs.readFileSync(path.join(templateDir, '.env.example'), 'utf8');
392
+ envContent = adaptEnvExample(envContent, config, scan);
393
+ const envPath = path.join(dest, '.env.example');
394
+ if (fs.existsSync(envPath)) {
395
+ const existingEnv = fs.readFileSync(envPath, 'utf8');
396
+ const freshLines = new Set(envContent.split('\n').map((l) => l.trim()));
397
+ const customLines = existingEnv.split('\n').filter((line) => {
398
+ const trimmed = line.trim();
399
+ // Keep lines that are not in the fresh version and are not empty
400
+ return trimmed.length > 0 && !freshLines.has(trimmed);
401
+ });
402
+ if (customLines.length > 0) {
403
+ envContent = envContent.trimEnd() + '\n\n# Project-specific variables (preserved from previous version)\n' + customLines.join('\n') + '\n';
404
+ }
405
+ }
406
+ fs.writeFileSync(envPath, envContent, 'utf8');
407
+ replaced++;
408
+
409
+ step('Replaced AGENTS.md, CLAUDE.md/GEMINI.md, .env.example');
410
+
411
+ // --- f) Adapt for project type ---
412
+ // Remove agents for single-stack projects
413
+ if (projectType === 'backend') {
414
+ for (const agent of FRONTEND_AGENTS) {
415
+ for (const dir of toolDirs) {
416
+ const agentPath = path.join(dest, dir, 'agents', agent);
417
+ try { fs.unlinkSync(agentPath); } catch { /* ignore */ }
418
+ }
419
+ }
420
+ // Remove frontend spec doc if it doesn't exist as user-owned
421
+ // (don't touch docs/specs/ — user-owned)
422
+ } else if (projectType === 'frontend') {
423
+ for (const agent of BACKEND_AGENTS) {
424
+ for (const dir of toolDirs) {
425
+ const agentPath = path.join(dest, dir, 'agents', agent);
426
+ try { fs.unlinkSync(agentPath); } catch { /* ignore */ }
427
+ }
428
+ }
429
+ }
430
+
431
+ // Adapt agent/skill content for project type
432
+ if (projectType !== 'fullstack') {
433
+ adaptAgentContentForProjectType(dest, config, regexReplaceInFile);
434
+ }
435
+
436
+ // Adapt copied files for detected stack (Zod, Prisma, DDD, etc.)
437
+ adaptCopiedFiles(dest, scan, config);
438
+
439
+ // Adapt documentation-standards for project type
440
+ const docStdPath2 = path.join(dest, 'ai-specs', 'specs', 'documentation-standards.mdc');
441
+ if (fs.existsSync(docStdPath2)) {
442
+ if (projectType === 'backend') {
443
+ regexReplaceInFile(docStdPath2, [
444
+ [/\| `ai-specs\/specs\/frontend-standards\.mdc` \|[^\n]*\n/, ''],
445
+ [/\| `docs\/specs\/ui-components\.md` \|[^\n]*\n/, ''],
446
+ [/ - UI component changes → `docs\/specs\/ui-components\.md`\n/, ''],
447
+ ]);
448
+ } else if (projectType === 'frontend') {
449
+ regexReplaceInFile(docStdPath2, [
450
+ [/\| `ai-specs\/specs\/backend-standards\.mdc` \|[^\n]*\n/, ''],
451
+ [/\| `docs\/specs\/api-spec\.yaml` \|[^\n]*\n/, ''],
452
+ ]);
453
+ }
454
+ }
455
+
456
+ step('Adapted files for project type and stack');
457
+
458
+ // --- g) Write version marker ---
459
+ const newVersion = getPackageVersion();
460
+ fs.writeFileSync(path.join(dest, '.sdd-version'), newVersion + '\n', 'utf8');
461
+ step(`Updated .sdd-version to ${newVersion}`);
462
+
463
+ // --- Show result ---
464
+ const updatedCount = standardsResults.filter((s) => !s.modified).length;
465
+ const preservedCount = modifiedStandards.length;
466
+
467
+ console.log(`\nāœ… Upgraded SDD DevFlow: ${config.installedVersion} → ${newVersion}\n`);
468
+ console.log(` Replaced: agents, skills, hooks, configs`);
469
+ if (customAgents.length > 0 || customCommands.length > 0 || settingsLocal) {
470
+ const items = [];
471
+ if (customAgents.length > 0) items.push(`${customAgents.length} custom agent(s)`);
472
+ if (customCommands.length > 0) items.push(`${customCommands.length} custom command(s)`);
473
+ if (settingsLocal) items.push('personal settings');
474
+ console.log(` Preserved: ${items.join(', ')}`);
475
+ }
476
+ if (standardsResults.length > 0) {
477
+ console.log(` Standards: ${updatedCount} updated, ${preservedCount} preserved (customized)`);
478
+ }
479
+
480
+ if (modifiedStandards.length > 0) {
481
+ console.log(`\n ⚠ Review preserved standards for compatibility:`);
482
+ for (const s of modifiedStandards) {
483
+ console.log(` - ${s.name} (customized — not updated)`);
484
+ }
485
+ }
486
+
487
+ console.log(`\nNext: git add -A && git commit -m "chore: upgrade SDD DevFlow to ${newVersion}"\n`);
488
+ }
489
+
490
+ function step(msg) {
491
+ console.log(` āœ“ ${msg}`);
492
+ }
493
+
494
+ module.exports = {
495
+ generateUpgrade,
496
+ readInstalledVersion,
497
+ getPackageVersion,
498
+ detectAiTools,
499
+ detectProjectType,
500
+ readAutonomyLevel,
501
+ collectCustomAgents,
502
+ collectCustomCommands,
503
+ buildSummary,
504
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sdd-project",
3
- "version": "0.4.2",
3
+ "version": "0.5.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"