UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

782 lines (758 loc) • 40.1 kB
import fs from 'fs-extra'; import path from 'path'; import { fileURLToPath } from 'url'; import yaml from 'js-yaml'; import logger from '../../logger.js'; import { fileStructureItemSchema } from './schema.js'; import { AppError, ParsingError, ConfigurationError, ToolExecutionError } from '../../utils/errors.js'; import { performFormatAwareLlmCallWithCentralizedConfig } from '../../utils/llmHelper.js'; import { performTemplateGenerationCall } from '../../utils/schemaAwareLlmHelper.js'; import { dynamicTemplateSchema, validateDynamicTemplateWithErrors } from './schemas/moduleSelection.js'; import { z } from 'zod'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const parsedYamlModuleSchema = z.object({ moduleName: z.string().min(1), description: z.string().optional(), type: z.string().optional(), placeholders: z.array(z.string()).optional(), provides: z.object({ techStack: z.record(z.object({ name: z.string(), version: z.string().optional(), rationale: z.string(), })).optional(), directoryStructure: z.array(fileStructureItemSchema).optional(), dependencies: z.object({ npm: z.object({ root: z.object({ dependencies: z.record(z.string()).optional(), devDependencies: z.record(z.string()).optional(), }).optional(), }).catchall(z.object({ dependencies: z.record(z.string()).optional(), devDependencies: z.record(z.string()).optional(), })).optional(), }).optional(), setupCommands: z.array(z.object({ context: z.string().optional(), command: z.string(), })).optional(), }), }); export function validateSetupCommandsFormat(setupCommands) { const errors = []; if (!Array.isArray(setupCommands)) { errors.push("setupCommands must be an array"); return { isValid: false, errors }; } setupCommands.forEach((cmd, index) => { if (typeof cmd === 'string') { errors.push(`setupCommands[${index}] is a string, expected object with 'command' field`); } else if (typeof cmd !== 'object' || cmd === null) { errors.push(`setupCommands[${index}] must be an object`); } else { if (!cmd.command || typeof cmd.command !== 'string') { errors.push(`setupCommands[${index}] missing required 'command' field (string)`); } if (cmd.context !== undefined && typeof cmd.context !== 'string') { errors.push(`setupCommands[${index}] 'context' field must be string or undefined`); } } }); return { isValid: errors.length === 0, errors }; } export function generateSetupCommandsErrorContext(setupCommands, modulePathSegment) { const validation = validateSetupCommandsFormat(setupCommands); if (validation.isValid) { return "setupCommands format is valid"; } const errorContext = [ `SetupCommands validation failed for ${modulePathSegment}:`, ...validation.errors.map(err => ` - ${err}`), "", "Expected format:", ' "setupCommands": [', ' {"command": "npm install", "context": "root"},', ' {"command": "npm test"}', ' ]', "", `Received: ${JSON.stringify(setupCommands, null, 2)}` ].join('\n'); return errorContext; } export class YAMLComposer { baseTemplatePath; config; generatedTemplateCache = new Map(); templateAliases = new Map([ ['database/postgres', 'database/postgresql'], ['database/postgresql', 'database/postgres'], ['auth/authentication', 'auth/jwt'], ['auth/jwt', 'auth/authentication'], ]); constructor(config, baseTemplatePath = path.join(__dirname, 'templates')) { this.baseTemplatePath = baseTemplatePath; this.config = config; } async generateTemplateWithLLM(category, technology, modulePathSegment) { const systemPrompt = `You are an expert YAML template generator for a full-stack starter kit. Your task is to generate a JSON object that represents the structure of a YAML module file. This JSON object must conform to the ParsedYamlModule TypeScript interface structure provided below. The generated module is for: Category '${category}', Technology '${technology}'. The module path segment is '${modulePathSegment}'. === CRITICAL JSON FORMATTING REQUIREMENTS === 🚨 RESPOND WITH ONLY A VALID JSON OBJECT - NO MARKDOWN, NO CODE BLOCKS, NO EXPLANATIONS, NO SURROUNDING TEXT 🚨 THE RESPONSE MUST BE A JSON OBJECT (starting with { and ending with }), NEVER AN ARRAY 🚨 DO NOT RETURN ARRAYS OF STRINGS - RETURN A COMPLETE OBJECT STRUCTURE 🚨 FAILURE TO FOLLOW THESE RULES WILL CAUSE SYSTEM FAILURE JSON SYNTAX RULES: 1. Use double quotes for ALL strings and property names (never single quotes) 2. Escape ALL special characters in string values: - Newlines: \\n (not actual line breaks) - Tabs: \\t - Backslashes: \\\\ - Double quotes: \\" - Carriage returns: \\r 3. For multi-line code content, use \\n for line breaks within the string 4. Ensure all braces {} and brackets [] are properly closed and balanced 5. Do NOT include trailing commas after the last property/element 6. Do NOT include comments (// or /* */) 7. Do NOT wrap response in markdown code blocks like \`\`\`json 8. Do NOT include any text before or after the JSON object EDGE CASE HANDLING: - File paths: Use forward slashes / (never backslashes \\) - Empty arrays: Use [] (not null) - Empty objects: Use {} (not null) - Boolean values: Use true/false (not "true"/"false") - Numbers: Use numeric values (not strings) for ports, versions when numeric - Null values: Use null (not "null", undefined, or empty string) - Unicode characters: Escape as \\uXXXX if problematic - Control characters (\\x00-\\x1F): Must be escaped as \\uXXXX - Large code blocks: Keep as single string with \\n separators CONTENT STRING FORMATTING EXAMPLES: āŒ WRONG: "content": "console.log('Hello'); console.log('World');" āœ… CORRECT: "content": "console.log('Hello');\\nconsole.log('World');" āŒ WRONG: "content": "const path = "src\\\\components"" āœ… CORRECT: "content": "const path = \\"src/components\\"" JSON Structure to follow: { "moduleName": "string (e.g., ${technology}-${category})", "description": "string (e.g., ${technology} ${category} module for {projectName})", "type": "string (e.g., ${category})", "placeholders": ["string"], // Optional: e.g., ["projectName", "portNumber"] "provides": { "techStack": { // Optional "uniqueKeyPerStackItem": { "name": "string", "version": "string (optional)", "rationale": "string" } }, "directoryStructure": [ // Optional: Array of FileStructureItem-like objects. Paths are relative to module root. // Example: { "path": "src/index.js", "type": "file", "content": "console.log('Hello {projectName}');", "generationPrompt": null }, // Example: { "path": "src/components/", "type": "directory", "content": null, "children": [] } ], "dependencies": { // Optional "npm": { // e.g., "{frontendPath}": { "dependencies": {"react": "^18.0.0"} } } }, "setupCommands": [ // Optional // { "context": "{${category}Path}", "command": "npm install" } ] } } CRITICAL SCHEMA REQUIREMENTS: - Generate ONLY the raw JSON object. Do NOT use Markdown, code blocks, or any surrounding text. - The response MUST be a complete object with moduleName, description, type, and provides fields. - NEVER return just an array of strings - always return the full object structure. - Ensure all paths in 'directoryStructure' are relative to the module's own root. - For 'directoryStructure' items: * Files (type: "file") MUST have "content" as a string OR "generationPrompt" as a string, but NOT both * Directories (type: "directory") MUST have "content": null and MAY have "children" array * ALL items MUST include the "content" field (string for files, null for directories) - If 'content' for a file is provided, 'generationPrompt' should be null/undefined, and vice-versa. - Use common placeholders like {projectName}, {backendPort}, {frontendPort}, {frontendPath}, {backendPath} where appropriate. EXAMPLE STRUCTURE (for reference - adapt for your specific technology): { "moduleName": "example-module", "description": "Example module description", "type": "database", "placeholders": ["projectName", "dbPort"], "provides": { "techStack": { "technology": { "name": "Technology Name", "version": "latest", "rationale": "Why this technology" } }, "directoryStructure": [ { "path": "config.yml", "type": "file", "content": "example: {projectName}", "generationPrompt": null } ], "dependencies": {}, "setupCommands": [ { "context": "root", "command": "setup command" } ] } } - Be comprehensive but sensible for a starter module of type '${category}' using '${technology}'. - Example: "dependencies": { "npm": { "{frontendPath}": { "dependencies": {"react": "^18.0.0"} } } } - If the module is self-contained, dependencies might be under "root": "dependencies": { "npm": { "root": { "devDependencies": {"husky": "^8.0.0"} } } } VALIDATION CHECKLIST BEFORE RESPONDING: āœ“ JSON object starts with { and ends with } āœ“ All strings use double quotes āœ“ All special characters are properly escaped āœ“ No trailing commas āœ“ No markdown code blocks or surrounding text āœ“ All required fields present (moduleName, description, type, provides) āœ“ Directory items have content: null āœ“ File items have content as string OR generationPrompt as string āœ“ Paths use forward slashes āœ“ Multi-line content uses \\n separators Generate the JSON for '${modulePathSegment}':`; const userPrompt = `Generate the JSON representation for a YAML module. Category: ${category} Technology: ${technology} Module Path Segment: ${modulePathSegment} Consider typical files, dependencies, and configurations for this type of module. For example, if it's a 'nodejs-express' backend, include basic Express setup, a sample route, package.json, tsconfig.json. If it's a 'react-vite' frontend, include basic React/Vite setup, sample components, package.json, vite.config.ts. Provide a sensible set of placeholders if needed (e.g. "{projectName}", "{backendPort}"). Ensure the output is a single, raw JSON object without any other text or formatting.`; try { logger.info(`Requesting LLM to generate template for: ${modulePathSegment}`); const rawResponse = await performFormatAwareLlmCallWithCentralizedConfig(userPrompt, systemPrompt, 'fullstack_starter_kit_dynamic_yaml_module_generation', 'json', undefined, 0.2); logger.debug({ modulePathSegment, rawResponseFromLLM: rawResponse }, "Raw LLM response for dynamic template"); return rawResponse; } catch (error) { logger.error({ err: error, modulePathSegment }, `LLM call failed during dynamic template generation for ${modulePathSegment}`); throw new ToolExecutionError(`LLM failed to generate template for ${modulePathSegment}: ${error.message}`, undefined, error instanceof Error ? error : undefined); } } buildTemplateGenerationPrompt(category, technology, modulePathSegment, researchContext = '') { const researchSection = researchContext ? ` Research Context (use this to make informed decisions): ${researchContext} Based on the research above, ensure your template incorporates the latest best practices, recommended technologies, and architectural patterns mentioned in the research.` : ''; return ` You are an expert Full-Stack Software Architect AI. Generate a YAML module template for ${technology} in the ${category} category. Module Path: ${modulePathSegment} Technology: ${technology} Category: ${category}${researchSection} Generate a complete module template that follows this exact structure. Respond with ONLY the JSON object - no markdown, no explanations: { "moduleName": "string (unique identifier for this module)", "description": "string (brief description of what this module provides)", "type": "string (one of: frontend, backend, database, fullstack, utility)", "placeholders": ["array of placeholder variables used in this template"], "provides": { "techStack": { "componentName": { "name": "Technology Name", "version": "^1.0.0", "rationale": "Why this technology was chosen" } }, "directoryStructure": [ { "path": "relative/path", "type": "file or directory", "content": "file content or null for directories", "children": [] } ], "dependencies": { "npm": { "root": { "dependencies": {"package": "version"}, "devDependencies": {"package": "version"} } } }, "setupCommands": [ { "command": "command to run", "context": "directory context" } ], "nextSteps": ["array of recommended next steps"] } } CRITICAL FORMAT REQUIREMENTS: - setupCommands MUST be an array of objects, NOT strings - Each setupCommand object MUST have a "command" field (string) - Each setupCommand object MAY have a "context" field (string, optional) - NEVER use string arrays for setupCommands INVALID EXAMPLES (DO NOT USE): āŒ "setupCommands": ["npm install", "npm test"] āŒ "setupCommands": [{"cmd": "npm install"}] āŒ "setupCommands": [{"command": "npm install", "context": null}] VALID EXAMPLES: āœ… "setupCommands": [{"command": "npm install", "context": "root"}] āœ… "setupCommands": [{"command": "npm test"}] āœ… "setupCommands": [] VALIDATION CHECKLIST BEFORE RESPONDING: āœ“ JSON object starts with { and ends with } āœ“ All strings use double quotes āœ“ All special characters are properly escaped āœ“ No trailing commas āœ“ No markdown code blocks or surrounding text āœ“ All required fields present (moduleName, description, type, provides) āœ“ Directory items have content: null āœ“ File items have content as string OR generationPrompt as string āœ“ Paths use forward slashes āœ“ Multi-line content uses \\n separators āœ“ setupCommands is array of objects with "command" field (NOT strings) āœ“ Each setupCommand object has required "command" field āœ“ Optional "context" field in setupCommands is string type Requirements: 1. Include realistic dependencies for ${technology} 2. Create a proper directory structure 3. Add appropriate setup commands as objects with "command" field 4. Use placeholders like {projectName}, {backendPort} where needed 5. Ensure all required fields are present and properly typed 6. Follow the setupCommands object format strictly`; } async generateDynamicTemplate(modulePathSegment, researchContext = '') { logger.info(`Attempting to dynamically generate YAML module: ${modulePathSegment}`); const parts = modulePathSegment.split('/'); const technology = parts.pop() || modulePathSegment; const category = parts.join('/') || 'general'; let parsedJson; let usedSchemaAware = false; try { logger.debug({ modulePathSegment }, 'Attempting schema-aware template generation...'); const templatePrompt = this.buildTemplateGenerationPrompt(category, technology, modulePathSegment, researchContext); const schemaAwareResult = await performTemplateGenerationCall(templatePrompt, '', this.config, dynamicTemplateSchema); parsedJson = schemaAwareResult.data; usedSchemaAware = true; logger.info({ modulePathSegment, attempts: schemaAwareResult.attempts, hadRetries: schemaAwareResult.hadRetries, processingTimeMs: schemaAwareResult.processingTimeMs, responseLength: schemaAwareResult.rawResponse.length }, 'Schema-aware template generation successful'); } catch (schemaError) { logger.warn({ modulePathSegment, error: schemaError instanceof Error ? schemaError.message : String(schemaError) }, 'Schema-aware template generation failed, falling back to existing method'); const llmResponse = await this.generateTemplateWithLLM(category, technology, modulePathSegment); try { const { intelligentJsonParse } = await import('../../utils/llmHelper.js'); const parsed = intelligentJsonParse(llmResponse, `dynamic-gen-${modulePathSegment}`); if (Array.isArray(parsed)) { logger.error({ modulePathSegment, parsedResponse: parsed, responsePreview: llmResponse.substring(0, 200) }, `LLM returned an array instead of object for ${modulePathSegment}`); if (parsed.every(item => typeof item === 'string')) { logger.warn({ modulePathSegment, placeholders: parsed }, `LLM returned placeholder array instead of full object. Attempting to construct minimal object.`); const minimalObject = { moduleName: `${modulePathSegment.replace('/', '-')}`, description: `${modulePathSegment} module for the project`, type: modulePathSegment.includes('/') ? modulePathSegment.split('/')[0] : 'utility', placeholders: parsed, provides: { techStack: {}, directoryStructure: [], dependencies: { npm: {} }, setupCommands: [], nextSteps: [] } }; logger.info({ modulePathSegment, constructedObject: minimalObject }, `Constructed minimal object from placeholder array`); parsedJson = minimalObject; } else { throw new ParsingError(`LLM returned an array instead of expected object structure for ${modulePathSegment}. Got: ${JSON.stringify(parsed)}`, { originalResponse: llmResponse, parsedResponse: parsed }); } } else if (typeof parsed !== 'object' || parsed === null) { logger.error({ modulePathSegment, parsedResponse: parsed, responsePreview: llmResponse.substring(0, 200) }, `LLM returned invalid type for ${modulePathSegment}`); throw new ParsingError(`LLM returned invalid type (expected object) for ${modulePathSegment}. Got: ${typeof parsed}`, { originalResponse: llmResponse, parsedResponse: parsed }); } else { parsedJson = parsed; } if (parsedJson) { const validation = validateDynamicTemplateWithErrors(parsedJson); if (!validation.success) { logger.warn({ modulePathSegment, validationErrors: validation.errors }, 'Fallback template validation failed, proceeding with preprocessing'); } } logger.debug({ modulePathSegment, responseLength: llmResponse.length, parsedSize: JSON.stringify(parsedJson).length, usedSchemaAware }, "Fallback parsing successful"); } catch (error) { logger.error({ err: error, modulePathSegment, responsePreview: llmResponse.substring(0, 200) }, `Failed to parse LLM JSON response for ${modulePathSegment} using intelligent parsing`); throw new ParsingError(`Failed to parse dynamically generated template for ${modulePathSegment} as JSON using intelligent parsing. Response preview: ${llmResponse.substring(0, 200)}`, { originalResponse: llmResponse }, error instanceof Error ? error : undefined); } } const preprocessedJson = this.preprocessTemplateForValidation(parsedJson, modulePathSegment); const validationResult = parsedYamlModuleSchema.safeParse(preprocessedJson); if (!validationResult.success) { logger.error({ err: validationResult.error.issues, modulePathSegment, parsedJson: preprocessedJson }, `Dynamically generated template for ${modulePathSegment} failed Zod validation after preprocessing.`); throw new ParsingError(`Dynamically generated template for ${modulePathSegment} failed validation: ${validationResult.error.message}`, { issues: validationResult.error.issues, parsedJson: preprocessedJson }); } const validatedModule = validationResult.data; validatedModule._sourcePath = path.resolve(this.baseTemplatePath, `${modulePathSegment}.yaml`); try { const yamlContent = yaml.dump(validatedModule); await fs.ensureDir(path.dirname(validatedModule._sourcePath)); await fs.writeFile(validatedModule._sourcePath, yamlContent, 'utf-8'); logger.info(`Successfully saved dynamically generated YAML template to: ${validatedModule._sourcePath}`); } catch (error) { logger.warn({ err: error, modulePathSegment, filePath: validatedModule._sourcePath }, `Failed to save dynamically generated template for ${modulePathSegment}. Proceeding with in-memory version.`); } this.generatedTemplateCache.set(modulePathSegment, validatedModule); logger.info(`Dynamically generated and cached YAML module: ${modulePathSegment}`); return validatedModule; } async loadAndParseYamlModule(modulePathSegment, researchContext = '') { if (this.generatedTemplateCache.has(modulePathSegment)) { logger.debug(`Returning cached YAML module for: ${modulePathSegment}`); return this.generatedTemplateCache.get(modulePathSegment); } let fullPath = path.resolve(this.baseTemplatePath, `${modulePathSegment}.yaml`); logger.debug(`Attempting to load YAML module from: ${fullPath}`); if (!(await fs.pathExists(fullPath)) && this.templateAliases.has(modulePathSegment)) { const aliasPath = this.templateAliases.get(modulePathSegment); const aliasFullPath = path.resolve(this.baseTemplatePath, `${aliasPath}.yaml`); logger.debug(`Original template not found, trying alias: ${aliasFullPath}`); if (await fs.pathExists(aliasFullPath)) { fullPath = aliasFullPath; logger.info(`Using template alias: ${modulePathSegment} -> ${aliasPath}`); } } if (await fs.pathExists(fullPath)) { logger.info(`Found existing YAML module: ${fullPath}`); try { const fileContent = await fs.readFile(fullPath, 'utf-8'); const parsed = yaml.load(fileContent); const validationResult = parsedYamlModuleSchema.safeParse(parsed); if (!validationResult.success) { logger.error({ errors: validationResult.error.issues, filePath: fullPath, parsedContent: parsed }, "Loaded YAML module failed schema validation"); throw new ParsingError(`Invalid YAML module structure in ${fullPath}. Validation failed: ${validationResult.error.message}`, { filePath: fullPath, issues: validationResult.error.issues }); } const validatedModule = validationResult.data; validatedModule._sourcePath = fullPath; this.generatedTemplateCache.set(modulePathSegment, validatedModule); logger.debug(`Loaded and cached YAML module from disk: ${modulePathSegment}`); return validatedModule; } catch (error) { if (error instanceof AppError) throw error; const cause = error instanceof Error ? error : undefined; logger.error({ err: error, filePath: fullPath }, `Failed to load or parse existing YAML module ${modulePathSegment}`); throw new ParsingError(`Failed to load or parse YAML module ${modulePathSegment} from ${fullPath}: ${cause?.message}`, { filePath: fullPath }, cause); } } else { logger.warn(`YAML module template not found on disk: ${fullPath}. Attempting dynamic generation.`); try { return await this.generateDynamicTemplate(modulePathSegment, researchContext); } catch (generationError) { logger.error({ err: generationError, modulePathSegment, filePath: fullPath }, `Dynamic generation failed for YAML module ${modulePathSegment}.`); throw new ConfigurationError(`YAML module template not found at '${fullPath}' and dynamic generation failed: ${generationError.message}`, { modulePathSegment, originalError: generationError }); } } } preprocessTemplateForValidation(parsedJson, modulePathSegment) { logger.debug({ modulePathSegment }, "Preprocessing template for schema validation"); const processed = JSON.parse(JSON.stringify(parsedJson)); if (processed.provides && processed.provides.directoryStructure && Array.isArray(processed.provides.directoryStructure)) { this.fixDirectoryStructureItems(processed.provides.directoryStructure, modulePathSegment); } if (processed.provides && processed.provides.setupCommands && Array.isArray(processed.provides.setupCommands)) { const originalCommands = [...processed.provides.setupCommands]; this.fixSetupCommandsFormat(processed.provides.setupCommands, modulePathSegment); this.trackSetupCommandsPreprocessing(modulePathSegment, originalCommands, processed.provides.setupCommands); } return processed; } fixDirectoryStructureItems(items, modulePathSegment) { for (const item of items) { if (typeof item === 'object' && item !== null) { if (item.type === 'file') { const hasContent = 'content' in item && item.content !== null && item.content !== undefined; const hasGenerationPrompt = 'generationPrompt' in item && item.generationPrompt !== null && item.generationPrompt !== undefined; if (hasContent && hasGenerationPrompt) { item.content = null; logger.debug({ modulePathSegment, path: item.path }, "Resolved content/generationPrompt conflict by prioritizing generationPrompt"); } else if (!hasContent && !hasGenerationPrompt) { item.content = ''; item.generationPrompt = null; logger.debug({ modulePathSegment, path: item.path }, "Added missing empty content for file"); } else if (hasGenerationPrompt && !('content' in item)) { item.content = null; logger.debug({ modulePathSegment, path: item.path }, "Added missing content: null for file with generationPrompt"); } else if (hasContent && !('generationPrompt' in item)) { item.generationPrompt = null; logger.debug({ modulePathSegment, path: item.path }, "Added missing generationPrompt: null for file with content"); } } else if (item.type === 'directory') { if (!('content' in item)) { item.content = null; logger.debug({ modulePathSegment, path: item.path }, "Added missing content: null for directory"); } else if (item.content !== null) { item.content = null; logger.debug({ modulePathSegment, path: item.path }, "Fixed non-null content for directory"); } } if (item.children && Array.isArray(item.children)) { this.fixDirectoryStructureItems(item.children, modulePathSegment); } } } } fixSetupCommandsFormat(setupCommands, modulePathSegment) { for (let i = 0; i < setupCommands.length; i++) { const cmd = setupCommands[i]; if (typeof cmd === 'string') { setupCommands[i] = { command: cmd, context: 'root' }; logger.debug({ modulePathSegment, originalCommand: cmd, convertedCommand: setupCommands[i] }, "Converted string setupCommand to object format"); } else if (typeof cmd === 'object' && cmd !== null) { if (!cmd.command || typeof cmd.command !== 'string') { logger.warn({ modulePathSegment, invalidCommand: cmd }, "Invalid setupCommand object missing 'command' field"); if (cmd.cmd && typeof cmd.cmd === 'string') { cmd.command = cmd.cmd; delete cmd.cmd; if (!cmd.context) { cmd.context = 'root'; } logger.debug({ modulePathSegment }, "Fixed 'cmd' -> 'command' field name"); } else { setupCommands.splice(i, 1); i--; logger.warn({ modulePathSegment }, "Removed invalid setupCommand object"); continue; } } if (cmd.context !== undefined && typeof cmd.context !== 'string') { cmd.context = String(cmd.context); logger.debug({ modulePathSegment }, "Converted context to string"); } } else { logger.warn({ modulePathSegment, invalidType: typeof cmd, command: cmd }, "Removing setupCommand with invalid type"); setupCommands.splice(i, 1); i--; } } } trackSetupCommandsPreprocessing(modulePathSegment, originalCommands, processedCommands) { const metrics = { modulePathSegment, originalCount: originalCommands.length, processedCount: processedCommands.length, conversions: 0, removals: 0, fixes: 0 }; originalCommands.forEach(original => { if (typeof original === 'string') { metrics.conversions++; } }); metrics.removals = originalCommands.length - processedCommands.length - metrics.conversions; if (metrics.removals < 0) metrics.removals = 0; originalCommands.forEach(original => { if (typeof original === 'object' && original !== null) { const obj = original; if (!obj.command && obj.cmd) { metrics.fixes++; } } }); if (metrics.conversions > 0 || metrics.removals > 0 || metrics.fixes > 0) { logger.info(metrics, "SetupCommands preprocessing completed with changes"); } else { logger.debug(metrics, "SetupCommands preprocessing completed with no changes"); } } substitutePlaceholders(data, params) { if (typeof data === 'string') { let result = data; for (const key in params) { const placeholder = `{${key}}`; result = result.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), String(params[key])); } return result; } if (Array.isArray(data)) { return data.map(item => this.substitutePlaceholders(item, params)); } if (typeof data === 'object' && data !== null) { const newData = { ...data }; for (const key in newData) { newData[key] = this.substitutePlaceholders(newData[key], params); } return newData; } return data; } mergeTechStacks(target, source) { if (source) { for (const key in source) { if (target[key] && target[key].name !== source[key].name) { logger.warn(`TechStack conflict for component '${key}'. Original: '${target[key].name}', New: '${source[key].name}'. Overwriting with new value from module: ${source[key].name}.`); } target[key] = { ...source[key] }; } } } mergeDirectoryStructures(target, sourceItems, moduleKey, moduleParams) { if (!sourceItems) return; const moduleRootPath = moduleParams[moduleKey] || '.'; const findOrCreateTargetDirectory = (pathSegments, currentLevel) => { if (pathSegments.length === 0) return currentLevel; const segment = pathSegments.shift(); let dir = currentLevel.find(i => i.path === segment && i.type === 'directory'); if (!dir) { dir = { path: segment, type: 'directory', content: null, children: [] }; currentLevel.push(dir); } if (!dir.children) dir.children = []; return dir.children; }; let baseTargetChildren = target; if (moduleRootPath && moduleRootPath !== '.') { const segments = moduleRootPath.split(path.posix.sep).filter(s => s); if (segments.length > 0) { baseTargetChildren = findOrCreateTargetDirectory(segments, target); } } sourceItems.forEach(sourceItem => { const processedSourceItem = this.substitutePlaceholders(sourceItem, moduleParams); const existingIndex = baseTargetChildren.findIndex(t => t.path === processedSourceItem.path); if (existingIndex !== -1) { const existingItem = baseTargetChildren[existingIndex]; if (existingItem.type === 'directory' && processedSourceItem.type === 'directory' && processedSourceItem.children) { logger.debug(`Merging children for directory: ${processedSourceItem.path} under ${moduleRootPath}`); if (!existingItem.children) existingItem.children = []; this.mergeDirectoryStructures(existingItem.children, processedSourceItem.children, '.', moduleParams); } else { logger.warn(`Directory structure conflict for path '${processedSourceItem.path}' under '${moduleRootPath}'. Overwriting with item from module.`); baseTargetChildren[existingIndex] = processedSourceItem; } } else { baseTargetChildren.push(processedSourceItem); } }); } mergeDependencies(target, source) { if (!source) return; if (!target.npm) target.npm = {}; if (source.npm) { for (const packageJsonKeyPlaceholder in source.npm) { const resolvedPackageJsonKey = this.substitutePlaceholders(packageJsonKeyPlaceholder, {}); const sourcePkgConfig = source.npm[packageJsonKeyPlaceholder]; if (!target.npm[resolvedPackageJsonKey]) { target.npm[resolvedPackageJsonKey] = {}; } const targetPkgConfig = target.npm[resolvedPackageJsonKey]; if (sourcePkgConfig.dependencies) { if (!targetPkgConfig.dependencies) targetPkgConfig.dependencies = {}; Object.assign(targetPkgConfig.dependencies, sourcePkgConfig.dependencies); } if (sourcePkgConfig.devDependencies) { if (!targetPkgConfig.devDependencies) targetPkgConfig.devDependencies = {}; Object.assign(targetPkgConfig.devDependencies, sourcePkgConfig.devDependencies); } } } } mergeSetupCommands(target, source, moduleParams) { if (source) { source.forEach(cmdObj => { let command = cmdObj.command; const resolvedContext = cmdObj.context ? this.substitutePlaceholders(cmdObj.context, moduleParams) : undefined; if (resolvedContext && resolvedContext !== '.') { command = `(cd ${resolvedContext} && ${command})`; } target.push(this.substitutePlaceholders(command, moduleParams)); }); } } async compose(moduleSelections, globalParams, researchContext = '') { const composedDefinition = { projectName: this.substitutePlaceholders(globalParams.projectName || 'my-new-project', globalParams), description: this.substitutePlaceholders(globalParams.projectDescription || 'A new project.', globalParams), techStack: {}, directoryStructure: [], dependencies: { npm: { root: { dependencies: {}, devDependencies: {} } } }, setupCommands: [], nextSteps: [], }; for (const selection of moduleSelections) { logger.info(`Processing YAML module: ${selection.modulePath} with params: ${JSON.stringify(selection.params)} and moduleKey: ${selection.moduleKey}`); const effectiveParams = { ...globalParams, ...selection.params }; const module = await this.loadAndParseYamlModule(selection.modulePath, researchContext); const processedModuleProvides = this.substitutePlaceholders(module.provides, effectiveParams); this.mergeTechStacks(composedDefinition.techStack, processedModuleProvides.techStack); this.mergeDirectoryStructures(composedDefinition.directoryStructure, processedModuleProvides.directoryStructure, selection.moduleKey || 'root', effectiveParams); this.mergeDependencies(composedDefinition.dependencies, this.substitutePlaceholders(processedModuleProvides.dependencies, effectiveParams)); this.mergeSetupCommands(composedDefinition.setupCommands, processedModuleProvides.setupCommands, effectiveParams); if (module.type === 'auth' && module.moduleName.includes('jwt')) { composedDefinition.nextSteps.push('Configure JWT secrets and token expiration settings.'); } } if (composedDefinition.nextSteps.length === 0) { composedDefinition.nextSteps.push("Review the generated project structure and files."); composedDefinition.nextSteps.push("Run package manager install commands (e.g., `npm install`) in relevant directories if not fully handled by setup commands."); composedDefinition.nextSteps.push("Configure environment variables (e.g., in .env files if created)."); composedDefinition.nextSteps.push("Consult individual module documentation or READMEs if available."); } logger.debug('Final composed definition (before final validation):', JSON.stringify(composedDefinition, null, 2)); return composedDefinition; } }