@oalacea/demon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - Project Detector
|
|
3
|
+
*
|
|
4
|
+
* Analyzes a project directory to detect:
|
|
5
|
+
* - Framework (Next.js, Remix, SvelteKit, Vite, etc.)
|
|
6
|
+
* - Language (TypeScript, JavaScript, Python, etc.)
|
|
7
|
+
* - Test Runner (Vitest, Jest, Pytest, etc.)
|
|
8
|
+
* - Database (Prisma, Drizzle, Neon, Supabase, local)
|
|
9
|
+
* - Existing tests
|
|
10
|
+
* - Coverage
|
|
11
|
+
* - Key dependencies
|
|
12
|
+
* - Target URL
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
// Framework patterns
|
|
20
|
+
const FRAMEWORK_PATTERNS = {
|
|
21
|
+
'Next.js': [
|
|
22
|
+
{ file: 'package.json', pattern: /"next"\s*:/ },
|
|
23
|
+
{ file: 'next.config.js', exists: true },
|
|
24
|
+
{ file: 'next.config.mjs', exists: true },
|
|
25
|
+
{ file: 'next.config.ts', exists: true },
|
|
26
|
+
],
|
|
27
|
+
'Remix': [
|
|
28
|
+
{ file: 'package.json', pattern: /"@remix-run\/node"\s*:/ },
|
|
29
|
+
{ file: 'remix.config.js', exists: true },
|
|
30
|
+
{ file: 'remix.config.ts', exists: true },
|
|
31
|
+
],
|
|
32
|
+
'SvelteKit': [
|
|
33
|
+
{ file: 'package.json', pattern: /"@sveltejs\/kit"\s*:/ },
|
|
34
|
+
{ file: 'svelte.config.js', exists: true },
|
|
35
|
+
],
|
|
36
|
+
'Nuxt': [
|
|
37
|
+
{ file: 'package.json', pattern: /"nuxt"\s*:/ },
|
|
38
|
+
{ file: 'nuxt.config.ts', exists: true },
|
|
39
|
+
{ file: 'nuxt.config.js', exists: true },
|
|
40
|
+
],
|
|
41
|
+
'Vite + React': [
|
|
42
|
+
{ file: 'package.json', pattern: /"vite"\s*:/ },
|
|
43
|
+
{ file: 'package.json', pattern: /"react"\s*:/ },
|
|
44
|
+
{ not: ['Next.js', 'Remix', 'SvelteKit', 'Nuxt'] },
|
|
45
|
+
],
|
|
46
|
+
'Vite + Vue': [
|
|
47
|
+
{ file: 'package.json', pattern: /"vite"\s*:/ },
|
|
48
|
+
{ file: 'package.json', pattern: /"vue"\s*:/ },
|
|
49
|
+
],
|
|
50
|
+
'Vite + Svelte': [
|
|
51
|
+
{ file: 'package.json', pattern: /"vite"\s*:/ },
|
|
52
|
+
{ file: 'package.json', pattern: /"svelte"\s*:/ },
|
|
53
|
+
],
|
|
54
|
+
'Astro': [
|
|
55
|
+
{ file: 'package.json', pattern: /"astro"\s*:/ },
|
|
56
|
+
],
|
|
57
|
+
'Gatsby': [
|
|
58
|
+
{ file: 'package.json', pattern: /"gatsby"\s*:/ },
|
|
59
|
+
],
|
|
60
|
+
'Angular': [
|
|
61
|
+
{ file: 'angular.json', exists: true },
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Database patterns
|
|
66
|
+
const DATABASE_PATTERNS = {
|
|
67
|
+
'Prisma Postgres': {
|
|
68
|
+
package: /@prisma\/client/,
|
|
69
|
+
schema: /provider\s*=\s*"postgresql"/,
|
|
70
|
+
},
|
|
71
|
+
'Prisma MySQL': {
|
|
72
|
+
package: /@prisma\/client/,
|
|
73
|
+
schema: /provider\s*=\s*"mysql"/,
|
|
74
|
+
},
|
|
75
|
+
'Prisma SQLite': {
|
|
76
|
+
package: /@prisma\/client/,
|
|
77
|
+
schema: /provider\s*=\s*"sqlite"/,
|
|
78
|
+
},
|
|
79
|
+
'Prisma': {
|
|
80
|
+
package: /@prisma\/client/,
|
|
81
|
+
},
|
|
82
|
+
'Drizzle': {
|
|
83
|
+
package: /drizzle-orm/,
|
|
84
|
+
},
|
|
85
|
+
'TypeORM': {
|
|
86
|
+
package: /typeorm/,
|
|
87
|
+
},
|
|
88
|
+
'MikroORM': {
|
|
89
|
+
package: /@mikro-orm/,
|
|
90
|
+
},
|
|
91
|
+
'Mongoose': {
|
|
92
|
+
package: /mongoose/,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Test runner patterns
|
|
97
|
+
const TEST_RUNNER_PATTERNS = {
|
|
98
|
+
'Vitest': {
|
|
99
|
+
package: /vitest/,
|
|
100
|
+
config: ['vitest.config.ts', 'vitest.config.js', 'vitest.config.mjs'],
|
|
101
|
+
},
|
|
102
|
+
'Jest': {
|
|
103
|
+
package: /jest/,
|
|
104
|
+
config: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs'],
|
|
105
|
+
},
|
|
106
|
+
'Mocha': {
|
|
107
|
+
package: /mocha/,
|
|
108
|
+
},
|
|
109
|
+
'Jasmine': {
|
|
110
|
+
package: /jasmine/,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Important dependency categories
|
|
115
|
+
const DEPS_CATEGORIES = {
|
|
116
|
+
'Router': ['@tanstack/react-router', '@tanstack/react-router', 'react-router', 'react-router-dom', 'next/router', '@remix-run/react', '@sveltejs/kit', 'vue-router', 'react-navigation'],
|
|
117
|
+
'State': ['zustand', '@reduxjs/toolkit', 'redux', 'jotai', 'recoil', 'valtio', 'mobx', 'pinia', 'vuex'],
|
|
118
|
+
'Query': ['@tanstack/react-query', '@tanstack/react-query', '@tanstack/solid-query', '@tanstack/vue-query', 'swr', 'react-query', '@apollo/client'],
|
|
119
|
+
'Forms': ['react-hook-form', 'formik', 'zod', 'yup', 'joi', 'superstruct', 'valibot'],
|
|
120
|
+
'UI': ['@radix-ui', '@headlessui', '@chakra-ui', '@mui/material', 'antd', 'mantine', 'shadcn/ui', 'tailwindcss'],
|
|
121
|
+
'Testing': ['@testing-library/react', '@testing-library/vue', '@testing-library/svelte', '@testing-library/dom'],
|
|
122
|
+
'E2E': ['@playwright/test', 'cypress', '@wdio/cli', 'nightwatch', 'testcafe'],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Detect database connection details from .env files
|
|
127
|
+
*/
|
|
128
|
+
function detectDatabaseConnection(projectDir, dbType) {
|
|
129
|
+
const envFiles = ['.env', '.env.local', '.env.development', '.env.test'];
|
|
130
|
+
let connection = 'DATABASE_URL';
|
|
131
|
+
let provider = null;
|
|
132
|
+
|
|
133
|
+
for (const envFile of envFiles) {
|
|
134
|
+
const envPath = path.join(projectDir, envFile);
|
|
135
|
+
if (!fs.existsSync(envPath)) continue;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
139
|
+
|
|
140
|
+
// Detect provider-specific URLs
|
|
141
|
+
if (envContent.includes('neon.tech') || envContent.includes('NEON')) {
|
|
142
|
+
provider = 'Neon Postgres';
|
|
143
|
+
connection = 'DATABASE_URL (Neon)';
|
|
144
|
+
} else if (envContent.includes('supabase.co') || envContent.includes('SUPABASE')) {
|
|
145
|
+
provider = 'Supabase Postgres';
|
|
146
|
+
connection = 'DATABASE_URL (Supabase)';
|
|
147
|
+
} else if (envContent.includes('planetscale.com') || envContent.includes('PLANETSCALE')) {
|
|
148
|
+
provider = 'PlanetScale MySQL';
|
|
149
|
+
connection = 'DATABASE_URL (PlanetScale)';
|
|
150
|
+
} else if (envContent.includes('turso') || envContent.includes('TURSO')) {
|
|
151
|
+
provider = 'Turso SQLite';
|
|
152
|
+
connection = 'DATABASE_URL (Turso)';
|
|
153
|
+
} else if (envContent.includes('localhost') || envContent.includes('127.0.0.1')) {
|
|
154
|
+
provider = 'Local Database';
|
|
155
|
+
connection = 'DATABASE_URL (localhost)';
|
|
156
|
+
} else if (envContent.includes('railway.app') || envContent.includes('RAILWAY')) {
|
|
157
|
+
provider = 'Railway';
|
|
158
|
+
connection = 'DATABASE_URL (Railway)';
|
|
159
|
+
} else if (envContent.includes('render.com') || envContent.includes('RENDER')) {
|
|
160
|
+
provider = 'Render';
|
|
161
|
+
connection = 'DATABASE_URL (Render)';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Extract port if local
|
|
165
|
+
if (provider === 'Local Database') {
|
|
166
|
+
const portMatch = envContent.match(/:(\d{4,5})/);
|
|
167
|
+
if (portMatch) {
|
|
168
|
+
connection = `localhost:${portMatch[1]}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (provider) break;
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// Skip files that can't be read
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
type: provider || dbType || 'Database detected',
|
|
180
|
+
connection: connection,
|
|
181
|
+
testStrategy: 'transaction-rollback',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read package.json safely
|
|
187
|
+
*/
|
|
188
|
+
function readPackageJson(projectDir) {
|
|
189
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
190
|
+
if (!fs.existsSync(pkgPath)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Detect framework from package.json and project structure
|
|
203
|
+
*/
|
|
204
|
+
function detectFramework(projectDir, pkg) {
|
|
205
|
+
if (!pkg) return null;
|
|
206
|
+
|
|
207
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
208
|
+
|
|
209
|
+
for (const [framework, patterns] of Object.entries(FRAMEWORK_PATTERNS)) {
|
|
210
|
+
let matchScore = 0;
|
|
211
|
+
|
|
212
|
+
for (const pattern of patterns) {
|
|
213
|
+
if (pattern.exists !== undefined) {
|
|
214
|
+
const filePath = path.join(projectDir, pattern.file);
|
|
215
|
+
if (fs.existsSync(filePath) === pattern.exists) {
|
|
216
|
+
matchScore++;
|
|
217
|
+
}
|
|
218
|
+
} else if (pattern.pattern) {
|
|
219
|
+
if (pattern.pattern.test(JSON.stringify(allDeps))) {
|
|
220
|
+
matchScore++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (matchScore > 0) {
|
|
226
|
+
// Check if it should be excluded
|
|
227
|
+
if (patterns.some(p => p.not && p.not.includes(framework))) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
return framework;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Fallback detection
|
|
235
|
+
if (allDeps.express) return 'Express';
|
|
236
|
+
if (allDeps.fastify) return 'Fastify';
|
|
237
|
+
if (allDeps.hono) return 'Hono';
|
|
238
|
+
if (allDeps.koa) return 'Koa';
|
|
239
|
+
if (allDeps.nest) return 'NestJS';
|
|
240
|
+
if (allDeps['@nestjs/core']) return 'NestJS';
|
|
241
|
+
if (allDeps.django || allDeps['django-rest-framework']) return 'Django';
|
|
242
|
+
if (allDeps.flask) return 'Flask';
|
|
243
|
+
if (allDeps.fastapi) return 'FastAPI';
|
|
244
|
+
if (allDeps.spring) return 'Spring Boot';
|
|
245
|
+
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Detect language
|
|
251
|
+
*/
|
|
252
|
+
function detectLanguage(projectDir, pkg) {
|
|
253
|
+
if (!pkg) return 'JavaScript';
|
|
254
|
+
|
|
255
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
256
|
+
|
|
257
|
+
// Check for TypeScript
|
|
258
|
+
if (allDeps.typescript || allDeps['@types/node']) {
|
|
259
|
+
return 'TypeScript';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check source files
|
|
263
|
+
const srcDir = path.join(projectDir, 'src');
|
|
264
|
+
if (fs.existsSync(srcDir)) {
|
|
265
|
+
const hasTs = hasFilesWithExtension(srcDir, ['.ts', '.tsx']);
|
|
266
|
+
const hasJs = hasFilesWithExtension(srcDir, ['.js', '.jsx']);
|
|
267
|
+
|
|
268
|
+
if (hasTs && !hasJs) return 'TypeScript';
|
|
269
|
+
if (hasTs && hasJs) return 'TypeScript + JavaScript';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return 'JavaScript';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Detect test runner
|
|
277
|
+
*/
|
|
278
|
+
function detectTestRunner(projectDir, pkg) {
|
|
279
|
+
if (!pkg) return 'Vitest';
|
|
280
|
+
|
|
281
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
282
|
+
|
|
283
|
+
// Check for Vitest first (most common in modern projects)
|
|
284
|
+
if (allDeps.vitest) return 'Vitest';
|
|
285
|
+
if (allDeps.jest) return 'Jest';
|
|
286
|
+
if (allDeps.mocha) return 'Mocha';
|
|
287
|
+
if (allDeps.jasmine) return 'Jasmine';
|
|
288
|
+
|
|
289
|
+
// Check for config files
|
|
290
|
+
if (fs.existsSync(path.join(projectDir, 'vitest.config.ts'))) return 'Vitest';
|
|
291
|
+
if (fs.existsSync(path.join(projectDir, 'vitest.config.js'))) return 'Vitest';
|
|
292
|
+
if (fs.existsSync(path.join(projectDir, 'jest.config.js'))) return 'Jest';
|
|
293
|
+
if (fs.existsSync(path.join(projectDir, 'jest.config.ts'))) return 'Jest';
|
|
294
|
+
|
|
295
|
+
return 'Vitest'; // Default for modern projects
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Detect database
|
|
300
|
+
*/
|
|
301
|
+
function detectDatabase(projectDir, pkg) {
|
|
302
|
+
if (!pkg) return null;
|
|
303
|
+
|
|
304
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
305
|
+
|
|
306
|
+
// Check for Prisma
|
|
307
|
+
if (allDeps['@prisma/client'] || allDeps['@prisma/client']) {
|
|
308
|
+
const schemaPath = path.join(projectDir, 'prisma', 'schema.prisma');
|
|
309
|
+
let provider = 'Prisma';
|
|
310
|
+
|
|
311
|
+
if (fs.existsSync(schemaPath)) {
|
|
312
|
+
try {
|
|
313
|
+
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
|
314
|
+
if (schema.includes('provider = "postgresql"')) {
|
|
315
|
+
provider = 'Prisma Postgres';
|
|
316
|
+
} else if (schema.includes('provider = "mysql"')) {
|
|
317
|
+
provider = 'Prisma MySQL';
|
|
318
|
+
} else if (schema.includes('provider = "sqlite"')) {
|
|
319
|
+
provider = 'Prisma SQLite';
|
|
320
|
+
} else if (schema.includes('provider = "mongodb"')) {
|
|
321
|
+
provider = 'Prisma MongoDB';
|
|
322
|
+
} else if (schema.includes('provider = "cockroachdb"')) {
|
|
323
|
+
provider = 'Prisma CockroachDB';
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
// Schema not readable
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return detectDatabaseConnection(projectDir, provider);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check for Drizzle
|
|
334
|
+
if (allDeps['drizzle-orm']) {
|
|
335
|
+
return detectDatabaseConnection(projectDir, 'Drizzle ORM');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for TypeORM
|
|
339
|
+
if (allDeps.typeorm) {
|
|
340
|
+
return detectDatabaseConnection(projectDir, 'TypeORM');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for MikroORM
|
|
344
|
+
if (allDeps['@mikro-orm'] || allDeps['@mikro-orm/core']) {
|
|
345
|
+
return detectDatabaseConnection(projectDir, 'MikroORM');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check for Mongoose
|
|
349
|
+
if (allDeps.mongoose) {
|
|
350
|
+
return detectDatabaseConnection(projectDir, 'MongoDB (Mongoose)');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Count existing test files
|
|
358
|
+
*/
|
|
359
|
+
function countExistingTests(projectDir) {
|
|
360
|
+
const testExtensions = ['.test.ts', '.test.tsx', '.test.js', '.test.jsx',
|
|
361
|
+
'.spec.ts', '.spec.tsx', '.spec.js', '.spec.jsx'];
|
|
362
|
+
|
|
363
|
+
let count = 0;
|
|
364
|
+
|
|
365
|
+
function countInDir(dir) {
|
|
366
|
+
try {
|
|
367
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
368
|
+
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const fullPath = path.join(dir, entry.name);
|
|
371
|
+
|
|
372
|
+
if (entry.isDirectory()) {
|
|
373
|
+
// Skip node_modules and other common exclusions
|
|
374
|
+
if (['node_modules', '.next', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
countInDir(fullPath);
|
|
378
|
+
} else if (entry.isFile()) {
|
|
379
|
+
const ext = path.extname(entry.name);
|
|
380
|
+
const baseName = path.basename(entry.name, ext);
|
|
381
|
+
const fullName = baseName + ext;
|
|
382
|
+
|
|
383
|
+
if (testExtensions.some(te => fullName.endsWith(te))) {
|
|
384
|
+
count++;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
// Skip directories that can't be read
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
countInDir(projectDir);
|
|
394
|
+
return count;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get coverage if available
|
|
399
|
+
*/
|
|
400
|
+
function getCoverage(projectDir) {
|
|
401
|
+
const coverageDirs = ['coverage', '.nyc_output'];
|
|
402
|
+
const coverageFiles = ['coverage/coverage-summary.json', 'coverage-summary.json'];
|
|
403
|
+
|
|
404
|
+
for (const file of coverageFiles) {
|
|
405
|
+
const filePath = path.join(projectDir, file);
|
|
406
|
+
if (fs.existsSync(filePath)) {
|
|
407
|
+
try {
|
|
408
|
+
const coverage = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
409
|
+
// Try to extract total coverage
|
|
410
|
+
if (coverage.total) {
|
|
411
|
+
const lines = coverage.total.lines?.pct || 0;
|
|
412
|
+
return `${Math.round(lines)}%`;
|
|
413
|
+
}
|
|
414
|
+
} catch (e) {
|
|
415
|
+
// Can't read coverage
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get key dependencies by category
|
|
425
|
+
*/
|
|
426
|
+
function getKeyDependencies(projectDir, pkg) {
|
|
427
|
+
if (!pkg) return [];
|
|
428
|
+
|
|
429
|
+
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
430
|
+
const keyDeps = [];
|
|
431
|
+
|
|
432
|
+
for (const [category, patterns] of Object.entries(DEPS_CATEGORIES)) {
|
|
433
|
+
for (const pattern of patterns) {
|
|
434
|
+
if (allDeps[pattern]) {
|
|
435
|
+
keyDeps.push(`${category}: ${pattern}`);
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Add some important standalone dependencies
|
|
442
|
+
if (allDeps.zod && !keyDeps.some(d => d.includes('zod'))) {
|
|
443
|
+
keyDeps.push('Forms: zod');
|
|
444
|
+
}
|
|
445
|
+
if (allDeps.next) {
|
|
446
|
+
keyDeps.push('Framework: next');
|
|
447
|
+
}
|
|
448
|
+
if (allDeps.react) {
|
|
449
|
+
keyDeps.push('UI: react');
|
|
450
|
+
}
|
|
451
|
+
if (allDeps.vue) {
|
|
452
|
+
keyDeps.push('UI: vue');
|
|
453
|
+
}
|
|
454
|
+
if (allDeps.svelte) {
|
|
455
|
+
keyDeps.push('UI: svelte');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return keyDeps;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Detect target URL for testing
|
|
463
|
+
*/
|
|
464
|
+
function detectTargetUrl(projectDir, pkg) {
|
|
465
|
+
const platform = process.platform;
|
|
466
|
+
|
|
467
|
+
// Determine host based on platform
|
|
468
|
+
let host;
|
|
469
|
+
if (platform === 'linux') {
|
|
470
|
+
host = 'localhost';
|
|
471
|
+
} else {
|
|
472
|
+
// macOS and Windows Docker runs in a VM
|
|
473
|
+
host = 'host.docker.internal';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Try to detect port from package.json scripts
|
|
477
|
+
let port = '3000'; // Default
|
|
478
|
+
|
|
479
|
+
if (pkg && pkg.scripts) {
|
|
480
|
+
const scripts = Object.values(pkg.scripts).join(' ');
|
|
481
|
+
|
|
482
|
+
const portMatches = scripts.match(/-p\s*(\d{4,5})/gi);
|
|
483
|
+
if (portMatches) {
|
|
484
|
+
const numbers = portMatches.map(m => parseInt(m.replace(/-p\s*/, '')));
|
|
485
|
+
port = numbers[0]?.toString() || port;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const devPortMatch = scripts.match(/PORT=(\d{4,5})/i);
|
|
489
|
+
if (devPortMatch) {
|
|
490
|
+
port = devPortMatch[1];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const nextPortMatch = scripts.match(/next\s+dev.*-p\s*(\d{4,5})/);
|
|
494
|
+
if (nextPortMatch) {
|
|
495
|
+
port = nextPortMatch[1];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Check .env files for PORT
|
|
500
|
+
const envFiles = ['.env', '.env.local', '.env.development'];
|
|
501
|
+
for (const envFile of envFiles) {
|
|
502
|
+
const envPath = path.join(projectDir, envFile);
|
|
503
|
+
if (fs.existsSync(envPath)) {
|
|
504
|
+
try {
|
|
505
|
+
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
506
|
+
const portMatch = envContent.match(/^PORT=(\d{4,5})/m);
|
|
507
|
+
if (portMatch) {
|
|
508
|
+
port = portMatch[1];
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
} catch (e) {
|
|
512
|
+
// Skip
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return `http://${host}:${port}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Helper: Check if directory has files with extension
|
|
522
|
+
*/
|
|
523
|
+
function hasFilesWithExtension(dir, extensions) {
|
|
524
|
+
try {
|
|
525
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
526
|
+
for (const entry of entries) {
|
|
527
|
+
if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
// Directory not readable
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Main analysis function
|
|
539
|
+
*/
|
|
540
|
+
async function analyze(projectDir) {
|
|
541
|
+
const pkg = readPackageJson(projectDir);
|
|
542
|
+
|
|
543
|
+
const context = {
|
|
544
|
+
framework: detectFramework(projectDir, pkg) || 'Unknown',
|
|
545
|
+
language: detectLanguage(projectDir, pkg),
|
|
546
|
+
testRunner: detectTestRunner(projectDir, pkg),
|
|
547
|
+
database: detectDatabase(projectDir, pkg),
|
|
548
|
+
existingTests: countExistingTests(projectDir),
|
|
549
|
+
coverage: getCoverage(projectDir),
|
|
550
|
+
dependencies: getKeyDependencies(projectDir, pkg),
|
|
551
|
+
target: detectTargetUrl(projectDir, pkg),
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
return context;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Export for use in CLI
|
|
558
|
+
if (require.main === module) {
|
|
559
|
+
const projectDir = process.argv[2] || process.cwd();
|
|
560
|
+
analyze(projectDir)
|
|
561
|
+
.then(context => {
|
|
562
|
+
console.log(JSON.stringify(context, null, 2));
|
|
563
|
+
})
|
|
564
|
+
.catch(err => {
|
|
565
|
+
console.error('Analysis failed:', err);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
module.exports = { analyze };
|