UNPKG

reppy

Version:

Let reppy generate documentation for the functions in your codebase.

1 lines 85.1 kB
{"version":3,"sources":["../src/lib/parser.ts","../node_modules/.pnpm/log-symbols@7.0.0/node_modules/log-symbols/symbols.js","../node_modules/.pnpm/yoctocolors@2.1.1/node_modules/yoctocolors/base.js","../node_modules/.pnpm/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js","../src/lib/generateDocs.ts","../src/lib/config.ts","../src/index.ts"],"sourcesContent":["import Parser from \"tree-sitter\"\nimport JavaScript from \"tree-sitter-javascript\"\nimport TypeScript from \"tree-sitter-typescript\"\nimport Python from \"tree-sitter-python\"\nimport Rust from \"tree-sitter-rust\"\nimport Java from \"tree-sitter-java\"\nimport Go from \"tree-sitter-go\"\nimport fs from \"fs\"\nimport path from \"path\"\nimport { glob } from \"glob\"\nimport logSymbols from \"log-symbols\"\nimport pc from \"picocolors\"\nimport {\n documentedFunctions,\n generateDocs,\n generateReadme,\n} from \"./generateDocs.js\"\nimport { parseCliOptions } from \"./config.js\"\nimport { ESLint } from \"eslint\"\nimport { CliOptions, DocumentedFunction } from \"./types/providers.js\"\nimport { confirm } from \"@inquirer/prompts\"\n\nconst SUPPORTED_LANGUAGES = {\n js: { parser: JavaScript, extensions: [\".js\", \".jsx\"] as const },\n ts: { parser: TypeScript.typescript, extensions: [\".ts\", \".tsx\"] as const },\n python: { parser: Python, extensions: [\".py\"] as const },\n rust: { parser: Rust, extensions: [\".rs\"] as const },\n java: { parser: Java, extensions: [\".java\"] as const },\n go: { parser: Go, extensions: [\".go\"] as const },\n} as const\n\ntype LanguageKey = keyof typeof SUPPORTED_LANGUAGES\n\nexport interface Function {\n name: string\n node: Parser.SyntaxNode\n filePath: string\n startLine: number\n endLine: number\n sourceCode: string\n isDocumented: boolean\n cleanedDoc?: string\n}\n\n/**\n * Parses source files to identify undocumented functions and generates documentation for them.\n * @param {CliOptions} options - Configuration options for documentation generation.\n * @returns {Promise<void>} A promise that resolves when the documentation generation is complete.\n */\nexport async function parseAndDocument(options: CliOptions) {\n const parser = new Parser()\n // Use the files from options if provided, otherwise use default pattern\n const patterns = options.files || [\"**/*.{ts,tsx,js,jsx,py,rs,java,go}\"]\n // Use ignore patterns from options\n const ignorePatterns = options.ignore || []\n\n try {\n for await (const file of findFiles(patterns, ignorePatterns)) {\n const language = getLanguageForFile(file)\n if (!language) continue\n\n let undocumentedFunctions: Function[] = []\n\n const functions = await processFile(file, parser, language)\n\n for await (const func of functions) {\n if (func.isDocumented) {\n console.log(logSymbols.success, func.name)\n } else {\n // Mark functions as documented after generation\n undocumentedFunctions.push(func)\n }\n }\n const returnedFunctions = await generateDocs(\n undocumentedFunctions,\n options\n )\n\n documentedFunctions.push(...returnedFunctions)\n }\n } catch (error) {\n console.error(pc.red(`Parser error: ${(error as Error).message}`))\n }\n const answer = await confirm({\n message: \"Generate a REPPY-README.md file to document the codebase?\",\n })\n if (answer) {\n await generateReadme(documentedFunctions, options)\n }\n}\n\n/**\n * Asynchronously finds files matching the specified patterns while ignoring certain directories and files.\n * It also reads patterns from a .gitignore file if it exists to further filter the results.\n * @param {string[]} patterns - An array of glob patterns to match files against.\n * @param {string[]} ignorePatterns - An array of additional glob patterns to ignore when searching for files.\n * @returns {AsyncGenerator<string>} An asynchronous generator that yields the paths of the matching files.\n */\nasync function* findFiles(patterns: string[], ignorePatterns: string[]) {\n // Read .gitignore if it exists\n let gitignorePatterns: string[] = []\n try {\n const gitignoreContent = fs.readFileSync(\".gitignore\", \"utf-8\")\n gitignorePatterns = gitignoreContent\n .split(\"\\n\")\n .filter((line) => line && !line.startsWith(\"#\"))\n } catch (error) {\n // .gitignore doesn't exist, continue without it\n }\n\n // Combine all ignore patterns\n const allIgnorePatterns = [\n \"node_modules/**\",\n \"dist/**\",\n \"build/**\",\n \".git/**\",\n \"**/*.d.ts\",\n \"**/vendor/**\",\n \"**/target/**\",\n \"**/__pycache__/**\",\n \"public/**\",\n \".*/**\",\n ...gitignorePatterns,\n ...ignorePatterns\n .map((pattern) => {\n // Ensure patterns work with both direct file names and glob patterns\n if (!pattern.includes(\"*\")) {\n return [pattern, `**/${pattern}`, `./${pattern}`]\n }\n return pattern\n })\n .flat(),\n ]\n\n if (process.env.DEBUG === \"true\") {\n console.debug(\"Patterns to match:\", patterns)\n console.debug(\"Ignore patterns:\", allIgnorePatterns)\n }\n\n const files = await glob(patterns, {\n ignore: allIgnorePatterns,\n nodir: true,\n absolute: true,\n })\n\n for (const file of files) {\n yield file\n }\n}\n\n/**\n * Retrieves the programming language associated with a given file based on its file extension.\n * @param {string} filePath - The path of the file for which to determine the programming language.\n * @returns {LanguageKey | undefined} The language key corresponding to the file's extension, or undefined if no matching language is found.\n */\nfunction getLanguageForFile(filePath: string): LanguageKey | undefined {\n const ext = path.extname(filePath)\n return Object.entries(SUPPORTED_LANGUAGES).find(([_, config]) =>\n (config.extensions as unknown as string[]).includes(ext)\n )?.[0] as LanguageKey | undefined\n}\n\n/**\n * Processes a JavaScript file to extract and return an array of functions defined within it, while also performing linting checks using ESLint.\n * @param {string} filePath - The path to the JavaScript file to be processed.\n * @returns {Promise<Function[]>} A promise that resolves to an array of functions extracted from the file, or an empty array if an error occurs.\n */\nasync function processJavaScriptFile(filePath: string): Promise<Function[]> {\n const functions: Function[] = []\n\n const eslint = new ESLint({\n cwd: process.cwd(),\n overrideConfigFile: true, // Enable flat config\n overrideConfig: [\n {\n files: [\"**/*.{js,jsx,ts,tsx}\"],\n languageOptions: {\n parser: (await import(\"@typescript-eslint/parser\")).default,\n ecmaVersion: 2022,\n sourceType: \"module\",\n parserOptions: {\n project: null, // Disable TypeScript project resolution\n },\n },\n },\n ],\n })\n\n try {\n const sourceCode = fs.readFileSync(filePath, \"utf-8\")\n const results = await eslint.lintText(sourceCode, { filePath })\n const relativePath = path.relative(process.cwd(), filePath)\n console.log(`\\nScanning ${pc.blue(relativePath)}:`)\n\n if (results[0]?.messages) {\n const ast = await parseJavaScriptAST(sourceCode, filePath)\n processJavaScriptAST(\n ast,\n sourceCode,\n filePath,\n results[0].messages,\n functions\n )\n }\n\n return functions\n } catch (error) {\n const relativePath = path.relative(process.cwd(), filePath)\n console.error(\n pc.red(`Error processing ${relativePath}: ${(error as Error).message}`)\n )\n return []\n }\n}\n\n/**\n * Processes a file to extract functions based on the specified programming language.\n * If the language is JavaScript or TypeScript, it uses a specific processing method; otherwise, it utilizes tree-sitter logic for other languages.\n * @param {string} filePath - The path to the file to be processed.\n * @param {Parser} parser - The parser instance used to parse the source code of the file.\n * @param {LanguageKey} language - The programming language key that determines the parsing strategy.\n * @returns {Promise<Function[]>} A promise that resolves to an array of extracted functions from the file.\n */\nasync function processFile(\n filePath: string,\n parser: Parser,\n language: LanguageKey\n): Promise<Function[]> {\n // Handle JavaScript/TypeScript files with ESLint\n if (language === \"js\" || language === \"ts\") {\n return processJavaScriptFile(filePath)\n }\n\n // Existing tree-sitter logic for other languages\n const functions: Function[] = []\n const langConfig = SUPPORTED_LANGUAGES[language]\n\n try {\n const sourceCode = fs.readFileSync(filePath, \"utf-8\")\n parser.setLanguage(langConfig.parser)\n const tree = parser.parse(sourceCode)\n const queryString = getFunctionQuery(language)\n const query = new Parser.Query(langConfig.parser, queryString)\n const matches = query.matches(tree.rootNode)\n\n if (matches.length > 0) {\n const relativePath = path.relative(process.cwd(), filePath)\n console.log(`\\nScanning ${pc.blue(relativePath)}:`)\n processMatchesAndCollect(matches, filePath, sourceCode, functions)\n }\n\n return functions\n } catch (error) {\n const relativePath = path.relative(process.cwd(), filePath)\n console.error(\n pc.red(`Error processing ${relativePath}: ${(error as Error).message}`)\n )\n return []\n }\n}\n\n/**\n * Generates a query string for extracting function-related information based on the specified programming language.\n * @param {LanguageKey} language - The programming language for which to generate the function query.\n * @returns {string} A query string that defines patterns for matching functions, methods, and their documentation in the specified language.\n */\nfunction getFunctionQuery(language: LanguageKey): string {\n switch (language) {\n case \"js\":\n case \"ts\":\n return `\n [\n ; Functions with documentation\n (\n [(comment) (comment)*] @doc ; Allow for multiple comments\n [\n ; Regular functions\n (function_declaration\n name: (identifier) @function_name\n )\n ; Exported functions\n (export_statement\n declaration: (function_declaration\n name: (identifier) @function_name\n )\n )\n ; Arrow functions\n (variable_declarator\n name: (identifier) @function_name\n value: (arrow_function)\n )\n (export_statement\n declaration: (variable_declaration\n (variable_declarator\n name: (identifier) @function_name\n value: (arrow_function)\n )\n )\n )\n ] @function\n )\n\n ; Functions without documentation\n [\n ; Regular functions\n (function_declaration\n name: (identifier) @function_name\n )\n ; Exported functions\n (export_statement\n declaration: (function_declaration\n name: (identifier) @function_name\n )\n )\n ; Arrow functions\n (variable_declarator\n name: (identifier) @function_name\n value: (arrow_function)\n )\n (export_statement\n declaration: (variable_declaration\n (variable_declarator\n name: (identifier) @function_name\n value: (arrow_function)\n )\n )\n )\n ] @function\n ]\n `\n\n case \"java\":\n return `\n [\n ; Methods with documentation\n (\n (block_comment) @doc\n (method_declaration\n name: (identifier) @function_name\n ) @function\n )\n\n ; Constructors with documentation\n (\n (block_comment) @doc\n (constructor_declaration\n name: (identifier) @function_name\n ) @function\n )\n\n ; Methods without documentation\n (\n method_declaration\n name: (identifier) @function_name\n ) @function\n\n ; Constructors without documentation\n (\n constructor_declaration\n name: (identifier) @function_name\n ) @function\n ]\n `\n case \"python\":\n return `\n [\n ; Functions with documentation\n (function_definition\n name: (identifier) @function_name\n body: (block\n (expression_statement\n (string) @doc) ; Docstring as first statement\n )\n ) @function\n\n ; Class methods with documentation\n (class_definition\n body: (block\n (function_definition\n name: (identifier) @function_name\n body: (block\n (expression_statement\n (string) @doc) ; Docstring as first statement\n )\n ) @function\n )\n )\n\n ; Functions without documentation\n (function_definition\n name: (identifier) @function_name\n ) @function\n\n ; Class methods without documentation\n (class_definition\n body: (block\n (function_definition\n name: (identifier) @function_name\n ) @function\n )\n )\n ]\n `\n case \"rust\":\n return `\n [\n ; Functions with documentation\n (\n (line_comment) @doc\n (function_item\n name: (identifier) @function_name\n ) @function\n )\n\n ; Functions without documentation\n (function_item\n name: (identifier) @function_name\n ) @function\n ]\n `\n case \"go\":\n return `\n [\n ; Functions with documentation\n (\n (comment)+ @doc ; One or more comments\n [\n ; Regular functions\n (function_declaration\n name: (identifier) @function_name\n ) @function\n\n ; Methods\n (method_declaration\n name: (field_identifier) @function_name\n ) @function\n ]\n )\n\n ; Functions without documentation\n [\n ; Regular functions without docs\n (function_declaration\n name: (identifier) @function_name\n ) @function\n\n ; Methods without docs\n (method_declaration\n name: (field_identifier) @function_name\n ) @function\n ]\n ]\n `\n }\n}\n\n/**\n * Processes an array of query matches to collect function information and documentation status.\n * @param {Parser.QueryMatch[]} matches - An array of query matches containing captured nodes for functions and documentation.\n * @param {string} filePath - The path of the file being processed.\n * @param {string} sourceCode - The source code of the file as a string.\n * @param {Function[]} functions - An array to which processed function information will be added.\n * @returns {void} This function does not return a value; it modifies the functions array in place.\n */\nfunction processMatchesAndCollect(\n matches: Parser.QueryMatch[],\n filePath: string,\n sourceCode: string,\n functions: Function[]\n) {\n const processedFunctions = new Set<string>()\n\n matches.forEach((match) => {\n const functionNode = match.captures.find(\n (capture) => capture.name === \"function\"\n )\n const docNodes = match.captures.filter((capture) => capture.name === \"doc\")\n const functionName = match.captures.find(\n (capture) => capture.name === \"function_name\"\n )\n\n if (functionNode && functionName) {\n const funcKey = `${functionName.node.text}-${functionNode.node.startPosition.row}`\n\n if (processedFunctions.has(funcKey)) return\n processedFunctions.add(funcKey)\n\n const language = getLanguageForFile(filePath)\n\n let isDocumented = false\n let cleanedDoc: string | undefined\n\n // Check each doc node and keep the first valid documentation\n for (const doc of docNodes) {\n const validation = isValidDocumentation(doc.node.text, language)\n if (validation.isValid) {\n isDocumented = true\n cleanedDoc = validation.doc\n\n documentedFunctions.push({\n name: functionName.node.text,\n documentation: cleanedDoc!,\n filePath,\n })\n break\n }\n }\n\n functions.push({\n name: functionName.node.text,\n node: functionNode.node,\n filePath,\n startLine: functionNode.node.startPosition.row,\n endLine: functionNode.node.endPosition.row,\n sourceCode: sourceCode\n .split(\"\\n\")\n .slice(\n functionNode.node.startPosition.row,\n functionNode.node.endPosition.row + 1\n )\n .join(\"\\n\"),\n isDocumented,\n cleanedDoc,\n })\n }\n })\n}\n\ninterface DocumentationValidation {\n isValid: boolean\n doc?: string\n}\n\n/**\n * Checks if the given comment text is a valid documentation comment for the specified language.\n * @param {string} commentText - The text content of the comment.\n * @param {LanguageKey | undefined} language - The programming language to check documentation format for.\n * @returns {DocumentationValidation} An object containing the validation status and the documentation content.\n */\nfunction isValidDocumentation(\n commentText: string,\n language: LanguageKey | undefined\n): DocumentationValidation {\n if (!language || !commentText) return { isValid: false }\n\n const trimmedComment = commentText.trim()\n\n switch (language) {\n case \"js\":\n case \"ts\":\n // Check for both JSDoc style and regular block comments\n if (\n (trimmedComment.startsWith(\"/**\") && trimmedComment.endsWith(\"*/\")) ||\n (trimmedComment.startsWith(\"/*\") && trimmedComment.endsWith(\"*/\"))\n ) {\n return {\n isValid: true,\n doc: trimmedComment,\n }\n }\n break\n\n case \"python\":\n if (commentText.includes('\"\"\"') || trimmedComment.startsWith(\"#\")) {\n return {\n isValid: true,\n doc: trimmedComment,\n }\n }\n break\n\n case \"rust\":\n if (\n commentText.includes(\"///\") ||\n commentText.includes(\"//!\") ||\n (commentText.includes(\"/*\") && commentText.includes(\"*/\"))\n ) {\n return {\n isValid: true,\n doc: trimmedComment,\n }\n }\n break\n\n case \"java\":\n if (\n trimmedComment.startsWith(\"/**\") ||\n trimmedComment.startsWith(\"/*\") ||\n trimmedComment.startsWith(\"//\")\n ) {\n return {\n isValid: true,\n doc: trimmedComment,\n }\n }\n break\n\n case \"go\":\n if (\n trimmedComment.startsWith(\"//\") &&\n !trimmedComment.includes(\"TODO\") &&\n trimmedComment.length > 2 &&\n trimmedComment.substring(2).trim().length > 0\n ) {\n return {\n isValid: true,\n doc: trimmedComment,\n }\n }\n break\n }\n\n return { isValid: false }\n}\n\n/**\n * Parses the provided JavaScript source code into an Abstract Syntax Tree (AST) using the TypeScript ESLint parser.\n * @param {string} sourceCode - The JavaScript source code to be parsed.\n * @param {string} filePath - The path of the file from which the source code was read, used for error reporting and location tracking.\n * @returns {Promise<Object>} A promise that resolves to the parsed AST object.\n */\nasync function parseJavaScriptAST(sourceCode: string, filePath: string) {\n const tsParser = await import(\"@typescript-eslint/parser\")\n return tsParser.parse(sourceCode, {\n sourceType: \"module\",\n ecmaVersion: 2022,\n loc: true,\n filePath,\n })\n}\n\n/**\n * Processes a JavaScript abstract syntax tree (AST) to identify and collect information about named functions and methods.\n * @param {any} ast - The abstract syntax tree representing the JavaScript code to be analyzed.\n * @param {string} sourceCode - The original source code as a string, used for extracting function definitions.\n * @param {string} filePath - The file path of the source code, used for reference in the collected information.\n * @param {any[]} lintMessages - An array to collect linting messages related to the identified functions.\n * @param {Function[]} functions - An array that will be populated with information about the identified functions and methods.\n * @returns {void} This function does not return a value; it modifies the functions array with details of the identified functions.\n */\nfunction processJavaScriptAST(\n ast: any,\n sourceCode: string,\n filePath: string,\n lintMessages: any[],\n functions: Function[]\n) {\n /**\n * Traverses an abstract syntax tree (AST) node to identify and collect information about named functions and methods.\n * @param {any} node - The AST node to traverse, which may represent a function declaration, method, function expression, or variable declaration containing a function.\n * @returns {void} This function does not return a value; it populates a global array with information about the identified functions.\n */\n function traverse(node: any) {\n if (!node) return\n\n // Only process named functions and methods\n if (\n (node.type === \"FunctionDeclaration\" && node.id?.name) || // Named function declarations\n (node.type === \"MethodDefinition\" && node.key?.name) || // Class methods\n (node.type === \"FunctionExpression\" && node.id?.name) || // Named function expressions\n (node.type === \"VariableDeclarator\" &&\n node.id?.name &&\n (node.init?.type === \"ArrowFunctionExpression\" ||\n node.init?.type === \"FunctionExpression\")) // Named variable functions\n ) {\n const functionName =\n node.id?.name || node.key?.name || (node.init ? node.id?.name : null)\n\n // Skip if no name was found\n if (!functionName) return\n\n const startLine = node.loc.start.line - 1\n const endLine = node.loc.end.line - 1\n\n // Check if function has JSDoc comment and get the comment text\n const docResult = getJSDocComment(node, sourceCode)\n const isDocumented = docResult.hasDoc\n\n // If the function is documented, add it to documentedFunctions\n if (isDocumented && docResult.docText) {\n documentedFunctions.push({\n name: functionName,\n documentation: docResult.docText,\n filePath,\n })\n }\n\n functions.push({\n name: functionName,\n node: node,\n filePath,\n startLine,\n endLine,\n sourceCode: sourceCode\n .split(\"\\n\")\n .slice(startLine, endLine + 1)\n .join(\"\\n\"),\n isDocumented,\n cleanedDoc: docResult.docText,\n })\n }\n\n // Traverse child nodes\n for (const key in node) {\n if (node[key] && typeof node[key] === \"object\") {\n traverse(node[key])\n }\n }\n }\n\n traverse(ast)\n}\n\n/**\n * Gets the JSDoc comment for a given node and returns both its presence and content.\n * @param {any} node - The AST node to check for a JSDoc comment.\n * @param {string} sourceCode - The source code as a string to search for comments.\n * @returns {{ hasDoc: boolean, docText?: string }} Object containing whether a JSDoc exists and its content if found.\n */\nfunction getJSDocComment(\n node: any,\n sourceCode: string\n): { hasDoc: boolean; docText?: string } {\n if (!node.loc) return { hasDoc: false }\n\n const lines = sourceCode.split(\"\\n\")\n const functionStartLine = node.loc.start.line - 1\n let currentLine = functionStartLine - 1\n let docLines: string[] = []\n let insideComment = false\n\n while (currentLine >= 0) {\n const line = lines[currentLine].trim()\n if (line === \"\") {\n currentLine--\n continue\n }\n if (line.startsWith(\"/**\")) {\n insideComment = true\n docLines.unshift(line)\n break\n }\n if (insideComment || line.startsWith(\"*\") || line.startsWith(\"*/\")) {\n docLines.unshift(line)\n }\n if (!line.startsWith(\"*\") && !line.startsWith(\"*/\") && !insideComment) {\n break\n }\n currentLine--\n }\n\n if (docLines.length > 0) {\n return {\n hasDoc: true,\n docText: docLines.join(\"\\n\"),\n }\n }\n\n return { hasDoc: false }\n}\n","import {\n\tblue,\n\tgreen,\n\tyellow,\n\tred,\n} from 'yoctocolors';\nimport isUnicodeSupported from 'is-unicode-supported';\n\nconst _isUnicodeSupported = isUnicodeSupported();\n\nexport const info = blue(_isUnicodeSupported ? 'ℹ' : 'i');\nexport const success = green(_isUnicodeSupported ? '✔' : '√');\nexport const warning = yellow(_isUnicodeSupported ? '⚠' : '‼');\nexport const error = red(_isUnicodeSupported ? '✖️' : '×');\n","import tty from 'node:tty';\n\n// eslint-disable-next-line no-warning-comments\n// TODO: Use a better method when it's added to Node.js (https://github.com/nodejs/node/pull/40240)\n// Lots of optionals here to support Deno.\nconst hasColors = tty?.WriteStream?.prototype?.hasColors?.() ?? false;\n\nconst format = (open, close) => {\n\tif (!hasColors) {\n\t\treturn input => input;\n\t}\n\n\tconst openCode = `\\u001B[${open}m`;\n\tconst closeCode = `\\u001B[${close}m`;\n\n\treturn input => {\n\t\tconst string = input + ''; // eslint-disable-line no-implicit-coercion -- This is faster.\n\t\tlet index = string.indexOf(closeCode);\n\n\t\tif (index === -1) {\n\t\t\t// Note: Intentionally not using string interpolation for performance reasons.\n\t\t\treturn openCode + string + closeCode;\n\t\t}\n\n\t\t// Handle nested colors.\n\n\t\t// We could have done this, but it's too slow (as of Node.js 22).\n\t\t// return openCode + string.replaceAll(closeCode, openCode) + closeCode;\n\n\t\tlet result = openCode;\n\t\tlet lastIndex = 0;\n\n\t\twhile (index !== -1) {\n\t\t\tresult += string.slice(lastIndex, index) + openCode;\n\t\t\tlastIndex = index + closeCode.length;\n\t\t\tindex = string.indexOf(closeCode, lastIndex);\n\t\t}\n\n\t\tresult += string.slice(lastIndex) + closeCode;\n\n\t\treturn result;\n\t};\n};\n\nexport const reset = format(0, 0);\nexport const bold = format(1, 22);\nexport const dim = format(2, 22);\nexport const italic = format(3, 23);\nexport const underline = format(4, 24);\nexport const overline = format(53, 55);\nexport const inverse = format(7, 27);\nexport const hidden = format(8, 28);\nexport const strikethrough = format(9, 29);\n\nexport const black = format(30, 39);\nexport const red = format(31, 39);\nexport const green = format(32, 39);\nexport const yellow = format(33, 39);\nexport const blue = format(34, 39);\nexport const magenta = format(35, 39);\nexport const cyan = format(36, 39);\nexport const white = format(37, 39);\nexport const gray = format(90, 39);\n\nexport const bgBlack = format(40, 49);\nexport const bgRed = format(41, 49);\nexport const bgGreen = format(42, 49);\nexport const bgYellow = format(43, 49);\nexport const bgBlue = format(44, 49);\nexport const bgMagenta = format(45, 49);\nexport const bgCyan = format(46, 49);\nexport const bgWhite = format(47, 49);\nexport const bgGray = format(100, 49);\n\nexport const redBright = format(91, 39);\nexport const greenBright = format(92, 39);\nexport const yellowBright = format(93, 39);\nexport const blueBright = format(94, 39);\nexport const magentaBright = format(95, 39);\nexport const cyanBright = format(96, 39);\nexport const whiteBright = format(97, 39);\n\nexport const bgRedBright = format(101, 49);\nexport const bgGreenBright = format(102, 49);\nexport const bgYellowBright = format(103, 49);\nexport const bgBlueBright = format(104, 49);\nexport const bgMagentaBright = format(105, 49);\nexport const bgCyanBright = format(106, 49);\nexport const bgWhiteBright = format(107, 49);\n","import process from 'node:process';\n\nexport default function isUnicodeSupported() {\n\tconst {env} = process;\n\tconst {TERM, TERM_PROGRAM} = env;\n\n\tif (process.platform !== 'win32') {\n\t\treturn TERM !== 'linux'; // Linux console (kernel)\n\t}\n\n\treturn Boolean(env.WT_SESSION) // Windows Terminal\n\t\t|| Boolean(env.TERMINUS_SUBLIME) // Terminus (<0.2.27)\n\t\t|| env.ConEmuTask === '{cmd::Cmder}' // ConEmu and cmder\n\t\t|| TERM_PROGRAM === 'Terminus-Sublime'\n\t\t|| TERM_PROGRAM === 'vscode'\n\t\t|| TERM === 'xterm-256color'\n\t\t|| TERM === 'alacritty'\n\t\t|| TERM === 'rxvt-unicode'\n\t\t|| TERM === 'rxvt-unicode-256color'\n\t\t|| env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';\n}\n","import { Function } from \"./parser.js\"\nimport { delay, Listr } from \"listr2\"\nimport { generateText } from \"ai\"\nimport { openai } from \"@ai-sdk/openai\"\nimport { anthropic } from \"@ai-sdk/anthropic\"\nimport { cohere } from \"@ai-sdk/cohere\"\nimport { mistral } from \"@ai-sdk/mistral\"\nimport { bedrock } from \"@ai-sdk/amazon-bedrock\"\nimport fs from \"fs/promises\"\nimport dotenv from \"dotenv\"\nimport path from \"path\"\nimport { CliOptions, DocumentedFunction } from \"./types/providers.js\"\nimport { groq } from \"@ai-sdk/groq\"\nimport { azure } from \"@ai-sdk/azure\"\nimport { confirm } from \"@inquirer/prompts\"\n\ndotenv.config()\n\nconst DOCUMENTATION_FORMATS = {\n ts: {\n format: \"JSDoc\",\n example: `/**\n * Function description\n * @param {type} paramName - Parameter description\n * @returns {type} Return value description\n */`,\n },\n js: {\n format: \"JSDoc\",\n example: `/**\n * Function description\n * @param {type} paramName - Parameter description\n * @returns {type} Return value description\n */`,\n },\n java: {\n format: \"Javadoc\",\n example: `/**\n * Method description\n * @param paramName Parameter description\n * @return Return value description\n */`,\n },\n py: {\n format: \"Docstring\",\n example: `\"\"\"\nFunction description\n\nArgs:\n param_name (type): Parameter description\n\nReturns:\n type: Return value description\n\"\"\"`,\n },\n rs: {\n format: \"Rustdoc\",\n example: `/// Function description\n/// \n/// # Arguments\n/// \n/// * \\`param_name\\` - Parameter description\n/// \n/// # Returns\n/// \n/// Return value description`,\n },\n go: {\n format: \"GoDoc\",\n example: `// FunctionName does something specific\n//\n// It takes some parameters and returns something else.\n//\n// Parameters:\n// - param1: description of param1\n// - param2: description of param2\n//\n// Returns:\n// description of return value`,\n },\n} as const\n\nexport const documentedFunctions: DocumentedFunction[] = []\n\n/**\n * Retrieves the appropriate AI provider function based on the specified options.\n * @param {CliOptions} options - The configuration options that include the provider type and model.\n * @returns {Function} The AI provider function corresponding to the specified provider.\n */\nconst getAiProvider = (options: CliOptions) => {\n switch (options.provider) {\n case \"openai\":\n return openai(options.model!)\n case \"anthropic\":\n return anthropic(options.model!)\n case \"cohere\":\n return cohere(options.model!)\n case \"mistral\":\n return mistral(options.model!)\n case \"bedrock\":\n return bedrock(options.model!)\n case \"groq\":\n return groq(options.model!)\n case \"azure\":\n return azure(options.model!)\n default:\n throw new Error(`Unsupported provider: ${options.provider}`)\n }\n}\n\n/**\n * Generates documentation for a list of undocumented functions by utilizing an AI provider to create JSDoc comments based on the function's source code.\n * @param {Function[]} undocumentedFunctions - An array of functions that lack documentation.\n * @param {CliOptions} options - Configuration options for the documentation generation process, including the AI provider and model settings.\n * @returns {Promise<void>} A promise that resolves when the documentation generation process is complete.\n */\nexport async function generateDocs(\n undocumentedFunctions: Function[],\n options: CliOptions\n) {\n let task: Listr<Function>\n let functionsToReturn: DocumentedFunction[] = []\n\n task = new Listr<Function>(\n undocumentedFunctions\n .sort((a, b) => b.startLine - a.startLine)\n .map((func) => ({\n title: `${func.name}`,\n task: async (): Promise<void> => {\n const fileExt = path.extname(func.filePath).slice(1)\n const langKey = fileExt.replace(\n \"tsx\",\n \"ts\"\n ) as keyof typeof DOCUMENTATION_FORMATS\n const docFormat = DOCUMENTATION_FORMATS[langKey]\n\n const prompt = `You are a documentation generator. Given this ${getLanguageName(\n fileExt\n )} function, write a ${\n docFormat.format\n } comment that describes what it does, its parameters, and return value.\n\nIMPORTANT: \n1. Respond ONLY with the documentation comment\n2. Do NOT include any markdown formatting or code blocks\n3. Follow this exact format:\n${docFormat.example}\n\nHere's the function to document:\n\n${func.sourceCode}`\n\n try {\n if (options.debug) {\n console.log(\"Debug: Generating documentation with options:\", {\n provider: options.provider,\n model: options.model,\n temperature: options.temperature,\n })\n }\n\n const { text: docComment } = await generateText({\n model: getAiProvider(options),\n temperature: options.temperature,\n prompt,\n })\n\n if (!docComment) throw new Error(\"No documentation generated\")\n\n // Validate the response format\n const cleanedDoc = validateAndCleanResponse(docComment, langKey)\n\n functionsToReturn.push({\n filePath: func.filePath,\n name: func.name,\n documentation: cleanedDoc,\n })\n\n // Read the file\n const fileContent = await fs.readFile(func.filePath, \"utf-8\")\n const lines = fileContent.split(\"\\n\")\n\n // Special handling for Python - insert after the def line\n if (langKey === \"py\") {\n // Find the first non-empty line in the function body\n let insertLine = func.startLine + 1\n while (\n insertLine <= func.endLine &&\n lines[insertLine].trim() === \"\"\n ) {\n insertLine++\n }\n\n // Add proper indentation\n const defLine = lines[func.startLine]\n const indentation = defLine.match(/^\\s*/)?.[0] || \"\"\n const indentedDoc = cleanedDoc\n .split(\"\\n\")\n .map((line) => indentation + \" \" + line) // Add 4 spaces for Python indentation\n .join(\"\\n\")\n\n // Insert the documentation\n lines.splice(insertLine, 0, indentedDoc)\n } else {\n // For other languages, insert before the function\n lines.splice(func.startLine, 0, cleanedDoc)\n }\n\n // Write the updated content back to the file\n await fs.writeFile(func.filePath, lines.join(\"\\n\"))\n\n // Update documented flag\n func.isDocumented = true\n\n // Apply rate limiting if specified\n await delay(options[\"rate-limit\"] ?? 0)\n } catch (error: any) {\n throw new Error(\n `Failed to generate docs for ${func.name}: ${error.message}`\n )\n }\n },\n })),\n {\n concurrent: options.concurrent ?? false,\n rendererOptions: {\n collapseSubtasks: options.output === \"minimal\",\n collapseErrors: options.output === \"minimal\",\n },\n }\n )\n\n try {\n await task.run()\n } catch (e: any) {\n console.error(e)\n }\n\n return functionsToReturn\n}\n\n/**\n * Returns the name of the programming language associated with a given file extension.\n * @param {string} ext - The file extension for which to retrieve the language name.\n * @returns {string} The name of the programming language, or the original extension if not recognized.\n */\nfunction getLanguageName(ext: string): string {\n const langMap: Record<string, string> = {\n ts: \"TypeScript\",\n tsx: \"TypeScript\",\n js: \"JavaScript\",\n jsx: \"JavaScript\",\n py: \"Python\",\n rs: \"Rust\",\n java: \"Java\",\n go: \"Go\",\n }\n return langMap[ext] || ext\n}\n\n/**\n * Validates and cleans a documentation response string based on the specified language format.\n * The function removes any markdown code block indicators and checks if the cleaned response\n * adheres to the expected documentation format for the given language key.\n * @param {string} response - The documentation response string to be validated and cleaned.\n * @param {keyof typeof DOCUMENTATION_FORMATS} langKey - The key representing the language format\n * for validation (e.g., 'ts' for TypeScript, 'java' for Java, etc.).\n * @returns {string} The cleaned and validated documentation response string.\n */\nfunction validateAndCleanResponse(\n response: string,\n langKey: keyof typeof DOCUMENTATION_FORMATS\n): string {\n // Remove any markdown code block indicators\n let cleaned = response.replace(/```[\\w-]*\\n?|\\n```/g, \"\").trim()\n\n // Validate based on language\n switch (langKey) {\n case \"ts\":\n case \"js\":\n if (!cleaned.startsWith(\"/**\") || !cleaned.endsWith(\"*/\")) {\n throw new Error(\"Invalid JSDoc format\")\n }\n break\n case \"java\":\n if (!cleaned.startsWith(\"/**\") || !cleaned.endsWith(\"*/\")) {\n throw new Error(\"Invalid Javadoc format\")\n }\n break\n case \"py\":\n if (!cleaned.startsWith('\"\"\"') || !cleaned.endsWith('\"\"\"')) {\n throw new Error(\"Invalid Python docstring format\")\n }\n break\n case \"rs\":\n if (!cleaned.startsWith(\"///\")) {\n throw new Error(\"Invalid Rustdoc format\")\n }\n break\n case \"go\":\n if (!cleaned.startsWith(\"//\")) {\n throw new Error(\"Invalid GoDoc format\")\n }\n // Ensure each line starts with //\n cleaned = cleaned\n .split(\"\\n\")\n .map((line) => (line.trim().startsWith(\"//\") ? line : `// ${line}`))\n .join(\"\\n\")\n break\n }\n\n return cleaned\n}\n\ninterface ReadmeContext {\n fileGroups: Record<string, DocumentedFunction[]>\n fileSummaries: Array<{\n filePath: string\n summary: string\n functions: DocumentedFunction[]\n }>\n readmeContent: string\n}\n\n/**\n * Generates a comprehensive README file documenting the codebase functionality\n * @param {Function[]} functions - Array of all documented functions\n * @param {CliOptions} options - Configuration options\n * @returns {Promise<void>}\n */\nexport async function generateReadme(\n functions: DocumentedFunction[],\n options: CliOptions\n) {\n const task = new Listr<ReadmeContext>(\n [\n {\n title: \"Analyzing codebase structure\",\n task: (ctx) => {\n ctx.fileGroups = functions.reduce((acc, func) => {\n if (!acc[func.filePath]) {\n acc[func.filePath] = []\n }\n acc[func.filePath].push(func)\n return acc\n }, {} as Record<string, DocumentedFunction[]>)\n ctx.fileSummaries = []\n },\n },\n {\n title: \"Generating file summaries\",\n task: (ctx, task): Listr =>\n task.newListr(\n Object.entries(ctx.fileGroups).map(([filePath, fileFunctions]) => ({\n title: `Summarizing ${filePath}`,\n task: async () => {\n 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.\n\nFunctions in this file:\n${fileFunctions\n .map(\n (f: DocumentedFunction) => `\nFunction Name: ${f.name}\nDocumentation: ${f.documentation}\n`\n )\n .join(\"\\n\\n\")}`\n\n const { text: fileSummary } = await generateText({\n model: getAiProvider(options),\n temperature: 0.3,\n prompt: filePrompt,\n })\n\n ctx.fileSummaries.push({\n filePath,\n summary: fileSummary,\n functions: fileFunctions,\n })\n },\n })),\n {\n concurrent: 5,\n rendererOptions: {\n collapseSubtasks: true,\n },\n }\n ),\n },\n {\n title: \"Generating README content\",\n task: async (ctx) => {\n 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.\n\nInclude these sections:\n1. Overview\n2. File Structure\n3. Key Features\n4. Architecture\n\nHere are the file summaries and their functions:\n\n${ctx.fileSummaries\n .map(\n (file) => `\n## ${file.filePath}\n${file.summary}\n`\n )\n .join(\"\\n\")}`\n\n const { text: readmeContent } = await generateText({\n model: getAiProvider(options),\n temperature: 0.3,\n prompt: readmePrompt,\n })\n\n ctx.readmeContent = readmeContent\n },\n },\n {\n title: \"Writing README file\",\n task: async (ctx) => {\n await fs.writeFile(\"REPPY-README.md\", ctx.readmeContent, \"utf-8\")\n },\n },\n ],\n {\n rendererOptions: {\n collapseSubtasks: options.output === \"minimal\",\n collapseErrors: options.output === \"minimal\",\n },\n }\n )\n\n try {\n console.log(\"\\n\")\n await task.run({} as ReadmeContext)\n\n if (options.debug) {\n console.log(\"Debug: Generated REPPY-README.md successfully\")\n if (task.errors.length > 0) {\n console.log(\"Debug: Encountered errors:\", task.errors)\n }\n }\n } catch (error: any) {\n console.error(\"Failed to generate README:\", error.message)\n }\n}\n","import commandLineArgs, { OptionDefinition } from \"command-line-args\"\nimport commandLineUsage from \"command-line-usage\"\nimport { CliOptions, SupportedProvider } from \"./types/providers.js\"\nimport pc from \"picocolors\"\nimport { execSync } from \"child_process\"\nimport { Listr } from \"listr2\"\nimport { glob } from \"glob\"\nimport path from \"path\"\nimport { minimatch } from \"minimatch\"\n\n// Extend OptionDefinition to include description\ninterface CommandOption extends OptionDefinition {\n description: string\n}\n\nconst SUPPORTED_PROVIDERS = [\n \"openai\",\n \"anthropic\",\n \"cohere\",\n \"mistral\",\n \"azure\",\n \"groq\",\n \"bedrock\",\n] as const\n\nconst optionDefinitions: CommandOption[] = [\n {\n name: \"help\",\n alias: \"h\",\n type: Boolean,\n description: \"Display this help message\",\n },\n {\n name: \"provider\",\n alias: \"p\",\n type: String,\n defaultValue: \"openai\",\n description:\n \"AI provider to use (openai, anthropic, cohere, mistral, azure, groq, bedrock)\",\n },\n {\n name: \"model\",\n alias: \"m\",\n type: String,\n description: \"Model to use for generation\",\n },\n {\n name: \"temperature\",\n alias: \"t\",\n type: Number,\n defaultValue: 0.1,\n description: \"Temperature for generation (0-1)\",\n },\n {\n name: \"files\",\n alias: \"f\",\n type: String,\n multiple: true,\n description: \"Files or globs to process\",\n },\n {\n name: \"ignore\",\n alias: \"i\",\n type: String,\n multiple: true,\n description: \"Files or globs to ignore\",\n },\n {\n name: \"debug\",\n alias: \"d\",\n type: Boolean,\n defaultValue: false,\n description: \"Enable debug logging\",\n },\n {\n name: \"concurrent\",\n alias: \"c\",\n type: Number,\n defaultValue: 1,\n description: \"Number of functions to process concurrently (default: 1)\",\n },\n {\n name: \"rate-limit\",\n type: Number,\n defaultValue: 1000,\n description: \"Rate limit between API calls in ms\",\n },\n {\n name: \"output\",\n alias: \"o\",\n type: String,\n defaultValue: \"normal\",\n description: \"Output verbosity (minimal, normal, verbose)\",\n },\n {\n name: \"unsafe\",\n type: Boolean,\n defaultValue: false,\n description: \"Skip Git repository checks\",\n },\n]\n\nconst helpSections = [\n {\n header: pc.cyan(\"Reppy\"),\n content: \"Automatically generate documentation for your codebase using AI.\",\n },\n {\n header: \"Usage\",\n content: [\n \"$ reppy [options]\",\n \"\",\n \"Example:\",\n '$ reppy -p anthropic -m \"claude-3-sonnet\" -t 0.2',\n ],\n },\n {\n header: \"Options\",\n optionList: optionDefinitions,\n },\n {\n header: \"Environment Variables\",\n content: [\n { name: \"OPENAI_API_KEY\", summary: \"Required for OpenAI provider\" },\n { name: \"ANTHROPIC_API_KEY\", summary: \"Required for Anthropic provider\" },\n { name: \"AZURE_API_KEY\", summary: \"Required for Azure provider\" },\n { name: \"AZURE_ENDPOINT\", summary: \"Required for Azure provider\" },\n { name: \"MISTRAL_API_KEY\", summary: \"Required for Mistral provider\" },\n { name: \"COHERE_API_KEY\", summary: \"Required for Cohere provider\" },\n { name: \"GROQ_API_KEY\", summary: \"Required for Groq provider\" },\n {\n name: \"AWS_ACCESS_KEY_ID\",\n summary: \"Required for Amazon Bedrock provider\",\n },\n {\n name: \"AWS_SECRET_ACCESS_KEY\",\n summary: \"Required for Amazon Bedrock provider\",\n },\n { name: \"AWS_REGION\", summary: \"Required for Amazon Bedrock provider\" },\n ],\n },\n {\n header: \"Examples\",\n content: [\n {\n desc: \"1. Use OpenAI with GPT-4\",\n example: \"$ reppy -p openai -m gpt-4\",\n },\n {\n desc: \"2. Use Anthropic with custom temperature\",\n example: \"$ reppy -p anthropic -t 0.2\",\n },\n {\n desc: \"3. Process specific files\",\n example: '$ reppy -f \"src/**/*.ts\"',\n },\n {\n desc: \"4. Ignore test files\",\n example: '$ reppy -i \"**/*.test.ts\" \"**/*.spec.ts\"',\n },\n {\n desc: \"5. Process 4 functions concurrently\",\n example: \"$ reppy --concurrent 4\",\n },\n {\n desc: \"6. Debug mode with minimal output\",\n example: \"$ reppy --debug --output minimal\",\n },\n ],\n },\n]\n\nconst defaultModels = {\n openai: \"gpt-4.1-mini\",\n anthropic: \"claude-3.5-sonnet\",\n cohere: \"command\",\n mistral: \"mistral-tiny\",\n bedrock: \"claude-3.5-sonnet\",\n groq: \"mixtral-8x7b-32768\",\n azure: \"gpt-4.1-mini\",\n} as const\n\nconst ENV_REQUIREMENTS = {\n openai: [\"OPENAI_API_KEY\"],\n anthropic: [\"ANTHROPIC_API_KEY\"],\n azure: [\"AZURE_API_KEY\", \"AZURE_RESOURCE_NAME\"],\n mistral: [\"MISTRAL_API_KEY\"],\n cohere: [\"COHERE_API_KEY\"],\n groq: [\"GROQ_API_KEY\"],\n bedrock: [\"AWS_ACCESS_KEY_ID\", \"AWS_SECRET_ACCESS_KEY\", \"AWS_REGION\"],\n} as const\n\n/**\n * Validates the presence of required environment variables for a given provider.\n * @param {SupportedProvider} provider - The provider for which to validate environment variables.\n * @throws {Error} Throws an error if any required environment variables are missing or empty.\n */\nfunction validateEnvironmentVariables(provider: SupportedProvider) {\n const requiredVars = ENV_REQUIREMENTS[provider]\n const missingVars = requiredVars.filter(\n (envVar) => !process.env[envVar] || process.env[envVar]?.trim() === \"\"\n )\n\n if (missingVars.length > 0) {\n throw new Error(\n `Missing required environment variables for ${provider}: ${missingVars.join(\n \", \"\n )}\\nPlease set these in your .env file.`\n )\n }\n}\n\n/**\n * Processes file patterns and returns matched files\n */\nfunction processFilePatterns(\n includePatterns: string[] = [],\n ignorePatterns: string[] = []\n): string[] {\n // If no include patterns specified, use default\n const patterns = includePatterns.length > 0 ? includePatterns : [\"**/*\"]\n\n // Build ignore patterns - always ignore node_modules and git\n const defaultIgnores = [\"**/nod