UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

238 lines (237 loc) 10 kB
"use strict"; /** * Generate Scope Handler - Project Setup Tool * PRD #177 - Scope-based workflow refactoring * * Step 3 of workflow: Generate ALL files in a scope at once */ Object.defineProperty(exports, "__esModule", { value: true }); exports.handleGenerateScope = handleGenerateScope; const error_handling_1 = require("../../core/error-handling"); const generic_session_manager_1 = require("../../core/generic-session-manager"); const shared_prompt_loader_1 = require("../../core/shared-prompt-loader"); /** * Handle generateScope stage - Step 3 of project setup workflow * * Generates ALL files for a scope at once based on user answers */ async function handleGenerateScope(sessionId, scope, answers, logger, requestId) { return await error_handling_1.ErrorHandler.withErrorHandling(async () => { logger.debug('Starting scope generation', { requestId, sessionId, scope }); // Initialize session manager const sessionManager = new generic_session_manager_1.GenericSessionManager('proj'); // Load session const session = sessionManager.getSession(sessionId); if (!session) { return { success: false, error: { message: `Session ${sessionId} not found`, details: 'Please start a new session with step: "discover"' } }; } // Validate inputs if (!scope) { return { success: false, error: { message: 'scope is required for generateScope step', details: 'Provide the scope name (e.g., "github-community")' } }; } if (!answers) { return { success: false, error: { message: 'answers are required for generateScope step', details: 'Provide answers to the questions for this scope' } }; } // Validate session state if (!session.data.allScopes || !session.data.existingFiles) { return { success: false, error: { message: 'Invalid session state', details: 'Session does not contain required data' } }; } const scopeConfig = session.data.allScopes[scope]; if (!scopeConfig) { return { success: false, error: { message: `Invalid scope: ${scope}`, details: `Available scopes: ${Object.keys(session.data.allScopes).join(', ')}` } }; } // Get files to generate (excluding existing ones) const existingFiles = session.data.existingFiles; const baseFiles = scopeConfig.files.filter(file => !existingFiles.includes(file)); // Add conditional-only files (files that exist ONLY in conditionalFiles, not in main files array) const conditionalFiles = scopeConfig.conditionalFiles || {}; const conditionalOnlyFiles = Object.keys(conditionalFiles).filter(file => !scopeConfig.files.includes(file) && !existingFiles.includes(file)); // Combine base files and conditional-only files const filesToGenerate = [...baseFiles, ...conditionalOnlyFiles]; // Evaluate conditional files and generate content const generatedFiles = []; const excludedFiles = []; for (const fileName of filesToGenerate) { // Check if this file has conditional generation rules const conditionalRule = conditionalFiles[fileName]; if (conditionalRule) { const shouldGenerate = evaluateCondition(conditionalRule.condition, answers); if (!shouldGenerate) { logger.info('File excluded due to conditional rule', { requestId, fileName, scope, condition: conditionalRule.condition, reason: conditionalRule.reason }); excludedFiles.push(fileName); continue; } } // Preprocess answers for template (convert comma-separated strings to arrays) const processedAnswers = preprocessAnswers(answers); // Generate file content const content = generateFileContent(fileName, processedAnswers, logger); generatedFiles.push({ path: fileName, content, reason: conditionalRule ? conditionalRule.reason : undefined }); logger.info('File generated', { requestId, sessionId, fileName, scope, contentLength: content.length }); } // Update session session.data.currentStep = 'complete'; sessionManager.updateSession(sessionId, session.data); logger.info('Scope generation complete', { requestId, sessionId, scope, generatedCount: generatedFiles.length, excludedCount: excludedFiles.length }); // Process additionalInstructions template if present let additionalInstructions; if (scopeConfig.additionalInstructions) { additionalInstructions = replaceTemplateVariables(scopeConfig.additionalInstructions, answers); } return { success: true, sessionId, scope, files: generatedFiles, excludedFiles: excludedFiles.length > 0 ? excludedFiles : undefined, instructions: `Generated ${generatedFiles.length} file(s) for scope "${scope}".\n\n` + `Files:\n${generatedFiles.map(f => `- ${f.path}`).join('\n')}\n\n` + (excludedFiles.length > 0 ? `Excluded ${excludedFiles.length} file(s):\n${excludedFiles.map(f => `- ${f}`).join('\n')}\n\n` : '') + `Write these files to your repository using the Write tool.`, additionalInstructions }; }, { operation: 'project_setup_generate_scope', component: 'ProjectSetupTool', requestId }); } /** * Generate file content from template using Handlebars */ function generateFileContent(fileName, answers, logger) { try { // Load template using shared prompt loader with Handlebars support // Templates use .hbs extension (e.g., README.md -> README.md.hbs) const content = (0, shared_prompt_loader_1.loadPrompt)(fileName, answers, 'assets/project-setup/templates', '.hbs' // Add .hbs extension to template files ); return content; } catch (error) { logger.error('Failed to generate file content', error, { fileName }); return `# ${fileName}\n\nError: Could not generate content for this file.\nTemplate may be missing at: assets/project-setup/templates/${fileName}.hbs\n`; } } /** * Evaluate conditional file generation rule * * Supports conditions: * - "false" -> always false * - "true" -> always true * - "variableName === 'value'" -> check if answers[variableName] === 'value' * - "variableName === true" -> check if answers[variableName] is boolean true or truthy string * - OR conditions: "condition1 || condition2 || condition3" */ function evaluateCondition(condition, answers) { const trimmed = condition.trim(); // Handle literal boolean strings if (trimmed === 'false') return false; if (trimmed === 'true') return true; // Handle OR conditions: split by || and evaluate each part if (trimmed.includes('||')) { const conditions = trimmed.split('||').map(c => c.trim()); return conditions.some(cond => evaluateCondition(cond, answers)); } // Handle equality checks with boolean: "variableName === true" const booleanMatch = trimmed.match(/^(\w+)\s*===\s*(true|false)$/); if (booleanMatch) { const [, variableName, expectedValue] = booleanMatch; const actualValue = answers[variableName]; // Check for truthy values: boolean true, string "yes", string "true" if (expectedValue === 'true') { return actualValue === true || actualValue === 'true' || actualValue === 'yes'; } // Check for falsy values return actualValue === false || actualValue === 'false' || actualValue === 'no'; } // Handle equality checks with strings: "variableName === 'value'" const stringMatch = trimmed.match(/^(\w+)\s*===\s*['"]([^'"]+)['"]$/); if (stringMatch) { const [, variableName, expectedValue] = stringMatch; return answers[variableName] === expectedValue; } // Unknown condition format - default to false for safety return false; } /** * Preprocess answers for Handlebars templates * Converts comma-separated strings to arrays where needed */ function preprocessAnswers(answers) { const processed = { ...answers }; // Convert maintainerUsernames from comma-separated string to array if (processed.maintainerUsernames && typeof processed.maintainerUsernames === 'string') { processed.maintainerUsernames = processed.maintainerUsernames .split(',') .map((username) => username.trim()) .filter((username) => username.length > 0); } return processed; } /** * Replace template variables in additionalInstructions * Simple replacement for {{variableName}} patterns */ function replaceTemplateVariables(template, answers) { let result = template; // Replace all {{variableName}} with actual values for (const [key, value] of Object.entries(answers)) { const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'g'); result = result.replace(pattern, String(value || '')); } return result; }