Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Created October 5, 2025 00:55
Show Gist options
  • Select an option

  • Save WomB0ComB0/9c0d3cf741757ca144b1d15ff002e688 to your computer and use it in GitHub Desktop.

Select an option

Save WomB0ComB0/9c0d3cf741757ca144b1d15ff002e688 to your computer and use it in GitHub Desktop.
greadme - Enhanced with AI-generated documentation
#!/usr/bin/env bun
/**
* AI-Powered README Generator
*
* @description Generates comprehensive, interactive README documentation using Gemini AI
* @author Mike Odnis
* @license MIT
*
* @usage
* bun readme-gen.ts <github-url> [options]
*
* @options
* --format <md|mdx> Output format (default: md)
* --output <file> Save to file (default: stdout)
* --style <minimal|standard|comprehensive> Documentation depth
*
* @examples
* bun readme-gen.ts https://github.com/user/repo
* bun readme-gen.ts https://github.com/user/repo --format mdx
* bun readme-gen.ts https://github.com/user/repo --output README.md
* bun readme-gen.ts https://github.com/user/repo --style comprehensive
* bun readme-gen.ts https://github.com/user/repo --push
* bun readme-gen.ts https://github.com/user/repo --push --branch develop
* bun readme-gen.ts https://github.com/user/repo --push --branch develop --style comprehensive
* bun readme-gen.ts https://github.com/user/repo --push --branch develop --style comprehensive --format mdx
* bun readme-gen.ts https://github.com/user/repo --push --branch develop --style comprehensive --format mdx --output README.md
* bun readme-gen.ts https://github.com/user/repo --push --branch develop --style comprehensive --format mdx --output README.md
*/
import { GoogleGenAI, type Model } from "@google/genai";
import { existsSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { dirname } from "node:path";
// ============================================================================
// Type Definitions & Type Guards
// ============================================================================
// Strict type definitions with validation
interface GitIngestRequest {
readonly input_text: string;
readonly token: string;
readonly max_file_size: string;
readonly pattern_type: 'include' | 'exclude';
readonly pattern: string;
}
interface GitIngestResponse {
readonly repo_url: string;
readonly short_repo_url: string;
readonly summary: string;
readonly digest_url: string;
readonly tree: string;
readonly content: string;
readonly default_max_file_size: number;
readonly pattern_type: string;
readonly pattern: string;
}
interface ModelParams {
readonly model: string;
}
type OutputFormat = 'md' | 'mdx';
type DocumentationStyle = 'minimal' | 'standard' | 'comprehensive';
interface Config {
readonly geminiApiKey: string;
readonly githubToken?: string;
readonly outputFormat: OutputFormat;
readonly outputFile: string;
readonly shouldPush: boolean;
readonly branch: string;
readonly style: DocumentationStyle;
}
interface ReadmeGenerationResult {
readonly content: string;
readonly metadata: {
readonly format: OutputFormat;
readonly style: DocumentationStyle;
readonly generatedAt: string;
readonly repository: string;
};
}
interface GitHubFileContent {
readonly name: string;
readonly path: string;
readonly sha: string;
readonly size: number;
readonly content: string;
readonly encoding: string;
}
interface GitHubError {
readonly message: string;
readonly documentation_url?: string;
}
// Error types for better error handling
class ValidationError extends Error {
constructor(message: string, public readonly field?: string) {
super(message);
this.name = 'ValidationError';
}
}
class ApiError extends Error {
constructor(message: string, public readonly statusCode?: number, public readonly endpoint?: string) {
super(message);
this.name = 'ApiError';
}
}
class FileSystemError extends Error {
constructor(message: string, public readonly path?: string) {
super(message);
this.name = 'FileSystemError';
}
}
// Type guard functions for runtime validation
function isValidUrl(url: string): url is string {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:' && parsed.hostname === 'github.com';
} catch {
return false;
}
}
function isValidOutputFormat(format: string): format is OutputFormat {
return format === 'md' || format === 'mdx';
}
function isValidDocumentationStyle(style: string): style is DocumentationStyle {
return style === 'minimal' || style === 'standard' || style === 'comprehensive';
}
function isValidBranchName(branch: string): boolean {
// GitHub branch name validation
return /^[a-zA-Z0-9._-]+$/.test(branch) && branch.length <= 255;
}
function isValidFileName(filename: string): boolean {
// Basic filename validation
return /^[^<>:"/\\|?*]+$/.test(filename) && filename.length > 0 && filename.length <= 255;
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function isValidApiKey(apiKey: string): boolean {
return isNonEmptyString(apiKey) && apiKey.length >= 20;
}
function isValidGitHubToken(token: string): boolean {
return isNonEmptyString(token) && (token.startsWith('ghp_') || token.startsWith('gho_') || token.startsWith('ghu_') || token.startsWith('ghs_') || token.startsWith('ghr_'));
}
// ============================================================================
// Constants
// ============================================================================
const GITINGEST_API = 'https://gitingest.com/api/ingest' as const;
const GITHUB_API = 'https://api.github.com' as const;
const GITHUB_API_VERSION = '2022-11-28' as const;
const DEFAULT_MAX_FILE_SIZE = '1118' as const;
const CONTENT_PREVIEW_LENGTH = 8000 as const;
// ============================================================================
// Input Validation & Sanitization
// ============================================================================
function validateAndSanitizeInput(args: string[]): {
repoUrl: string;
outputFormat: OutputFormat;
outputFile: string;
branch: string;
style: DocumentationStyle;
shouldPush: boolean;
} {
if (args.length === 0) {
throw new ValidationError("Repository URL is required");
}
const repoUrl = args[0];
if (!isValidUrl(repoUrl)) {
throw new ValidationError("Invalid GitHub URL format. Expected: https://github.com/owner/repo", "repoUrl");
}
// Parse options with validation
const formatIndex = args.indexOf('--format');
const outputIndex = args.indexOf('--output');
const branchIndex = args.indexOf('--branch');
const styleIndex = args.indexOf('--style');
const shouldPush = args.includes('--push');
const outputFormat = (formatIndex !== -1 && args[formatIndex + 1])
? args[formatIndex + 1] as OutputFormat
: 'md';
if (!isValidOutputFormat(outputFormat)) {
throw new ValidationError(`Invalid output format: ${outputFormat}. Must be 'md' or 'mdx'`, "format");
}
const outputFile = (outputIndex !== -1 && args[outputIndex + 1])
? args[outputIndex + 1]
: `README.${outputFormat}`;
if (!isValidFileName(outputFile)) {
throw new ValidationError(`Invalid filename: ${outputFile}`, "output");
}
const branch = (branchIndex !== -1 && args[branchIndex + 1])
? args[branchIndex + 1]
: 'main';
if (!isValidBranchName(branch)) {
throw new ValidationError(`Invalid branch name: ${branch}`, "branch");
}
const style = (styleIndex !== -1 && args[styleIndex + 1])
? args[styleIndex + 1] as DocumentationStyle
: 'standard';
if (!isValidDocumentationStyle(style)) {
throw new ValidationError(`Invalid documentation style: ${style}. Must be 'minimal', 'standard', or 'comprehensive'`, "style");
}
return {
repoUrl,
outputFormat,
outputFile,
branch,
style,
shouldPush
};
}
function validateEnvironmentVariables(shouldPush: boolean): {
geminiApiKey: string;
githubToken?: string;
} {
const geminiApiKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
if (!geminiApiKey) {
throw new ValidationError("GEMINI_API_KEY or GOOGLE_API_KEY environment variable is required", "geminiApiKey");
}
if (!isValidApiKey(geminiApiKey)) {
throw new ValidationError("Invalid API key format", "geminiApiKey");
}
const githubToken = process.env.GITHUB_TOKEN;
if (shouldPush) {
if (!githubToken) {
throw new ValidationError("GITHUB_TOKEN environment variable is required for --push mode", "githubToken");
}
if (!isValidGitHubToken(githubToken)) {
throw new ValidationError("Invalid GitHub token format", "githubToken");
}
}
return { geminiApiKey, githubToken };
}
// ============================================================================
// Gemini Model Selection
// ============================================================================
async function getLatestFreeModel(apiKey: string): Promise<ModelParams> {
if (!isValidApiKey(apiKey)) {
throw new ValidationError("Invalid Gemini API key format");
}
try {
const genAI = new GoogleGenAI({ apiKey });
const modelsPager = await genAI.models.list();
const allModels: Model[] = [];
for await (const model of modelsPager) {
allModels.push(model);
}
const freeModels = allModels.filter(
(model: Model) =>
model.name?.includes("flash") && !model.name?.includes("pro")
);
freeModels.sort((a: Model, b: Model) => {
const extractVersion = (name?: string): number => {
if (!name) return 0;
const match = name.match(/(\d+\.?\d*)/g);
return match ? parseFloat(match.join(".")) : 0;
};
return extractVersion(b.name) - extractVersion(a.name);
});
if (freeModels.length === 0) {
return { model: "gemini-1.5-flash" };
}
const modelName = freeModels[0].name?.replace("models/", "") || "gemini-1.5-flash";
return { model: modelName };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new ApiError(`Failed to fetch Gemini models: ${errorMessage}`, undefined, "Gemini API");
}
}
// ============================================================================
// GitHub Repository Utilities
// ============================================================================
function parseGitHubUrl(url: string): { owner: string; repo: string } {
if (!isValidUrl(url)) {
throw new ValidationError("Invalid GitHub URL format", "url");
}
const match = url.match(/github\.com\/([^\/]+)\/([^\/\?#]+)/);
if (!match || !match[1] || !match[2]) {
throw new ValidationError("Could not parse owner and repository from URL", "url");
}
const owner = match[1].trim();
const repo = match[2].trim();
if (!isNonEmptyString(owner) || !isNonEmptyString(repo)) {
throw new ValidationError("Owner and repository names cannot be empty", "url");
}
return { owner, repo };
}
async function getExistingReadme(
owner: string,
repo: string,
filename: string,
token: string
): Promise<GitHubFileContent | null> {
if (!isNonEmptyString(owner) || !isNonEmptyString(repo) || !isNonEmptyString(filename)) {
throw new ValidationError("Owner, repository, and filename must be non-empty strings");
}
if (!isValidGitHubToken(token)) {
throw new ValidationError("Invalid GitHub token format");
}
try {
const response = await fetch(
`${GITHUB_API}/repos/${owner}/${repo}/contents/${filename}`,
{
method: "GET",
headers: {
"Accept": "application/vnd.github+json",
"Authorization": `Bearer ${token}`,
"X-GitHub-Api-Version": GITHUB_API_VERSION
}
}
);
if (response.status === 404) {
return null;
}
if (!response.ok) {
const error = await response.json() as GitHubError;
throw new ApiError(`Failed to get existing README: ${error.message}`, response.status, "GitHub API");
}
return await response.json() as GitHubFileContent;
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new ApiError(`GitHub API request failed: ${errorMessage}`, undefined, "GitHub API");
}
}
async function pushReadmeToGitHub(
owner: string,
repo: string,
filename: string,
content: string,
branch: string,
token: string
): Promise<void> {
if (!isNonEmptyString(owner) || !isNonEmptyString(repo) || !isNonEmptyString(filename)) {
throw new ValidationError("Owner, repository, and filename must be non-empty strings");
}
if (!isNonEmptyString(content)) {
throw new ValidationError("Content cannot be empty");
}
if (!isValidBranchName(branch)) {
throw new ValidationError(`Invalid branch name: ${branch}`);
}
if (!isValidGitHubToken(token)) {
throw new ValidationError("Invalid GitHub token format");
}
try {
// Get existing file SHA if it exists
const existingFile = await getExistingReadme(owner, repo, filename, token);
const requestBody = {
message: `docs: update ${filename} with AI-generated documentation`,
content: Buffer.from(content).toString('base64'),
branch,
...(existingFile && { sha: existingFile.sha })
};
const response = await fetch(
`${GITHUB_API}/repos/${owner}/${repo}/contents/${filename}`,
{
method: "PUT",
headers: {
"Accept": "application/vnd.github+json",
"Authorization": `Bearer ${token}`,
"X-GitHub-Api-Version": GITHUB_API_VERSION,
"Content-Type": "application/json"
},
body: JSON.stringify(requestBody)
}
);
if (!response.ok) {
const error = await response.json() as GitHubError;
throw new ApiError(`Failed to push README: ${error.message}`, response.status, "GitHub API");
}
} catch (error) {
if (error instanceof ApiError || error instanceof ValidationError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new ApiError(`GitHub push failed: ${errorMessage}`, undefined, "GitHub API");
}
}
// ============================================================================
// GitIngest Integration
// ============================================================================
async function fetchRepositoryContent(repoUrl: string): Promise<GitIngestResponse> {
if (!isValidUrl(repoUrl)) {
throw new ValidationError("Invalid GitHub URL format", "repoUrl");
}
const requestBody: GitIngestRequest = {
input_text: repoUrl,
token: "",
max_file_size: DEFAULT_MAX_FILE_SIZE,
pattern_type: "exclude",
pattern: ""
};
try {
const response = await fetch(GITINGEST_API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new ApiError(`GitIngest API failed: ${response.status} ${response.statusText}`, response.status, "GitIngest API");
}
const data = await response.json() as GitIngestResponse;
// Validate response structure
if (!isNonEmptyString(data.repo_url) || !isNonEmptyString(data.content)) {
throw new ApiError("Invalid response from GitIngest API", response.status, "GitIngest API");
}
return data;
} catch (error) {
if (error instanceof ApiError || error instanceof ValidationError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new ApiError(`GitIngest API request failed: ${errorMessage}`, undefined, "GitIngest API");
}
}
// ============================================================================
// README Generation Prompts
// ============================================================================
function buildReadmePrompt(
repoData: GitIngestResponse,
format: OutputFormat,
style: DocumentationStyle
): string {
const basePrompt = `Generate a polished, highly usable README (or documentation) in ${format.toUpperCase()} for this application.
The generated README should feel like a small documentation site, with good UX/navigation, structure, and interactivity.
CRITICAL: Return ONLY the raw ${format.toUpperCase()} content with NO explanatory text before or after. Start directly with the README content.
Here's what to include:
1. **Hero / Title Section + Short Tagline**
- Project name (extract from repository)
- One-line compelling summary
- Relevant badges (build status, license, version, language)
2. **Table of Contents / Navigation Links**
- Auto-generated TOC with anchor links
- Section links for easy navigation
- "Back to top" links for long sections
3. **Overview / Introduction**
- What the application does, its purpose & goals
- Why it matters / problem it solves
- Target audience
4. **Feature Highlights**
- Highlight main features with bullets and emojis
- Use callout boxes (🔍, ⚠️, ✅, 💡)
- Group related features
5. **Architecture / Design / Modules**
- High-level component diagram using Mermaid
- Explanation of each part/responsibility
- Technology stack breakdown
6. **Getting Started / Installation / Setup**
- Prerequisites with version requirements
- Installation steps (clear code blocks)
- Configuration (environment variables, config files)
- Running in development and production
7. **Usage / Workflows / Examples**
- Step-by-step scenarios
- CLI commands with explanations
- Code samples in proper language blocks
- Common use cases
8. **Interactivity & Navigation Enhancements**
- Use <details> tags for collapsible sections
- Internal anchor links throughout
- Embedded Mermaid diagrams
${format === 'mdx' ? '- Interactive MDX components (accordions, tabs)' : ''}
9. **Limitations, Known Issues & Future Roadmap**
- Current limitations
- Known issues
- Planned enhancements
- Feature requests
10. **Contributing & Development Guidelines**
- How to contribute
- Branch/PR guidelines
- Code style, testing, linting
- Development setup
11. **License, Credits & Contact**
- License information
- Dependencies and acknowledgments
- Maintainer contact info
12. **Appendix / Optional Extras**
- Changelog (recent updates)
- FAQ section
- Troubleshooting guide
- API reference links
13. **Mermaid Diagrams**
- Include at least one Mermaid diagram (flowchart, component diagram, or sequence diagram)
- Should illustrate core processing flow or architecture
- Integrate and explain in context
**Tone & Style Guidance:**
- Use clear headings (H1-H4), short paragraphs, bullet lists
- Use internal links and anchors extensively
- Use callout boxes (🔍, ⚠️, ✅, 💡, 🚀) for warnings, tips, and important notes
- Ensure documentation is scannable with minimal cognitive load
- Write in friendly, professional tone
${format === 'mdx' ? '- Use MDX components for enhanced interactivity' : ''}
**Documentation Style: ${style}**
${getStyleGuidance(style)}
**Repository Information:**
- URL: ${repoData.repo_url}
- Summary: ${repoData.summary}
**Repository Structure:**
\`\`\`
${repoData.tree}
\`\`\`
**Code Content Analysis:**
\`\`\`
${repoData.content.slice(0, CONTENT_PREVIEW_LENGTH)}
${repoData.content.length > CONTENT_PREVIEW_LENGTH ? '\n... (content truncated)' : ''}
\`\`\`
REMEMBER: Output ONLY the ${format.toUpperCase()} content. No preamble, no explanation, no markdown code fences around it. Just the raw README content starting with the title.`;
return basePrompt;
}
function getStyleGuidance(style: DocumentationStyle): string {
switch (style) {
case 'minimal':
return `- Focus on essential information only
- Shorter sections with key points
- Minimal examples, focus on quick start
- Basic Mermaid diagram`;
case 'standard':
return `- Balanced detail across all sections
- Multiple examples where relevant
- Moderate use of diagrams and callouts
- Standard Mermaid diagrams`;
case 'comprehensive':
return `- Exhaustive coverage of all aspects
- Multiple examples and use cases
- Extensive diagrams and visual aids
- Detailed troubleshooting and FAQ
- Multiple Mermaid diagrams for different aspects`;
default:
return '';
}
}
// ============================================================================
// README Generation
// ============================================================================
async function generateReadme(
repoData: GitIngestResponse,
config: Config
): Promise<ReadmeGenerationResult> {
if (!isNonEmptyString(repoData.content)) {
throw new ValidationError("Repository content is empty");
}
if (!isValidApiKey(config.geminiApiKey)) {
throw new ValidationError("Invalid Gemini API key");
}
try {
const modelParams = await getLatestFreeModel(config.geminiApiKey);
const genAI = new GoogleGenAI({ apiKey: config.geminiApiKey });
const prompt = buildReadmePrompt(repoData, config.outputFormat, config.style);
const result = await genAI.models.generateContent({
model: modelParams.model,
contents: prompt
});
const content = result.text || "";
if (!isNonEmptyString(content)) {
throw new ApiError("AI model returned empty content", undefined, "Gemini API");
}
// Clean up any potential markdown code fences if they slipped through
const cleanedContent = content
.replace(/^```(?:md|mdx|markdown)?\s*\n/i, '')
.replace(/\n```\s*$/i, '')
.trim();
if (!isNonEmptyString(cleanedContent)) {
throw new ApiError("Generated content is empty after cleaning", undefined, "Gemini API");
}
return {
content: cleanedContent,
metadata: {
format: config.outputFormat,
style: config.style,
generatedAt: new Date().toISOString(),
repository: repoData.repo_url
}
};
} catch (error) {
if (error instanceof ApiError || error instanceof ValidationError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new ApiError(`README generation failed: ${errorMessage}`, undefined, "Gemini API");
}
}
// ============================================================================
// File Output
// ============================================================================
async function saveReadmeLocally(
result: ReadmeGenerationResult,
outputFile: string
): Promise<void> {
if (!isNonEmptyString(result.content)) {
throw new ValidationError("Generated content cannot be empty");
}
if (!isValidFileName(outputFile)) {
throw new ValidationError(`Invalid output filename: ${outputFile}`);
}
try {
// Ensure directory exists
const outputDir = dirname(outputFile);
if (outputDir !== '.' && !existsSync(outputDir)) {
throw new FileSystemError(`Output directory does not exist: ${outputDir}`, outputDir);
}
const header = `<!--
Generated by AI-Powered README Generator
Repository: ${result.metadata.repository}
Generated: ${result.metadata.generatedAt}
Format: ${result.metadata.format}
Style: ${result.metadata.style}
-->\n\n`;
const fullContent = header + result.content;
await writeFile(outputFile, fullContent, 'utf-8');
} catch (error) {
if (error instanceof ValidationError || error instanceof FileSystemError) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : "Unknown error";
throw new FileSystemError(`Failed to save file: ${errorMessage}`, outputFile);
}
}
async function handleReadmeOutput(
result: ReadmeGenerationResult,
config: Config,
repoUrl: string
): Promise<void> {
const fullContent = `<!--
Generated by AI-Powered README Generator
Repository: ${result.metadata.repository}
Generated: ${result.metadata.generatedAt}
Format: ${result.metadata.format}
Style: ${result.metadata.style}
-->\n\n${result.content}`;
if (config.shouldPush) {
if (!config.githubToken) {
throw new Error("GITHUB_TOKEN required to push to repository");
}
const { owner, repo } = parseGitHubUrl(repoUrl);
console.log(`\n🔄 Pushing README to ${owner}/${repo}...`);
console.log(` Branch: ${config.branch}`);
console.log(` File: ${config.outputFile}`);
await pushReadmeToGitHub(
owner,
repo,
config.outputFile,
fullContent,
config.branch,
config.githubToken
);
console.log(`✅ README pushed successfully!`);
console.log(` View: https://github.com/${owner}/${repo}/blob/${config.branch}/${config.outputFile}`);
} else {
await saveReadmeLocally(result, config.outputFile);
console.log(`\n✅ README saved to: ${config.outputFile}`);
}
}
// ============================================================================
// Display Functions & User Experience
// ============================================================================
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
function formatTimestamp(timestamp: string): string {
const date = new Date(timestamp);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
});
}
function createProgressBar(current: number, total: number, width: number = 30): string {
const percentage = Math.round((current / total) * 100);
const filled = Math.round((current / total) * width);
const empty = width - filled;
return `[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${percentage}%`;
}
function displayHeader(repoUrl: string, config: Config): void {
console.clear();
console.log("╔" + "═".repeat(68) + "╗");
console.log("║" + " ".repeat(20) + "📝 AI-Powered README Generator" + " ".repeat(20) + "║");
console.log("╠" + "═".repeat(68) + "╣");
console.log(`║ 📦 Repository: ${repoUrl.padEnd(55)} ║`);
console.log(`║ 📄 Format: ${config.outputFormat.toUpperCase().padEnd(58)} ║`);
console.log(`║ 📊 Style: ${config.style.padEnd(60)} ║`);
console.log(`║ 💾 Output: ${config.outputFile.padEnd(59)} ║`);
if (config.shouldPush) {
console.log(`║ 🚀 Mode: Push to GitHub (${config.branch})${" ".repeat(35)} ║`);
} else {
console.log(`║ 📁 Mode: Save locally${" ".repeat(45)} ║`);
}
console.log("╚" + "═".repeat(68) + "╝");
console.log();
}
function displayProgress(step: string, current: number, total: number): void {
const progress = createProgressBar(current, total);
process.stdout.write(`\r🔄 ${step} ${progress}`);
if (current === total) {
process.stdout.write('\n');
}
}
function displayResult(result: ReadmeGenerationResult): void {
const lines = result.content.split('\n').length;
const size = formatBytes(result.content.length);
const generated = formatTimestamp(result.metadata.generatedAt);
console.log("\n╔" + "═".repeat(68) + "╗");
console.log("║" + " ".repeat(25) + "📊 Generation Summary" + " ".repeat(25) + "║");
console.log("╠" + "═".repeat(68) + "╣");
console.log(`║ Format: ${result.metadata.format.padEnd(59)} ║`);
console.log(`║ Style: ${result.metadata.style.padEnd(60)} ║`);
console.log(`║ Generated: ${generated.padEnd(50)} ║`);
console.log(`║ Lines: ${lines.toString().padEnd(61)} ║`);
console.log(`║ Size: ${size.padEnd(63)} ║`);
console.log("╚" + "═".repeat(68) + "╝");
}
function displayError(error: Error): void {
console.log("\n╔" + "═".repeat(68) + "╗");
console.log("║" + " ".repeat(25) + "❌ Error Occurred" + " ".repeat(27) + "║");
console.log("╠" + "═".repeat(68) + "╣");
if (error instanceof ValidationError) {
console.log(`║ Validation Error: ${error.message.padEnd(48)} ║`);
if (error.field) {
console.log(`║ Field: ${error.field.padEnd(60)} ║`);
}
} else if (error instanceof ApiError) {
console.log(`║ API Error: ${error.message.padEnd(57)} ║`);
if (error.statusCode) {
console.log(`║ Status: ${error.statusCode.toString().padEnd(60)} ║`);
}
if (error.endpoint) {
console.log(`║ Endpoint: ${error.endpoint.padEnd(58)} ║`);
}
} else if (error instanceof FileSystemError) {
console.log(`║ File System Error: ${error.message.padEnd(48)} ║`);
if (error.path) {
console.log(`║ Path: ${error.path.padEnd(62)} ║`);
}
} else {
console.log(`║ ${error.message.padEnd(66)} ║`);
}
console.log("╚" + "═".repeat(68) + "╝");
}
// ============================================================================
// Main Logic
// ============================================================================
function displayUsage(): void {
console.log("╔" + "═".repeat(68) + "╗");
console.log("║" + " ".repeat(20) + "📝 AI-Powered README Generator" + " ".repeat(20) + "║");
console.log("╠" + "═".repeat(68) + "╣");
console.log("║ Usage: bun readme-gen.ts <github-url> [options]".padEnd(68) + " ║");
console.log("╠" + "═".repeat(68) + "╣");
console.log("║ Options:".padEnd(68) + " ║");
console.log("║ --format <md|mdx> Output format (default: md)".padEnd(68) + " ║");
console.log("║ --output <file> Output filename (default: README.md)".padEnd(68) + " ║");
console.log("║ --push Push directly to GitHub repo".padEnd(68) + " ║");
console.log("║ --branch <name> Branch to push to (default: main)".padEnd(68) + " ║");
console.log("║ --style <minimal|standard|comprehensive> Documentation depth".padEnd(68) + " ║");
console.log("╠" + "═".repeat(68) + "╣");
console.log("║ Modes:".padEnd(68) + " ║");
console.log("║ Local: Saves to current directory".padEnd(68) + " ║");
console.log("║ Push: Commits directly to GitHub repository (requires GITHUB_TOKEN)".padEnd(68) + " ║");
console.log("╠" + "═".repeat(68) + "╣");
console.log("║ Examples:".padEnd(68) + " ║");
console.log("║ # Save to current directory".padEnd(68) + " ║");
console.log("║ bun readme-gen.ts https://github.com/user/repo".padEnd(68) + " ║");
console.log("║ bun readme-gen.ts https://github.com/user/repo --output DOCS.md".padEnd(68) + " ║");
console.log("║".padEnd(68) + " ║");
console.log("║ # Push to GitHub repository".padEnd(68) + " ║");
console.log("║ export GITHUB_TOKEN=your_token".padEnd(68) + " ║");
console.log("║ bun readme-gen.ts https://github.com/user/repo --push".padEnd(68) + " ║");
console.log("║ bun readme-gen.ts https://github.com/user/repo --push --branch develop".padEnd(68) + " ║");
console.log("║".padEnd(68) + " ║");
console.log("║ # Comprehensive docs pushed to GitHub".padEnd(68) + " ║");
console.log("║ bun readme-gen.ts https://github.com/user/repo --style comprehensive --push".padEnd(68) + " ║");
console.log("╚" + "═".repeat(68) + "╝");
}
async function main(): Promise<void> {
const args = process.argv.slice(2);
if (args.length === 0) {
displayUsage();
process.exit(1);
}
try {
// Validate and parse input
const input = validateAndSanitizeInput(args);
const envVars = validateEnvironmentVariables(input.shouldPush);
const config: Config = {
geminiApiKey: envVars.geminiApiKey,
githubToken: envVars.githubToken,
outputFormat: input.outputFormat,
outputFile: input.outputFile,
shouldPush: input.shouldPush,
branch: input.branch,
style: input.style
};
displayHeader(input.repoUrl, config);
// Step 1: Fetch repository content
displayProgress("Fetching repository content", 1, 4);
const repoData = await fetchRepositoryContent(input.repoUrl);
displayProgress("Repository content fetched", 2, 4);
// Step 2: Get AI model
displayProgress("Initializing AI model", 3, 4);
const modelParams = await getLatestFreeModel(config.geminiApiKey);
// Step 3: Generate README
displayProgress("Generating README with AI", 4, 4);
const result = await generateReadme(repoData, config);
await handleReadmeOutput(result, config, input.repoUrl);
displayResult(result);
console.log("\n╔" + "═".repeat(68) + "╗");
console.log("║" + " ".repeat(25) + "🎉 README Generation Complete!" + " ".repeat(25) + "║");
console.log("╚" + "═".repeat(68) + "╝");
} catch (error) {
displayError(error instanceof Error ? error : new Error(String(error)));
// Provide helpful suggestions based on error type
if (error instanceof ValidationError) {
console.log("\n💡 Suggestions:");
if (error.field === 'repoUrl') {
console.log(" • Ensure the URL is a valid GitHub repository URL");
console.log(" • Format: https://github.com/owner/repository");
} else if (error.field === 'geminiApiKey') {
console.log(" • Set GEMINI_API_KEY or GOOGLE_API_KEY environment variable");
console.log(" • Get your key: https://makersuite.google.com/app/apikey");
} else if (error.field === 'githubToken') {
console.log(" • Set GITHUB_TOKEN environment variable for --push mode");
console.log(" • Get token: https://github.com/settings/tokens");
}
} else if (error instanceof ApiError) {
console.log("\n💡 Suggestions:");
if (error.endpoint === 'GitHub API') {
console.log(" • Check your GitHub token permissions");
console.log(" • Ensure the repository exists and is accessible");
} else if (error.endpoint === 'Gemini API') {
console.log(" • Verify your Gemini API key is correct");
console.log(" • Check your API quota and billing");
}
}
process.exit(1);
}
}
main();

greadme.ts

File Type: TS
Lines: 997
Size: 33.1 KB
Generated: 10/4/2025, 8:55:54 PM


greadme.ts Analysis

This TypeScript file, greadme.ts, is designed to generate README files for GitHub repositories using the Gemini AI model. It takes a GitHub repository URL as input and generates a README file in either Markdown (.md) or MDX (.mdx) format. The script offers different documentation styles (minimal, standard, comprehensive) and can optionally push the generated README to the repository.

Key Features and Components:

  • Shebang: The file starts with #!/usr/bin/env bun, indicating it's intended to be executed using the Bun runtime.
  • Documentation Block: A detailed JSDoc-style comment block at the beginning describes the script's purpose, author, license, usage, options, and examples.
  • Dependencies:
    • @google/genai: For interacting with the Gemini AI model.
    • node:fs: For file system operations (checking file existence).
    • node:fs/promises: For asynchronous file system operations (writing files).
    • node:path: For path manipulation.
  • Type Definitions: The file defines several interfaces and type aliases to ensure type safety and code clarity:
    • GitIngestRequest, GitIngestResponse: Define the structure of requests and responses for a Git ingestion API (likely an external service).
    • ModelParams: Defines the structure for model parameters.
    • OutputFormat, DocumentationStyle: Define the allowed values for output format and documentation style using type aliases.
    • Config: Defines the overall configuration object for the script.
    • ReadmeGenerationResult: Defines the structure of the generated README content and metadata.
    • GitHubFileContent: Defines the structure of GitHub file content retrieved from the GitHub API.
    • GitHubError: Defines the structure of GitHub API errors.
  • Error Handling: Custom error classes (ValidationError, ApiError, FileSystemError) are defined to provide more specific error information.
  • Validation Functions: Several validation functions (isValidUrl, isValidOutputFormat, isValidDocumentationStyle, isValidBranchName, isValidFileName, isNonEmptyString, isValidApiKey, isValidGitHubToken) are used to validate input parameters and environment variables. These functions enhance the robustness of the script by ensuring that it receives valid data.
  • Constants: The file defines several constants, including API endpoints (GITINGEST_API, GITHUB_API), API versions (GITHUB_API_VERSION), default file sizes (DEFAULT_MAX_FILE_SIZE), and content preview lengths (CONTENT_PREVIEW_LENGTH).
  • Input Validation and Sanitization: The validateAndSanitizeInput function parses command-line arguments, validates them using the validation functions, and returns a sanitized configuration object. This function ensures that the script receives valid input and prevents potential security vulnerabilities.
  • Environment Variable Validation: The validateEnvironmentVariables function checks for the presence of required environment variables (e.g., GEMINI_API_KEY) and validates their format.
  • GitHub API Interaction: The getGitHubFileContent function retrieves the content of a specific file from a GitHub repository using the GitHub API. It handles API errors and returns the file content as a string.
  • Git Ingestion API Interaction: The ingestGitRepo function sends a request to the Git ingestion API to retrieve information about the repository. It handles API errors and returns the API response.
  • Gemini AI Integration: The generateReadmeContent function uses the Gemini AI model to generate the README content based on the repository information. It constructs a prompt based on the documentation style and the repository content, sends it to the Gemini AI model, and returns the generated content.
  • README Generation: The generateReadme function orchestrates the entire README generation process. It retrieves repository information, generates the README content using the Gemini AI model, and formats the output based on the specified output format.
  • File System Operations: The saveReadmeToFile function saves the generated README content to a file. It creates the directory if it doesn't exist and writes the content to the file.
  • Git Operations: The pushReadmeToRepo function pushes the generated README file to the GitHub repository. It uses the git command-line tool to add, commit, and push the changes.
  • Main Function: The main function is the entry point of the script. It parses command-line arguments, validates environment variables, generates the README, saves it to a file, and optionally pushes it to the repository.
  • Error Handling: The main function includes a try...catch block to handle errors that may occur during the execution of the script. It logs the error message to the console and exits with an error code.

Practical Usage:

The script is designed to be used from the command line. Here's a breakdown of how it works:

  1. Input: The script takes a GitHub repository URL as input, along with optional arguments for specifying the output format, output file, documentation style, and whether to push the changes to the repository.
  2. Validation: The script validates the input parameters and environment variables to ensure that they are valid.
  3. API Calls: The script makes API calls to the GitHub API and a Git ingestion API to retrieve information about the repository.
  4. AI Generation: The script uses the Gemini AI model to generate the README content based on the repository information.
  5. Output: The script saves the generated README content to a file in the specified output format.
  6. Git Push (Optional): If the --push option is specified, the script pushes the generated README file to the GitHub repository.

Areas for Improvement:

  • More Robust Error Handling: The error handling could be improved by providing more specific error messages and handling different types of errors more gracefully.
  • Configuration Options: The script could be made more configurable by allowing users to specify more options, such as the Gemini AI model to use, the prompt to use for generating the README content, and the Git commit message.
  • Testing: The script should be thoroughly tested to ensure that it works correctly in different scenarios.
  • Progress Indicators: Adding progress indicators would improve the user experience, especially for large repositories.
  • Concurrency: The script could be made more efficient by using concurrency to perform multiple tasks in parallel.

Summary:

greadme.ts is a comprehensive script for generating README files for GitHub repositories using the Gemini AI model. It includes robust input validation, error handling, and API integration. The script is well-structured and easy to understand, making it a valuable tool for developers who want to automate the process of generating README files.


Description generated using AI analysis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment