ctx-gen
Version:
AI-Enhanced Documentation Generator for Code Understanding
233 lines • 8.95 kB
JavaScript
import fs from 'fs-extra';
import { glob } from 'glob';
import { isDirectory } from 'path-type';
import chalk from 'chalk';
import path from 'path';
import { generateMetadata } from '../generators/metadataGenerator.js';
import { generateDiagrams } from '../generators/diagramGenerator.js';
import { generateBatchDocumentation } from '../generators/fileDocGenerator.js';
import { createIndex } from '../generators/indexGenerator.js';
import { generateMachineReadable } from '../generators/machineReadableGenerator.js';
import { updateGitIgnore, readGitignorePatterns, shouldExcludeFile } from '../utils/fileUtils.js';
/**
* Generate full documentation for a codebase
* @param options - Configuration options for doc generation
*/
export async function generateFullDocs(options) {
// Create output directory if it doesn't exist
await fs.ensureDir(options.docsDir);
// Initialize empty cache
const cache = {};
// Process files
const files = await getFilesToProcess(options);
// Generate file documentation
await generateDocumentation(files, options, cache);
// Generate metadata
await generateMetadata(options);
// Generate diagrams if requested
if (options.diagrams && options.diagrams.length > 0) {
await generateDiagrams(options);
}
// Generate machine-readable formats if requested
if (options.machineFormats) {
await generateMachineReadable(options);
}
// Create index file
await createIndex(options);
// Update cache file
await fs.writeJSON(options.cacheFile, cache, { spaces: 2 });
// Add docs directory to .gitignore if requested
if (options.autoIgnore) {
await updateGitIgnore(options.docsDir);
}
}
/**
* Update existing documentation
* @param options - Configuration options for doc update
*/
export async function updateDocs(options) {
// Check if cache file exists
let cache = {};
if (await fs.pathExists(options.cacheFile)) {
cache = await fs.readJSON(options.cacheFile);
}
// Get files to process
const files = await getFilesToProcess(options);
// Generate documentation for modified files
await generateDocumentation(files, options, cache);
// Regenerate metadata
await generateMetadata(options);
// Regenerate diagrams if requested
if (options.diagrams && options.diagrams.length > 0) {
await generateDiagrams(options);
}
// Regenerate machine-readable formats if requested
if (options.machineFormats) {
await generateMachineReadable(options);
}
// Update index file
await createIndex(options);
// Update cache file
await fs.writeJSON(options.cacheFile, cache, { spaces: 2 });
}
/**
* Get the list of files to process based on options
* @param options - Configuration options
* @returns Array of file paths to process
*/
async function getFilesToProcess(options) {
console.log(chalk.blue('Finding files to process...'));
const excludePatterns = options.exclude.split(',');
const languages = options.languages.split(',');
console.log(`Languages to analyze: ${chalk.cyan(languages.join(', '))}`);
// Get gitignore patterns if requested
const gitignorePatterns = options.respectGitignore ? await readGitignorePatterns() : [];
if (options.respectGitignore && gitignorePatterns.length > 0) {
console.log(`Found ${chalk.yellow(gitignorePatterns.length)} patterns in .gitignore`);
}
// Log excluded patterns
console.log(`Excluded patterns: ${chalk.yellow(excludePatterns.join(', '))}`);
// Map language names to file extensions
const extensionMap = {
typescript: ['ts', 'tsx'],
javascript: ['js', 'jsx', 'mjs', 'cjs'],
python: ['py'],
java: ['java'],
csharp: ['cs'],
cpp: ['cpp', 'cc', 'hpp', 'h'],
c: ['c', 'h'],
ruby: ['rb'],
php: ['php'],
go: ['go'],
rust: ['rs'],
swift: ['swift'],
kotlin: ['kt'],
scala: ['scala'],
};
// Create glob patterns for each language, handling both direct extensions and mapped languages
const patterns = [];
for (const lang of languages) {
const extensions = extensionMap[lang.trim().toLowerCase()];
if (extensions) {
// If we have a mapping for this language, use all its extensions
extensions.forEach(ext => patterns.push(`**/*.${ext}`));
}
else {
// Otherwise assume the language name is the extension
patterns.push(`**/*.${lang}`);
}
}
console.log(`Looking for file patterns: ${chalk.cyan(patterns.join(', '))}`);
// First, get all potential files without ignoring anything
const potentialFiles = await glob(patterns, {
cwd: process.cwd(),
absolute: true,
});
console.log(`Found ${chalk.green(potentialFiles.length)} potential files before applying exclusions`);
// Then manually filter out the excluded files
const files = [];
const ignoredFiles = [];
const ignoredFolders = new Set();
for (const file of potentialFiles) {
// Skip directories
if (await isDirectory(file)) {
continue;
}
// Check if file should be excluded
const relativePath = path.relative(process.cwd(), file);
// First check explicit exclude patterns
let excluded = false;
let _excludeReason = '';
for (const pattern of excludePatterns) {
if (relativePath.includes(pattern)) {
excluded = true;
_excludeReason = `matched exclude pattern: ${pattern}`;
// Track excluded folder
const parts = relativePath.split('/');
for (let i = 0; i < parts.length; i++) {
const folderPath = parts.slice(0, i + 1).join('/');
if (folderPath.includes(pattern)) {
ignoredFolders.add(folderPath);
break;
}
}
break;
}
}
// Then check gitignore patterns
if (!excluded && options.respectGitignore) {
for (const pattern of gitignorePatterns) {
if (shouldExcludeFile(file, [], [pattern])) {
excluded = true;
_excludeReason = `matched gitignore pattern: ${pattern}`;
break;
}
}
}
if (excluded) {
ignoredFiles.push(relativePath);
}
else {
files.push(file);
}
}
// Summarize ignored information
if (ignoredFiles.length > 0) {
console.log(`${chalk.yellow('Ignored')} ${chalk.bold(ignoredFiles.length)} files based on exclusion patterns`);
// Log first few ignored files as examples
const exampleCount = Math.min(5, ignoredFiles.length);
console.log(chalk.gray(`Examples of ignored files (showing ${exampleCount} of ${ignoredFiles.length}):`));
for (let i = 0; i < exampleCount; i++) {
console.log(chalk.gray(` - ${ignoredFiles[i]}`));
}
// Log ignored folders
if (ignoredFolders.size > 0) {
console.log(chalk.gray(`Ignored folders (${ignoredFolders.size}):`));
Array.from(ignoredFolders)
.slice(0, 5)
.forEach(folder => {
console.log(chalk.gray(` - ${folder}/`));
});
if (ignoredFolders.size > 5) {
console.log(chalk.gray(` ... and ${ignoredFolders.size - 5} more`));
}
}
}
console.log(`${chalk.green('Found')} ${chalk.bold(files.length)} files to process`);
return files;
}
/**
* Generate documentation for a list of files
* @param files - Array of file paths
* @param options - Configuration options
* @param cache - File cache object
*/
async function generateDocumentation(files, options, cache) {
// Filter files that need to be processed (not in cache or modified)
const filesToProcess = [];
for (const file of files) {
const stats = await fs.stat(file);
const modTime = stats.mtime.getTime();
// Skip if file hasn't been modified since last run
if (cache[file] && cache[file].modTime === modTime) {
continue;
}
filesToProcess.push(file);
}
if (filesToProcess.length === 0) {
console.log('No files need updating.');
return;
}
// Generate documentation for files in batches
await generateBatchDocumentation(filesToProcess, options);
// Update cache for processed files
for (const file of filesToProcess) {
const stats = await fs.stat(file);
const modTime = stats.mtime.getTime();
cache[file] = {
modTime,
path: file,
};
}
}
//# sourceMappingURL=ctxGen.js.map