forgedev 1.0.0 → 1.0.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 +246 -246
- package/bin/devforge.js +4 -4
- package/package.json +33 -33
- package/src/claude-configurator.js +260 -260
- package/src/cli.js +119 -119
- package/src/composer.js +214 -214
- package/src/doctor-checks.js +743 -743
- package/src/doctor-prompts.js +295 -295
- package/src/doctor.js +281 -281
- package/src/guided.js +315 -315
- package/src/index.js +148 -148
- package/src/init-mode.js +138 -134
- package/src/prompts.js +155 -155
- package/src/scanner.js +368 -368
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -41
- package/templates/claude-code/agents/production-readiness.md +55 -55
- package/templates/claude-code/agents/security-reviewer.md +41 -41
- package/templates/claude-code/agents/spec-validator.md +34 -34
- package/templates/claude-code/agents/uat-validator.md +37 -37
- package/templates/claude-code/claude-md/base.md +33 -33
- package/templates/claude-code/commands/done.md +19 -19
- package/templates/claude-code/commands/generate-prd.md +45 -45
- package/templates/claude-code/commands/generate-uat.md +35 -35
- package/templates/claude-code/commands/help.md +26 -26
- package/templates/claude-code/commands/next.md +20 -20
- package/templates/claude-code/commands/optimize-claude-md.md +31 -31
- package/templates/claude-code/commands/status.md +24 -24
- package/templates/claude-code/hooks/polyglot.json +36 -36
- package/templates/claude-code/hooks/python.json +36 -36
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -16
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -14
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -14
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -21
- package/templates/claude-code/hooks/typescript.json +36 -36
|
@@ -1,260 +1,260 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, log } from './utils.js';
|
|
4
|
-
|
|
5
|
-
const CLAUDE_TEMPLATES_DIR = path.join(ROOT_DIR, 'templates', 'claude-code');
|
|
6
|
-
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
|
|
7
|
-
|
|
8
|
-
export async function generateClaudeConfig(outputDir, stackConfig, options = {}) {
|
|
9
|
-
const vars = buildClaudeVars(stackConfig);
|
|
10
|
-
|
|
11
|
-
if (!options.skipClaudeMd) {
|
|
12
|
-
generateClaudeMd(outputDir, stackConfig, vars);
|
|
13
|
-
}
|
|
14
|
-
generateHooks(outputDir, stackConfig, { merge: options.mergeSettings });
|
|
15
|
-
generateSkills(outputDir, stackConfig);
|
|
16
|
-
generateAgents(outputDir, stackConfig, vars);
|
|
17
|
-
generateCommands(outputDir, stackConfig, vars);
|
|
18
|
-
copyPromptLibrary(outputDir);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function buildClaudeVars(config) {
|
|
22
|
-
const vars = {
|
|
23
|
-
PROJECT_NAME: config.projectName,
|
|
24
|
-
PROJECT_NAME_PASCAL: config.projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
28
|
-
vars.STACK_SUMMARY = 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
|
|
29
|
-
vars.LINT_COMMAND = 'npx eslint .';
|
|
30
|
-
vars.TYPE_CHECK_COMMAND = 'npx tsc --noEmit';
|
|
31
|
-
vars.TEST_COMMAND = 'npx vitest run';
|
|
32
|
-
vars.BUILD_COMMAND = 'npm run build';
|
|
33
|
-
vars.DEV_COMMAND = 'npm run dev';
|
|
34
|
-
vars.DIR_MAP = `- src/app/ — Next.js App Router pages and API routes
|
|
35
|
-
- src/lib/ — Shared utilities, database client, error handling
|
|
36
|
-
- src/components/ — React components
|
|
37
|
-
- prisma/ — Database schema and migrations
|
|
38
|
-
- e2e/ — Playwright E2E tests`;
|
|
39
|
-
} else if (config.stackId === 'fastapi-backend') {
|
|
40
|
-
vars.STACK_SUMMARY = 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic';
|
|
41
|
-
vars.LINT_COMMAND = 'ruff check .';
|
|
42
|
-
vars.TYPE_CHECK_COMMAND = 'pyright';
|
|
43
|
-
vars.TEST_COMMAND = 'pytest';
|
|
44
|
-
vars.BUILD_COMMAND = 'docker build -t app .';
|
|
45
|
-
vars.DEV_COMMAND = 'uvicorn app.main:app --reload';
|
|
46
|
-
vars.DIR_MAP = `- backend/app/ — FastAPI application
|
|
47
|
-
- backend/app/api/ — API route handlers
|
|
48
|
-
- backend/app/core/ — Config, security, error handling
|
|
49
|
-
- backend/app/db/ — Database session and models
|
|
50
|
-
- backend/app/models/ — SQLAlchemy models
|
|
51
|
-
- backend/app/schemas/ — Pydantic schemas
|
|
52
|
-
- backend/tests/ — Pytest tests
|
|
53
|
-
- backend/alembic/ — Database migrations`;
|
|
54
|
-
} else if (config.stackId === 'polyglot-fullstack') {
|
|
55
|
-
vars.STACK_SUMMARY = 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL';
|
|
56
|
-
vars.LINT_COMMAND = 'cd frontend && npx eslint . && cd ../backend && ruff check .';
|
|
57
|
-
vars.TYPE_CHECK_COMMAND = 'cd frontend && npx tsc --noEmit';
|
|
58
|
-
vars.TEST_COMMAND = 'cd frontend && npx vitest run && cd ../backend && pytest';
|
|
59
|
-
vars.BUILD_COMMAND = 'docker compose build';
|
|
60
|
-
vars.DEV_COMMAND = 'docker compose up';
|
|
61
|
-
vars.DIR_MAP = `- frontend/ — Next.js 15 App Router application
|
|
62
|
-
- frontend/src/app/ — Pages and API routes
|
|
63
|
-
- frontend/src/lib/ — Shared utilities
|
|
64
|
-
- backend/ — FastAPI application
|
|
65
|
-
- backend/app/api/ — API route handlers
|
|
66
|
-
- backend/app/core/ — Config, security, error handling
|
|
67
|
-
- backend/app/db/ — Database session and models
|
|
68
|
-
- e2e/ — Playwright E2E tests`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Build skills list for CLAUDE.md reference
|
|
72
|
-
const skillsList = [];
|
|
73
|
-
if (config.frontend?.framework === 'nextjs') {
|
|
74
|
-
skillsList.push('- `@.claude/skills/nextjs/` — Next.js patterns and conventions');
|
|
75
|
-
skillsList.push('- `@.claude/skills/security-web/` — Frontend security practices');
|
|
76
|
-
}
|
|
77
|
-
if (config.backend?.framework === 'fastapi') {
|
|
78
|
-
skillsList.push('- `@.claude/skills/fastapi/` — FastAPI patterns and conventions');
|
|
79
|
-
skillsList.push('- `@.claude/skills/security-api/` — API security practices');
|
|
80
|
-
}
|
|
81
|
-
if (config.testing?.e2e === 'playwright') {
|
|
82
|
-
skillsList.push('- `@.claude/skills/playwright/` — E2E testing patterns');
|
|
83
|
-
}
|
|
84
|
-
if (config.ai) {
|
|
85
|
-
skillsList.push('- `@.claude/skills/ai-prompts/` — AI/LLM prompt patterns');
|
|
86
|
-
}
|
|
87
|
-
vars.SKILLS_LIST = skillsList.length > 0 ? skillsList.join('\n') : '- (none generated)';
|
|
88
|
-
|
|
89
|
-
return vars;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function generateClaudeMd(outputDir, config, vars) {
|
|
93
|
-
// Read base template
|
|
94
|
-
const basePath = path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'base.md');
|
|
95
|
-
let content = readTemplate(basePath);
|
|
96
|
-
|
|
97
|
-
// Read stack-specific section
|
|
98
|
-
let stackSection = '';
|
|
99
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
100
|
-
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'nextjs.md'));
|
|
101
|
-
} else if (config.stackId === 'fastapi-backend') {
|
|
102
|
-
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fastapi.md'));
|
|
103
|
-
} else if (config.stackId === 'polyglot-fullstack') {
|
|
104
|
-
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
content = content.replace('{{STACK_SPECIFIC_RULES}}', stackSection);
|
|
108
|
-
content = replaceVars(content, vars);
|
|
109
|
-
|
|
110
|
-
writeFile(path.join(outputDir, 'CLAUDE.md'), content);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function generateHooks(outputDir, config, options = {}) {
|
|
114
|
-
let hookFile;
|
|
115
|
-
let scriptFiles = ['guard-protected-files.sh'];
|
|
116
|
-
|
|
117
|
-
if (config.stackId === 'nextjs-fullstack') {
|
|
118
|
-
hookFile = 'typescript.json';
|
|
119
|
-
scriptFiles.push('autofix-typescript.sh');
|
|
120
|
-
} else if (config.stackId === 'fastapi-backend') {
|
|
121
|
-
hookFile = 'python.json';
|
|
122
|
-
scriptFiles.push('autofix-python.sh');
|
|
123
|
-
} else if (config.stackId === 'polyglot-fullstack') {
|
|
124
|
-
hookFile = 'polyglot.json';
|
|
125
|
-
scriptFiles.push('autofix-polyglot.sh');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const settingsPath = path.join(outputDir, '.claude', 'settings.json');
|
|
129
|
-
|
|
130
|
-
if (options.merge && fs.existsSync(settingsPath)) {
|
|
131
|
-
// Merge hooks into existing settings.json
|
|
132
|
-
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
133
|
-
const incoming = JSON.parse(readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile)));
|
|
134
|
-
const merged = deepMergeSettings(existing, incoming);
|
|
135
|
-
writeFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
136
|
-
} else {
|
|
137
|
-
const hookPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile);
|
|
138
|
-
const content = readTemplate(hookPath);
|
|
139
|
-
writeFile(settingsPath, content);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Copy hook scripts
|
|
143
|
-
const hooksDir = path.join(outputDir, '.claude', 'hooks');
|
|
144
|
-
ensureDir(hooksDir);
|
|
145
|
-
for (const script of scriptFiles) {
|
|
146
|
-
const srcPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', 'scripts', script);
|
|
147
|
-
if (fs.existsSync(srcPath)) {
|
|
148
|
-
fs.copyFileSync(srcPath, path.join(hooksDir, script));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function deepMergeSettings(existing, incoming) {
|
|
154
|
-
const merged = { ...existing };
|
|
155
|
-
if (incoming.hooks) {
|
|
156
|
-
merged.hooks = merged.hooks || {};
|
|
157
|
-
for (const [hookType, hookArr] of Object.entries(incoming.hooks)) {
|
|
158
|
-
if (!merged.hooks[hookType]) {
|
|
159
|
-
merged.hooks[hookType] = hookArr;
|
|
160
|
-
} else {
|
|
161
|
-
// Append incoming hooks that don't already exist (by command)
|
|
162
|
-
const existingCommands = new Set(
|
|
163
|
-
merged.hooks[hookType].flatMap(h => (h.hooks || []).map(hk => hk.command))
|
|
164
|
-
);
|
|
165
|
-
for (const hook of hookArr) {
|
|
166
|
-
const isNew = (hook.hooks || []).some(hk => !existingCommands.has(hk.command));
|
|
167
|
-
if (isNew) {
|
|
168
|
-
merged.hooks[hookType].push(hook);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return merged;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function generateSkills(outputDir, config) {
|
|
178
|
-
const skillsToInclude = [];
|
|
179
|
-
|
|
180
|
-
if (config.frontend?.framework === 'nextjs') {
|
|
181
|
-
skillsToInclude.push('nextjs');
|
|
182
|
-
skillsToInclude.push('security-web');
|
|
183
|
-
}
|
|
184
|
-
if (config.backend?.framework === 'fastapi') {
|
|
185
|
-
skillsToInclude.push('fastapi');
|
|
186
|
-
skillsToInclude.push('security-api');
|
|
187
|
-
}
|
|
188
|
-
if (config.testing?.e2e === 'playwright') {
|
|
189
|
-
skillsToInclude.push('playwright');
|
|
190
|
-
}
|
|
191
|
-
if (config.ai) {
|
|
192
|
-
skillsToInclude.push('ai-prompts');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
for (const skill of skillsToInclude) {
|
|
196
|
-
const srcPath = path.join(CLAUDE_TEMPLATES_DIR, 'skills', skill, 'SKILL.md');
|
|
197
|
-
if (fs.existsSync(srcPath)) {
|
|
198
|
-
const destPath = path.join(outputDir, '.claude', 'skills', skill, 'SKILL.md');
|
|
199
|
-
ensureDir(path.dirname(destPath));
|
|
200
|
-
fs.copyFileSync(srcPath, destPath);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function generateAgents(outputDir, config, vars) {
|
|
206
|
-
const agentsDir = path.join(CLAUDE_TEMPLATES_DIR, 'agents');
|
|
207
|
-
const agents = [
|
|
208
|
-
'code-quality-reviewer.md',
|
|
209
|
-
'security-reviewer.md',
|
|
210
|
-
'spec-validator.md',
|
|
211
|
-
'production-readiness.md',
|
|
212
|
-
'uat-validator.md',
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
for (const agent of agents) {
|
|
216
|
-
const srcPath = path.join(agentsDir, agent);
|
|
217
|
-
if (fs.existsSync(srcPath)) {
|
|
218
|
-
let content = readTemplate(srcPath);
|
|
219
|
-
content = replaceVars(content, vars);
|
|
220
|
-
writeFile(path.join(outputDir, '.claude', 'agents', agent), content);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function generateCommands(outputDir, config, vars) {
|
|
226
|
-
const commandsDir = path.join(CLAUDE_TEMPLATES_DIR, 'commands');
|
|
227
|
-
const commands = [
|
|
228
|
-
'help.md',
|
|
229
|
-
'status.md',
|
|
230
|
-
'next.md',
|
|
231
|
-
'done.md',
|
|
232
|
-
'verify-all.md',
|
|
233
|
-
'audit-spec.md',
|
|
234
|
-
'audit-wiring.md',
|
|
235
|
-
'audit-security.md',
|
|
236
|
-
'pre-pr.md',
|
|
237
|
-
'run-uat.md',
|
|
238
|
-
'generate-prd.md',
|
|
239
|
-
'generate-uat.md',
|
|
240
|
-
'optimize-claude-md.md',
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
for (const cmd of commands) {
|
|
244
|
-
const srcPath = path.join(commandsDir, cmd);
|
|
245
|
-
if (fs.existsSync(srcPath)) {
|
|
246
|
-
let content = readTemplate(srcPath);
|
|
247
|
-
content = replaceVars(content, vars);
|
|
248
|
-
writeFile(path.join(outputDir, '.claude', 'commands', cmd), content);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function copyPromptLibrary(outputDir) {
|
|
254
|
-
const srcPath = path.join(DOCS_DIR, '01-universal-prompt-library.md');
|
|
255
|
-
if (fs.existsSync(srcPath)) {
|
|
256
|
-
const destPath = path.join(outputDir, 'docs', 'prompt-library.md');
|
|
257
|
-
ensureDir(path.dirname(destPath));
|
|
258
|
-
fs.copyFileSync(srcPath, destPath);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ROOT_DIR, ensureDir, writeFile, readTemplate, replaceVars, log } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const CLAUDE_TEMPLATES_DIR = path.join(ROOT_DIR, 'templates', 'claude-code');
|
|
6
|
+
const DOCS_DIR = path.join(ROOT_DIR, 'docs');
|
|
7
|
+
|
|
8
|
+
export async function generateClaudeConfig(outputDir, stackConfig, options = {}) {
|
|
9
|
+
const vars = buildClaudeVars(stackConfig);
|
|
10
|
+
|
|
11
|
+
if (!options.skipClaudeMd) {
|
|
12
|
+
generateClaudeMd(outputDir, stackConfig, vars);
|
|
13
|
+
}
|
|
14
|
+
generateHooks(outputDir, stackConfig, { merge: options.mergeSettings });
|
|
15
|
+
generateSkills(outputDir, stackConfig);
|
|
16
|
+
generateAgents(outputDir, stackConfig, vars);
|
|
17
|
+
generateCommands(outputDir, stackConfig, vars);
|
|
18
|
+
copyPromptLibrary(outputDir);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildClaudeVars(config) {
|
|
22
|
+
const vars = {
|
|
23
|
+
PROJECT_NAME: config.projectName,
|
|
24
|
+
PROJECT_NAME_PASCAL: config.projectName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (config.stackId === 'nextjs-fullstack') {
|
|
28
|
+
vars.STACK_SUMMARY = 'Next.js 15 (App Router) + TypeScript + Tailwind CSS + Prisma + PostgreSQL';
|
|
29
|
+
vars.LINT_COMMAND = 'npx eslint .';
|
|
30
|
+
vars.TYPE_CHECK_COMMAND = 'npx tsc --noEmit';
|
|
31
|
+
vars.TEST_COMMAND = 'npx vitest run';
|
|
32
|
+
vars.BUILD_COMMAND = 'npm run build';
|
|
33
|
+
vars.DEV_COMMAND = 'npm run dev';
|
|
34
|
+
vars.DIR_MAP = `- src/app/ — Next.js App Router pages and API routes
|
|
35
|
+
- src/lib/ — Shared utilities, database client, error handling
|
|
36
|
+
- src/components/ — React components
|
|
37
|
+
- prisma/ — Database schema and migrations
|
|
38
|
+
- e2e/ — Playwright E2E tests`;
|
|
39
|
+
} else if (config.stackId === 'fastapi-backend') {
|
|
40
|
+
vars.STACK_SUMMARY = 'FastAPI + Python + SQLAlchemy 2.0 + PostgreSQL + Alembic';
|
|
41
|
+
vars.LINT_COMMAND = 'ruff check .';
|
|
42
|
+
vars.TYPE_CHECK_COMMAND = 'pyright';
|
|
43
|
+
vars.TEST_COMMAND = 'pytest';
|
|
44
|
+
vars.BUILD_COMMAND = 'docker build -t app .';
|
|
45
|
+
vars.DEV_COMMAND = 'uvicorn app.main:app --reload';
|
|
46
|
+
vars.DIR_MAP = `- backend/app/ — FastAPI application
|
|
47
|
+
- backend/app/api/ — API route handlers
|
|
48
|
+
- backend/app/core/ — Config, security, error handling
|
|
49
|
+
- backend/app/db/ — Database session and models
|
|
50
|
+
- backend/app/models/ — SQLAlchemy models
|
|
51
|
+
- backend/app/schemas/ — Pydantic schemas
|
|
52
|
+
- backend/tests/ — Pytest tests
|
|
53
|
+
- backend/alembic/ — Database migrations`;
|
|
54
|
+
} else if (config.stackId === 'polyglot-fullstack') {
|
|
55
|
+
vars.STACK_SUMMARY = 'Next.js 15 (frontend) + FastAPI (backend) + PostgreSQL';
|
|
56
|
+
vars.LINT_COMMAND = 'cd frontend && npx eslint . && cd ../backend && ruff check .';
|
|
57
|
+
vars.TYPE_CHECK_COMMAND = 'cd frontend && npx tsc --noEmit';
|
|
58
|
+
vars.TEST_COMMAND = 'cd frontend && npx vitest run && cd ../backend && pytest';
|
|
59
|
+
vars.BUILD_COMMAND = 'docker compose build';
|
|
60
|
+
vars.DEV_COMMAND = 'docker compose up';
|
|
61
|
+
vars.DIR_MAP = `- frontend/ — Next.js 15 App Router application
|
|
62
|
+
- frontend/src/app/ — Pages and API routes
|
|
63
|
+
- frontend/src/lib/ — Shared utilities
|
|
64
|
+
- backend/ — FastAPI application
|
|
65
|
+
- backend/app/api/ — API route handlers
|
|
66
|
+
- backend/app/core/ — Config, security, error handling
|
|
67
|
+
- backend/app/db/ — Database session and models
|
|
68
|
+
- e2e/ — Playwright E2E tests`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build skills list for CLAUDE.md reference
|
|
72
|
+
const skillsList = [];
|
|
73
|
+
if (config.frontend?.framework === 'nextjs') {
|
|
74
|
+
skillsList.push('- `@.claude/skills/nextjs/` — Next.js patterns and conventions');
|
|
75
|
+
skillsList.push('- `@.claude/skills/security-web/` — Frontend security practices');
|
|
76
|
+
}
|
|
77
|
+
if (config.backend?.framework === 'fastapi') {
|
|
78
|
+
skillsList.push('- `@.claude/skills/fastapi/` — FastAPI patterns and conventions');
|
|
79
|
+
skillsList.push('- `@.claude/skills/security-api/` — API security practices');
|
|
80
|
+
}
|
|
81
|
+
if (config.testing?.e2e === 'playwright') {
|
|
82
|
+
skillsList.push('- `@.claude/skills/playwright/` — E2E testing patterns');
|
|
83
|
+
}
|
|
84
|
+
if (config.ai) {
|
|
85
|
+
skillsList.push('- `@.claude/skills/ai-prompts/` — AI/LLM prompt patterns');
|
|
86
|
+
}
|
|
87
|
+
vars.SKILLS_LIST = skillsList.length > 0 ? skillsList.join('\n') : '- (none generated)';
|
|
88
|
+
|
|
89
|
+
return vars;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function generateClaudeMd(outputDir, config, vars) {
|
|
93
|
+
// Read base template
|
|
94
|
+
const basePath = path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'base.md');
|
|
95
|
+
let content = readTemplate(basePath);
|
|
96
|
+
|
|
97
|
+
// Read stack-specific section
|
|
98
|
+
let stackSection = '';
|
|
99
|
+
if (config.stackId === 'nextjs-fullstack') {
|
|
100
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'nextjs.md'));
|
|
101
|
+
} else if (config.stackId === 'fastapi-backend') {
|
|
102
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fastapi.md'));
|
|
103
|
+
} else if (config.stackId === 'polyglot-fullstack') {
|
|
104
|
+
stackSection = readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'claude-md', 'fullstack.md'));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
content = content.replace('{{STACK_SPECIFIC_RULES}}', stackSection);
|
|
108
|
+
content = replaceVars(content, vars);
|
|
109
|
+
|
|
110
|
+
writeFile(path.join(outputDir, 'CLAUDE.md'), content);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function generateHooks(outputDir, config, options = {}) {
|
|
114
|
+
let hookFile;
|
|
115
|
+
let scriptFiles = ['guard-protected-files.sh'];
|
|
116
|
+
|
|
117
|
+
if (config.stackId === 'nextjs-fullstack') {
|
|
118
|
+
hookFile = 'typescript.json';
|
|
119
|
+
scriptFiles.push('autofix-typescript.sh');
|
|
120
|
+
} else if (config.stackId === 'fastapi-backend') {
|
|
121
|
+
hookFile = 'python.json';
|
|
122
|
+
scriptFiles.push('autofix-python.sh');
|
|
123
|
+
} else if (config.stackId === 'polyglot-fullstack') {
|
|
124
|
+
hookFile = 'polyglot.json';
|
|
125
|
+
scriptFiles.push('autofix-polyglot.sh');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const settingsPath = path.join(outputDir, '.claude', 'settings.json');
|
|
129
|
+
|
|
130
|
+
if (options.merge && fs.existsSync(settingsPath)) {
|
|
131
|
+
// Merge hooks into existing settings.json
|
|
132
|
+
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
133
|
+
const incoming = JSON.parse(readTemplate(path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile)));
|
|
134
|
+
const merged = deepMergeSettings(existing, incoming);
|
|
135
|
+
writeFile(settingsPath, JSON.stringify(merged, null, 2));
|
|
136
|
+
} else {
|
|
137
|
+
const hookPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', hookFile);
|
|
138
|
+
const content = readTemplate(hookPath);
|
|
139
|
+
writeFile(settingsPath, content);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Copy hook scripts
|
|
143
|
+
const hooksDir = path.join(outputDir, '.claude', 'hooks');
|
|
144
|
+
ensureDir(hooksDir);
|
|
145
|
+
for (const script of scriptFiles) {
|
|
146
|
+
const srcPath = path.join(CLAUDE_TEMPLATES_DIR, 'hooks', 'scripts', script);
|
|
147
|
+
if (fs.existsSync(srcPath)) {
|
|
148
|
+
fs.copyFileSync(srcPath, path.join(hooksDir, script));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function deepMergeSettings(existing, incoming) {
|
|
154
|
+
const merged = { ...existing };
|
|
155
|
+
if (incoming.hooks) {
|
|
156
|
+
merged.hooks = merged.hooks || {};
|
|
157
|
+
for (const [hookType, hookArr] of Object.entries(incoming.hooks)) {
|
|
158
|
+
if (!merged.hooks[hookType]) {
|
|
159
|
+
merged.hooks[hookType] = hookArr;
|
|
160
|
+
} else {
|
|
161
|
+
// Append incoming hooks that don't already exist (by command)
|
|
162
|
+
const existingCommands = new Set(
|
|
163
|
+
merged.hooks[hookType].flatMap(h => (h.hooks || []).map(hk => hk.command))
|
|
164
|
+
);
|
|
165
|
+
for (const hook of hookArr) {
|
|
166
|
+
const isNew = (hook.hooks || []).some(hk => !existingCommands.has(hk.command));
|
|
167
|
+
if (isNew) {
|
|
168
|
+
merged.hooks[hookType].push(hook);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return merged;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generateSkills(outputDir, config) {
|
|
178
|
+
const skillsToInclude = [];
|
|
179
|
+
|
|
180
|
+
if (config.frontend?.framework === 'nextjs') {
|
|
181
|
+
skillsToInclude.push('nextjs');
|
|
182
|
+
skillsToInclude.push('security-web');
|
|
183
|
+
}
|
|
184
|
+
if (config.backend?.framework === 'fastapi') {
|
|
185
|
+
skillsToInclude.push('fastapi');
|
|
186
|
+
skillsToInclude.push('security-api');
|
|
187
|
+
}
|
|
188
|
+
if (config.testing?.e2e === 'playwright') {
|
|
189
|
+
skillsToInclude.push('playwright');
|
|
190
|
+
}
|
|
191
|
+
if (config.ai) {
|
|
192
|
+
skillsToInclude.push('ai-prompts');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const skill of skillsToInclude) {
|
|
196
|
+
const srcPath = path.join(CLAUDE_TEMPLATES_DIR, 'skills', skill, 'SKILL.md');
|
|
197
|
+
if (fs.existsSync(srcPath)) {
|
|
198
|
+
const destPath = path.join(outputDir, '.claude', 'skills', skill, 'SKILL.md');
|
|
199
|
+
ensureDir(path.dirname(destPath));
|
|
200
|
+
fs.copyFileSync(srcPath, destPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function generateAgents(outputDir, config, vars) {
|
|
206
|
+
const agentsDir = path.join(CLAUDE_TEMPLATES_DIR, 'agents');
|
|
207
|
+
const agents = [
|
|
208
|
+
'code-quality-reviewer.md',
|
|
209
|
+
'security-reviewer.md',
|
|
210
|
+
'spec-validator.md',
|
|
211
|
+
'production-readiness.md',
|
|
212
|
+
'uat-validator.md',
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
for (const agent of agents) {
|
|
216
|
+
const srcPath = path.join(agentsDir, agent);
|
|
217
|
+
if (fs.existsSync(srcPath)) {
|
|
218
|
+
let content = readTemplate(srcPath);
|
|
219
|
+
content = replaceVars(content, vars);
|
|
220
|
+
writeFile(path.join(outputDir, '.claude', 'agents', agent), content);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function generateCommands(outputDir, config, vars) {
|
|
226
|
+
const commandsDir = path.join(CLAUDE_TEMPLATES_DIR, 'commands');
|
|
227
|
+
const commands = [
|
|
228
|
+
'help.md',
|
|
229
|
+
'status.md',
|
|
230
|
+
'next.md',
|
|
231
|
+
'done.md',
|
|
232
|
+
'verify-all.md',
|
|
233
|
+
'audit-spec.md',
|
|
234
|
+
'audit-wiring.md',
|
|
235
|
+
'audit-security.md',
|
|
236
|
+
'pre-pr.md',
|
|
237
|
+
'run-uat.md',
|
|
238
|
+
'generate-prd.md',
|
|
239
|
+
'generate-uat.md',
|
|
240
|
+
'optimize-claude-md.md',
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
for (const cmd of commands) {
|
|
244
|
+
const srcPath = path.join(commandsDir, cmd);
|
|
245
|
+
if (fs.existsSync(srcPath)) {
|
|
246
|
+
let content = readTemplate(srcPath);
|
|
247
|
+
content = replaceVars(content, vars);
|
|
248
|
+
writeFile(path.join(outputDir, '.claude', 'commands', cmd), content);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function copyPromptLibrary(outputDir) {
|
|
254
|
+
const srcPath = path.join(DOCS_DIR, '01-universal-prompt-library.md');
|
|
255
|
+
if (fs.existsSync(srcPath)) {
|
|
256
|
+
const destPath = path.join(outputDir, 'docs', 'prompt-library.md');
|
|
257
|
+
ensureDir(path.dirname(destPath));
|
|
258
|
+
fs.copyFileSync(srcPath, destPath);
|
|
259
|
+
}
|
|
260
|
+
}
|