reppy
Version:
Let reppy generate documentation for the functions in your codebase.
1,390 lines (1,360 loc) • 44.7 kB
JavaScript
;
;
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