@codexa/cli 8.6.0 → 8.6.9
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/commands/architect.ts +760 -760
- package/commands/check.ts +131 -131
- package/commands/clear.ts +170 -170
- package/commands/decide.ts +249 -249
- package/commands/discover.ts +1071 -1071
- package/commands/knowledge.ts +361 -361
- package/commands/patterns.ts +621 -621
- package/commands/plan.ts +376 -376
- package/commands/product.ts +626 -626
- package/commands/research.ts +754 -754
- package/commands/review.ts +463 -463
- package/commands/standards.ts +200 -200
- package/commands/task.ts +623 -623
- package/commands/utils.ts +1021 -1021
- package/db/connection.ts +32 -32
- package/db/schema.ts +719 -719
- package/detectors/README.md +109 -109
- package/detectors/dotnet.ts +357 -357
- package/detectors/flutter.ts +350 -350
- package/detectors/go.ts +324 -324
- package/detectors/index.ts +387 -387
- package/detectors/jvm.ts +433 -433
- package/detectors/loader.ts +128 -128
- package/detectors/node.ts +493 -493
- package/detectors/python.ts +423 -423
- package/detectors/rust.ts +348 -348
- package/gates/standards-validator.ts +204 -204
- package/gates/validator.ts +441 -441
- package/package.json +44 -43
- package/protocol/process-return.ts +450 -450
- package/protocol/subagent-protocol.ts +401 -401
- package/workflow.ts +783 -782
package/detectors/index.ts
CHANGED
|
@@ -1,388 +1,388 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Universal Stack Detection System
|
|
3
|
-
*
|
|
4
|
-
* Modular detector architecture for language-agnostic stack detection.
|
|
5
|
-
* Each detector is responsible for identifying technologies in its ecosystem.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
9
|
-
import { join, extname } from "path";
|
|
10
|
-
import { Glob } from "bun";
|
|
11
|
-
|
|
12
|
-
// ============================================================
|
|
13
|
-
// TYPES
|
|
14
|
-
// ============================================================
|
|
15
|
-
|
|
16
|
-
export interface DetectedTechnology {
|
|
17
|
-
name: string;
|
|
18
|
-
version?: string;
|
|
19
|
-
confidence: number; // 0-1
|
|
20
|
-
source: string; // File/marker that triggered detection
|
|
21
|
-
category: "frontend" | "backend" | "database" | "orm" | "styling" | "auth" | "testing" | "runtime" | "build" | "devops";
|
|
22
|
-
metadata?: Record<string, any>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface DetectorResult {
|
|
26
|
-
ecosystem: string;
|
|
27
|
-
technologies: DetectedTechnology[];
|
|
28
|
-
structure: Record<string, string>;
|
|
29
|
-
configFiles: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface Detector {
|
|
33
|
-
name: string;
|
|
34
|
-
ecosystem: string;
|
|
35
|
-
priority: number; // Higher = checked first
|
|
36
|
-
markers: MarkerConfig[];
|
|
37
|
-
detect: (cwd: string) => Promise<DetectorResult | null>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface MarkerConfig {
|
|
41
|
-
type: "file" | "glob" | "directory" | "content";
|
|
42
|
-
pattern: string;
|
|
43
|
-
contentMatch?: RegExp;
|
|
44
|
-
weight: number; // 0-1, contributes to confidence
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ============================================================
|
|
48
|
-
// DETECTOR REGISTRY
|
|
49
|
-
// ============================================================
|
|
50
|
-
|
|
51
|
-
const detectors: Detector[] = [];
|
|
52
|
-
|
|
53
|
-
export function registerDetector(detector: Detector): void {
|
|
54
|
-
detectors.push(detector);
|
|
55
|
-
detectors.sort((a, b) => b.priority - a.priority);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function getDetectors(): Detector[] {
|
|
59
|
-
return [...detectors];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ============================================================
|
|
63
|
-
// UTILITY FUNCTIONS
|
|
64
|
-
// ============================================================
|
|
65
|
-
|
|
66
|
-
export function fileExists(path: string): boolean {
|
|
67
|
-
try {
|
|
68
|
-
return existsSync(path) && statSync(path).isFile();
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function dirExists(path: string): boolean {
|
|
75
|
-
try {
|
|
76
|
-
return existsSync(path) && statSync(path).isDirectory();
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function findFiles(cwd: string, pattern: string): string[] {
|
|
83
|
-
try {
|
|
84
|
-
const glob = new Glob(pattern);
|
|
85
|
-
const results: string[] = [];
|
|
86
|
-
for (const file of glob.scanSync({ cwd, onlyFiles: true })) {
|
|
87
|
-
results.push(file);
|
|
88
|
-
}
|
|
89
|
-
return results;
|
|
90
|
-
} catch {
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function readJson(path: string): any | null {
|
|
96
|
-
try {
|
|
97
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
98
|
-
} catch {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function readText(path: string): string | null {
|
|
104
|
-
try {
|
|
105
|
-
return readFileSync(path, "utf-8");
|
|
106
|
-
} catch {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function parseToml(content: string): Record<string, any> {
|
|
112
|
-
// Simple TOML parser for basic cases
|
|
113
|
-
const result: Record<string, any> = {};
|
|
114
|
-
let currentSection = result;
|
|
115
|
-
|
|
116
|
-
for (const line of content.split("\n")) {
|
|
117
|
-
const trimmed = line.trim();
|
|
118
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
119
|
-
|
|
120
|
-
// Section header
|
|
121
|
-
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
122
|
-
if (sectionMatch) {
|
|
123
|
-
const path = sectionMatch[1].split(".");
|
|
124
|
-
currentSection = result;
|
|
125
|
-
for (const part of path) {
|
|
126
|
-
if (!currentSection[part]) currentSection[part] = {};
|
|
127
|
-
currentSection = currentSection[part];
|
|
128
|
-
}
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Key-value pair
|
|
133
|
-
const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
|
|
134
|
-
if (kvMatch) {
|
|
135
|
-
const key = kvMatch[1].trim();
|
|
136
|
-
let value = kvMatch[2].trim();
|
|
137
|
-
|
|
138
|
-
// Parse value
|
|
139
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
140
|
-
value = value.slice(1, -1);
|
|
141
|
-
} else if (value === "true") {
|
|
142
|
-
currentSection[key] = true;
|
|
143
|
-
continue;
|
|
144
|
-
} else if (value === "false") {
|
|
145
|
-
currentSection[key] = false;
|
|
146
|
-
continue;
|
|
147
|
-
} else if (!isNaN(Number(value))) {
|
|
148
|
-
currentSection[key] = Number(value);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
currentSection[key] = value;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function parseYaml(content: string): Record<string, any> {
|
|
160
|
-
// Simple YAML parser for basic cases (key: value only)
|
|
161
|
-
const result: Record<string, any> = {};
|
|
162
|
-
const lines = content.split("\n");
|
|
163
|
-
let currentIndent = 0;
|
|
164
|
-
const stack: { obj: Record<string, any>; indent: number }[] = [{ obj: result, indent: -1 }];
|
|
165
|
-
|
|
166
|
-
for (const line of lines) {
|
|
167
|
-
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
168
|
-
|
|
169
|
-
const indent = line.search(/\S/);
|
|
170
|
-
const trimmed = line.trim();
|
|
171
|
-
|
|
172
|
-
// Pop stack to correct level
|
|
173
|
-
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
174
|
-
stack.pop();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const current = stack[stack.length - 1].obj;
|
|
178
|
-
|
|
179
|
-
// Check for key: value
|
|
180
|
-
const colonIdx = trimmed.indexOf(":");
|
|
181
|
-
if (colonIdx > 0) {
|
|
182
|
-
const key = trimmed.slice(0, colonIdx).trim();
|
|
183
|
-
const value = trimmed.slice(colonIdx + 1).trim();
|
|
184
|
-
|
|
185
|
-
if (value) {
|
|
186
|
-
// Parse value
|
|
187
|
-
if (value.startsWith('"') && value.endsWith('"')) {
|
|
188
|
-
current[key] = value.slice(1, -1);
|
|
189
|
-
} else if (value === "true") {
|
|
190
|
-
current[key] = true;
|
|
191
|
-
} else if (value === "false") {
|
|
192
|
-
current[key] = false;
|
|
193
|
-
} else if (!isNaN(Number(value))) {
|
|
194
|
-
current[key] = Number(value);
|
|
195
|
-
} else {
|
|
196
|
-
current[key] = value;
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
// Nested object
|
|
200
|
-
current[key] = {};
|
|
201
|
-
stack.push({ obj: current[key], indent });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ============================================================
|
|
210
|
-
// MAIN DETECTION ENGINE
|
|
211
|
-
// ============================================================
|
|
212
|
-
|
|
213
|
-
export interface UnifiedDetectionResult {
|
|
214
|
-
primary: {
|
|
215
|
-
language: string;
|
|
216
|
-
runtime?: string;
|
|
217
|
-
framework?: string;
|
|
218
|
-
};
|
|
219
|
-
stack: {
|
|
220
|
-
frontend?: string[];
|
|
221
|
-
backend?: string[];
|
|
222
|
-
database?: string[];
|
|
223
|
-
orm?: string[];
|
|
224
|
-
styling?: string[];
|
|
225
|
-
auth?: string[];
|
|
226
|
-
testing?: string[];
|
|
227
|
-
devops?: string[];
|
|
228
|
-
};
|
|
229
|
-
structure: Record<string, string>;
|
|
230
|
-
configFiles: string[];
|
|
231
|
-
allTechnologies: DetectedTechnology[];
|
|
232
|
-
ecosystems: string[];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export async function detectUniversal(cwd: string = process.cwd()): Promise<UnifiedDetectionResult> {
|
|
236
|
-
const results: DetectorResult[] = [];
|
|
237
|
-
|
|
238
|
-
// Run all detectors in parallel
|
|
239
|
-
const detectorPromises = detectors.map(async (detector) => {
|
|
240
|
-
try {
|
|
241
|
-
return await detector.detect(cwd);
|
|
242
|
-
} catch (err) {
|
|
243
|
-
console.error(`Detector ${detector.name} failed:`, err);
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const detectorResults = await Promise.all(detectorPromises);
|
|
249
|
-
|
|
250
|
-
for (const result of detectorResults) {
|
|
251
|
-
if (result && result.technologies.length > 0) {
|
|
252
|
-
results.push(result);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Merge results
|
|
257
|
-
return mergeResults(results);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function mergeResults(results: DetectorResult[]): UnifiedDetectionResult {
|
|
261
|
-
const allTechnologies: DetectedTechnology[] = [];
|
|
262
|
-
const structure: Record<string, string> = {};
|
|
263
|
-
const configFiles: string[] = [];
|
|
264
|
-
const ecosystems: string[] = [];
|
|
265
|
-
|
|
266
|
-
for (const result of results) {
|
|
267
|
-
allTechnologies.push(...result.technologies);
|
|
268
|
-
Object.assign(structure, result.structure);
|
|
269
|
-
configFiles.push(...result.configFiles);
|
|
270
|
-
if (!ecosystems.includes(result.ecosystem)) {
|
|
271
|
-
ecosystems.push(result.ecosystem);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Sort by confidence
|
|
276
|
-
allTechnologies.sort((a, b) => b.confidence - a.confidence);
|
|
277
|
-
|
|
278
|
-
// Group by category
|
|
279
|
-
const stack: UnifiedDetectionResult["stack"] = {};
|
|
280
|
-
const categoryMap: Record<string, DetectedTechnology[]> = {};
|
|
281
|
-
|
|
282
|
-
for (const tech of allTechnologies) {
|
|
283
|
-
if (!categoryMap[tech.category]) {
|
|
284
|
-
categoryMap[tech.category] = [];
|
|
285
|
-
}
|
|
286
|
-
categoryMap[tech.category].push(tech);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
for (const [category, techs] of Object.entries(categoryMap)) {
|
|
290
|
-
// Take unique names, sorted by confidence
|
|
291
|
-
const uniqueNames = [...new Set(techs.map(t => t.name))];
|
|
292
|
-
(stack as any)[category] = uniqueNames;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Determine primary language/runtime
|
|
296
|
-
const primary = determinePrimary(allTechnologies, ecosystems);
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
primary,
|
|
300
|
-
stack,
|
|
301
|
-
structure,
|
|
302
|
-
configFiles: [...new Set(configFiles)],
|
|
303
|
-
allTechnologies,
|
|
304
|
-
ecosystems,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function determinePrimary(technologies: DetectedTechnology[], ecosystems: string[]): UnifiedDetectionResult["primary"] {
|
|
309
|
-
// Find highest confidence runtime
|
|
310
|
-
const runtimes = technologies.filter(t => t.category === "runtime");
|
|
311
|
-
const backends = technologies.filter(t => t.category === "backend");
|
|
312
|
-
const frontends = technologies.filter(t => t.category === "frontend");
|
|
313
|
-
|
|
314
|
-
// Language mapping
|
|
315
|
-
const ecosystemToLanguage: Record<string, string> = {
|
|
316
|
-
"dotnet": "C#",
|
|
317
|
-
"node": "TypeScript/JavaScript",
|
|
318
|
-
"python": "Python",
|
|
319
|
-
"go": "Go",
|
|
320
|
-
"rust": "Rust",
|
|
321
|
-
"java": "Java",
|
|
322
|
-
"ruby": "Ruby",
|
|
323
|
-
"php": "PHP",
|
|
324
|
-
"flutter": "Dart",
|
|
325
|
-
"swift": "Swift",
|
|
326
|
-
"kotlin": "Kotlin",
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const primaryEcosystem = ecosystems[0] || "unknown";
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
language: ecosystemToLanguage[primaryEcosystem] || primaryEcosystem,
|
|
333
|
-
runtime: runtimes[0]?.name,
|
|
334
|
-
framework: backends[0]?.name || frontends[0]?.name,
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ============================================================
|
|
339
|
-
// LEGACY COMPATIBILITY ADAPTER
|
|
340
|
-
// ============================================================
|
|
341
|
-
|
|
342
|
-
export interface LegacyStackDetection {
|
|
343
|
-
frontend?: string;
|
|
344
|
-
backend?: string;
|
|
345
|
-
database?: string;
|
|
346
|
-
orm?: string;
|
|
347
|
-
styling?: string;
|
|
348
|
-
auth?: string;
|
|
349
|
-
testing?: string;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export interface LegacyStructureDetection {
|
|
353
|
-
components?: string;
|
|
354
|
-
services?: string;
|
|
355
|
-
schema?: string;
|
|
356
|
-
types?: string;
|
|
357
|
-
hooks?: string;
|
|
358
|
-
utils?: string;
|
|
359
|
-
api?: string;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export async function detectStackLegacy(cwd: string = process.cwd()): Promise<{
|
|
363
|
-
stack: LegacyStackDetection;
|
|
364
|
-
structure: LegacyStructureDetection;
|
|
365
|
-
}> {
|
|
366
|
-
const result = await detectUniversal(cwd);
|
|
367
|
-
|
|
368
|
-
// Convert to legacy format (single value per category)
|
|
369
|
-
const stack: LegacyStackDetection = {};
|
|
370
|
-
const structure: LegacyStructureDetection = {};
|
|
371
|
-
|
|
372
|
-
if (result.stack.frontend?.length) stack.frontend = result.stack.frontend[0];
|
|
373
|
-
if (result.stack.backend?.length) stack.backend = result.stack.backend[0];
|
|
374
|
-
if (result.stack.database?.length) stack.database = result.stack.database[0];
|
|
375
|
-
if (result.stack.orm?.length) stack.orm = result.stack.orm[0];
|
|
376
|
-
if (result.stack.styling?.length) stack.styling = result.stack.styling[0];
|
|
377
|
-
if (result.stack.auth?.length) stack.auth = result.stack.auth[0];
|
|
378
|
-
if (result.stack.testing?.length) stack.testing = result.stack.testing[0];
|
|
379
|
-
|
|
380
|
-
// Map structure
|
|
381
|
-
for (const [key, value] of Object.entries(result.structure)) {
|
|
382
|
-
if (key in structure || ["components", "services", "schema", "types", "hooks", "utils", "api"].includes(key)) {
|
|
383
|
-
(structure as any)[key] = value;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return { stack, structure };
|
|
1
|
+
/**
|
|
2
|
+
* Universal Stack Detection System
|
|
3
|
+
*
|
|
4
|
+
* Modular detector architecture for language-agnostic stack detection.
|
|
5
|
+
* Each detector is responsible for identifying technologies in its ecosystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
9
|
+
import { join, extname } from "path";
|
|
10
|
+
import { Glob } from "bun";
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// TYPES
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
export interface DetectedTechnology {
|
|
17
|
+
name: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
confidence: number; // 0-1
|
|
20
|
+
source: string; // File/marker that triggered detection
|
|
21
|
+
category: "frontend" | "backend" | "database" | "orm" | "styling" | "auth" | "testing" | "runtime" | "build" | "devops";
|
|
22
|
+
metadata?: Record<string, any>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface DetectorResult {
|
|
26
|
+
ecosystem: string;
|
|
27
|
+
technologies: DetectedTechnology[];
|
|
28
|
+
structure: Record<string, string>;
|
|
29
|
+
configFiles: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Detector {
|
|
33
|
+
name: string;
|
|
34
|
+
ecosystem: string;
|
|
35
|
+
priority: number; // Higher = checked first
|
|
36
|
+
markers: MarkerConfig[];
|
|
37
|
+
detect: (cwd: string) => Promise<DetectorResult | null>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MarkerConfig {
|
|
41
|
+
type: "file" | "glob" | "directory" | "content";
|
|
42
|
+
pattern: string;
|
|
43
|
+
contentMatch?: RegExp;
|
|
44
|
+
weight: number; // 0-1, contributes to confidence
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ============================================================
|
|
48
|
+
// DETECTOR REGISTRY
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
const detectors: Detector[] = [];
|
|
52
|
+
|
|
53
|
+
export function registerDetector(detector: Detector): void {
|
|
54
|
+
detectors.push(detector);
|
|
55
|
+
detectors.sort((a, b) => b.priority - a.priority);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getDetectors(): Detector[] {
|
|
59
|
+
return [...detectors];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// UTILITY FUNCTIONS
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
export function fileExists(path: string): boolean {
|
|
67
|
+
try {
|
|
68
|
+
return existsSync(path) && statSync(path).isFile();
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function dirExists(path: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function findFiles(cwd: string, pattern: string): string[] {
|
|
83
|
+
try {
|
|
84
|
+
const glob = new Glob(pattern);
|
|
85
|
+
const results: string[] = [];
|
|
86
|
+
for (const file of glob.scanSync({ cwd, onlyFiles: true })) {
|
|
87
|
+
results.push(file);
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function readJson(path: string): any | null {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function readText(path: string): string | null {
|
|
104
|
+
try {
|
|
105
|
+
return readFileSync(path, "utf-8");
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function parseToml(content: string): Record<string, any> {
|
|
112
|
+
// Simple TOML parser for basic cases
|
|
113
|
+
const result: Record<string, any> = {};
|
|
114
|
+
let currentSection = result;
|
|
115
|
+
|
|
116
|
+
for (const line of content.split("\n")) {
|
|
117
|
+
const trimmed = line.trim();
|
|
118
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
119
|
+
|
|
120
|
+
// Section header
|
|
121
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
122
|
+
if (sectionMatch) {
|
|
123
|
+
const path = sectionMatch[1].split(".");
|
|
124
|
+
currentSection = result;
|
|
125
|
+
for (const part of path) {
|
|
126
|
+
if (!currentSection[part]) currentSection[part] = {};
|
|
127
|
+
currentSection = currentSection[part];
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Key-value pair
|
|
133
|
+
const kvMatch = trimmed.match(/^([^=]+)=\s*(.+)$/);
|
|
134
|
+
if (kvMatch) {
|
|
135
|
+
const key = kvMatch[1].trim();
|
|
136
|
+
let value = kvMatch[2].trim();
|
|
137
|
+
|
|
138
|
+
// Parse value
|
|
139
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
140
|
+
value = value.slice(1, -1);
|
|
141
|
+
} else if (value === "true") {
|
|
142
|
+
currentSection[key] = true;
|
|
143
|
+
continue;
|
|
144
|
+
} else if (value === "false") {
|
|
145
|
+
currentSection[key] = false;
|
|
146
|
+
continue;
|
|
147
|
+
} else if (!isNaN(Number(value))) {
|
|
148
|
+
currentSection[key] = Number(value);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
currentSection[key] = value;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parseYaml(content: string): Record<string, any> {
|
|
160
|
+
// Simple YAML parser for basic cases (key: value only)
|
|
161
|
+
const result: Record<string, any> = {};
|
|
162
|
+
const lines = content.split("\n");
|
|
163
|
+
let currentIndent = 0;
|
|
164
|
+
const stack: { obj: Record<string, any>; indent: number }[] = [{ obj: result, indent: -1 }];
|
|
165
|
+
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
168
|
+
|
|
169
|
+
const indent = line.search(/\S/);
|
|
170
|
+
const trimmed = line.trim();
|
|
171
|
+
|
|
172
|
+
// Pop stack to correct level
|
|
173
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
174
|
+
stack.pop();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const current = stack[stack.length - 1].obj;
|
|
178
|
+
|
|
179
|
+
// Check for key: value
|
|
180
|
+
const colonIdx = trimmed.indexOf(":");
|
|
181
|
+
if (colonIdx > 0) {
|
|
182
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
183
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
184
|
+
|
|
185
|
+
if (value) {
|
|
186
|
+
// Parse value
|
|
187
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
188
|
+
current[key] = value.slice(1, -1);
|
|
189
|
+
} else if (value === "true") {
|
|
190
|
+
current[key] = true;
|
|
191
|
+
} else if (value === "false") {
|
|
192
|
+
current[key] = false;
|
|
193
|
+
} else if (!isNaN(Number(value))) {
|
|
194
|
+
current[key] = Number(value);
|
|
195
|
+
} else {
|
|
196
|
+
current[key] = value;
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
// Nested object
|
|
200
|
+
current[key] = {};
|
|
201
|
+
stack.push({ obj: current[key], indent });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================
|
|
210
|
+
// MAIN DETECTION ENGINE
|
|
211
|
+
// ============================================================
|
|
212
|
+
|
|
213
|
+
export interface UnifiedDetectionResult {
|
|
214
|
+
primary: {
|
|
215
|
+
language: string;
|
|
216
|
+
runtime?: string;
|
|
217
|
+
framework?: string;
|
|
218
|
+
};
|
|
219
|
+
stack: {
|
|
220
|
+
frontend?: string[];
|
|
221
|
+
backend?: string[];
|
|
222
|
+
database?: string[];
|
|
223
|
+
orm?: string[];
|
|
224
|
+
styling?: string[];
|
|
225
|
+
auth?: string[];
|
|
226
|
+
testing?: string[];
|
|
227
|
+
devops?: string[];
|
|
228
|
+
};
|
|
229
|
+
structure: Record<string, string>;
|
|
230
|
+
configFiles: string[];
|
|
231
|
+
allTechnologies: DetectedTechnology[];
|
|
232
|
+
ecosystems: string[];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function detectUniversal(cwd: string = process.cwd()): Promise<UnifiedDetectionResult> {
|
|
236
|
+
const results: DetectorResult[] = [];
|
|
237
|
+
|
|
238
|
+
// Run all detectors in parallel
|
|
239
|
+
const detectorPromises = detectors.map(async (detector) => {
|
|
240
|
+
try {
|
|
241
|
+
return await detector.detect(cwd);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error(`Detector ${detector.name} failed:`, err);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const detectorResults = await Promise.all(detectorPromises);
|
|
249
|
+
|
|
250
|
+
for (const result of detectorResults) {
|
|
251
|
+
if (result && result.technologies.length > 0) {
|
|
252
|
+
results.push(result);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Merge results
|
|
257
|
+
return mergeResults(results);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function mergeResults(results: DetectorResult[]): UnifiedDetectionResult {
|
|
261
|
+
const allTechnologies: DetectedTechnology[] = [];
|
|
262
|
+
const structure: Record<string, string> = {};
|
|
263
|
+
const configFiles: string[] = [];
|
|
264
|
+
const ecosystems: string[] = [];
|
|
265
|
+
|
|
266
|
+
for (const result of results) {
|
|
267
|
+
allTechnologies.push(...result.technologies);
|
|
268
|
+
Object.assign(structure, result.structure);
|
|
269
|
+
configFiles.push(...result.configFiles);
|
|
270
|
+
if (!ecosystems.includes(result.ecosystem)) {
|
|
271
|
+
ecosystems.push(result.ecosystem);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Sort by confidence
|
|
276
|
+
allTechnologies.sort((a, b) => b.confidence - a.confidence);
|
|
277
|
+
|
|
278
|
+
// Group by category
|
|
279
|
+
const stack: UnifiedDetectionResult["stack"] = {};
|
|
280
|
+
const categoryMap: Record<string, DetectedTechnology[]> = {};
|
|
281
|
+
|
|
282
|
+
for (const tech of allTechnologies) {
|
|
283
|
+
if (!categoryMap[tech.category]) {
|
|
284
|
+
categoryMap[tech.category] = [];
|
|
285
|
+
}
|
|
286
|
+
categoryMap[tech.category].push(tech);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const [category, techs] of Object.entries(categoryMap)) {
|
|
290
|
+
// Take unique names, sorted by confidence
|
|
291
|
+
const uniqueNames = [...new Set(techs.map(t => t.name))];
|
|
292
|
+
(stack as any)[category] = uniqueNames;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Determine primary language/runtime
|
|
296
|
+
const primary = determinePrimary(allTechnologies, ecosystems);
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
primary,
|
|
300
|
+
stack,
|
|
301
|
+
structure,
|
|
302
|
+
configFiles: [...new Set(configFiles)],
|
|
303
|
+
allTechnologies,
|
|
304
|
+
ecosystems,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function determinePrimary(technologies: DetectedTechnology[], ecosystems: string[]): UnifiedDetectionResult["primary"] {
|
|
309
|
+
// Find highest confidence runtime
|
|
310
|
+
const runtimes = technologies.filter(t => t.category === "runtime");
|
|
311
|
+
const backends = technologies.filter(t => t.category === "backend");
|
|
312
|
+
const frontends = technologies.filter(t => t.category === "frontend");
|
|
313
|
+
|
|
314
|
+
// Language mapping
|
|
315
|
+
const ecosystemToLanguage: Record<string, string> = {
|
|
316
|
+
"dotnet": "C#",
|
|
317
|
+
"node": "TypeScript/JavaScript",
|
|
318
|
+
"python": "Python",
|
|
319
|
+
"go": "Go",
|
|
320
|
+
"rust": "Rust",
|
|
321
|
+
"java": "Java",
|
|
322
|
+
"ruby": "Ruby",
|
|
323
|
+
"php": "PHP",
|
|
324
|
+
"flutter": "Dart",
|
|
325
|
+
"swift": "Swift",
|
|
326
|
+
"kotlin": "Kotlin",
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const primaryEcosystem = ecosystems[0] || "unknown";
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
language: ecosystemToLanguage[primaryEcosystem] || primaryEcosystem,
|
|
333
|
+
runtime: runtimes[0]?.name,
|
|
334
|
+
framework: backends[0]?.name || frontends[0]?.name,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============================================================
|
|
339
|
+
// LEGACY COMPATIBILITY ADAPTER
|
|
340
|
+
// ============================================================
|
|
341
|
+
|
|
342
|
+
export interface LegacyStackDetection {
|
|
343
|
+
frontend?: string;
|
|
344
|
+
backend?: string;
|
|
345
|
+
database?: string;
|
|
346
|
+
orm?: string;
|
|
347
|
+
styling?: string;
|
|
348
|
+
auth?: string;
|
|
349
|
+
testing?: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface LegacyStructureDetection {
|
|
353
|
+
components?: string;
|
|
354
|
+
services?: string;
|
|
355
|
+
schema?: string;
|
|
356
|
+
types?: string;
|
|
357
|
+
hooks?: string;
|
|
358
|
+
utils?: string;
|
|
359
|
+
api?: string;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function detectStackLegacy(cwd: string = process.cwd()): Promise<{
|
|
363
|
+
stack: LegacyStackDetection;
|
|
364
|
+
structure: LegacyStructureDetection;
|
|
365
|
+
}> {
|
|
366
|
+
const result = await detectUniversal(cwd);
|
|
367
|
+
|
|
368
|
+
// Convert to legacy format (single value per category)
|
|
369
|
+
const stack: LegacyStackDetection = {};
|
|
370
|
+
const structure: LegacyStructureDetection = {};
|
|
371
|
+
|
|
372
|
+
if (result.stack.frontend?.length) stack.frontend = result.stack.frontend[0];
|
|
373
|
+
if (result.stack.backend?.length) stack.backend = result.stack.backend[0];
|
|
374
|
+
if (result.stack.database?.length) stack.database = result.stack.database[0];
|
|
375
|
+
if (result.stack.orm?.length) stack.orm = result.stack.orm[0];
|
|
376
|
+
if (result.stack.styling?.length) stack.styling = result.stack.styling[0];
|
|
377
|
+
if (result.stack.auth?.length) stack.auth = result.stack.auth[0];
|
|
378
|
+
if (result.stack.testing?.length) stack.testing = result.stack.testing[0];
|
|
379
|
+
|
|
380
|
+
// Map structure
|
|
381
|
+
for (const [key, value] of Object.entries(result.structure)) {
|
|
382
|
+
if (key in structure || ["components", "services", "schema", "types", "hooks", "utils", "api"].includes(key)) {
|
|
383
|
+
(structure as any)[key] = value;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { stack, structure };
|
|
388
388
|
}
|