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
JavaScript
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;
}
}