@codexa/cli 8.5.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.
@@ -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
  }