This commit is contained in:
192
app/src/generators/contextGenerator.ts
Normal file
192
app/src/generators/contextGenerator.ts
Normal file
@@ -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<ConfigOptions>
|
||||||
|
): Promise<ContextOutput> {
|
||||||
|
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<ConfigOptions>
|
||||||
|
): Promise<string> {
|
||||||
|
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<ConfigOptions>
|
||||||
|
): Promise<string> {
|
||||||
|
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<string>();
|
||||||
|
const sourceDirectories = new Set<string>();
|
||||||
|
const testDirectories = new Set<string>();
|
||||||
|
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<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user