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/scanner.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function scanProject(projectDir) {
|
|
5
|
+
const scan = {
|
|
6
|
+
stackId: null,
|
|
7
|
+
projectName: path.basename(projectDir),
|
|
8
|
+
|
|
9
|
+
frontend: detectFrontend(projectDir),
|
|
10
|
+
backend: detectBackend(projectDir),
|
|
11
|
+
database: detectDatabase(projectDir),
|
|
12
|
+
auth: detectAuth(projectDir),
|
|
13
|
+
testing: detectTesting(projectDir),
|
|
14
|
+
deployment: detectDeployment(projectDir),
|
|
15
|
+
ai: detectAI(projectDir),
|
|
16
|
+
|
|
17
|
+
infrastructure: {
|
|
18
|
+
hasClaudeMd: false,
|
|
19
|
+
claudeMdLines: 0,
|
|
20
|
+
hasHooks: false,
|
|
21
|
+
hasAgents: false,
|
|
22
|
+
hasCommands: false,
|
|
23
|
+
hasSkills: false,
|
|
24
|
+
hasUAT: false,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
files: {
|
|
28
|
+
packageJson: fileExists(projectDir, 'package.json'),
|
|
29
|
+
requirementsTxt: fileExists(projectDir, 'requirements.txt') || fileExists(projectDir, 'backend/requirements.txt'),
|
|
30
|
+
prismaSchema: fileExists(projectDir, 'prisma/schema.prisma') || fileExists(projectDir, 'frontend/prisma/schema.prisma'),
|
|
31
|
+
alembicIni: fileExists(projectDir, 'alembic.ini') || fileExists(projectDir, 'backend/alembic.ini'),
|
|
32
|
+
dockerCompose: fileExists(projectDir, 'docker-compose.yml') || fileExists(projectDir, 'docker-compose.yaml'),
|
|
33
|
+
nextConfig: hasNextConfig(projectDir),
|
|
34
|
+
pyprojectToml: fileExists(projectDir, 'pyproject.toml') || fileExists(projectDir, 'backend/pyproject.toml'),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Detect stack
|
|
39
|
+
scan.stackId = detectStack(projectDir, scan);
|
|
40
|
+
|
|
41
|
+
// Detect Claude infrastructure
|
|
42
|
+
scan.infrastructure = detectClaudeInfra(projectDir);
|
|
43
|
+
|
|
44
|
+
return scan;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function detectStack(projectDir, scan) {
|
|
48
|
+
const hasNext = scan ? scan.files.nextConfig : hasNextConfig(projectDir);
|
|
49
|
+
const hasFastapi = scan ? scan.backend.detected && scan.backend.framework === 'fastapi' : detectBackend(projectDir).detected;
|
|
50
|
+
const hasFrontendNext = hasNext || fileExists(projectDir, 'frontend/next.config.ts') || fileExists(projectDir, 'frontend/next.config.js') || fileExists(projectDir, 'frontend/next.config.mjs');
|
|
51
|
+
|
|
52
|
+
if (hasFrontendNext && hasFastapi) {
|
|
53
|
+
return 'polyglot-fullstack';
|
|
54
|
+
}
|
|
55
|
+
if (hasNext && !hasFastapi) {
|
|
56
|
+
return 'nextjs-fullstack';
|
|
57
|
+
}
|
|
58
|
+
if (hasFastapi && !hasNext) {
|
|
59
|
+
return 'fastapi-backend';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Try to detect from package.json
|
|
63
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
64
|
+
if (pkg) {
|
|
65
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
66
|
+
if (deps.next) return 'nextjs-fullstack';
|
|
67
|
+
if (deps.react) return 'nextjs-fullstack'; // best guess
|
|
68
|
+
if (deps.express) return 'nextjs-fullstack'; // close enough
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try requirements.txt for Python
|
|
72
|
+
if (fileExists(projectDir, 'requirements.txt') || fileExists(projectDir, 'backend/requirements.txt')) {
|
|
73
|
+
const reqPath = fileExists(projectDir, 'requirements.txt')
|
|
74
|
+
? path.join(projectDir, 'requirements.txt')
|
|
75
|
+
: path.join(projectDir, 'backend/requirements.txt');
|
|
76
|
+
const content = fs.readFileSync(reqPath, 'utf-8').toLowerCase();
|
|
77
|
+
if (content.includes('fastapi')) return 'fastapi-backend';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 'unknown';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function detectFrontend(projectDir) {
|
|
84
|
+
const result = { detected: false, framework: null, language: null, directory: null };
|
|
85
|
+
|
|
86
|
+
// Check for Next.js
|
|
87
|
+
if (hasNextConfig(projectDir)) {
|
|
88
|
+
result.detected = true;
|
|
89
|
+
result.framework = 'nextjs';
|
|
90
|
+
result.language = fileExists(projectDir, 'tsconfig.json') ? 'typescript' : 'javascript';
|
|
91
|
+
result.directory = 'src';
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check in frontend/ subdirectory
|
|
96
|
+
if (hasNextConfig(path.join(projectDir, 'frontend'))) {
|
|
97
|
+
result.detected = true;
|
|
98
|
+
result.framework = 'nextjs';
|
|
99
|
+
result.language = fileExists(path.join(projectDir, 'frontend'), 'tsconfig.json') ? 'typescript' : 'javascript';
|
|
100
|
+
result.directory = 'frontend';
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check package.json for React
|
|
105
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
106
|
+
if (pkg) {
|
|
107
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
108
|
+
if (deps.next) {
|
|
109
|
+
result.detected = true;
|
|
110
|
+
result.framework = 'nextjs';
|
|
111
|
+
result.language = fileExists(projectDir, 'tsconfig.json') ? 'typescript' : 'javascript';
|
|
112
|
+
result.directory = 'src';
|
|
113
|
+
} else if (deps.react) {
|
|
114
|
+
result.detected = true;
|
|
115
|
+
result.framework = 'react';
|
|
116
|
+
result.language = fileExists(projectDir, 'tsconfig.json') ? 'typescript' : 'javascript';
|
|
117
|
+
result.directory = 'src';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function detectBackend(projectDir) {
|
|
125
|
+
const result = { detected: false, framework: null, language: null, directory: null };
|
|
126
|
+
|
|
127
|
+
// Check for FastAPI
|
|
128
|
+
const backendPaths = ['backend/app/main.py', 'app/main.py', 'main.py'];
|
|
129
|
+
for (const p of backendPaths) {
|
|
130
|
+
if (fileExists(projectDir, p)) {
|
|
131
|
+
const content = fs.readFileSync(path.join(projectDir, p), 'utf-8');
|
|
132
|
+
if (content.includes('FastAPI') || content.includes('fastapi')) {
|
|
133
|
+
result.detected = true;
|
|
134
|
+
result.framework = 'fastapi';
|
|
135
|
+
result.language = 'python';
|
|
136
|
+
result.directory = p.includes('backend/') ? 'backend' : '.';
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check requirements.txt for FastAPI
|
|
143
|
+
const reqPaths = ['requirements.txt', 'backend/requirements.txt'];
|
|
144
|
+
for (const p of reqPaths) {
|
|
145
|
+
if (fileExists(projectDir, p)) {
|
|
146
|
+
const content = fs.readFileSync(path.join(projectDir, p), 'utf-8').toLowerCase();
|
|
147
|
+
if (content.includes('fastapi')) {
|
|
148
|
+
result.detected = true;
|
|
149
|
+
result.framework = 'fastapi';
|
|
150
|
+
result.language = 'python';
|
|
151
|
+
result.directory = p.includes('backend/') ? 'backend' : '.';
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
if (content.includes('flask')) {
|
|
155
|
+
result.detected = true;
|
|
156
|
+
result.framework = 'flask';
|
|
157
|
+
result.language = 'python';
|
|
158
|
+
result.directory = p.includes('backend/') ? 'backend' : '.';
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
if (content.includes('django')) {
|
|
162
|
+
result.detected = true;
|
|
163
|
+
result.framework = 'django';
|
|
164
|
+
result.language = 'python';
|
|
165
|
+
result.directory = p.includes('backend/') ? 'backend' : '.';
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check for Express/Node backend
|
|
172
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
173
|
+
if (pkg) {
|
|
174
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
175
|
+
if (deps.express) {
|
|
176
|
+
result.detected = true;
|
|
177
|
+
result.framework = 'express';
|
|
178
|
+
result.language = 'typescript';
|
|
179
|
+
result.directory = 'src';
|
|
180
|
+
} else if (deps.hono) {
|
|
181
|
+
result.detected = true;
|
|
182
|
+
result.framework = 'hono';
|
|
183
|
+
result.language = 'typescript';
|
|
184
|
+
result.directory = 'src';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detectDatabase(projectDir) {
|
|
192
|
+
const result = { detected: false, type: null, orm: null };
|
|
193
|
+
|
|
194
|
+
if (fileExists(projectDir, 'prisma/schema.prisma') || fileExists(projectDir, 'frontend/prisma/schema.prisma')) {
|
|
195
|
+
result.detected = true;
|
|
196
|
+
result.orm = 'prisma';
|
|
197
|
+
const schemaPath = fileExists(projectDir, 'prisma/schema.prisma')
|
|
198
|
+
? path.join(projectDir, 'prisma/schema.prisma')
|
|
199
|
+
: path.join(projectDir, 'frontend/prisma/schema.prisma');
|
|
200
|
+
const content = fs.readFileSync(schemaPath, 'utf-8').toLowerCase();
|
|
201
|
+
result.type = content.includes('postgresql') ? 'postgresql' :
|
|
202
|
+
content.includes('mysql') ? 'mysql' :
|
|
203
|
+
content.includes('sqlite') ? 'sqlite' : 'postgresql';
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (fileExists(projectDir, 'alembic.ini') || fileExists(projectDir, 'backend/alembic.ini')) {
|
|
208
|
+
result.detected = true;
|
|
209
|
+
result.orm = 'sqlalchemy';
|
|
210
|
+
result.type = 'postgresql';
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function detectAuth(projectDir) {
|
|
218
|
+
const result = { detected: false, type: null };
|
|
219
|
+
|
|
220
|
+
// NextAuth
|
|
221
|
+
if (fileExists(projectDir, 'src/lib/auth.ts') || fileExists(projectDir, 'src/lib/auth.js') ||
|
|
222
|
+
fileExists(projectDir, 'frontend/src/lib/auth.ts')) {
|
|
223
|
+
result.detected = true;
|
|
224
|
+
result.type = 'nextauth';
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// JWT custom
|
|
229
|
+
if (fileExists(projectDir, 'backend/app/core/security.py') || fileExists(projectDir, 'app/core/security.py')) {
|
|
230
|
+
result.detected = true;
|
|
231
|
+
result.type = 'jwt-custom';
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check package.json for auth packages
|
|
236
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
237
|
+
if (pkg) {
|
|
238
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
239
|
+
if (deps['next-auth'] || deps['@auth/core']) {
|
|
240
|
+
result.detected = true;
|
|
241
|
+
result.type = 'nextauth';
|
|
242
|
+
} else if (deps.passport) {
|
|
243
|
+
result.detected = true;
|
|
244
|
+
result.type = 'passport';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function detectTesting(projectDir) {
|
|
252
|
+
const result = { unit: null, e2e: null, backend: null };
|
|
253
|
+
|
|
254
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
255
|
+
if (pkg) {
|
|
256
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
257
|
+
if (deps.vitest) result.unit = 'vitest';
|
|
258
|
+
else if (deps.jest) result.unit = 'jest';
|
|
259
|
+
if (deps['@playwright/test'] || deps.playwright) result.e2e = 'playwright';
|
|
260
|
+
else if (deps.cypress) result.e2e = 'cypress';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Frontend subdir
|
|
264
|
+
const frontendPkg = readJsonSafe(path.join(projectDir, 'frontend'), 'package.json');
|
|
265
|
+
if (frontendPkg) {
|
|
266
|
+
const deps = { ...frontendPkg.dependencies, ...frontendPkg.devDependencies };
|
|
267
|
+
if (deps.vitest && !result.unit) result.unit = 'vitest';
|
|
268
|
+
if (deps['@playwright/test'] && !result.e2e) result.e2e = 'playwright';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Python tests
|
|
272
|
+
if (fileExists(projectDir, 'backend/tests/conftest.py') || fileExists(projectDir, 'tests/conftest.py') ||
|
|
273
|
+
fileExists(projectDir, 'backend/pytest.ini') || fileExists(projectDir, 'pytest.ini')) {
|
|
274
|
+
result.backend = 'pytest';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function detectDeployment(projectDir) {
|
|
281
|
+
if (fileExists(projectDir, 'docker-compose.yml') || fileExists(projectDir, 'docker-compose.yaml')) return 'docker';
|
|
282
|
+
if (fileExists(projectDir, 'vercel.json')) return 'vercel';
|
|
283
|
+
if (fileExists(projectDir, 'Dockerfile')) return 'docker';
|
|
284
|
+
if (fileExists(projectDir, '.github/workflows')) return 'github-actions';
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function detectAI(projectDir) {
|
|
289
|
+
// Check package.json
|
|
290
|
+
const pkg = readJsonSafe(projectDir, 'package.json');
|
|
291
|
+
if (pkg) {
|
|
292
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
293
|
+
if (deps.openai || deps['@anthropic-ai/sdk'] || deps['@google/generative-ai'] || deps.ai) return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check requirements.txt
|
|
297
|
+
const reqPaths = ['requirements.txt', 'backend/requirements.txt'];
|
|
298
|
+
for (const p of reqPaths) {
|
|
299
|
+
if (fileExists(projectDir, p)) {
|
|
300
|
+
const content = fs.readFileSync(path.join(projectDir, p), 'utf-8').toLowerCase();
|
|
301
|
+
if (content.includes('openai') || content.includes('anthropic') || content.includes('google-generativeai') || content.includes('langchain')) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function detectClaudeInfra(projectDir) {
|
|
311
|
+
const infra = {
|
|
312
|
+
hasClaudeMd: false,
|
|
313
|
+
claudeMdLines: 0,
|
|
314
|
+
hasHooks: false,
|
|
315
|
+
hasAgents: false,
|
|
316
|
+
hasCommands: false,
|
|
317
|
+
hasSkills: false,
|
|
318
|
+
hasUAT: false,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
if (fileExists(projectDir, 'CLAUDE.md')) {
|
|
322
|
+
infra.hasClaudeMd = true;
|
|
323
|
+
const content = fs.readFileSync(path.join(projectDir, 'CLAUDE.md'), 'utf-8');
|
|
324
|
+
infra.claudeMdLines = content.split('\n').length;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
infra.hasHooks = fileExists(projectDir, '.claude/settings.json');
|
|
328
|
+
infra.hasAgents = dirHasFiles(projectDir, '.claude/agents');
|
|
329
|
+
infra.hasCommands = dirHasFiles(projectDir, '.claude/commands');
|
|
330
|
+
infra.hasSkills = dirHasFiles(projectDir, '.claude/skills');
|
|
331
|
+
infra.hasUAT = dirExists(projectDir, 'docs/uat');
|
|
332
|
+
|
|
333
|
+
return infra;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Helpers
|
|
337
|
+
|
|
338
|
+
function fileExists(base, ...segments) {
|
|
339
|
+
return fs.existsSync(path.join(base, ...segments));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function dirExists(base, ...segments) {
|
|
343
|
+
const p = path.join(base, ...segments);
|
|
344
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function dirHasFiles(base, rel) {
|
|
348
|
+
const dir = path.join(base, rel);
|
|
349
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
350
|
+
const entries = fs.readdirSync(dir);
|
|
351
|
+
return entries.length > 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function hasNextConfig(dir) {
|
|
355
|
+
return fileExists(dir, 'next.config.ts') ||
|
|
356
|
+
fileExists(dir, 'next.config.js') ||
|
|
357
|
+
fileExists(dir, 'next.config.mjs');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readJsonSafe(base, file) {
|
|
361
|
+
const p = path.join(base, file);
|
|
362
|
+
if (!fs.existsSync(p)) return null;
|
|
363
|
+
try {
|
|
364
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { writeFile, ensureDir } from './utils.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export async function generateUAT(outputDir, stackConfig) {
|
|
5
|
+
const scenarios = buildScenarios(stackConfig);
|
|
6
|
+
generateUATTemplate(outputDir, stackConfig, scenarios);
|
|
7
|
+
generateUATChecklist(outputDir, scenarios);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildScenarios(config) {
|
|
11
|
+
const scenarios = [];
|
|
12
|
+
let id = 1;
|
|
13
|
+
|
|
14
|
+
// Health check — every project gets this
|
|
15
|
+
scenarios.push({
|
|
16
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
17
|
+
feature: 'Health Check',
|
|
18
|
+
priority: 'P0',
|
|
19
|
+
preconditions: 'Application is running',
|
|
20
|
+
steps: [
|
|
21
|
+
'Send GET request to the health endpoint',
|
|
22
|
+
'Verify response status is 200',
|
|
23
|
+
'Verify response body contains status: "ok"',
|
|
24
|
+
],
|
|
25
|
+
expected: 'Health endpoint responds with 200 and status "ok"',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Database connectivity
|
|
29
|
+
if (config.database) {
|
|
30
|
+
scenarios.push({
|
|
31
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
32
|
+
feature: 'Database Connectivity',
|
|
33
|
+
priority: 'P0',
|
|
34
|
+
preconditions: 'Application and database are running',
|
|
35
|
+
steps: [
|
|
36
|
+
'Send GET request to deep health endpoint (/healthz)',
|
|
37
|
+
'Verify response includes database status',
|
|
38
|
+
'Verify database status is "connected"',
|
|
39
|
+
],
|
|
40
|
+
expected: 'Deep health check confirms database connectivity',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Homepage (frontend projects)
|
|
45
|
+
if (config.frontend) {
|
|
46
|
+
scenarios.push({
|
|
47
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
48
|
+
feature: 'Home Page Load',
|
|
49
|
+
priority: 'P0',
|
|
50
|
+
preconditions: 'Application is running',
|
|
51
|
+
steps: [
|
|
52
|
+
'Navigate to the application root URL',
|
|
53
|
+
'Verify the page loads without errors',
|
|
54
|
+
'Verify the page title is correct',
|
|
55
|
+
],
|
|
56
|
+
expected: 'Home page loads successfully with correct title',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Authentication
|
|
61
|
+
if (config.auth) {
|
|
62
|
+
scenarios.push({
|
|
63
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
64
|
+
feature: 'User Registration',
|
|
65
|
+
priority: 'P0',
|
|
66
|
+
preconditions: 'Application is running, no existing test user',
|
|
67
|
+
steps: [
|
|
68
|
+
'Navigate to registration page',
|
|
69
|
+
'Fill in valid email, password, and name',
|
|
70
|
+
'Submit the registration form',
|
|
71
|
+
'Verify account is created successfully',
|
|
72
|
+
],
|
|
73
|
+
expected: 'User account is created and user is authenticated',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
scenarios.push({
|
|
77
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
78
|
+
feature: 'User Login',
|
|
79
|
+
priority: 'P0',
|
|
80
|
+
preconditions: 'Test user account exists',
|
|
81
|
+
steps: [
|
|
82
|
+
'Navigate to login page',
|
|
83
|
+
'Enter valid credentials',
|
|
84
|
+
'Submit the login form',
|
|
85
|
+
'Verify user is redirected to dashboard/home',
|
|
86
|
+
],
|
|
87
|
+
expected: 'User is authenticated and redirected appropriately',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
scenarios.push({
|
|
91
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
92
|
+
feature: 'Invalid Login',
|
|
93
|
+
priority: 'P1',
|
|
94
|
+
preconditions: 'Application is running',
|
|
95
|
+
steps: [
|
|
96
|
+
'Navigate to login page',
|
|
97
|
+
'Enter invalid credentials',
|
|
98
|
+
'Submit the login form',
|
|
99
|
+
'Verify error message is displayed',
|
|
100
|
+
],
|
|
101
|
+
expected: 'User sees a generic error message, no sensitive info leaked',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (config.frontend) {
|
|
105
|
+
scenarios.push({
|
|
106
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
107
|
+
feature: 'Protected Route Access',
|
|
108
|
+
priority: 'P1',
|
|
109
|
+
preconditions: 'User is not authenticated',
|
|
110
|
+
steps: [
|
|
111
|
+
'Navigate to a protected route (e.g., /dashboard)',
|
|
112
|
+
'Verify redirect to login page',
|
|
113
|
+
'Log in with valid credentials',
|
|
114
|
+
'Verify redirect back to protected route',
|
|
115
|
+
],
|
|
116
|
+
expected: 'Unauthenticated users are redirected to login, then back after auth',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Error handling
|
|
122
|
+
scenarios.push({
|
|
123
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
124
|
+
feature: 'Structured Error Response',
|
|
125
|
+
priority: 'P1',
|
|
126
|
+
preconditions: 'Application is running',
|
|
127
|
+
steps: [
|
|
128
|
+
'Send a request to a non-existent endpoint',
|
|
129
|
+
'Verify response has structured error format',
|
|
130
|
+
'Verify no stack traces or internal details are leaked',
|
|
131
|
+
],
|
|
132
|
+
expected: 'Error response follows format: { error: { code, message } }',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Graceful shutdown
|
|
136
|
+
scenarios.push({
|
|
137
|
+
id: `UAT-${String(id++).padStart(3, '0')}`,
|
|
138
|
+
feature: 'Graceful Shutdown',
|
|
139
|
+
priority: 'P1',
|
|
140
|
+
preconditions: 'Application is running with active connections',
|
|
141
|
+
steps: [
|
|
142
|
+
'Send SIGTERM to the application process',
|
|
143
|
+
'Verify in-flight requests complete',
|
|
144
|
+
'Verify database connections are closed',
|
|
145
|
+
'Verify process exits with code 0',
|
|
146
|
+
],
|
|
147
|
+
expected: 'Application shuts down gracefully without dropping requests',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return scenarios;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generateUATTemplate(outputDir, config, scenarios) {
|
|
154
|
+
let content = `# UAT Scenario Pack: ${config.projectName}\n\n`;
|
|
155
|
+
content += `## Pre-Conditions\n`;
|
|
156
|
+
content += `- [ ] Application is deployed to staging\n`;
|
|
157
|
+
content += `- [ ] Test accounts are created\n`;
|
|
158
|
+
content += `- [ ] Test data is seeded\n`;
|
|
159
|
+
if (config.database) {
|
|
160
|
+
content += `- [ ] Database is running and migrated\n`;
|
|
161
|
+
}
|
|
162
|
+
content += `\n## Scenarios\n`;
|
|
163
|
+
|
|
164
|
+
for (const s of scenarios) {
|
|
165
|
+
content += `\n### ${s.id}: ${s.feature}\n`;
|
|
166
|
+
content += `**Priority:** ${s.priority}\n`;
|
|
167
|
+
content += `**Preconditions:** ${s.preconditions}\n`;
|
|
168
|
+
content += `**Steps:**\n`;
|
|
169
|
+
s.steps.forEach((step, i) => {
|
|
170
|
+
content += `${i + 1}. ${step}\n`;
|
|
171
|
+
});
|
|
172
|
+
content += `**Expected Result:** ${s.expected}\n`;
|
|
173
|
+
content += `**Actual Result:** ___\n`;
|
|
174
|
+
content += `**Status:** NOT RUN\n`;
|
|
175
|
+
content += `**Tester:** ___\n`;
|
|
176
|
+
content += `**Date:** ___\n`;
|
|
177
|
+
content += `**Notes:** ___\n`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
writeFile(path.join(outputDir, 'docs', 'uat', 'UAT_TEMPLATE.md'), content);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function generateUATChecklist(outputDir, scenarios) {
|
|
184
|
+
let csv = 'UAT_ID,Feature,Priority,Status,Tester,Date,Defect_ID,Notes\n';
|
|
185
|
+
for (const s of scenarios) {
|
|
186
|
+
csv += `${s.id},${s.feature},${s.priority},NOT RUN,,,,\n`;
|
|
187
|
+
}
|
|
188
|
+
writeFile(path.join(outputDir, 'docs', 'uat', 'UAT_CHECKLIST.csv'), csv);
|
|
189
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
export const ROOT_DIR = path.resolve(__dirname, '..');
|
|
10
|
+
|
|
11
|
+
export const log = {
|
|
12
|
+
info: (msg) => console.log(chalk.cyan(msg)),
|
|
13
|
+
success: (msg) => console.log(chalk.green(msg)),
|
|
14
|
+
warn: (msg) => console.log(chalk.yellow(msg)),
|
|
15
|
+
error: (msg) => console.error(chalk.red(msg)),
|
|
16
|
+
step: (n, total, msg) => console.log(chalk.blue(`[${n}/${total}] ${msg}`)),
|
|
17
|
+
dim: (msg) => console.log(chalk.dim(msg)),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function ensureDir(dirPath) {
|
|
21
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function copyFile(src, dest) {
|
|
25
|
+
ensureDir(path.dirname(dest));
|
|
26
|
+
fs.copyFileSync(src, dest);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeFile(dest, content) {
|
|
30
|
+
ensureDir(path.dirname(dest));
|
|
31
|
+
fs.writeFileSync(dest, content, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readTemplate(templatePath) {
|
|
35
|
+
return fs.readFileSync(templatePath, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function replaceVars(content, vars) {
|
|
39
|
+
return content.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
40
|
+
return key in vars ? vars[key] : match;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function toPascalCase(str) {
|
|
45
|
+
return str
|
|
46
|
+
.split(/[-_\s]+/)
|
|
47
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
48
|
+
.join('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function toSnakeCase(str) {
|
|
52
|
+
return str.replace(/[-\s]+/g, '_').toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function toKebabCase(str) {
|
|
56
|
+
return str.replace(/[\s_]+/g, '-').toLowerCase();
|
|
57
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
from pydantic import BaseModel, EmailStr
|
|
3
|
+
|
|
4
|
+
from app.core.security import create_access_token, hash_password, verify_password
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LoginRequest(BaseModel):
|
|
10
|
+
email: EmailStr
|
|
11
|
+
password: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RegisterRequest(BaseModel):
|
|
15
|
+
email: EmailStr
|
|
16
|
+
password: str
|
|
17
|
+
name: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenResponse(BaseModel):
|
|
21
|
+
access_token: str
|
|
22
|
+
token_type: str = "bearer"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.post("/login", response_model=TokenResponse)
|
|
26
|
+
async def login(request: LoginRequest):
|
|
27
|
+
# TODO: Replace with actual database lookup
|
|
28
|
+
# user = await get_user_by_email(request.email)
|
|
29
|
+
# if not user or not verify_password(request.password, user.hashed_password):
|
|
30
|
+
# raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
31
|
+
raise HTTPException(status_code=501, detail="Implement user lookup")
|
|
32
|
+
|
|
33
|
+
token = create_access_token(data={"sub": request.email})
|
|
34
|
+
return TokenResponse(access_token=token)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.post("/register", response_model=TokenResponse)
|
|
38
|
+
async def register(request: RegisterRequest):
|
|
39
|
+
# TODO: Replace with actual database operations
|
|
40
|
+
# hashed = hash_password(request.password)
|
|
41
|
+
# user = await create_user(email=request.email, name=request.name, hashed_password=hashed)
|
|
42
|
+
raise HTTPException(status_code=501, detail="Implement user creation")
|
|
43
|
+
|
|
44
|
+
token = create_access_token(data={"sub": request.email})
|
|
45
|
+
return TokenResponse(access_token=token)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from fastapi import Depends, HTTPException
|
|
2
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
3
|
+
|
|
4
|
+
from app.core.security import decode_access_token
|
|
5
|
+
|
|
6
|
+
security = HTTPBearer()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_current_user(
|
|
10
|
+
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
11
|
+
) -> dict:
|
|
12
|
+
"""Dependency that extracts and validates the current user from the JWT token."""
|
|
13
|
+
payload = decode_access_token(credentials.credentials)
|
|
14
|
+
if payload is None:
|
|
15
|
+
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
|
16
|
+
return payload
|