diff --git a/app/src/generators/contextGenerator.ts b/app/src/generators/contextGenerator.ts new file mode 100644 index 0000000..63a5153 --- /dev/null +++ b/app/src/generators/contextGenerator.ts @@ -0,0 +1,192 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import yaml from 'js-yaml'; +import { + ContextOutput, + TemplateContext, + OutputFormat, + TemplateType, + ConfigOptions +} from '../types'; +import { getProjectInfo } from '../analyzers/projectTypeDetector'; +import { analyzeDependencies } from '../analyzers/dependencyAnalyzer'; +import { extractConventions } from '../analyzers/conventionExtractor'; +import { getAllFiles } from '../utils/fileUtils'; +import { formatContextForAI } from '../templates/templateLoader'; +import { loadConfig } from '../config/configLoader'; + +const SOURCE_DIR_PATTERNS = ['src/', 'lib/', 'app/', 'packages/', 'internal/']; +const TEST_DIR_PATTERNS = ['test/', 'tests/', '__tests__/', 'spec/', 'specs/']; + +export async function generateContext( + directory: string, + options?: Partial +): Promise { + const config = loadConfig(undefined, directory); + const mergedOptions = { ...config, ...options }; + + const projectInfo = await getProjectInfo(directory); + const dependencyAnalysis = await analyzeDependencies(directory, projectInfo.type); + const conventions = await extractConventions(directory, projectInfo.type); + + const fileStructure = await analyzeFileStructure(directory, mergedOptions); + + const context: ContextOutput = { + version: '1.0.0', + generatedAt: new Date().toISOString(), + project: projectInfo, + dependencies: dependencyAnalysis, + conventions, + structure: fileStructure, + config: mergedOptions + }; + + return context; +} + +export async function generateFormattedContext( + directory: string, + format: OutputFormat = 'yaml', + _templateType: TemplateType = 'generic', + options?: Partial +): Promise { + const context = await generateContext(directory, options); + + if (format === 'yaml') { + return yaml.dump(context, { indent: 2, lineWidth: -1, noRefs: true }); + } + + return JSON.stringify(context, null, 2); +} + +export async function generateAIContext( + directory: string, + templateType: TemplateType = 'generic', + options?: Partial +): Promise { + const context = await generateContext(directory, options); + + const templateContext: TemplateContext = { + projectInfo: context.project, + dependencyAnalysis: context.dependencies, + conventions: context.conventions, + fileStructure: context.structure + }; + + return formatContextForAI(templateContext, templateType); +} + +export function saveContext( + output: string, + content: string, + format: OutputFormat +): boolean { + try { + if (output === '-' || output === 'stdout') { + console.log(content); + return true; + } + + let finalContent = content; + + if (format === 'yaml' && !output.endsWith('.yaml') && !output.endsWith('.yml')) { + finalContent = '# YAML format context\n' + content; + } + + fs.writeFileSync(output, finalContent, 'utf-8'); + return true; + } catch { + return false; + } +} + +async function analyzeFileStructure( + directory: string, + options: ConfigOptions +) { + const allFiles = getAllFiles(directory, 5); + + const directories = new Set(); + const sourceDirectories = new Set(); + const testDirectories = new Set(); + const keyFiles: string[] = []; + + for (const file of allFiles) { + if (file.endsWith('/')) { + const relativePath = path.relative(directory, file); + + if (relativePath) { + directories.add(relativePath); + + for (const pattern of SOURCE_DIR_PATTERNS) { + if (relativePath.startsWith(pattern)) { + sourceDirectories.add(relativePath); + break; + } + } + + for (const pattern of TEST_DIR_PATTERNS) { + if (relativePath.startsWith(pattern)) { + testDirectories.add(relativePath); + break; + } + } + } + } else { + const shouldInclude = options.includes.some(pattern => { + if (pattern.startsWith('**')) return true; + if (pattern.startsWith('*')) return pattern.endsWith(path.extname(file)); + return true; + }); + + const shouldExclude = options.excludes.some(pattern => { + if (pattern.endsWith('**')) { + return file.includes(pattern.slice(0, -3)); + } + return file.endsWith(pattern.replace('*', '')); + }); + + if (shouldInclude && !shouldExclude) { + const relativePath = path.relative(directory, file); + keyFiles.push(relativePath); + } + } + } + + const sortedKeyFiles = keyFiles + .sort((a, b) => { + const aDepth = a.split('/').length; + const bDepth = b.split('/').length; + + if (aDepth !== bDepth) return aDepth - bDepth; + return a.localeCompare(b); + }) + .slice(0, 50); + + return { + totalFiles: allFiles.filter(f => !f.endsWith('/')).length, + directories: Array.from(directories).sort(), + keyFiles: sortedKeyFiles, + sourceDirectories: Array.from(sourceDirectories).sort(), + testDirectories: Array.from(testDirectories).sort() + }; +} + +export async function validateContext(context: ContextOutput): Promise { + if (!context.version) { + console.error('Error: Context missing version'); + return false; + } + + if (!context.project) { + console.error('Error: Context missing project info'); + return false; + } + + if (!context.generatedAt) { + console.error('Error: Context missing generatedAt timestamp'); + return false; + } + + return true; +}