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.
Files changed (99) hide show
  1. package/CLAUDE.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +246 -0
  4. package/bin/devforge.js +4 -0
  5. package/package.json +33 -0
  6. package/src/claude-configurator.js +260 -0
  7. package/src/cli.js +119 -0
  8. package/src/composer.js +214 -0
  9. package/src/doctor-checks.js +743 -0
  10. package/src/doctor-prompts.js +295 -0
  11. package/src/doctor.js +281 -0
  12. package/src/guided.js +315 -0
  13. package/src/index.js +148 -0
  14. package/src/init-mode.js +134 -0
  15. package/src/prompts.js +155 -0
  16. package/src/recommender.js +186 -0
  17. package/src/scanner.js +368 -0
  18. package/src/uat-generator.js +189 -0
  19. package/src/utils.js +57 -0
  20. package/templates/auth/jwt-custom/backend/app/api/auth.py.template +45 -0
  21. package/templates/auth/jwt-custom/backend/app/api/deps.py.template +16 -0
  22. package/templates/auth/jwt-custom/backend/app/core/security.py.template +34 -0
  23. package/templates/auth/nextauth/src/app/api/auth/[...nextauth]/route.ts.template +3 -0
  24. package/templates/auth/nextauth/src/lib/auth.ts.template +30 -0
  25. package/templates/auth/nextauth/src/middleware.ts.template +14 -0
  26. package/templates/backend/fastapi/backend/Dockerfile.template +12 -0
  27. package/templates/backend/fastapi/backend/app/__init__.py +0 -0
  28. package/templates/backend/fastapi/backend/app/api/__init__.py +0 -0
  29. package/templates/backend/fastapi/backend/app/api/health.py.template +32 -0
  30. package/templates/backend/fastapi/backend/app/core/__init__.py +0 -0
  31. package/templates/backend/fastapi/backend/app/core/config.py.template +25 -0
  32. package/templates/backend/fastapi/backend/app/core/errors.py +37 -0
  33. package/templates/backend/fastapi/backend/app/core/retry.py +32 -0
  34. package/templates/backend/fastapi/backend/app/main.py.template +58 -0
  35. package/templates/backend/fastapi/backend/app/models/__init__.py +0 -0
  36. package/templates/backend/fastapi/backend/app/schemas/__init__.py +0 -0
  37. package/templates/backend/fastapi/backend/pyproject.toml.template +19 -0
  38. package/templates/backend/fastapi/backend/requirements.txt.template +14 -0
  39. package/templates/base/.gitignore.template +29 -0
  40. package/templates/base/README.md.template +25 -0
  41. package/templates/claude-code/agents/code-quality-reviewer.md +41 -0
  42. package/templates/claude-code/agents/production-readiness.md +55 -0
  43. package/templates/claude-code/agents/security-reviewer.md +41 -0
  44. package/templates/claude-code/agents/spec-validator.md +34 -0
  45. package/templates/claude-code/agents/uat-validator.md +37 -0
  46. package/templates/claude-code/claude-md/base.md +33 -0
  47. package/templates/claude-code/claude-md/fastapi.md +12 -0
  48. package/templates/claude-code/claude-md/fullstack.md +12 -0
  49. package/templates/claude-code/claude-md/nextjs.md +11 -0
  50. package/templates/claude-code/commands/audit-security.md +11 -0
  51. package/templates/claude-code/commands/audit-spec.md +9 -0
  52. package/templates/claude-code/commands/audit-wiring.md +17 -0
  53. package/templates/claude-code/commands/done.md +19 -0
  54. package/templates/claude-code/commands/generate-prd.md +45 -0
  55. package/templates/claude-code/commands/generate-uat.md +35 -0
  56. package/templates/claude-code/commands/help.md +26 -0
  57. package/templates/claude-code/commands/next.md +20 -0
  58. package/templates/claude-code/commands/optimize-claude-md.md +31 -0
  59. package/templates/claude-code/commands/pre-pr.md +19 -0
  60. package/templates/claude-code/commands/run-uat.md +21 -0
  61. package/templates/claude-code/commands/status.md +24 -0
  62. package/templates/claude-code/commands/verify-all.md +11 -0
  63. package/templates/claude-code/hooks/polyglot.json +36 -0
  64. package/templates/claude-code/hooks/python.json +36 -0
  65. package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -0
  66. package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -0
  67. package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -0
  68. package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -0
  69. package/templates/claude-code/hooks/typescript.json +36 -0
  70. package/templates/claude-code/skills/ai-prompts/SKILL.md +43 -0
  71. package/templates/claude-code/skills/fastapi/SKILL.md +38 -0
  72. package/templates/claude-code/skills/nextjs/SKILL.md +39 -0
  73. package/templates/claude-code/skills/playwright/SKILL.md +37 -0
  74. package/templates/claude-code/skills/security-api/SKILL.md +47 -0
  75. package/templates/claude-code/skills/security-web/SKILL.md +41 -0
  76. package/templates/database/prisma-postgres/.env.example +1 -0
  77. package/templates/database/prisma-postgres/prisma/schema.prisma.template +18 -0
  78. package/templates/database/sqlalchemy-postgres/.env.example +1 -0
  79. package/templates/database/sqlalchemy-postgres/backend/alembic/env.py.template +40 -0
  80. package/templates/database/sqlalchemy-postgres/backend/alembic/versions/.gitkeep +0 -0
  81. package/templates/database/sqlalchemy-postgres/backend/alembic.ini.template +36 -0
  82. package/templates/database/sqlalchemy-postgres/backend/app/db/__init__.py +0 -0
  83. package/templates/database/sqlalchemy-postgres/backend/app/db/base.py +5 -0
  84. package/templates/database/sqlalchemy-postgres/backend/app/db/session.py.template +48 -0
  85. package/templates/frontend/nextjs/next.config.ts.template +7 -0
  86. package/templates/frontend/nextjs/package.json.template +41 -0
  87. package/templates/frontend/nextjs/postcss.config.mjs +7 -0
  88. package/templates/frontend/nextjs/src/app/api/health/route.ts.template +10 -0
  89. package/templates/frontend/nextjs/src/app/globals.css +1 -0
  90. package/templates/frontend/nextjs/src/app/layout.tsx.template +22 -0
  91. package/templates/frontend/nextjs/src/app/page.tsx.template +10 -0
  92. package/templates/frontend/nextjs/src/lib/db.ts.template +40 -0
  93. package/templates/frontend/nextjs/src/lib/errors.ts +28 -0
  94. package/templates/frontend/nextjs/src/lib/utils.ts +6 -0
  95. package/templates/frontend/nextjs/tsconfig.json +23 -0
  96. package/templates/infra/docker-compose/docker-compose.yml.template +19 -0
  97. package/templates/testing/playwright/e2e/example.spec.ts.template +15 -0
  98. package/templates/testing/playwright/playwright.config.ts.template +22 -0
  99. 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