prjct-cli 0.9.2 → 0.10.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 +111 -0
- package/core/__tests__/agentic/context-filter.test.js +2 -2
- package/core/__tests__/agentic/prompt-builder.test.js +39 -47
- package/core/__tests__/domain/agent-generator.test.js +29 -36
- package/core/__tests__/domain/agent-loader.test.js +179 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/command-executor.js +61 -13
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/prompt-builder.js +51 -1
- package/core/commands.js +85 -59
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +2 -2
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskAnalyzer - Deep Semantic Task Analysis
|
|
3
|
+
*
|
|
4
|
+
* Analyzes tasks semantically, not just keywords
|
|
5
|
+
* Considers project context, history, and relationships
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises
|
|
11
|
+
const path = require('path')
|
|
12
|
+
const TechDetector = require('./tech-detector')
|
|
13
|
+
const configManager = require('../infrastructure/config-manager')
|
|
14
|
+
const pathManager = require('../infrastructure/path-manager')
|
|
15
|
+
|
|
16
|
+
class TaskAnalyzer {
|
|
17
|
+
constructor(projectPath) {
|
|
18
|
+
this.projectPath = projectPath
|
|
19
|
+
this.projectId = null
|
|
20
|
+
this.techDetector = null
|
|
21
|
+
this.taskHistory = null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize analyzer with project context
|
|
26
|
+
*/
|
|
27
|
+
async initialize() {
|
|
28
|
+
this.projectId = await configManager.getProjectId(this.projectPath)
|
|
29
|
+
this.techDetector = new TechDetector(this.projectPath)
|
|
30
|
+
await this.loadTaskHistory()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Deep semantic analysis of a task
|
|
35
|
+
* @param {Object} task - Task object {description, type}
|
|
36
|
+
* @returns {Promise<Object>} Analysis result
|
|
37
|
+
*/
|
|
38
|
+
async analyzeTask(task) {
|
|
39
|
+
if (!this.techDetector) {
|
|
40
|
+
await this.initialize()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const description = (task.description || '').toLowerCase()
|
|
44
|
+
const type = (task.type || '').toLowerCase()
|
|
45
|
+
const fullText = `${description} ${type}`.trim()
|
|
46
|
+
|
|
47
|
+
// Get project technologies
|
|
48
|
+
const projectTech = await this.techDetector.detectAll()
|
|
49
|
+
|
|
50
|
+
// Multi-domain detection
|
|
51
|
+
const domains = this.detectDomains(fullText, projectTech)
|
|
52
|
+
|
|
53
|
+
// Semantic understanding
|
|
54
|
+
const semantic = this.analyzeSemantics(fullText, projectTech)
|
|
55
|
+
|
|
56
|
+
// Historical patterns
|
|
57
|
+
const historical = await this.analyzeHistory(fullText)
|
|
58
|
+
|
|
59
|
+
// Complexity estimation
|
|
60
|
+
const complexity = this.estimateComplexity(fullText, domains)
|
|
61
|
+
|
|
62
|
+
// Combine all signals
|
|
63
|
+
const primaryDomain = this.selectPrimaryDomain(domains, semantic, historical)
|
|
64
|
+
const confidence = this.calculateConfidence(domains, semantic, historical)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
primaryDomain,
|
|
68
|
+
domains, // All detected domains
|
|
69
|
+
confidence,
|
|
70
|
+
semantic,
|
|
71
|
+
historical,
|
|
72
|
+
complexity,
|
|
73
|
+
projectTechnologies: projectTech,
|
|
74
|
+
matchedKeywords: domains[primaryDomain]?.keywords || [],
|
|
75
|
+
reason: this.buildReason(primaryDomain, domains, semantic, historical),
|
|
76
|
+
alternatives: this.getAlternatives(primaryDomain, domains)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect multiple domains from task description
|
|
82
|
+
*/
|
|
83
|
+
detectDomains(text, projectTech) {
|
|
84
|
+
const domains = {}
|
|
85
|
+
|
|
86
|
+
// Enhanced patterns with project context
|
|
87
|
+
const patterns = {
|
|
88
|
+
frontend: [
|
|
89
|
+
'component', 'ui', 'user interface', 'frontend', 'client',
|
|
90
|
+
'style', 'css', 'layout', 'responsive', 'design',
|
|
91
|
+
'page', 'view', 'template', 'render', 'display',
|
|
92
|
+
'button', 'form', 'input', 'modal', 'dialog',
|
|
93
|
+
...(projectTech.frameworks.filter(f => ['react', 'vue', 'angular', 'svelte', 'next', 'nuxt'].includes(f.toLowerCase())).map(f => f.toLowerCase()))
|
|
94
|
+
],
|
|
95
|
+
backend: [
|
|
96
|
+
'api', 'server', 'endpoint', 'route', 'middleware',
|
|
97
|
+
'auth', 'authentication', 'authorization', 'jwt', 'session',
|
|
98
|
+
'backend', 'service', 'controller', 'handler',
|
|
99
|
+
'database', 'query', 'model', 'schema',
|
|
100
|
+
...(projectTech.frameworks.filter(f => ['express', 'fastify', 'django', 'flask', 'rails', 'phoenix'].includes(f.toLowerCase())).map(f => f.toLowerCase()))
|
|
101
|
+
],
|
|
102
|
+
database: [
|
|
103
|
+
'database', 'db', 'query', 'migration', 'schema', 'model',
|
|
104
|
+
'sql', 'data', 'table', 'collection', 'index', 'relation',
|
|
105
|
+
'postgres', 'mysql', 'mongodb', 'redis'
|
|
106
|
+
],
|
|
107
|
+
devops: [
|
|
108
|
+
'deploy', 'deployment', 'docker', 'kubernetes', 'k8s',
|
|
109
|
+
'ci/cd', 'pipeline', 'build', 'ship', 'release',
|
|
110
|
+
'production', 'infrastructure', 'container', 'orchestration'
|
|
111
|
+
],
|
|
112
|
+
qa: [
|
|
113
|
+
'test', 'testing', 'bug', 'error', 'fix', 'debug', 'issue',
|
|
114
|
+
'quality', 'coverage', 'unit test', 'integration test',
|
|
115
|
+
'e2e', 'spec', 'assertion', 'validation'
|
|
116
|
+
],
|
|
117
|
+
architecture: [
|
|
118
|
+
'design', 'architecture', 'pattern', 'structure',
|
|
119
|
+
'refactor', 'refactoring', 'organize', 'plan',
|
|
120
|
+
'feature', 'system', 'module', 'component design'
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Score each domain
|
|
125
|
+
for (const [domain, keywords] of Object.entries(patterns)) {
|
|
126
|
+
const matches = keywords.filter(keyword => text.includes(keyword))
|
|
127
|
+
if (matches.length > 0) {
|
|
128
|
+
domains[domain] = {
|
|
129
|
+
keywords: matches,
|
|
130
|
+
count: matches.length,
|
|
131
|
+
score: matches.length + (matches.length > 2 ? 1 : 0) // Bonus for multiple matches
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return domains
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Semantic analysis - understand intent, not just keywords
|
|
141
|
+
*/
|
|
142
|
+
analyzeSemantics(text, projectTech) {
|
|
143
|
+
const semantic = {
|
|
144
|
+
intent: null,
|
|
145
|
+
requiresMultipleAgents: false,
|
|
146
|
+
complexity: 'medium'
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Detect intent patterns
|
|
150
|
+
if (text.match(/\b(create|add|build|implement|make)\b/)) {
|
|
151
|
+
semantic.intent = 'create'
|
|
152
|
+
} else if (text.match(/\b(fix|repair|debug|resolve)\b/)) {
|
|
153
|
+
semantic.intent = 'fix'
|
|
154
|
+
} else if (text.match(/\b(improve|optimize|enhance|refactor)\b/)) {
|
|
155
|
+
semantic.intent = 'improve'
|
|
156
|
+
} else if (text.match(/\b(test|verify|validate)\b/)) {
|
|
157
|
+
semantic.intent = 'test'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Detect multi-agent requirements
|
|
161
|
+
if (text.match(/\b(api|endpoint).*\b(test|spec)\b/) ||
|
|
162
|
+
text.match(/\b(test|spec).*\b(api|endpoint)\b/)) {
|
|
163
|
+
semantic.requiresMultipleAgents = true
|
|
164
|
+
semantic.agents = ['backend', 'qa']
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (text.match(/\b(component|ui).*\b(test|spec)\b/) ||
|
|
168
|
+
text.match(/\b(test|spec).*\b(component|ui)\b/)) {
|
|
169
|
+
semantic.requiresMultipleAgents = true
|
|
170
|
+
semantic.agents = ['frontend', 'qa']
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return semantic
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analyze historical patterns
|
|
178
|
+
*/
|
|
179
|
+
async analyzeHistory(text) {
|
|
180
|
+
if (!this.taskHistory) {
|
|
181
|
+
return { confidence: 0, patterns: [] }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Find similar tasks in history
|
|
185
|
+
const similar = this.taskHistory.filter(task => {
|
|
186
|
+
const similarity = this.calculateTextSimilarity(text, task.description)
|
|
187
|
+
return similarity > 0.5
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
if (similar.length === 0) {
|
|
191
|
+
return { confidence: 0, patterns: [] }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Find most common domain for similar tasks
|
|
195
|
+
const domainCounts = {}
|
|
196
|
+
similar.forEach(task => {
|
|
197
|
+
if (task.domain) {
|
|
198
|
+
domainCounts[task.domain] = (domainCounts[task.domain] || 0) + 1
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const mostCommon = Object.entries(domainCounts)
|
|
203
|
+
.sort((a, b) => b[1] - a[1])[0]
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
confidence: mostCommon ? mostCommon[1] / similar.length : 0,
|
|
207
|
+
patterns: similar.map(t => ({ domain: t.domain, description: t.description })),
|
|
208
|
+
suggestedDomain: mostCommon ? mostCommon[0] : null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Estimate task complexity
|
|
214
|
+
*/
|
|
215
|
+
estimateComplexity(text, domains) {
|
|
216
|
+
let complexity = 'medium'
|
|
217
|
+
|
|
218
|
+
// Complexity indicators
|
|
219
|
+
const simpleIndicators = ['add', 'create', 'simple', 'basic']
|
|
220
|
+
const complexIndicators = ['refactor', 'optimize', 'architecture', 'migration', 'redesign']
|
|
221
|
+
|
|
222
|
+
const simpleCount = simpleIndicators.filter(ind => text.includes(ind)).length
|
|
223
|
+
const complexCount = complexIndicators.filter(ind => text.includes(ind)).length
|
|
224
|
+
|
|
225
|
+
if (complexCount > simpleCount) {
|
|
226
|
+
complexity = 'high'
|
|
227
|
+
} else if (simpleCount > 0 && complexCount === 0) {
|
|
228
|
+
complexity = 'low'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Multiple domains = more complex
|
|
232
|
+
if (Object.keys(domains).length > 1) {
|
|
233
|
+
complexity = 'high'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return complexity
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Select primary domain from all signals
|
|
241
|
+
*/
|
|
242
|
+
selectPrimaryDomain(domains, semantic, historical) {
|
|
243
|
+
// Priority: historical > semantic > keyword matching
|
|
244
|
+
if (historical.suggestedDomain && historical.confidence > 0.7) {
|
|
245
|
+
return historical.suggestedDomain
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (semantic.agents && semantic.agents.length > 0) {
|
|
249
|
+
return semantic.agents[0] // Primary agent for multi-agent tasks
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Find domain with highest score
|
|
253
|
+
const sorted = Object.entries(domains)
|
|
254
|
+
.sort((a, b) => b[1].score - a[1].score)
|
|
255
|
+
|
|
256
|
+
return sorted.length > 0 ? sorted[0][0] : 'generalist'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Calculate confidence in domain selection
|
|
261
|
+
*/
|
|
262
|
+
calculateConfidence(domains, semantic, historical) {
|
|
263
|
+
let confidence = 0.5 // Base confidence
|
|
264
|
+
|
|
265
|
+
// Boost from keyword matches
|
|
266
|
+
const primaryDomain = this.selectPrimaryDomain(domains, semantic, historical)
|
|
267
|
+
if (domains[primaryDomain]) {
|
|
268
|
+
confidence += domains[primaryDomain].score * 0.1
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Boost from historical patterns
|
|
272
|
+
if (historical.confidence > 0) {
|
|
273
|
+
confidence += historical.confidence * 0.3
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Boost from semantic understanding
|
|
277
|
+
if (semantic.intent) {
|
|
278
|
+
confidence += 0.1
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return Math.min(confidence, 1.0)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build human-readable reason
|
|
286
|
+
*/
|
|
287
|
+
buildReason(primaryDomain, domains, semantic, historical) {
|
|
288
|
+
const parts = []
|
|
289
|
+
|
|
290
|
+
if (historical.suggestedDomain && historical.confidence > 0.7) {
|
|
291
|
+
parts.push(`Historical pattern: similar tasks used ${primaryDomain}`)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (domains[primaryDomain]) {
|
|
295
|
+
parts.push(`Keywords: ${domains[primaryDomain].keywords.join(', ')}`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (semantic.intent) {
|
|
299
|
+
parts.push(`Intent: ${semantic.intent}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return parts.join(' | ') || `Detected ${primaryDomain} domain`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get alternative domains
|
|
307
|
+
*/
|
|
308
|
+
getAlternatives(primaryDomain, domains) {
|
|
309
|
+
return Object.keys(domains)
|
|
310
|
+
.filter(d => d !== primaryDomain)
|
|
311
|
+
.sort((a, b) => domains[b].score - domains[a].score)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Load task history from memory
|
|
316
|
+
*/
|
|
317
|
+
async loadTaskHistory() {
|
|
318
|
+
try {
|
|
319
|
+
const memoryPath = pathManager.getFilePath(this.projectId, 'memory', 'context.jsonl')
|
|
320
|
+
const content = await fs.readFile(memoryPath, 'utf-8')
|
|
321
|
+
const lines = content.split('\n').filter(Boolean)
|
|
322
|
+
|
|
323
|
+
this.taskHistory = lines
|
|
324
|
+
.map(line => {
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(line)
|
|
327
|
+
} catch {
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
.filter(entry => entry && entry.type === 'task_start' && entry.domain)
|
|
332
|
+
.slice(-100) // Last 100 tasks
|
|
333
|
+
} catch {
|
|
334
|
+
this.taskHistory = []
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Calculate text similarity (simple Jaccard)
|
|
340
|
+
*/
|
|
341
|
+
calculateTextSimilarity(text1, text2) {
|
|
342
|
+
const words1 = new Set(text1.toLowerCase().split(/\s+/))
|
|
343
|
+
const words2 = new Set(text2.toLowerCase().split(/\s+/))
|
|
344
|
+
|
|
345
|
+
const intersection = new Set([...words1].filter(w => words2.has(w)))
|
|
346
|
+
const union = new Set([...words1, ...words2])
|
|
347
|
+
|
|
348
|
+
return intersection.size / union.size
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = TaskAnalyzer
|
|
353
|
+
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TechDetector - Dynamic Technology Detection
|
|
3
|
+
*
|
|
4
|
+
* NO HARDCODING - Detects technologies from actual project files
|
|
5
|
+
* Works with ANY technology stack (Elixir, Phoenix, Svelte, etc.)
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs').promises
|
|
11
|
+
const path = require('path')
|
|
12
|
+
|
|
13
|
+
class TechDetector {
|
|
14
|
+
constructor(projectPath) {
|
|
15
|
+
this.projectPath = projectPath
|
|
16
|
+
this.cache = null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect all technologies in the project
|
|
21
|
+
* Returns structured data about languages, frameworks, tools
|
|
22
|
+
* @returns {Promise<Object>} - { languages: [], frameworks: [], tools: [], packageManagers: [] }
|
|
23
|
+
*/
|
|
24
|
+
async detectAll() {
|
|
25
|
+
if (this.cache) {
|
|
26
|
+
return this.cache
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const detected = {
|
|
30
|
+
languages: [],
|
|
31
|
+
frameworks: [],
|
|
32
|
+
tools: [],
|
|
33
|
+
packageManagers: [],
|
|
34
|
+
databases: [],
|
|
35
|
+
buildTools: [],
|
|
36
|
+
testFrameworks: []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Detect from package managers (most reliable source)
|
|
40
|
+
await this.detectFromPackageJson(detected)
|
|
41
|
+
await this.detectFromCargoToml(detected)
|
|
42
|
+
await this.detectFromGoMod(detected)
|
|
43
|
+
await this.detectFromRequirements(detected)
|
|
44
|
+
await this.detectFromGemfile(detected)
|
|
45
|
+
await this.detectFromMixExs(detected) // Elixir
|
|
46
|
+
await this.detectFromPomXml(detected) // Maven/Java
|
|
47
|
+
await this.detectFromComposerJson(detected) // PHP
|
|
48
|
+
|
|
49
|
+
// Detect from config files
|
|
50
|
+
await this.detectFromConfigFiles(detected)
|
|
51
|
+
|
|
52
|
+
// Detect from directory structure
|
|
53
|
+
await this.detectFromStructure(detected)
|
|
54
|
+
|
|
55
|
+
// Cache result
|
|
56
|
+
this.cache = detected
|
|
57
|
+
return detected
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect from package.json (Node.js/JavaScript/TypeScript)
|
|
62
|
+
*/
|
|
63
|
+
async detectFromPackageJson(detected) {
|
|
64
|
+
try {
|
|
65
|
+
const packagePath = path.join(this.projectPath, 'package.json')
|
|
66
|
+
const content = await fs.readFile(packagePath, 'utf-8')
|
|
67
|
+
const pkg = JSON.parse(content)
|
|
68
|
+
|
|
69
|
+
detected.packageManagers.push('npm')
|
|
70
|
+
|
|
71
|
+
// Language
|
|
72
|
+
if (pkg.dependencies?.typescript || pkg.devDependencies?.typescript) {
|
|
73
|
+
detected.languages.push('TypeScript')
|
|
74
|
+
} else {
|
|
75
|
+
detected.languages.push('JavaScript')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Collect all dependencies (no hardcoding - just list them)
|
|
79
|
+
const allDeps = {
|
|
80
|
+
...(pkg.dependencies || {}),
|
|
81
|
+
...(pkg.devDependencies || {})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Frameworks and tools are in dependencies - let Claude decide what's important
|
|
85
|
+
// We just collect the names, no assumptions
|
|
86
|
+
const depNames = Object.keys(allDeps)
|
|
87
|
+
|
|
88
|
+
// Common patterns (but not hardcoded - just helpers)
|
|
89
|
+
for (const dep of depNames) {
|
|
90
|
+
// Frontend frameworks
|
|
91
|
+
if (['react', 'vue', 'angular', 'svelte'].includes(dep.toLowerCase())) {
|
|
92
|
+
detected.frameworks.push(dep)
|
|
93
|
+
}
|
|
94
|
+
// Meta-frameworks
|
|
95
|
+
else if (['next', 'nuxt', 'sveltekit', 'remix'].includes(dep.toLowerCase())) {
|
|
96
|
+
detected.frameworks.push(dep)
|
|
97
|
+
}
|
|
98
|
+
// Backend frameworks
|
|
99
|
+
else if (['express', 'fastify', 'koa', 'hapi', 'nest'].includes(dep.toLowerCase())) {
|
|
100
|
+
detected.frameworks.push(dep)
|
|
101
|
+
}
|
|
102
|
+
// Build tools
|
|
103
|
+
else if (['vite', 'webpack', 'rollup', 'esbuild', 'parcel'].includes(dep.toLowerCase())) {
|
|
104
|
+
detected.buildTools.push(dep)
|
|
105
|
+
}
|
|
106
|
+
// Test frameworks
|
|
107
|
+
else if (['jest', 'vitest', 'mocha', 'jasmine', 'cypress', 'playwright'].includes(dep.toLowerCase())) {
|
|
108
|
+
detected.testFrameworks.push(dep)
|
|
109
|
+
}
|
|
110
|
+
// Databases
|
|
111
|
+
else if (['mongoose', 'sequelize', 'prisma', 'typeorm'].includes(dep.toLowerCase())) {
|
|
112
|
+
detected.databases.push(dep)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Also check for yarn/pnpm
|
|
117
|
+
if (await this.fileExists('yarn.lock')) {
|
|
118
|
+
detected.packageManagers.push('yarn')
|
|
119
|
+
}
|
|
120
|
+
if (await this.fileExists('pnpm-lock.yaml')) {
|
|
121
|
+
detected.packageManagers.push('pnpm')
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// File doesn't exist or invalid JSON - skip
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Detect from Cargo.toml (Rust)
|
|
130
|
+
*/
|
|
131
|
+
async detectFromCargoToml(detected) {
|
|
132
|
+
try {
|
|
133
|
+
const cargoPath = path.join(this.projectPath, 'Cargo.toml')
|
|
134
|
+
await fs.readFile(cargoPath, 'utf-8')
|
|
135
|
+
|
|
136
|
+
detected.languages.push('Rust')
|
|
137
|
+
detected.packageManagers.push('Cargo')
|
|
138
|
+
} catch (error) {
|
|
139
|
+
// File doesn't exist - skip
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Detect from go.mod (Go)
|
|
145
|
+
*/
|
|
146
|
+
async detectFromGoMod(detected) {
|
|
147
|
+
try {
|
|
148
|
+
const goModPath = path.join(this.projectPath, 'go.mod')
|
|
149
|
+
await fs.readFile(goModPath, 'utf-8')
|
|
150
|
+
|
|
151
|
+
detected.languages.push('Go')
|
|
152
|
+
detected.packageManagers.push('Go Modules')
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// File doesn't exist - skip
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Detect from requirements.txt (Python)
|
|
160
|
+
*/
|
|
161
|
+
async detectFromRequirements(detected) {
|
|
162
|
+
try {
|
|
163
|
+
const reqPath = path.join(this.projectPath, 'requirements.txt')
|
|
164
|
+
const content = await fs.readFile(reqPath, 'utf-8')
|
|
165
|
+
|
|
166
|
+
detected.languages.push('Python')
|
|
167
|
+
detected.packageManagers.push('pip')
|
|
168
|
+
|
|
169
|
+
// Detect common frameworks
|
|
170
|
+
const lines = content.split('\n').map(l => l.trim().toLowerCase())
|
|
171
|
+
if (lines.some(l => l.includes('django'))) detected.frameworks.push('Django')
|
|
172
|
+
if (lines.some(l => l.includes('flask'))) detected.frameworks.push('Flask')
|
|
173
|
+
if (lines.some(l => l.includes('fastapi'))) detected.frameworks.push('FastAPI')
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// File doesn't exist - skip
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Detect from Gemfile (Ruby)
|
|
181
|
+
*/
|
|
182
|
+
async detectFromGemfile(detected) {
|
|
183
|
+
try {
|
|
184
|
+
const gemfilePath = path.join(this.projectPath, 'Gemfile')
|
|
185
|
+
const content = await fs.readFile(gemfilePath, 'utf-8')
|
|
186
|
+
|
|
187
|
+
detected.languages.push('Ruby')
|
|
188
|
+
detected.packageManagers.push('Bundler')
|
|
189
|
+
|
|
190
|
+
if (content.includes('rails')) {
|
|
191
|
+
detected.frameworks.push('Rails')
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// File doesn't exist - skip
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect from mix.exs (Elixir)
|
|
200
|
+
*/
|
|
201
|
+
async detectFromMixExs(detected) {
|
|
202
|
+
try {
|
|
203
|
+
const mixPath = path.join(this.projectPath, 'mix.exs')
|
|
204
|
+
const content = await fs.readFile(mixPath, 'utf-8')
|
|
205
|
+
|
|
206
|
+
detected.languages.push('Elixir')
|
|
207
|
+
detected.packageManagers.push('Mix')
|
|
208
|
+
|
|
209
|
+
if (content.includes('phoenix')) {
|
|
210
|
+
detected.frameworks.push('Phoenix')
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
// File doesn't exist - skip
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Detect from pom.xml (Maven/Java)
|
|
219
|
+
*/
|
|
220
|
+
async detectFromPomXml(detected) {
|
|
221
|
+
try {
|
|
222
|
+
const pomPath = path.join(this.projectPath, 'pom.xml')
|
|
223
|
+
await fs.readFile(pomPath, 'utf-8')
|
|
224
|
+
|
|
225
|
+
detected.languages.push('Java')
|
|
226
|
+
detected.packageManagers.push('Maven')
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// File doesn't exist - skip
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Detect from composer.json (PHP)
|
|
234
|
+
*/
|
|
235
|
+
async detectFromComposerJson(detected) {
|
|
236
|
+
try {
|
|
237
|
+
const composerPath = path.join(this.projectPath, 'composer.json')
|
|
238
|
+
const content = await fs.readFile(composerPath, 'utf-8')
|
|
239
|
+
const composer = JSON.parse(content)
|
|
240
|
+
|
|
241
|
+
detected.languages.push('PHP')
|
|
242
|
+
detected.packageManagers.push('Composer')
|
|
243
|
+
|
|
244
|
+
const allDeps = {
|
|
245
|
+
...(composer.require || {}),
|
|
246
|
+
...(composer['require-dev'] || {})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Object.keys(allDeps).some(d => d.includes('laravel'))) {
|
|
250
|
+
detected.frameworks.push('Laravel')
|
|
251
|
+
}
|
|
252
|
+
if (Object.keys(allDeps).some(d => d.includes('symfony'))) {
|
|
253
|
+
detected.frameworks.push('Symfony')
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
// File doesn't exist - skip
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Detect from config files (Docker, etc.)
|
|
262
|
+
*/
|
|
263
|
+
async detectFromConfigFiles(detected) {
|
|
264
|
+
// Docker
|
|
265
|
+
if (await this.fileExists('Dockerfile')) {
|
|
266
|
+
detected.tools.push('Docker')
|
|
267
|
+
}
|
|
268
|
+
if (await this.fileExists('docker-compose.yml') || await this.fileExists('docker-compose.yaml')) {
|
|
269
|
+
detected.tools.push('Docker Compose')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Kubernetes
|
|
273
|
+
if (await this.fileExists('k8s') || await this.fileExists('kubernetes')) {
|
|
274
|
+
detected.tools.push('Kubernetes')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Terraform
|
|
278
|
+
if (await this.fileExists('.tf') || await this.findFiles('*.tf')) {
|
|
279
|
+
detected.tools.push('Terraform')
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Detect from directory structure
|
|
285
|
+
*/
|
|
286
|
+
async detectFromStructure(_detected) {
|
|
287
|
+
try {
|
|
288
|
+
const entries = await fs.readdir(this.projectPath, { withFileTypes: true })
|
|
289
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
|
|
290
|
+
|
|
291
|
+
// Common patterns (but not assumptions - just hints)
|
|
292
|
+
// Could analyze structure here in the future
|
|
293
|
+
if (dirs.includes('src') && dirs.includes('lib')) {
|
|
294
|
+
// Could be Elixir, but don't assume
|
|
295
|
+
}
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// Can't read directory - skip
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get a summary string of detected technologies
|
|
303
|
+
* Useful for agent generation
|
|
304
|
+
*/
|
|
305
|
+
async getSummary() {
|
|
306
|
+
const tech = await this.detectAll()
|
|
307
|
+
const parts = []
|
|
308
|
+
|
|
309
|
+
if (tech.languages.length > 0) {
|
|
310
|
+
parts.push(`Languages: ${tech.languages.join(', ')}`)
|
|
311
|
+
}
|
|
312
|
+
if (tech.frameworks.length > 0) {
|
|
313
|
+
parts.push(`Frameworks: ${tech.frameworks.join(', ')}`)
|
|
314
|
+
}
|
|
315
|
+
if (tech.tools.length > 0) {
|
|
316
|
+
parts.push(`Tools: ${tech.tools.join(', ')}`)
|
|
317
|
+
}
|
|
318
|
+
if (tech.databases.length > 0) {
|
|
319
|
+
parts.push(`Databases: ${tech.databases.join(', ')}`)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return parts.join(' | ')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Helper: Check if file exists
|
|
327
|
+
*/
|
|
328
|
+
async fileExists(filename) {
|
|
329
|
+
try {
|
|
330
|
+
await fs.access(path.join(this.projectPath, filename))
|
|
331
|
+
return true
|
|
332
|
+
} catch {
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Helper: Find files matching pattern
|
|
339
|
+
*/
|
|
340
|
+
async findFiles(pattern) {
|
|
341
|
+
try {
|
|
342
|
+
const { exec } = require('child_process')
|
|
343
|
+
const { promisify } = require('util')
|
|
344
|
+
const execAsync = promisify(exec)
|
|
345
|
+
|
|
346
|
+
const { stdout } = await execAsync(
|
|
347
|
+
`find . -name "${pattern}" -type f ! -path "*/node_modules/*" ! -path "*/.git/*" | head -1`,
|
|
348
|
+
{ cwd: this.projectPath }
|
|
349
|
+
)
|
|
350
|
+
return stdout.trim().length > 0
|
|
351
|
+
} catch {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Clear cache (useful after project changes)
|
|
358
|
+
*/
|
|
359
|
+
clearCache() {
|
|
360
|
+
this.cache = null
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = TechDetector
|
|
365
|
+
|