@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
JavaScript
;
/**
* 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;
}