Add analyzers, generators, and templates
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-01 01:02:30 +00:00
parent 343e064e50
commit 9e6d2af238

View File

@@ -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, (data: TemplateData) => 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<boolean> {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
}