forgedev 1.0.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/CLAUDE.md +38 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/bin/devforge.js +4 -0
- package/package.json +33 -0
- package/src/claude-configurator.js +260 -0
- package/src/cli.js +119 -0
- package/src/composer.js +214 -0
- package/src/doctor-checks.js +743 -0
- package/src/doctor-prompts.js +295 -0
- package/src/doctor.js +281 -0
- package/src/guided.js +315 -0
- package/src/index.js +148 -0
- package/src/init-mode.js +134 -0
- package/src/prompts.js +155 -0
- package/src/recommender.js +186 -0
- package/src/scanner.js +368 -0
- package/src/uat-generator.js +189 -0
- package/src/utils.js +57 -0
- package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
- package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
- package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
- package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
- package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
- package/templates/auth/nextauth/src/middleware.ts.template +14 -0
- package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
- package/templates/backend/fastapi/backend/app/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
- package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
- package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
- package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
- package/templates/backend/fastapi/backend/app/main.py.template +58 -0
- package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
- package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
- package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
- package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
- package/templates/base/.gitignore.template +29 -0
- package/templates/base/README.md.template +25 -0
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
- package/templates/claude-code/agents/production-readiness.md +55 -0
- package/templates/claude-code/agents/security-reviewer.md +41 -0
- package/templates/claude-code/agents/spec-validator.md +34 -0
- package/templates/claude-code/agents/uat-validator.md +37 -0
- package/templates/claude-code/claude-md/base.md +33 -0
- package/templates/claude-code/claude-md/fastapi.md +12 -0
- package/templates/claude-code/claude-md/fullstack.md +12 -0
- package/templates/claude-code/claude-md/nextjs.md +11 -0
- package/templates/claude-code/commands/audit-security.md +11 -0
- package/templates/claude-code/commands/audit-spec.md +9 -0
- package/templates/claude-code/commands/audit-wiring.md +17 -0
- package/templates/claude-code/commands/done.md +19 -0
- package/templates/claude-code/commands/generate-prd.md +45 -0
- package/templates/claude-code/commands/generate-uat.md +35 -0
- package/templates/claude-code/commands/help.md +26 -0
- package/templates/claude-code/commands/next.md +20 -0
- package/templates/claude-code/commands/optimize-claude-md.md +31 -0
- package/templates/claude-code/commands/pre-pr.md +19 -0
- package/templates/claude-code/commands/run-uat.md +21 -0
- package/templates/claude-code/commands/status.md +24 -0
- package/templates/claude-code/commands/verify-all.md +11 -0
- package/templates/claude-code/hooks/polyglot.json +36 -0
- package/templates/claude-code/hooks/python.json +36 -0
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
- package/templates/claude-code/hooks/typescript.json +36 -0
- package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
- package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
- package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
- package/templates/claude-code/skills/playwright/SKILL.md +37 -0
- package/templates/claude-code/skills/security-api/SKILL.md +47 -0
- package/templates/claude-code/skills/security-web/SKILL.md +41 -0
- package/templates/database/prisma-postgres/.env.example +1 -0
- package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
- package/templates/database/sqlalchemy-postgres/.env.example +1 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
- package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
- package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
- package/templates/frontend/nextjs/next.config.ts.template +7 -0
- package/templates/frontend/nextjs/package.json.template +41 -0
- package/templates/frontend/nextjs/postcss.config.mjs +7 -0
- package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
- package/templates/frontend/nextjs/src/app/globals.css +1 -0
- package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
- package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
- package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
- package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
- package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
- package/templates/frontend/nextjs/tsconfig.json +23 -0
- package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
- package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
- package/templates/testing/playwright/playwright.config.ts.template +22 -0
- package/templates/testing/vitest/src/__tests__/example.test.ts.template +12 -0
package/src/guided.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log, writeFile } from './utils.js';
|
|
4
|
+
import { askDescription, askAdjustment, askGuidedConfirm } from './prompts.js';
|
|
5
|
+
import { recommend } from './recommender.js';
|
|
6
|
+
|
|
7
|
+
const FEATURE_MAP = {
|
|
8
|
+
auth: ['login', 'sign up', 'signup', 'account', 'user', 'password', 'register', 'authentication'],
|
|
9
|
+
realtime: ['real-time', 'realtime', 'live', 'instant', 'track', 'notify', 'chat', 'websocket', 'stream'],
|
|
10
|
+
ai: ['ai', 'smart', 'auto-generate', 'recommend', 'suggest', 'intelligent', 'gpt', 'llm', 'chatbot', 'openai', 'anthropic', 'claude', 'embedding', 'vector', 'rag', 'ml', 'machine learning'],
|
|
11
|
+
payments: ['pay', 'order', 'purchase', 'subscribe', 'billing', 'stripe', 'price', 'cart', 'checkout', 'invoice'],
|
|
12
|
+
uploads: ['upload', 'image', 'photo', 'file', 'document', 'pdf', 'attachment', 'media', 's3', 'storage'],
|
|
13
|
+
dashboard: ['dashboard', 'analytics', 'report', 'chart', 'stats', 'metrics', 'admin panel'],
|
|
14
|
+
roles: ['admin', 'role', 'permission', 'owner', 'manager', 'staff', 'customer', 'moderator'],
|
|
15
|
+
email: ['email', 'notification', 'send', 'newsletter', 'invite', 'alert'],
|
|
16
|
+
search: ['search', 'filter', 'browse', 'find', 'catalog', 'list', 'sort'],
|
|
17
|
+
scheduling: ['schedule', 'book', 'appointment', 'calendar', 'reserve', 'time slot', 'booking'],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const FRONTEND_SIGNALS = ['dashboard', 'landing', 'ui', 'page', 'form', 'website', 'portal', 'interface', 'responsive', 'frontend', 'display', 'view', 'browse', 'menu'];
|
|
21
|
+
const BACKEND_ONLY_SIGNALS = ['api', 'endpoint', 'webhook', 'microservice', 'worker', 'cron', 'queue', 'batch', 'pipeline'];
|
|
22
|
+
const PYTHON_SIGNALS = ['python', 'fastapi', 'flask', 'django', 'ml', 'data science', 'scraping', 'pandas', 'numpy', 'tensorflow'];
|
|
23
|
+
|
|
24
|
+
const ENTITY_PATTERNS = [
|
|
25
|
+
/manage (?:their |the )?(\w+)/gi,
|
|
26
|
+
/(?:create|add|edit|update|delete|view|list|track|monitor) (?:their |the |a |an )?(\w+)/gi,
|
|
27
|
+
/(\w+) can /gi,
|
|
28
|
+
/store (\w+)/gi,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const STOP_WORDS = new Set([
|
|
32
|
+
'i', 'a', 'an', 'the', 'and', 'or', 'but', 'is', 'are', 'was', 'were',
|
|
33
|
+
'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will',
|
|
34
|
+
'would', 'could', 'should', 'may', 'might', 'can', 'shall', 'to', 'of',
|
|
35
|
+
'in', 'for', 'on', 'with', 'at', 'by', 'from', 'it', 'its', 'this',
|
|
36
|
+
'that', 'these', 'those', 'my', 'your', 'their', 'our', 'them', 'they',
|
|
37
|
+
'we', 'you', 'me', 'him', 'her', 'us', 'what', 'which', 'who', 'whom',
|
|
38
|
+
'want', 'need', 'like', 'app', 'application', 'system', 'platform',
|
|
39
|
+
'where', 'when', 'how', 'not', 'no', 'so', 'if', 'then', 'also',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export function analyzeDescription(text) {
|
|
43
|
+
if (!text || !text.trim()) {
|
|
44
|
+
return { features: [], entities: [], suggestedServiceType: 'full_stack', suggestedLanguage: 'typescript' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const lower = text.toLowerCase();
|
|
48
|
+
const features = [];
|
|
49
|
+
|
|
50
|
+
for (const [feature, keywords] of Object.entries(FEATURE_MAP)) {
|
|
51
|
+
if (keywords.some(k => lower.includes(k))) {
|
|
52
|
+
features.push(feature);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract entities
|
|
57
|
+
const entitySet = new Set();
|
|
58
|
+
for (const pattern of ENTITY_PATTERNS) {
|
|
59
|
+
let match;
|
|
60
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
61
|
+
while ((match = regex.exec(lower)) !== null) {
|
|
62
|
+
const entity = match[1];
|
|
63
|
+
if (entity && entity.length > 2 && !STOP_WORDS.has(entity)) {
|
|
64
|
+
entitySet.add(entity);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const entities = [...entitySet];
|
|
69
|
+
|
|
70
|
+
// Determine if frontend/backend needed
|
|
71
|
+
const needsFrontend = FRONTEND_SIGNALS.some(s => lower.includes(s)) ||
|
|
72
|
+
features.some(f => ['dashboard', 'search', 'uploads'].includes(f)) ||
|
|
73
|
+
!BACKEND_ONLY_SIGNALS.some(s => lower.includes(s));
|
|
74
|
+
const needsBackend = BACKEND_ONLY_SIGNALS.some(s => lower.includes(s)) ||
|
|
75
|
+
features.some(f => ['auth', 'payments', 'email', 'ai'].includes(f)) ||
|
|
76
|
+
needsFrontend; // full-stack apps always need a backend
|
|
77
|
+
|
|
78
|
+
// Determine language
|
|
79
|
+
const suggestPython = PYTHON_SIGNALS.some(s => lower.includes(s));
|
|
80
|
+
const suggestedLanguage = suggestPython ? 'python' : 'typescript';
|
|
81
|
+
|
|
82
|
+
// Determine service type
|
|
83
|
+
let suggestedServiceType;
|
|
84
|
+
if (needsFrontend && needsBackend && suggestPython) {
|
|
85
|
+
suggestedServiceType = 'full_stack'; // will become polyglot
|
|
86
|
+
} else if (needsFrontend && needsBackend) {
|
|
87
|
+
suggestedServiceType = 'full_stack';
|
|
88
|
+
} else if (!needsFrontend && needsBackend) {
|
|
89
|
+
suggestedServiceType = 'api_service';
|
|
90
|
+
} else {
|
|
91
|
+
suggestedServiceType = 'web_app';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// AI service override — if AI is the primary focus
|
|
95
|
+
if (features.includes('ai') && !features.includes('dashboard') && !features.includes('payments')) {
|
|
96
|
+
suggestedServiceType = 'ai_service';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { features, entities, suggestedServiceType, suggestedLanguage };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function mapFeaturesToStack(analysis) {
|
|
103
|
+
const refinements = {
|
|
104
|
+
language: analysis.suggestedLanguage,
|
|
105
|
+
auth: analysis.features.includes('auth') || analysis.features.includes('roles'),
|
|
106
|
+
ai: analysis.features.includes('ai'),
|
|
107
|
+
fileUploads: analysis.features.includes('uploads'),
|
|
108
|
+
realtime: analysis.features.includes('realtime'),
|
|
109
|
+
deployment: 'docker',
|
|
110
|
+
claudeCode: true,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const stackConfig = recommend(analysis.suggestedServiceType, refinements);
|
|
114
|
+
return stackConfig;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function buildGuidedPreview(analysis, stackConfig) {
|
|
118
|
+
const lines = [];
|
|
119
|
+
|
|
120
|
+
lines.push(chalk.bold(' What you\'ll get:'));
|
|
121
|
+
|
|
122
|
+
// Group by detected entities/roles
|
|
123
|
+
const hasRoles = analysis.features.includes('roles');
|
|
124
|
+
const entities = analysis.entities;
|
|
125
|
+
|
|
126
|
+
if (entities.length > 0) {
|
|
127
|
+
lines.push(' ├── A website where you can:');
|
|
128
|
+
for (const entity of entities.slice(0, 5)) {
|
|
129
|
+
lines.push(` │ • Manage ${entity}`);
|
|
130
|
+
}
|
|
131
|
+
lines.push(' │');
|
|
132
|
+
} else {
|
|
133
|
+
lines.push(' ├── A website for your application');
|
|
134
|
+
lines.push(' │');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Feature descriptions in plain English
|
|
138
|
+
if (analysis.features.includes('auth') || analysis.features.includes('roles')) {
|
|
139
|
+
const roleDesc = hasRoles ? ' (with different roles like admin and user)' : '';
|
|
140
|
+
lines.push(` ├── User accounts${roleDesc}`);
|
|
141
|
+
}
|
|
142
|
+
if (analysis.features.includes('dashboard')) {
|
|
143
|
+
lines.push(' ├── A dashboard to see your data at a glance');
|
|
144
|
+
}
|
|
145
|
+
if (analysis.features.includes('realtime')) {
|
|
146
|
+
lines.push(' ├── Real-time updates (changes appear instantly)');
|
|
147
|
+
}
|
|
148
|
+
if (analysis.features.includes('payments')) {
|
|
149
|
+
lines.push(' ├── Payment processing for orders or subscriptions');
|
|
150
|
+
}
|
|
151
|
+
if (analysis.features.includes('uploads')) {
|
|
152
|
+
lines.push(' ├── File uploads (images, documents, etc.)');
|
|
153
|
+
}
|
|
154
|
+
if (analysis.features.includes('search')) {
|
|
155
|
+
lines.push(' ├── Search and filtering');
|
|
156
|
+
}
|
|
157
|
+
if (analysis.features.includes('scheduling')) {
|
|
158
|
+
lines.push(' ├── Scheduling and bookings');
|
|
159
|
+
}
|
|
160
|
+
if (analysis.features.includes('email')) {
|
|
161
|
+
lines.push(' ├── Email notifications');
|
|
162
|
+
}
|
|
163
|
+
if (analysis.features.includes('ai')) {
|
|
164
|
+
lines.push(' ├── AI-powered features');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Always included
|
|
168
|
+
lines.push(' ├── A database to store all your data');
|
|
169
|
+
lines.push(' │');
|
|
170
|
+
lines.push(' └── Developer tools:');
|
|
171
|
+
lines.push(' • Code quality checks (catches errors automatically)');
|
|
172
|
+
lines.push(' • Guided workflows (type /project:help anytime you\'re stuck)');
|
|
173
|
+
lines.push(' • Testing templates');
|
|
174
|
+
|
|
175
|
+
return lines.join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function generateGettingStarted(projectName, stackConfig) {
|
|
179
|
+
const isPolyglot = stackConfig.stackId === 'polyglot-fullstack';
|
|
180
|
+
const isFastapi = stackConfig.stackId === 'fastapi-backend';
|
|
181
|
+
|
|
182
|
+
let setupCommands;
|
|
183
|
+
if (isFastapi) {
|
|
184
|
+
setupCommands = `cd backend
|
|
185
|
+
python -m venv venv
|
|
186
|
+
source venv/bin/activate # or venv\\\\Scripts\\\\activate on Windows
|
|
187
|
+
pip install -r requirements.txt
|
|
188
|
+
uvicorn app.main:app --reload`;
|
|
189
|
+
} else if (isPolyglot) {
|
|
190
|
+
setupCommands = `# Start the database:
|
|
191
|
+
docker compose up -d postgres
|
|
192
|
+
|
|
193
|
+
# Start the frontend:
|
|
194
|
+
cd frontend && npm install && npm run dev
|
|
195
|
+
|
|
196
|
+
# In another terminal, start the backend:
|
|
197
|
+
cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload`;
|
|
198
|
+
} else {
|
|
199
|
+
setupCommands = `npm install
|
|
200
|
+
npm run dev`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const devUrl = isFastapi ? 'http://localhost:8000/docs' : 'http://localhost:3000';
|
|
204
|
+
|
|
205
|
+
return `# Getting Started with ${projectName}
|
|
206
|
+
|
|
207
|
+
## What Just Happened?
|
|
208
|
+
|
|
209
|
+
DevForge created a starter version of your app with all the basic
|
|
210
|
+
building blocks in place. Think of it like getting a house with the
|
|
211
|
+
foundation, walls, and plumbing already done — you just need to
|
|
212
|
+
furnish and decorate it.
|
|
213
|
+
|
|
214
|
+
## Your Project Structure (Plain English)
|
|
215
|
+
|
|
216
|
+
\`\`\`
|
|
217
|
+
📁 ${projectName}/
|
|
218
|
+
├── src/ ← This is where your app's code lives
|
|
219
|
+
│ ├── app/ ← The pages people see in their browser
|
|
220
|
+
│ └── lib/ ← Helper code (like a toolbox)
|
|
221
|
+
├── docs/ ← Guides and documentation (you're reading one now!)
|
|
222
|
+
├── .claude/ ← AI assistant configuration (helps catch mistakes)
|
|
223
|
+
└── README.md ← Overview of your project
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
## First Steps
|
|
227
|
+
|
|
228
|
+
### 1. Start your app
|
|
229
|
+
Open a terminal in this folder and run:
|
|
230
|
+
\`\`\`bash
|
|
231
|
+
${setupCommands}
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
Then open ${devUrl} in your browser. You should see your app!
|
|
235
|
+
|
|
236
|
+
### 2. Make your first change
|
|
237
|
+
Open a file in your code editor. Change some text. Save.
|
|
238
|
+
Your browser should update automatically.
|
|
239
|
+
|
|
240
|
+
### 3. Get AI help (if you have Claude Code)
|
|
241
|
+
Type \`/project:help\` in Claude Code. It will ask what you're trying
|
|
242
|
+
to do and give you step-by-step guidance.
|
|
243
|
+
|
|
244
|
+
## Common Tasks
|
|
245
|
+
|
|
246
|
+
| I want to... | Do this |
|
|
247
|
+
|-----------------------------|------------------------------------------------|
|
|
248
|
+
| Add a new page | Create a new folder in src/app/ with a page file |
|
|
249
|
+
| Change how something looks | Edit the styles in the component |
|
|
250
|
+
| Add a new feature | Type /project:help → "Start a new feature" |
|
|
251
|
+
| Fix a bug | Type /project:help → "Fix a bug" |
|
|
252
|
+
| Check if everything works | Type /project:status |
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function runGuidedFlow(projectName, outputDir, scaffoldFn) {
|
|
257
|
+
let description = await askDescription();
|
|
258
|
+
|
|
259
|
+
if (!description || !description.trim()) {
|
|
260
|
+
log.error('Please provide a description of what you want to build.');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let analysis = analyzeDescription(description);
|
|
265
|
+
let stackConfig = mapFeaturesToStack(analysis);
|
|
266
|
+
|
|
267
|
+
if (stackConfig.supported === false) {
|
|
268
|
+
log.warn('Could not determine a supported stack from your description.');
|
|
269
|
+
log.info('Try describing a web app, API, or full-stack application.');
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
stackConfig.projectName = projectName;
|
|
274
|
+
|
|
275
|
+
// Show preview loop
|
|
276
|
+
let confirmed = false;
|
|
277
|
+
while (!confirmed) {
|
|
278
|
+
console.log('');
|
|
279
|
+
console.log(chalk.bold(' Got it! Here\'s what I\'ll set up for you:'));
|
|
280
|
+
console.log('');
|
|
281
|
+
console.log(buildGuidedPreview(analysis, stackConfig));
|
|
282
|
+
console.log('');
|
|
283
|
+
|
|
284
|
+
const choice = await askGuidedConfirm();
|
|
285
|
+
|
|
286
|
+
if (choice === 'yes') {
|
|
287
|
+
confirmed = true;
|
|
288
|
+
} else {
|
|
289
|
+
const adjustment = await askAdjustment();
|
|
290
|
+
if (adjustment && adjustment.trim()) {
|
|
291
|
+
// Re-analyze with the combined description
|
|
292
|
+
description = description + '. ' + adjustment;
|
|
293
|
+
analysis = analyzeDescription(description);
|
|
294
|
+
stackConfig = mapFeaturesToStack(analysis);
|
|
295
|
+
if (stackConfig.supported === false) {
|
|
296
|
+
log.warn('Could not determine a supported stack. Using previous configuration.');
|
|
297
|
+
stackConfig = mapFeaturesToStack(analyzeDescription(description.split('. ').slice(0, -1).join('. ')));
|
|
298
|
+
}
|
|
299
|
+
stackConfig.projectName = projectName;
|
|
300
|
+
console.log('');
|
|
301
|
+
log.info('Updated!');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Scaffold
|
|
307
|
+
await scaffoldFn(outputDir, stackConfig);
|
|
308
|
+
|
|
309
|
+
// Generate getting-started.md for guided mode
|
|
310
|
+
const gettingStartedContent = generateGettingStarted(projectName, stackConfig);
|
|
311
|
+
writeFile(
|
|
312
|
+
path.join(outputDir, 'docs', 'getting-started.md'),
|
|
313
|
+
gettingStartedContent
|
|
314
|
+
);
|
|
315
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { log, toKebabCase } from './utils.js';
|
|
6
|
+
import { askServiceType, askRefinements, askNewMode, confirmStack } from './prompts.js';
|
|
7
|
+
import { recommend } from './recommender.js';
|
|
8
|
+
import { compose } from './composer.js';
|
|
9
|
+
import { generateClaudeConfig } from './claude-configurator.js';
|
|
10
|
+
import { generateUAT } from './uat-generator.js';
|
|
11
|
+
|
|
12
|
+
export async function runNew(projectName) {
|
|
13
|
+
const safeName = toKebabCase(projectName);
|
|
14
|
+
const outputDir = path.resolve(process.cwd(), safeName);
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(outputDir)) {
|
|
17
|
+
log.error(`Directory "${safeName}" already exists. Choose a different name or delete it first.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('');
|
|
22
|
+
console.log(chalk.bold.cyan(' 🔨 DevForge') + chalk.dim(' — Let\'s build something.'));
|
|
23
|
+
console.log('');
|
|
24
|
+
|
|
25
|
+
const mode = await askNewMode();
|
|
26
|
+
|
|
27
|
+
if (mode === 'guided') {
|
|
28
|
+
const { runGuidedFlow } = await import('./guided.js');
|
|
29
|
+
await runGuidedFlow(safeName, outputDir, scaffold);
|
|
30
|
+
} else {
|
|
31
|
+
await runDeveloperFlow(safeName, outputDir);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runDeveloperFlow(safeName, outputDir) {
|
|
36
|
+
const serviceType = await askServiceType();
|
|
37
|
+
const refinements = await askRefinements(serviceType);
|
|
38
|
+
const stackConfig = recommend(serviceType, refinements);
|
|
39
|
+
|
|
40
|
+
if (stackConfig.supported === false) {
|
|
41
|
+
log.warn(stackConfig.message);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
stackConfig.projectName = safeName;
|
|
46
|
+
|
|
47
|
+
const confirmed = await confirmStack(stackConfig);
|
|
48
|
+
if (!confirmed) {
|
|
49
|
+
log.info('Cancelled.');
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await scaffold(outputDir, stackConfig);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function scaffold(outputDir, stackConfig) {
|
|
57
|
+
console.log('');
|
|
58
|
+
const totalSteps = stackConfig.claudeCode ? 4 : 3;
|
|
59
|
+
let step = 0;
|
|
60
|
+
|
|
61
|
+
step++;
|
|
62
|
+
log.step(step, totalSteps, 'Scaffolding project structure...');
|
|
63
|
+
await compose(outputDir, stackConfig);
|
|
64
|
+
|
|
65
|
+
if (stackConfig.claudeCode) {
|
|
66
|
+
step++;
|
|
67
|
+
log.step(step, totalSteps, 'Generating Claude Code infrastructure...');
|
|
68
|
+
await generateClaudeConfig(outputDir, stackConfig);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
step++;
|
|
72
|
+
log.step(step, totalSteps, 'Generating UAT templates...');
|
|
73
|
+
await generateUAT(outputDir, stackConfig);
|
|
74
|
+
|
|
75
|
+
step++;
|
|
76
|
+
log.step(step, totalSteps, 'Initializing git repository...');
|
|
77
|
+
initGit(outputDir);
|
|
78
|
+
|
|
79
|
+
console.log('');
|
|
80
|
+
log.success('Done! Your project is ready.');
|
|
81
|
+
console.log('');
|
|
82
|
+
printNextSteps(stackConfig.projectName, stackConfig);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function initGit(outputDir) {
|
|
86
|
+
try {
|
|
87
|
+
execSync('git init', { cwd: outputDir, stdio: 'ignore' });
|
|
88
|
+
} catch {
|
|
89
|
+
// git not available — skip silently
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function printNextSteps(projectName, config, isGuided = false) {
|
|
94
|
+
if (isGuided) {
|
|
95
|
+
console.log(chalk.bold(' To start building:'));
|
|
96
|
+
console.log(` cd ${projectName}`);
|
|
97
|
+
console.log(' npm install');
|
|
98
|
+
console.log(' npm run dev');
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(` Then open your browser to ${chalk.cyan('http://localhost:3000')}`);
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log(chalk.bold(' 📖 New to coding? Here\'s what to do next:'));
|
|
103
|
+
console.log(' 1. Open this folder in VS Code (or any code editor)');
|
|
104
|
+
console.log(' 2. If you have Claude Code installed, type /project:help');
|
|
105
|
+
console.log(' — it will guide you step by step');
|
|
106
|
+
console.log(' 3. Or read docs/getting-started.md for a beginner-friendly walkthrough');
|
|
107
|
+
console.log('');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(chalk.bold(' Next steps:'));
|
|
112
|
+
console.log(` cd ${projectName}`);
|
|
113
|
+
|
|
114
|
+
if (config.stackId === 'nextjs-fullstack') {
|
|
115
|
+
console.log(' npm install');
|
|
116
|
+
console.log(' cp .env.example .env');
|
|
117
|
+
console.log(' npx prisma db push');
|
|
118
|
+
console.log(' npm run dev');
|
|
119
|
+
} else if (config.stackId === 'fastapi-backend') {
|
|
120
|
+
console.log(' cd backend');
|
|
121
|
+
console.log(' python -m venv venv');
|
|
122
|
+
console.log(' source venv/bin/activate # or venv\\Scripts\\activate on Windows');
|
|
123
|
+
console.log(' pip install -r requirements.txt');
|
|
124
|
+
console.log(' cp .env.example .env');
|
|
125
|
+
console.log(' uvicorn app.main:app --reload');
|
|
126
|
+
} else if (config.stackId === 'polyglot-fullstack') {
|
|
127
|
+
console.log(' docker compose up -d postgres');
|
|
128
|
+
console.log(' # Frontend:');
|
|
129
|
+
console.log(' cd frontend && npm install && npm run dev');
|
|
130
|
+
console.log(' # Backend:');
|
|
131
|
+
console.log(' cd backend && pip install -r requirements.txt && uvicorn app.main:app --reload');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (config.claudeCode) {
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log(chalk.bold(' Claude Code:'));
|
|
137
|
+
console.log(' Your project includes pre-configured Claude Code infrastructure:');
|
|
138
|
+
console.log(chalk.dim(' - CLAUDE.md (project context + rules)'));
|
|
139
|
+
console.log(chalk.dim(' - .claude/hooks/ (auto-lint, quality gates)'));
|
|
140
|
+
console.log(chalk.dim(' - .claude/skills/ (framework knowledge)'));
|
|
141
|
+
console.log(chalk.dim(' - .claude/agents/ (verification chain)'));
|
|
142
|
+
console.log(chalk.dim(' - .claude/commands/ (audit, verify, pre-pr)'));
|
|
143
|
+
console.log(chalk.dim(' - docs/prompt-library.md'));
|
|
144
|
+
console.log(chalk.dim(' - docs/uat/ (acceptance testing)'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
package/src/init-mode.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { log } from './utils.js';
|
|
4
|
+
import { scanProject } from './scanner.js';
|
|
5
|
+
import { generateClaudeConfig } from './claude-configurator.js';
|
|
6
|
+
import { generateUAT } from './uat-generator.js';
|
|
7
|
+
|
|
8
|
+
export async function runInit(projectDir) {
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log(chalk.bold.cyan(' 🔨 DevForge') + chalk.dim(' — Adding dev guardrails'));
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(' Scanning...');
|
|
13
|
+
console.log('');
|
|
14
|
+
|
|
15
|
+
const scan = scanProject(projectDir);
|
|
16
|
+
|
|
17
|
+
if (scan.stackId === 'unknown') {
|
|
18
|
+
log.warn('Could not detect a supported stack in this directory.');
|
|
19
|
+
log.dim(' Supported: Next.js, FastAPI, or polyglot (Next.js + FastAPI)');
|
|
20
|
+
log.dim(' Make sure you\'re in the project root directory.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Print what was detected
|
|
25
|
+
const detected = [];
|
|
26
|
+
if (scan.frontend.detected) {
|
|
27
|
+
detected.push(`${scan.frontend.framework} (${scan.frontend.language})`);
|
|
28
|
+
}
|
|
29
|
+
if (scan.backend.detected) {
|
|
30
|
+
detected.push(`${scan.backend.framework} (${scan.backend.language})`);
|
|
31
|
+
}
|
|
32
|
+
if (scan.database.detected) {
|
|
33
|
+
detected.push(`${scan.database.type} (${scan.database.orm})`);
|
|
34
|
+
}
|
|
35
|
+
if (scan.testing.unit) detected.push(scan.testing.unit);
|
|
36
|
+
if (scan.testing.e2e) detected.push(scan.testing.e2e);
|
|
37
|
+
if (scan.testing.backend) detected.push(scan.testing.backend);
|
|
38
|
+
|
|
39
|
+
console.log(chalk.bold(' Detected: ') + detected.join(' + '));
|
|
40
|
+
if (scan.infrastructure.hasClaudeMd) {
|
|
41
|
+
console.log(chalk.dim(` CLAUDE.md exists (${scan.infrastructure.claudeMdLines} lines)`));
|
|
42
|
+
}
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Build a synthetic stackConfig from scan results
|
|
46
|
+
const stackConfig = buildConfigFromScan(scan);
|
|
47
|
+
|
|
48
|
+
// Install infrastructure
|
|
49
|
+
console.log(chalk.bold(' Installing:'));
|
|
50
|
+
|
|
51
|
+
// Generate Claude Code infrastructure (skips CLAUDE.md if it exists, merges settings.json if it exists)
|
|
52
|
+
const skipClaudeMd = scan.infrastructure.hasClaudeMd;
|
|
53
|
+
const mergeSettings = scan.infrastructure.hasHooks;
|
|
54
|
+
await generateClaudeConfig(projectDir, stackConfig, { skipClaudeMd, mergeSettings });
|
|
55
|
+
|
|
56
|
+
if (!skipClaudeMd) {
|
|
57
|
+
console.log(chalk.green(' ✓ ') + 'CLAUDE.md — project context + rules');
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.yellow(' ⊘ ') + `CLAUDE.md — exists (tip: run ${chalk.cyan('/project:optimize-claude-md')} to slim it)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!scan.infrastructure.hasHooks) {
|
|
63
|
+
console.log(chalk.green(' ✓ ') + '.claude/hooks/ — auto-lint, quality gate, file protection');
|
|
64
|
+
} else {
|
|
65
|
+
console.log(chalk.yellow(' ⊘ ') + '.claude/hooks/ — already configured');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!scan.infrastructure.hasAgents) {
|
|
69
|
+
console.log(chalk.green(' ✓ ') + '.claude/agents/ — code quality, security, spec validator');
|
|
70
|
+
}
|
|
71
|
+
if (!scan.infrastructure.hasCommands) {
|
|
72
|
+
console.log(chalk.green(' ✓ ') + '.claude/commands/ — help, status, next, done, audit, pre-pr');
|
|
73
|
+
}
|
|
74
|
+
if (!scan.infrastructure.hasSkills) {
|
|
75
|
+
console.log(chalk.green(' ✓ ') + '.claude/skills/ — framework-specific knowledge');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Generate UAT templates
|
|
79
|
+
if (!scan.infrastructure.hasUAT) {
|
|
80
|
+
await generateUAT(projectDir, stackConfig);
|
|
81
|
+
console.log(chalk.green(' ✓ ') + 'docs/uat/ — acceptance test templates');
|
|
82
|
+
} else {
|
|
83
|
+
console.log(chalk.yellow(' ⊘ ') + 'docs/uat/ — already exists');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('');
|
|
87
|
+
log.success(' Done. Type /project:help to get started.');
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildConfigFromScan(scan) {
|
|
92
|
+
const config = {
|
|
93
|
+
projectName: scan.projectName,
|
|
94
|
+
stackId: scan.stackId,
|
|
95
|
+
serviceType: scan.stackId === 'fastapi-backend' ? 'api_service' : 'full_stack',
|
|
96
|
+
frontend: null,
|
|
97
|
+
backend: null,
|
|
98
|
+
database: null,
|
|
99
|
+
auth: scan.auth.detected ? scan.auth.type : null,
|
|
100
|
+
testing: scan.testing,
|
|
101
|
+
ai: scan.ai,
|
|
102
|
+
realtime: false,
|
|
103
|
+
fileUploads: false,
|
|
104
|
+
deployment: scan.deployment || 'docker',
|
|
105
|
+
claudeCode: true,
|
|
106
|
+
templateModules: [],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (scan.frontend.detected) {
|
|
110
|
+
config.frontend = {
|
|
111
|
+
framework: scan.frontend.framework,
|
|
112
|
+
language: scan.frontend.language,
|
|
113
|
+
styling: 'tailwind',
|
|
114
|
+
ui: 'shadcn',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (scan.backend.detected) {
|
|
119
|
+
config.backend = {
|
|
120
|
+
framework: scan.backend.framework,
|
|
121
|
+
language: scan.backend.language,
|
|
122
|
+
orm: scan.database.detected ? scan.database.orm : (scan.backend.language === 'python' ? 'sqlalchemy' : 'prisma'),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (scan.database.detected) {
|
|
127
|
+
config.database = {
|
|
128
|
+
type: scan.database.type,
|
|
129
|
+
orm: scan.database.orm,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return config;
|
|
134
|
+
}
|