forgedev 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -246
- package/bin/devforge.js +4 -4
- package/package.json +33 -33
- package/src/claude-configurator.js +260 -260
- package/src/cli.js +119 -119
- package/src/composer.js +214 -214
- package/src/doctor-checks.js +743 -743
- package/src/doctor-prompts.js +295 -295
- package/src/doctor.js +281 -281
- package/src/guided.js +315 -315
- package/src/index.js +148 -148
- package/src/init-mode.js +138 -134
- package/src/prompts.js +155 -155
- package/src/scanner.js +368 -368
- package/templates/claude-code/agents/code-quality-reviewer.md +41 -41
- package/templates/claude-code/agents/production-readiness.md +55 -55
- package/templates/claude-code/agents/security-reviewer.md +41 -41
- package/templates/claude-code/agents/spec-validator.md +34 -34
- package/templates/claude-code/agents/uat-validator.md +37 -37
- package/templates/claude-code/claude-md/base.md +33 -33
- package/templates/claude-code/commands/done.md +19 -19
- package/templates/claude-code/commands/generate-prd.md +45 -45
- package/templates/claude-code/commands/generate-uat.md +35 -35
- package/templates/claude-code/commands/help.md +26 -26
- package/templates/claude-code/commands/next.md +20 -20
- package/templates/claude-code/commands/optimize-claude-md.md +31 -31
- package/templates/claude-code/commands/status.md +24 -24
- package/templates/claude-code/hooks/polyglot.json +36 -36
- package/templates/claude-code/hooks/python.json +36 -36
- package/templates/claude-code/hooks/scripts/autofix-polyglot.sh +16 -16
- package/templates/claude-code/hooks/scripts/autofix-python.sh +14 -14
- package/templates/claude-code/hooks/scripts/autofix-typescript.sh +14 -14
- package/templates/claude-code/hooks/scripts/guard-protected-files.sh +21 -21
- package/templates/claude-code/hooks/typescript.json +36 -36
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
|
+
}
|