forgedev 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +246 -246
  2. package/bin/devforge.js +4 -4
  3. package/package.json +33 -33
  4. package/src/claude-configurator.js +260 -260
  5. package/src/cli.js +119 -119
  6. package/src/composer.js +214 -214
  7. package/src/doctor-checks.js +743 -743
  8. package/src/doctor-prompts.js +295 -295
  9. package/src/doctor.js +281 -281
  10. package/src/guided.js +315 -315
  11. package/src/index.js +148 -148
  12. package/src/init-mode.js +138 -134
  13. package/src/prompts.js +155 -155
  14. package/src/scanner.js +368 -368
  15. package/templates/claude-code/agents/code-quality-reviewer.md +41 -41
  16. package/templates/claude-code/agents/production-readiness.md +55 -55
  17. package/templates/claude-code/agents/security-reviewer.md +41 -41
  18. package/templates/claude-code/agents/spec-validator.md +34 -34
  19. package/templates/claude-code/agents/uat-validator.md +37 -37
  20. package/templates/claude-code/claude-md/base.md +33 -33
  21. package/templates/claude-code/commands/done.md +19 -19
  22. package/templates/claude-code/commands/generate-prd.md +45 -45
  23. package/templates/claude-code/commands/generate-uat.md +35 -35
  24. package/templates/claude-code/commands/help.md +26 -26
  25. package/templates/claude-code/commands/next.md +20 -20
  26. package/templates/claude-code/commands/optimize-claude-md.md +31 -31
  27. package/templates/claude-code/commands/status.md +24 -24
  28. package/templates/claude-code/hooks/polyglot.json +36 -36
  29. package/templates/claude-code/hooks/python.json +36 -36
  30. package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -16
  31. package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -14
  32. package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -14
  33. package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -21
  34. package/templates/claude-code/hooks/typescript.json +36 -36
package/src/scanner.js CHANGED
@@ -1,368 +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
- }
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
+ }