diff --git a/app/ai-context-generator-cli/src/templates/templateLoader.ts b/app/ai-context-generator-cli/src/templates/templateLoader.ts new file mode 100644 index 0000000..1ba0335 --- /dev/null +++ b/app/ai-context-generator-cli/src/templates/templateLoader.ts @@ -0,0 +1,238 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { TemplateData } from '../types'; + +export class TemplateLoader { + private templates: Map string>; + + constructor() { + this.templates = new Map(); + this.registerDefaultTemplates(); + } + + private registerDefaultTemplates(): void { + this.templates.set('default', this.renderDefaultTemplate.bind(this)); + this.templates.set('cursor', this.renderCursorTemplate.bind(this)); + this.templates.set('copilot', this.renderCopilotTemplate.bind(this)); + this.templates.set('generic', this.renderGenericTemplate.bind(this)); + } + + async loadTemplate(name: string): Promise<(data: TemplateData) => string> { + const builtInTemplates = ['default', 'cursor', 'copilot', 'generic']; + + if (builtInTemplates.includes(name)) { + return this.templates.get(name)!; + } + + const templatePath = path.resolve(name); + if (await this.fileExists(templatePath)) { + const content = await fs.promises.readFile(templatePath, 'utf-8'); + return this.compileTemplate(content); + } + + const customTemplateDir = path.join(process.cwd(), 'templates'); + const customTemplatePath = path.join(customTemplateDir, `${name}.template`); + if (await this.fileExists(customTemplatePath)) { + const content = await fs.promises.readFile(customTemplatePath, 'utf-8'); + return this.compileTemplate(content); + } + + throw new Error(`Template not found: ${name}`); + } + + compileTemplate(templateContent: string): (data: TemplateData) => string { + return (data: TemplateData): string => { + let result = templateContent; + + result = result.replace(/\{\{\s*project\.type\s*\}\}/g, + data.projectInfo.projectType.primaryLanguage); + result = result.replace(/\{\{\s*project\.languages\s*\}\}/g, + data.projectInfo.projectType.languages.join(', ')); + result = result.replace(/\{\{\s*project\.frameworks\s*\}\}/g, + data.projectInfo.projectType.frameworks.join(', ')); + result = result.replace(/\{\{\s*project\.fileCount\s*\}\}/g, + String(data.projectInfo.fileCount)); + result = result.replace(/\{\{\s*dependencies\.total\s*\}\}/g, + String(data.projectInfo.dependencies.total)); + result = result.replace(/\{\{\s*generatedAt\s*\}\}/g, + data.generatedAt); + + result = this.renderDependencies(result, data); + result = this.renderConventions(result, data); + result = this.renderFileList(result, data); + + return result; + }; + } + + private renderDefaultTemplate(data: TemplateData): string { + const jsonOutput = JSON.stringify({ + project: data.projectInfo, + files: data.files, + generatedAt: data.generatedAt, + }, null, 2); + + return `## AI Context + +\`\`\`json +${jsonOutput} +\`\`\` + +## Summary + +- **Language**: ${data.projectInfo.projectType.primaryLanguage} +- **Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None detected'} +- **Dependencies**: ${data.projectInfo.dependencies.total} +- **Files Analyzed**: ${data.projectInfo.fileCount} +`; + } + + private renderCursorTemplate(data: TemplateData): string { + const topDeps = data.projectInfo.dependencies.direct + .slice(0, 15) + .map(d => ` - ${d.name}@${d.version}`) + .join('\n'); + + return `## Project Context + +**Language**: ${data.projectInfo.projectType.primaryLanguage} +**Frameworks**: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'} +**Build Tools**: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'} + +### Dependencies +${topDeps || ' No dependencies detected'} + +### Conventions +${data.projectInfo.conventions ? ` +- **File Naming**: ${data.projectInfo.conventions.namingConvention.files} +- **Import Style**: ${data.projectInfo.conventions.importStyle.style} +- **Testing Framework**: ${data.projectInfo.conventions.testingFramework || 'None'} +- **Code Style**: + - Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType} + - Quotes: ${data.projectInfo.conventions.codeStyle.quoteStyle} +` : ' Not analyzed'} + +### Key Files +${data.files.slice(0, 10).map(f => `- \`${f.path}\``).join('\n')} +`; + } + + private renderCopilotTemplate(data: TemplateData): string { + const deps = data.projectInfo.dependencies.direct + .map(d => ` "${d.name}": "${d.version}"`) + .join(',\n'); + + return `/* Project Context */ +Language: ${data.projectInfo.projectType.primaryLanguage} +Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'} +Dependencies: ${data.projectInfo.dependencies.total} + +/* Dependencies */ +{ +${deps} +} + +/* Conventions */ +File Naming: ${data.projectInfo.conventions?.namingConvention.files || 'Unknown'} +Import Style: ${data.projectInfo.conventions?.importStyle.style || 'Unknown'} +Testing: ${data.projectInfo.conventions?.testingFramework || 'None'} +`; + } + + private renderGenericTemplate(data: TemplateData): string { + return `=== PROJECT CONTEXT === + +Project Type: ${data.projectInfo.projectType.primaryLanguage} +Languages: ${data.projectInfo.projectType.languages.join(', ')} +Frameworks: ${data.projectInfo.projectType.frameworks.join(', ') || 'None'} +Build Tools: ${data.projectInfo.projectType.buildTools.join(', ') || 'None'} + +=== DEPENDENCIES === +Total: ${data.projectInfo.dependencies.total} +Production: ${data.projectInfo.dependencies.direct.length} +Development: ${data.projectInfo.dependencies.dev.length} + +Top Dependencies: +${data.projectInfo.dependencies.direct.slice(0, 10).map(d => ` - ${d.name} (${d.version})`).join('\n')} + +=== CONVENTIONS === +${data.projectInfo.conventions ? ` +Naming: + Files: ${data.projectInfo.conventions.namingConvention.files} + Variables: ${data.projectInfo.conventions.namingConvention.variables} + Functions: ${data.projectInfo.conventions.namingConvention.functions} + Classes: ${data.projectInfo.conventions.namingConvention.classes} + +Import Style: ${data.projectInfo.conventions.importStyle.style} +${data.projectInfo.conventions.importStyle.aliasPrefix ? `Alias Prefix: ${data.projectInfo.conventions.importStyle.aliasPrefix}` : ''} +Testing Framework: ${data.projectInfo.conventions.testingFramework || 'None'} + +Code Style: + Indent: ${data.projectInfo.conventions.codeStyle.indentSize} ${data.projectInfo.conventions.codeStyle.indentType} + Line Endings: ${data.projectInfo.conventions.codeStyle.lineEndings} + Quote Style: ${data.projectInfo.conventions.codeStyle.quoteStyle} +` : ' Not analyzed'} + +=== FILES === +Total Files: ${data.projectInfo.fileCount} +${data.files.slice(0, 20).map(f => ` - ${f.path}`).join('\n')} +`; + } + + private renderDependencies( + template: string, + data: TemplateData + ): string { + const deps = data.projectInfo.dependencies.direct + .map(d => ` - ${d.name}@${d.version}`) + .join('\n'); + + return template + .replace(/\{\{\s*dependencies\s*\}\}/g, deps) + .replace(/\{\{\s*dependencies\.count\s*\}\}/g, + String(data.projectInfo.dependencies.total)); + } + + private renderConventions( + template: string, + data: TemplateData + ): string { + if (!data.projectInfo.conventions) { + return template; + } + + const conventions = data.projectInfo.conventions; + + return template + .replace(/\{\{\s*conventions\.naming\s*\}\}/g, + conventions.namingConvention.files) + .replace(/\{\{\s*conventions\.importStyle\s*\}\}/g, + conventions.importStyle.style) + .replace(/\{\{\s*conventions\.testing\s*\}\}/g, + conventions.testingFramework || 'None'); + } + + private renderFileList( + template: string, + data: TemplateData + ): string { + const fileList = data.files + .slice(0, 30) + .map(f => ` - ${f.path}`) + .join('\n'); + + return template + .replace(/\{\{\s*files\s*\}\}/g, fileList) + .replace(/\{\{\s*files\.count\s*\}\}/g, + String(data.projectInfo.fileCount)); + } + + private async fileExists(filePath: string): Promise { + try { + await fs.promises.access(filePath, fs.constants.F_OK); + return true; + } catch { + return false; + } + } +}