UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

320 lines (314 loc) 12.2 kB
import { promises as fs } from "node:fs"; import path from "node:path"; import { log } from "../util/logging.js"; import { getProvider } from "../providers/index.js"; import { loadConfig } from "../state/config.js"; import { estimateTokens } from "../util/costs.js"; // Generate documentation prompt based on code content function generateDocPrompt(code, request, existingDocs) { const { type, includeExamples, includeTypes, style } = request; let prompt = `Generate comprehensive documentation for this ${type}.\n\n`; if (existingDocs) { prompt += `**Existing Documentation:**\n\`\`\`\n${existingDocs}\n\`\`\`\n\n`; } prompt += `**Code:**\n\`\`\`\n${code}\n\`\`\`\n\n`; prompt += `Please generate documentation that includes:\n`; prompt += `1. **Overview**: Brief description of purpose and functionality\n`; prompt += `2. **Parameters/Arguments**: Detailed parameter descriptions with types\n`; prompt += `3. **Return Values**: What the function/module returns\n`; prompt += `4. **Usage**: How to use this code\n`; if (includeExamples) { prompt += `5. **Examples**: Practical code examples showing usage\n`; } if (includeTypes && type !== "readme") { prompt += `6. **Type Information**: TypeScript types, interfaces, or type annotations\n`; } prompt += `7. **Notes**: Any important considerations, edge cases, or caveats\n\n`; // Style-specific instructions switch (style) { case "jsdoc": prompt += `Format the documentation using JSDoc syntax with proper @param, @returns, @example tags.\n`; break; case "sphinx": prompt += `Format the documentation using reStructuredText syntax for Sphinx.\n`; break; case "markdown": prompt += `Format the documentation using clean Markdown with proper headings and code blocks.\n`; break; default: prompt += `Use clear, well-structured formatting appropriate for the codebase.\n`; } if (type === "readme") { prompt += `\nGenerate a complete README.md that includes:\n`; prompt += `- Project title and description\n`; prompt += `- Installation instructions\n`; prompt += `- Quick start guide\n`; prompt += `- API reference\n`; prompt += `- Contributing guidelines\n`; prompt += `- License information\n`; } prompt += `\nFocus on being concise but comprehensive. Write for developers who are unfamiliar with the code.`; return prompt; } // Analyze file and determine what documentation is needed async function analyzeFileForDocs(filePath) { const content = await fs.readFile(filePath, "utf8"); const ext = path.extname(filePath); const analysis = { hasExistingDocs: false, needsModuleDocs: true, needsFunctionDocs: [], needsClassDocs: [], language: getLanguageFromExtension(ext) }; // Check for existing documentation patterns const docPatterns = [ /\/\*\*([\s\S]*?)\*\//g, // JSDoc /"""([\s\S]*?)"""/g, // Python docstrings /''([\s\S]*?)''/g, // Python docstrings /\/\/\/ (.*)/g, // Dart/Swift doc comments /#\s+(.*)/g // General comments ]; for (const pattern of docPatterns) { if (pattern.test(content)) { analysis.hasExistingDocs = true; break; } } // Find functions that need documentation const functionPatterns = [ /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g, // JS/TS functions /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\(/g, // Arrow functions /def\s+(\w+)\s*\(/g, // Python functions /func\s+(\w+)\s*\(/g, // Go/Swift functions /fn\s+(\w+)\s*\(/g, // Rust functions /public\s+(?:static\s+)?(?:\w+\s+)*(\w+)\s*\(/g // Java methods ]; for (const pattern of functionPatterns) { let match; const patternCopy = new RegExp(pattern.source, pattern.flags); while ((match = patternCopy.exec(content)) !== null) { analysis.needsFunctionDocs.push(match[1]); } } // Find classes that need documentation const classPatterns = [ /(?:export\s+)?class\s+(\w+)/g, // JS/TS/Java classes /class\s+(\w+)(?:\([^)]*\))?:/g, // Python classes /struct\s+(\w+)/g, // Go/Rust structs /interface\s+(\w+)/g // Interfaces ]; for (const pattern of classPatterns) { let match; const patternCopy = new RegExp(pattern.source, pattern.flags); while ((match = patternCopy.exec(content)) !== null) { analysis.needsClassDocs.push(match[1]); } } return analysis; } // Get programming language from file extension function getLanguageFromExtension(ext) { const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.go': 'go', '.rs': 'rust', '.java': 'java', '.kt': 'kotlin', '.swift': 'swift', '.php': 'php', '.rb': 'ruby', '.cs': 'csharp', '.cpp': 'cpp', '.c': 'c', '.h': 'c' }; return languageMap[ext.toLowerCase()] || 'text'; } // Generate documentation for a specific file or module export async function generateDocumentation(repoPath, request) { try { const config = await loadConfig(); if (!config) { return { success: false, generatedDocs: "", filesToUpdate: [], error: "No configuration found" }; } const provider = getProvider(config.defaultProvider); const model = config.models[config.defaultProvider]?.chat; if (!model) { return { success: false, generatedDocs: "", filesToUpdate: [], error: `No model configured for ${config.defaultProvider}` }; } const targetPath = path.resolve(repoPath, request.targetPath); // Handle README generation differently if (request.type === "readme") { return generateReadmeDocumentation(repoPath, provider, model); } // Read the target file let fileContent; try { fileContent = await fs.readFile(targetPath, "utf8"); } catch (error) { return { success: false, generatedDocs: "", filesToUpdate: [], error: `Failed to read file: ${request.targetPath}` }; } // Check for existing documentation let existingDocs = ""; const analysis = await analyzeFileForDocs(targetPath); // Generate the documentation prompt const prompt = generateDocPrompt(fileContent, request, existingDocs); log.step("Generating docs", `for ${request.targetPath}...`); // Estimate cost before proceeding const inputTokens = estimateTokens(prompt, model); if (inputTokens > 8000) { log.warn("Large file detected - documentation may be truncated or expensive"); } // Generate documentation using AI const documentation = await provider.chat([ { role: "user", content: prompt } ], { model, temperature: 0.3 }); // Determine where to place the documentation const filesToUpdate = await determineFillesToUpdate(targetPath, documentation, request, analysis); return { success: true, generatedDocs: documentation, filesToUpdate, }; } catch (error) { log.warn("Documentation generation failed:", error); return { success: false, generatedDocs: "", filesToUpdate: [], error: error instanceof Error ? error.message : "Unknown error" }; } } // Generate README documentation by analyzing the entire project async function generateReadmeDocumentation(repoPath, provider, model) { try { // Collect project information let projectInfo = ""; // Try to read package.json try { const pkgPath = path.join(repoPath, "package.json"); const pkgContent = await fs.readFile(pkgPath, "utf8"); const pkg = JSON.parse(pkgContent); projectInfo += `Project: ${pkg.name || "Unnamed Project"}\n`; if (pkg.description) projectInfo += `Description: ${pkg.description}\n`; if (pkg.version) projectInfo += `Version: ${pkg.version}\n`; if (pkg.scripts) { projectInfo += `Available Scripts: ${Object.keys(pkg.scripts).join(", ")}\n`; } if (pkg.dependencies) { const deps = Object.keys(pkg.dependencies).slice(0, 10); projectInfo += `Main Dependencies: ${deps.join(", ")}\n`; } } catch { // No package.json or not Node.js project } // Scan for main entry files const entryFiles = ["index.js", "index.ts", "main.py", "main.go", "src/index.ts", "src/main.ts"]; let mainFileContent = ""; for (const entryFile of entryFiles) { try { const entryPath = path.join(repoPath, entryFile); const content = await fs.readFile(entryPath, "utf8"); mainFileContent = `Main file (${entryFile}):\n\`\`\`\n${content.substring(0, 1000)}\n\`\`\`\n\n`; break; } catch { continue; } } // Generate README prompt const readmePrompt = `Generate a comprehensive README.md for this project. **Project Information:** ${projectInfo} **Main Code Sample:** ${mainFileContent} Please create a README.md that includes: 1. **Project Title & Description** 2. **Installation Instructions** (appropriate for the project type) 3. **Quick Start Guide** with basic usage examples 4. **API Documentation** (if applicable) 5. **Configuration Options** (if any) 6. **Contributing Guidelines** 7. **License Information** Make it professional, clear, and helpful for both users and contributors. Use proper Markdown formatting with badges if appropriate.`; const readme = await provider.chat([ { role: "user", content: readmePrompt } ], { model, temperature: 0.3 }); return { success: true, generatedDocs: readme, filesToUpdate: [ { path: path.join(repoPath, "README.md"), content: readme, type: "create" } ] }; } catch (error) { return { success: false, generatedDocs: "", filesToUpdate: [], error: error instanceof Error ? error.message : "README generation failed" }; } } // Determine which files need to be updated with documentation async function determineFillesToUpdate(targetPath, documentation, request, analysis) { const filesToUpdate = []; if (request.style === "jsdoc" || request.style === "sphinx") { // For JSDoc/Sphinx, we need to inject into the source file const originalContent = await fs.readFile(targetPath, "utf8"); // Simple approach: add documentation at the top of the file const updatedContent = `/**\n * ${documentation.replace(/\n/g, '\n * ')}\n */\n\n${originalContent}`; filesToUpdate.push({ path: targetPath, content: updatedContent, type: "update" }); } else { // For markdown or separate docs, create a .md file const docPath = targetPath.replace(path.extname(targetPath), ".md"); filesToUpdate.push({ path: docPath, content: documentation, type: "create" }); } return filesToUpdate; }