UNPKG

reppy

Version:

Let reppy generate documentation for the functions in your codebase.

1,390 lines (1,360 loc) 44.7 kB
#!/usr/bin/env node 'use strict'; "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // src/lib/parser.ts var import_tree_sitter = __toESM(require("tree-sitter"), 1); var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1); var import_tree_sitter_typescript = __toESM(require("tree-sitter-typescript"), 1); var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1); var import_tree_sitter_rust = __toESM(require("tree-sitter-rust"), 1); var import_tree_sitter_java = __toESM(require("tree-sitter-java"), 1); var import_tree_sitter_go = __toESM(require("tree-sitter-go"), 1); var import_fs = __toESM(require("fs"), 1); var import_path2 = __toESM(require("path"), 1); var import_glob = require("glob"); // node_modules/.pnpm/log-symbols@7.0.0/node_modules/log-symbols/symbols.js var symbols_exports = {}; __export(symbols_exports, { error: () => error, info: () => info, success: () => success, warning: () => warning }); // node_modules/.pnpm/yoctocolors@2.1.1/node_modules/yoctocolors/base.js var import_node_tty = __toESM(require("tty"), 1); var _a, _b, _c, _d; var hasColors = ((_d = (_c = (_b = (_a = import_node_tty.default) == null ? void 0 : _a.WriteStream) == null ? void 0 : _b.prototype) == null ? void 0 : _c.hasColors) == null ? void 0 : _d.call(_c)) ?? false; var format = (open, close) => { if (!hasColors) { return (input) => input; } const openCode = `\x1B[${open}m`; const closeCode = `\x1B[${close}m`; return (input) => { const string = input + ""; let index = string.indexOf(closeCode); if (index === -1) { return openCode + string + closeCode; } let result = openCode; let lastIndex = 0; while (index !== -1) { result += string.slice(lastIndex, index) + openCode; lastIndex = index + closeCode.length; index = string.indexOf(closeCode, lastIndex); } result += string.slice(lastIndex) + closeCode; return result; }; }; var reset = format(0, 0); var bold = format(1, 22); var dim = format(2, 22); var italic = format(3, 23); var underline = format(4, 24); var overline = format(53, 55); var inverse = format(7, 27); var hidden = format(8, 28); var strikethrough = format(9, 29); var black = format(30, 39); var red = format(31, 39); var green = format(32, 39); var yellow = format(33, 39); var blue = format(34, 39); var magenta = format(35, 39); var cyan = format(36, 39); var white = format(37, 39); var gray = format(90, 39); var bgBlack = format(40, 49); var bgRed = format(41, 49); var bgGreen = format(42, 49); var bgYellow = format(43, 49); var bgBlue = format(44, 49); var bgMagenta = format(45, 49); var bgCyan = format(46, 49); var bgWhite = format(47, 49); var bgGray = format(100, 49); var redBright = format(91, 39); var greenBright = format(92, 39); var yellowBright = format(93, 39); var blueBright = format(94, 39); var magentaBright = format(95, 39); var cyanBright = format(96, 39); var whiteBright = format(97, 39); var bgRedBright = format(101, 49); var bgGreenBright = format(102, 49); var bgYellowBright = format(103, 49); var bgBlueBright = format(104, 49); var bgMagentaBright = format(105, 49); var bgCyanBright = format(106, 49); var bgWhiteBright = format(107, 49); // node_modules/.pnpm/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js var import_node_process = __toESM(require("process"), 1); function isUnicodeSupported() { const { env } = import_node_process.default; const { TERM, TERM_PROGRAM } = env; if (import_node_process.default.platform !== "win32") { return TERM !== "linux"; } return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm"; } // node_modules/.pnpm/log-symbols@7.0.0/node_modules/log-symbols/symbols.js var _isUnicodeSupported = isUnicodeSupported(); var info = blue(_isUnicodeSupported ? "\u2139" : "i"); var success = green(_isUnicodeSupported ? "\u2714" : "\u221A"); var warning = yellow(_isUnicodeSupported ? "\u26A0" : "\u203C"); var error = red(_isUnicodeSupported ? "\u2716\uFE0F" : "\xD7"); // src/lib/parser.ts var import_picocolors = __toESM(require("picocolors"), 1); // src/lib/generateDocs.ts var import_listr2 = require("listr2"); var import_ai = require("ai"); var import_openai = require("@ai-sdk/openai"); var import_anthropic = require("@ai-sdk/anthropic"); var import_cohere = require("@ai-sdk/cohere"); var import_mistral = require("@ai-sdk/mistral"); var import_amazon_bedrock = require("@ai-sdk/amazon-bedrock"); var import_promises = __toESM(require("fs/promises"), 1); var import_dotenv = __toESM(require("dotenv"), 1); var import_path = __toESM(require("path"), 1); var import_groq = require("@ai-sdk/groq"); var import_azure = require("@ai-sdk/azure"); import_dotenv.default.config(); var DOCUMENTATION_FORMATS = { ts: { format: "JSDoc", example: `/** * Function description * @param {type} paramName - Parameter description * @returns {type} Return value description */` }, js: { format: "JSDoc", example: `/** * Function description * @param {type} paramName - Parameter description * @returns {type} Return value description */` }, java: { format: "Javadoc", example: `/** * Method description * @param paramName Parameter description * @return Return value description */` }, py: { format: "Docstring", example: `""" Function description Args: param_name (type): Parameter description Returns: type: Return value description """` }, rs: { format: "Rustdoc", example: `/// Function description /// /// # Arguments /// /// * \`param_name\` - Parameter description /// /// # Returns /// /// Return value description` }, go: { format: "GoDoc", example: `// FunctionName does something specific // // It takes some parameters and returns something else. // // Parameters: // - param1: description of param1 // - param2: description of param2 // // Returns: // description of return value` } }; var documentedFunctions = []; var getAiProvider = (options) => { switch (options.provider) { case "openai": return (0, import_openai.openai)(options.model); case "anthropic": return (0, import_anthropic.anthropic)(options.model); case "cohere": return (0, import_cohere.cohere)(options.model); case "mistral": return (0, import_mistral.mistral)(options.model); case "bedrock": return (0, import_amazon_bedrock.bedrock)(options.model); case "groq": return (0, import_groq.groq)(options.model); case "azure": return (0, import_azure.azure)(options.model); default: throw new Error(`Unsupported provider: ${options.provider}`); } }; async function generateDocs(undocumentedFunctions, options) { let task; let functionsToReturn = []; task = new import_listr2.Listr( undocumentedFunctions.sort((a, b) => b.startLine - a.startLine).map((func) => ({ title: `${func.name}`, task: async () => { var _a2; const fileExt = import_path.default.extname(func.filePath).slice(1); const langKey = fileExt.replace( "tsx", "ts" ); const docFormat = DOCUMENTATION_FORMATS[langKey]; const prompt = `You are a documentation generator. Given this ${getLanguageName( fileExt )} function, write a ${docFormat.format} comment that describes what it does, its parameters, and return value. IMPORTANT: 1. Respond ONLY with the documentation comment 2. Do NOT include any markdown formatting or code blocks 3. Follow this exact format: ${docFormat.example} Here's the function to document: ${func.sourceCode}`; try { if (options.debug) { console.log("Debug: Generating documentation with options:", { provider: options.provider, model: options.model, temperature: options.temperature }); } const { text: docComment } = await (0, import_ai.generateText)({ model: getAiProvider(options), temperature: options.temperature, prompt }); if (!docComment) throw new Error("No documentation generated"); const cleanedDoc = validateAndCleanResponse(docComment, langKey); functionsToReturn.push({ filePath: func.filePath, name: func.name, documentation: cleanedDoc }); const fileContent = await import_promises.default.readFile(func.filePath, "utf-8"); const lines = fileContent.split("\n"); if (langKey === "py") { let insertLine = func.startLine + 1; while (insertLine <= func.endLine && lines[insertLine].trim() === "") { insertLine++; } const defLine = lines[func.startLine]; const indentation = ((_a2 = defLine.match(/^\s*/)) == null ? void 0 : _a2[0]) || ""; const indentedDoc = cleanedDoc.split("\n").map((line) => indentation + " " + line).join("\n"); lines.splice(insertLine, 0, indentedDoc); } else { lines.splice(func.startLine, 0, cleanedDoc); } await import_promises.default.writeFile(func.filePath, lines.join("\n")); func.isDocumented = true; await (0, import_listr2.delay)(options["rate-limit"] ?? 0); } catch (error2) { throw new Error( `Failed to generate docs for ${func.name}: ${error2.message}` ); } } })), { concurrent: options.concurrent ?? false, rendererOptions: { collapseSubtasks: options.output === "minimal", collapseErrors: options.output === "minimal" } } ); try { await task.run(); } catch (e) { console.error(e); } return functionsToReturn; } function getLanguageName(ext) { const langMap = { ts: "TypeScript", tsx: "TypeScript", js: "JavaScript", jsx: "JavaScript", py: "Python", rs: "Rust", java: "Java", go: "Go" }; return langMap[ext] || ext; } function validateAndCleanResponse(response, langKey) { let cleaned = response.replace(/```[\w-]*\n?|\n```/g, "").trim(); switch (langKey) { case "ts": case "js": if (!cleaned.startsWith("/**") || !cleaned.endsWith("*/")) { throw new Error("Invalid JSDoc format"); } break; case "java": if (!cleaned.startsWith("/**") || !cleaned.endsWith("*/")) { throw new Error("Invalid Javadoc format"); } break; case "py": if (!cleaned.startsWith('"""') || !cleaned.endsWith('"""')) { throw new Error("Invalid Python docstring format"); } break; case "rs": if (!cleaned.startsWith("///")) { throw new Error("Invalid Rustdoc format"); } break; case "go": if (!cleaned.startsWith("//")) { throw new Error("Invalid GoDoc format"); } cleaned = cleaned.split("\n").map((line) => line.trim().startsWith("//") ? line : `// ${line}`).join("\n"); break; } return cleaned; } async function generateReadme(functions, options) { const task = new import_listr2.Listr( [ { title: "Analyzing codebase structure", task: (ctx) => { ctx.fileGroups = functions.reduce((acc, func) => { if (!acc[func.filePath]) { acc[func.filePath] = []; } acc[func.filePath].push(func); return acc; }, {}); ctx.fileSummaries = []; } }, { title: "Generating file summaries", task: (ctx, task2) => task2.newListr( Object.entries(ctx.fileGroups).map(([filePath, fileFunctions]) => ({ title: `Summarizing ${filePath}`, task: async () => { const filePrompt = `You are a technical documentation expert. Given these documented functions from the file ${filePath}, provide a brief summary of what this file's purpose is and how its functions work together. Functions in this file: ${fileFunctions.map( (f) => ` Function Name: ${f.name} Documentation: ${f.documentation} ` ).join("\n\n")}`; const { text: fileSummary } = await (0, import_ai.generateText)({ model: getAiProvider(options), temperature: 0.3, prompt: filePrompt }); ctx.fileSummaries.push({ filePath, summary: fileSummary, functions: fileFunctions }); } })), { concurrent: 5, rendererOptions: { collapseSubtasks: true } } ) }, { title: "Generating README content", task: async (ctx) => { const readmePrompt = `You are a technical documentation expert. Based on these file summaries, generate a comprehensive README.md file that explains the codebase from a functional perspective. Focus on explaining how the different parts work together and what the codebase does. Include these sections: 1. Overview 2. File Structure 3. Key Features 4. Architecture Here are the file summaries and their functions: ${ctx.fileSummaries.map( (file) => ` ## ${file.filePath} ${file.summary} ` ).join("\n")}`; const { text: readmeContent } = await (0, import_ai.generateText)({ model: getAiProvider(options), temperature: 0.3, prompt: readmePrompt }); ctx.readmeContent = readmeContent; } }, { title: "Writing README file", task: async (ctx) => { await import_promises.default.writeFile("REPPY-README.md", ctx.readmeContent, "utf-8"); } } ], { rendererOptions: { collapseSubtasks: options.output === "minimal", collapseErrors: options.output === "minimal" } } ); try { console.log("\n"); await task.run({}); if (options.debug) { console.log("Debug: Generated REPPY-README.md successfully"); if (task.errors.length > 0) { console.log("Debug: Encountered errors:", task.errors); } } } catch (error2) { console.error("Failed to generate README:", error2.message); } } // src/lib/parser.ts var import_eslint = require("eslint"); var import_prompts = require("@inquirer/prompts"); var SUPPORTED_LANGUAGES = { js: { parser: import_tree_sitter_javascript.default, extensions: [".js", ".jsx"] }, ts: { parser: import_tree_sitter_typescript.default.typescript, extensions: [".ts", ".tsx"] }, python: { parser: import_tree_sitter_python.default, extensions: [".py"] }, rust: { parser: import_tree_sitter_rust.default, extensions: [".rs"] }, java: { parser: import_tree_sitter_java.default, extensions: [".java"] }, go: { parser: import_tree_sitter_go.default, extensions: [".go"] } }; async function parseAndDocument(options) { const parser = new import_tree_sitter.default(); const patterns = options.files || ["**/*.{ts,tsx,js,jsx,py,rs,java,go}"]; const ignorePatterns = options.ignore || []; try { for await (const file of findFiles(patterns, ignorePatterns)) { const language = getLanguageForFile(file); if (!language) continue; let undocumentedFunctions = []; const functions = await processFile(file, parser, language); for await (const func of functions) { if (func.isDocumented) { console.log(symbols_exports.success, func.name); } else { undocumentedFunctions.push(func); } } const returnedFunctions = await generateDocs( undocumentedFunctions, options ); documentedFunctions.push(...returnedFunctions); } } catch (error2) { console.error(import_picocolors.default.red(`Parser error: ${error2.message}`)); } const answer = await (0, import_prompts.confirm)({ message: "Generate a REPPY-README.md file to document the codebase?" }); if (answer) { await generateReadme(documentedFunctions, options); } } async function* findFiles(patterns, ignorePatterns) { let gitignorePatterns = []; try { const gitignoreContent = import_fs.default.readFileSync(".gitignore", "utf-8"); gitignorePatterns = gitignoreContent.split("\n").filter((line) => line && !line.startsWith("#")); } catch (error2) { } const allIgnorePatterns = [ "node_modules/**", "dist/**", "build/**", ".git/**", "**/*.d.ts", "**/vendor/**", "**/target/**", "**/__pycache__/**", "public/**", ".*/**", ...gitignorePatterns, ...ignorePatterns.map((pattern) => { if (!pattern.includes("*")) { return [pattern, `**/${pattern}`, `./${pattern}`]; } return pattern; }).flat() ]; if (process.env.DEBUG === "true") { console.debug("Patterns to match:", patterns); console.debug("Ignore patterns:", allIgnorePatterns); } const files = await (0, import_glob.glob)(patterns, { ignore: allIgnorePatterns, nodir: true, absolute: true }); for (const file of files) { yield file; } } function getLanguageForFile(filePath) { var _a2; const ext = import_path2.default.extname(filePath); return (_a2 = Object.entries(SUPPORTED_LANGUAGES).find( ([_, config]) => config.extensions.includes(ext) )) == null ? void 0 : _a2[0]; } async function processJavaScriptFile(filePath) { var _a2; const functions = []; const eslint = new import_eslint.ESLint({ cwd: process.cwd(), overrideConfigFile: true, // Enable flat config overrideConfig: [ { files: ["**/*.{js,jsx,ts,tsx}"], languageOptions: { parser: (await import("@typescript-eslint/parser")).default, ecmaVersion: 2022, sourceType: "module", parserOptions: { project: null // Disable TypeScript project resolution } } } ] }); try { const sourceCode = import_fs.default.readFileSync(filePath, "utf-8"); const results = await eslint.lintText(sourceCode, { filePath }); const relativePath = import_path2.default.relative(process.cwd(), filePath); console.log(` Scanning ${import_picocolors.default.blue(relativePath)}:`); if ((_a2 = results[0]) == null ? void 0 : _a2.messages) { const ast = await parseJavaScriptAST(sourceCode, filePath); processJavaScriptAST( ast, sourceCode, filePath, results[0].messages, functions ); } return functions; } catch (error2) { const relativePath = import_path2.default.relative(process.cwd(), filePath); console.error( import_picocolors.default.red(`Error processing ${relativePath}: ${error2.message}`) ); return []; } } async function processFile(filePath, parser, language) { if (language === "js" || language === "ts") { return processJavaScriptFile(filePath); } const functions = []; const langConfig = SUPPORTED_LANGUAGES[language]; try { const sourceCode = import_fs.default.readFileSync(filePath, "utf-8"); parser.setLanguage(langConfig.parser); const tree = parser.parse(sourceCode); const queryString = getFunctionQuery(language); const query = new import_tree_sitter.default.Query(langConfig.parser, queryString); const matches = query.matches(tree.rootNode); if (matches.length > 0) { const relativePath = import_path2.default.relative(process.cwd(), filePath); console.log(` Scanning ${import_picocolors.default.blue(relativePath)}:`); processMatchesAndCollect(matches, filePath, sourceCode, functions); } return functions; } catch (error2) { const relativePath = import_path2.default.relative(process.cwd(), filePath); console.error( import_picocolors.default.red(`Error processing ${relativePath}: ${error2.message}`) ); return []; } } function getFunctionQuery(language) { switch (language) { case "js": case "ts": return ` [ ; Functions with documentation ( [(comment) (comment)*] @doc ; Allow for multiple comments [ ; Regular functions (function_declaration name: (identifier) @function_name ) ; Exported functions (export_statement declaration: (function_declaration name: (identifier) @function_name ) ) ; Arrow functions (variable_declarator name: (identifier) @function_name value: (arrow_function) ) (export_statement declaration: (variable_declaration (variable_declarator name: (identifier) @function_name value: (arrow_function) ) ) ) ] @function ) ; Functions without documentation [ ; Regular functions (function_declaration name: (identifier) @function_name ) ; Exported functions (export_statement declaration: (function_declaration name: (identifier) @function_name ) ) ; Arrow functions (variable_declarator name: (identifier) @function_name value: (arrow_function) ) (export_statement declaration: (variable_declaration (variable_declarator name: (identifier) @function_name value: (arrow_function) ) ) ) ] @function ] `; case "java": return ` [ ; Methods with documentation ( (block_comment) @doc (method_declaration name: (identifier) @function_name ) @function ) ; Constructors with documentation ( (block_comment) @doc (constructor_declaration name: (identifier) @function_name ) @function ) ; Methods without documentation ( method_declaration name: (identifier) @function_name ) @function ; Constructors without documentation ( constructor_declaration name: (identifier) @function_name ) @function ] `; case "python": return ` [ ; Functions with documentation (function_definition name: (identifier) @function_name body: (block (expression_statement (string) @doc) ; Docstring as first statement ) ) @function ; Class methods with documentation (class_definition body: (block (function_definition name: (identifier) @function_name body: (block (expression_statement (string) @doc) ; Docstring as first statement ) ) @function ) ) ; Functions without documentation (function_definition name: (identifier) @function_name ) @function ; Class methods without documentation (class_definition body: (block (function_definition name: (identifier) @function_name ) @function ) ) ] `; case "rust": return ` [ ; Functions with documentation ( (line_comment) @doc (function_item name: (identifier) @function_name ) @function ) ; Functions without documentation (function_item name: (identifier) @function_name ) @function ] `; case "go": return ` [ ; Functions with documentation ( (comment)+ @doc ; One or more comments [ ; Regular functions (function_declaration name: (identifier) @function_name ) @function ; Methods (method_declaration name: (field_identifier) @function_name ) @function ] ) ; Functions without documentation [ ; Regular functions without docs (function_declaration name: (identifier) @function_name ) @function ; Methods without docs (method_declaration name: (field_identifier) @function_name ) @function ] ] `; } } function processMatchesAndCollect(matches, filePath, sourceCode, functions) { const processedFunctions = /* @__PURE__ */ new Set(); matches.forEach((match) => { const functionNode = match.captures.find( (capture) => capture.name === "function" ); const docNodes = match.captures.filter((capture) => capture.name === "doc"); const functionName = match.captures.find( (capture) => capture.name === "function_name" ); if (functionNode && functionName) { const funcKey = `${functionName.node.text}-${functionNode.node.startPosition.row}`; if (processedFunctions.has(funcKey)) return; processedFunctions.add(funcKey); const language = getLanguageForFile(filePath); let isDocumented = false; let cleanedDoc; for (const doc of docNodes) { const validation = isValidDocumentation(doc.node.text, language); if (validation.isValid) { isDocumented = true; cleanedDoc = validation.doc; documentedFunctions.push({ name: functionName.node.text, documentation: cleanedDoc, filePath }); break; } } functions.push({ name: functionName.node.text, node: functionNode.node, filePath, startLine: functionNode.node.startPosition.row, endLine: functionNode.node.endPosition.row, sourceCode: sourceCode.split("\n").slice( functionNode.node.startPosition.row, functionNode.node.endPosition.row + 1 ).join("\n"), isDocumented, cleanedDoc }); } }); } function isValidDocumentation(commentText, language) { if (!language || !commentText) return { isValid: false }; const trimmedComment = commentText.trim(); switch (language) { case "js": case "ts": if (trimmedComment.startsWith("/**") && trimmedComment.endsWith("*/") || trimmedComment.startsWith("/*") && trimmedComment.endsWith("*/")) { return { isValid: true, doc: trimmedComment }; } break; case "python": if (commentText.includes('"""') || trimmedComment.startsWith("#")) { return { isValid: true, doc: trimmedComment }; } break; case "rust": if (commentText.includes("///") || commentText.includes("//!") || commentText.includes("/*") && commentText.includes("*/")) { return { isValid: true, doc: trimmedComment }; } break; case "java": if (trimmedComment.startsWith("/**") || trimmedComment.startsWith("/*") || trimmedComment.startsWith("//")) { return { isValid: true, doc: trimmedComment }; } break; case "go": if (trimmedComment.startsWith("//") && !trimmedComment.includes("TODO") && trimmedComment.length > 2 && trimmedComment.substring(2).trim().length > 0) { return { isValid: true, doc: trimmedComment }; } break; } return { isValid: false }; } async function parseJavaScriptAST(sourceCode, filePath) { const tsParser = await import("@typescript-eslint/parser"); return tsParser.parse(sourceCode, { sourceType: "module", ecmaVersion: 2022, loc: true, filePath }); } function processJavaScriptAST(ast, sourceCode, filePath, lintMessages, functions) { function traverse(node) { var _a2, _b2, _c2, _d2, _e, _f, _g, _h, _i; if (!node) return; if (node.type === "FunctionDeclaration" && ((_a2 = node.id) == null ? void 0 : _a2.name) || // Named function declarations node.type === "MethodDefinition" && ((_b2 = node.key) == null ? void 0 : _b2.name) || // Class methods node.type === "FunctionExpression" && ((_c2 = node.id) == null ? void 0 : _c2.name) || // Named function expressions node.type === "VariableDeclarator" && ((_d2 = node.id) == null ? void 0 : _d2.name) && (((_e = node.init) == null ? void 0 : _e.type) === "ArrowFunctionExpression" || ((_f = node.init) == null ? void 0 : _f.type) === "FunctionExpression")) { const functionName = ((_g = node.id) == null ? void 0 : _g.name) || ((_h = node.key) == null ? void 0 : _h.name) || (node.init ? (_i = node.id) == null ? void 0 : _i.name : null); if (!functionName) return; const startLine = node.loc.start.line - 1; const endLine = node.loc.end.line - 1; const docResult = getJSDocComment(node, sourceCode); const isDocumented = docResult.hasDoc; if (isDocumented && docResult.docText) { documentedFunctions.push({ name: functionName, documentation: docResult.docText, filePath }); } functions.push({ name: functionName, node, filePath, startLine, endLine, sourceCode: sourceCode.split("\n").slice(startLine, endLine + 1).join("\n"), isDocumented, cleanedDoc: docResult.docText }); } for (const key in node) { if (node[key] && typeof node[key] === "object") { traverse(node[key]); } } } traverse(ast); } function getJSDocComment(node, sourceCode) { if (!node.loc) return { hasDoc: false }; const lines = sourceCode.split("\n"); const functionStartLine = node.loc.start.line - 1; let currentLine = functionStartLine - 1; let docLines = []; let insideComment = false; while (currentLine >= 0) { const line = lines[currentLine].trim(); if (line === "") { currentLine--; continue; } if (line.startsWith("/**")) { insideComment = true; docLines.unshift(line); break; } if (insideComment || line.startsWith("*") || line.startsWith("*/")) { docLines.unshift(line); } if (!line.startsWith("*") && !line.startsWith("*/") && !insideComment) { break; } currentLine--; } if (docLines.length > 0) { return { hasDoc: true, docText: docLines.join("\n") }; } return { hasDoc: false }; } // src/lib/config.ts var import_command_line_args = __toESM(require("command-line-args"), 1); var import_command_line_usage = __toESM(require("command-line-usage"), 1); var import_picocolors2 = __toESM(require("picocolors"), 1); var import_child_process = require("child_process"); var import_listr22 = require("listr2"); var import_glob2 = require("glob"); var import_path3 = __toESM(require("path"), 1); var import_minimatch = require("minimatch"); var SUPPORTED_PROVIDERS = [ "openai", "anthropic", "cohere", "mistral", "azure", "groq", "bedrock" ]; var optionDefinitions = [ { name: "help", alias: "h", type: Boolean, description: "Display this help message" }, { name: "provider", alias: "p", type: String, defaultValue: "openai", description: "AI provider to use (openai, anthropic, cohere, mistral, azure, groq, bedrock)" }, { name: "model", alias: "m", type: String, description: "Model to use for generation" }, { name: "temperature", alias: "t", type: Number, defaultValue: 0.1, description: "Temperature for generation (0-1)" }, { name: "files", alias: "f", type: String, multiple: true, description: "Files or globs to process" }, { name: "ignore", alias: "i", type: String, multiple: true, description: "Files or globs to ignore" }, { name: "debug", alias: "d", type: Boolean, defaultValue: false, description: "Enable debug logging" }, { name: "concurrent", alias: "c", type: Number, defaultValue: 1, description: "Number of functions to process concurrently (default: 1)" }, { name: "rate-limit", type: Number, defaultValue: 1e3, description: "Rate limit between API calls in ms" }, { name: "output", alias: "o", type: String, defaultValue: "normal", description: "Output verbosity (minimal, normal, verbose)" }, { name: "unsafe", type: Boolean, defaultValue: false, description: "Skip Git repository checks" } ]; var helpSections = [ { header: import_picocolors2.default.cyan("Reppy"), content: "Automatically generate documentation for your codebase using AI." }, { header: "Usage", content: [ "$ reppy [options]", "", "Example:", '$ reppy -p anthropic -m "claude-3-sonnet" -t 0.2' ] }, { header: "Options", optionList: optionDefinitions }, { header: "Environment Variables", content: [ { name: "OPENAI_API_KEY", summary: "Required for OpenAI provider" }, { name: "ANTHROPIC_API_KEY", summary: "Required for Anthropic provider" }, { name: "AZURE_API_KEY", summary: "Required for Azure provider" }, { name: "AZURE_ENDPOINT", summary: "Required for Azure provider" }, { name: "MISTRAL_API_KEY", summary: "Required for Mistral provider" }, { name: "COHERE_API_KEY", summary: "Required for Cohere provider" }, { name: "GROQ_API_KEY", summary: "Required for Groq provider" }, { name: "AWS_ACCESS_KEY_ID", summary: "Required for Amazon Bedrock provider" }, { name: "AWS_SECRET_ACCESS_KEY", summary: "Required for Amazon Bedrock provider" }, { name: "AWS_REGION", summary: "Required for Amazon Bedrock provider" } ] }, { header: "Examples", content: [ { desc: "1. Use OpenAI with GPT-4", example: "$ reppy -p openai -m gpt-4" }, { desc: "2. Use Anthropic with custom temperature", example: "$ reppy -p anthropic -t 0.2" }, { desc: "3. Process specific files", example: '$ reppy -f "src/**/*.ts"' }, { desc: "4. Ignore test files", example: '$ reppy -i "**/*.test.ts" "**/*.spec.ts"' }, { desc: "5. Process 4 functions concurrently", example: "$ reppy --concurrent 4" }, { desc: "6. Debug mode with minimal output", example: "$ reppy --debug --output minimal" } ] } ]; var defaultModels = { openai: "gpt-4.1-mini", anthropic: "claude-3.5-sonnet", cohere: "command", mistral: "mistral-tiny", bedrock: "claude-3.5-sonnet", groq: "mixtral-8x7b-32768", azure: "gpt-4.1-mini" }; var ENV_REQUIREMENTS = { openai: ["OPENAI_API_KEY"], anthropic: ["ANTHROPIC_API_KEY"], azure: ["AZURE_API_KEY", "AZURE_RESOURCE_NAME"], mistral: ["MISTRAL_API_KEY"], cohere: ["COHERE_API_KEY"], groq: ["GROQ_API_KEY"], bedrock: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION"] }; function validateEnvironmentVariables(provider) { const requiredVars = ENV_REQUIREMENTS[provider]; const missingVars = requiredVars.filter( (envVar) => { var _a2; return !process.env[envVar] || ((_a2 = process.env[envVar]) == null ? void 0 : _a2.trim()) === ""; } ); if (missingVars.length > 0) { throw new Error( `Missing required environment variables for ${provider}: ${missingVars.join( ", " )} Please set these in your .env file.` ); } } function processFilePatterns(includePatterns = [], ignorePatterns = []) { const patterns = includePatterns.length > 0 ? includePatterns : ["**/*"]; const defaultIgnores = ["**/node_modules/**", "**/.git/**"]; const allIgnorePatterns = [...defaultIgnores, ...ignorePatterns]; const allFiles = patterns.flatMap((pattern) => { pattern = pattern.replace(/^\.\//, "").replace(/\\/g, "/"); return import_glob2.glob.sync(pattern, { nodir: true, absolute: true, dot: true }); }); const relativeFiles = allFiles.map( (file) => import_path3.default.relative(process.cwd(), file).replace(/\\/g, "/") ); const filteredFiles = relativeFiles.filter((file) => { for (const ignorePattern of allIgnorePatterns) { const normalizedPattern = ignorePattern.replace(/^\.\//, "").replace(/\\/g, "/"); if (!normalizedPattern.includes("*")) { if (file === normalizedPattern || file === `./${normalizedPattern}` || file.endsWith(`/${normalizedPattern}`) || file === ignorePattern) { if (process.env.DEBUG === "true") { console.debug( `File ${file} matched ignore pattern ${ignorePattern}` ); } return false; } } else if ((0, import_minimatch.minimatch)(file, normalizedPattern)) { if (process.env.DEBUG === "true") { console.debug(`File ${file} matched ignore pattern ${ignorePattern}`); } return false; } } return true; }); if (process.env.DEBUG === "true") { console.debug("Working directory:", process.cwd()); console.debug("Include patterns:", patterns); console.debug("Ignore patterns:", allIgnorePatterns); console.debug("All matched files:", relativeFiles); console.debug("After ignore filtering:", filteredFiles); } return [...new Set(filteredFiles)]; } function validateGitState(options) { if (options.unsafe) { return; } try { (0, import_child_process.execSync)("git rev-parse --is-inside-work-tree", { stdio: "ignore" }); const status = (0, import_child_process.execSync)("git status --porcelain").toString(); if (status.length > 0) { throw new Error( "There are uncommitted changes in your repository. Please commit or stash your changes before running reppy." ); } } catch (error2) { if (error2 instanceof Error) { if (error2.message.includes("uncommitted changes")) { throw error2; } else { throw new Error( "Not a git repository. Please initialize a git repository and commit your changes before running reppy." ); } } } } function parseCliOptions() { const options = (0, import_command_line_args.default)(optionDefinitions); if (options.help) { console.log((0, import_command_line_usage.default)(helpSections)); process.exit(0); } if (options.provider && !SUPPORTED_PROVIDERS.includes(options.provider)) { throw new Error( `Invalid provider: ${options.provider}. Supported providers are: ${SUPPORTED_PROVIDERS.join(", ")}` ); } if (options.debug) { process.env.DEBUG = "true"; console.debug = (...args) => { if (process.env.DEBUG === "true") { console.log(import_picocolors2.default.gray("[debug]"), ...args); } }; } let processedFiles = []; if (!options.files && !options.ignore) { processedFiles = processFilePatterns(); } else { processedFiles = processFilePatterns(options.files, options.ignore); if (processedFiles.length === 0) { throw new Error("No files matched the specified patterns"); } } options.files = processedFiles; const validOutputLevels = ["minimal", "normal", "verbose"]; if (options.output && !validOutputLevels.includes(options.output)) { throw new Error( `Invalid output level: ${options.output}. Must be one of: ${validOutputLevels.join(", ")}` ); } validateEnvironmentVariables(options.provider); if (!options.model) { options.model = defaultModels[options.provider]; } if (options.temperature !== void 0 && (options.temperature < 0 || options.temperature > 1)) { throw new Error("Temperature must be between 0 and 1"); } if (options["rate-limit"] !== void 0 && options["rate-limit"] < 0) { throw new Error("Rate limit must be a positive number"); } return options; } async function commitDocumentationChanges() { const task = new import_listr22.Listr([ { title: "Committing documentation changes", task: async (_, task2) => { return new Promise((resolve, reject) => { try { const status = (0, import_child_process.execSync)("git status --porcelain").toString(); if (status.length === 0) { task2.title = "No documentation changes to commit"; return resolve("No documentation changes to commit"); } (0, import_child_process.execSync)("git add .", { stdio: "ignore" }); const stagedStatus = (0, import_child_process.execSync)( "git diff --cached --name-only" ).toString(); if (stagedStatus.length === 0) { task2.title = "No documentation files were modified"; return resolve("No documentation files were modified"); } (0, import_child_process.execSync)('git commit -m "docs: documented with reppy"', { stdio: "ignore" }); task2.title = "Documentation changes committed to Git"; resolve("Documentation changes committed to Git"); } catch (error2) { if (error2 instanceof Error) { task2.title = "Failed to commit documentation changes"; reject( `Failed to commit documentation changes - ${error2.message}` ); } else { task2.title = "Failed to commit documentation changes"; reject("Failed to commit documentation changes"); } } }); } } ]); await task.run(); } // src/index.ts var import_picocolors3 = __toESM(require("picocolors"), 1); var import_cfonts = __toESM(require("cfonts"), 1); async function main() { import_cfonts.default.say("Reppy", { font: "tiny", gradient: ["blue", "cyan"], transitionGradient: true }); try { const options = parseCliOptions(); validateGitState(options); console.log( import_picocolors3.default.blue(`Using ${options.provider} with model ${options.model}`) ); await parseAndDocument(options); if (!options.unsafe) { commitDocumentationChanges(); } } catch (error2) { console.error(import_picocolors3.default.red(import_picocolors3.default.bold(error2.message))); process.exit(1); } } main(); //# sourceMappingURL=index.cjs.map