readmebot
Version:
AI-powered README generator for projects. Automatically creates beautiful, structured documentation for Node.js, Python, React, and more.
319 lines (276 loc) • 10.7 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import chalk from 'chalk';
import ModelClient, { isUnexpected } from '@azure-rest/ai-inference';
import { AzureKeyCredential } from '@azure/core-auth';
// Initialize environment
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config();
// Configuration
const CONFIG = {
ENV_KEY: 'github_token',
ENDPOINT: 'https://models.github.ai/inference',
MODEL: 'openai/gpt-4.1',
MAX_SCAN_DEPTH: 3,
IGNORE_DIRS: new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', 'venv']),
IGNORE_FILES: new Set(['.DS_Store', 'Thumbs.db']),
SOURCE_EXTS: new Set([
'.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.rb', '.php',
'.java', '.go', '.rs', '.c', '.cpp', '.h', '.sh', '.ino'
]),
TEMPLATE_PARAMS: {
max_tokens: 3000,
temperature: 0.7
}
};
// --- Core Functions ---
const getApiToken = () => {
const args = process.argv.slice(2);
const cliKeyIndex = args.indexOf('--key');
if (cliKeyIndex !== -1 && args[cliKeyIndex + 1]) {
return args[cliKeyIndex + 1];
}
const envToken = process.env[CONFIG.ENV_KEY];
if (envToken) return envToken;
console.log(chalk.red(`
${chalk.bold('API token required!')}
${chalk.underline('Options:')}
1. ${chalk.green('Env File:')} Create '.env' with:
${CONFIG.ENV_KEY}=your_api_token
2. ${chalk.green('CLI Argument:')} Run with:
readmegen --key your_api_token
`));
process.exit(1);
};
const readPackageJson = () => {
try {
return JSON.parse(fs.readFileSync('package.json', 'utf-8'));
} catch {
return {
name: path.basename(process.cwd()),
version: '0.1.0',
description: ''
};
}
};
const scanDirectory = (dirPath = process.cwd(), depth = 0) => {
if (depth > CONFIG.MAX_SCAN_DEPTH) return [];
try {
return fs.readdirSync(dirPath, { withFileTypes: true })
.filter(dirent => !CONFIG.IGNORE_DIRS.has(dirent.name))
.map(dirent => {
const fullPath = path.join(dirPath, dirent.name);
const relativePath = path.relative(process.cwd(), fullPath);
if (dirent.isDirectory()) {
return {
type: 'directory',
name: dirent.name,
path: relativePath,
contents: scanDirectory(fullPath, depth + 1)
};
} else if (dirent.isFile() && CONFIG.SOURCE_EXTS.has(path.extname(dirent.name))) {
return {
type: 'file',
name: dirent.name,
path: relativePath,
ext: path.extname(dirent.name)
};
}
return null;
})
.filter(Boolean);
} catch (error) {
console.log(chalk.yellow(`⚠ Could not scan directory: ${dirPath}`));
return [];
}
};
const detectProjectType = (structure) => {
const extensions = new Set();
structure.forEach(item => {
if (item.type === 'file') extensions.add(item.ext);
if (item.type === 'directory') item.contents.forEach(file => extensions.add(file.ext));
});
return {
isNode: extensions.has('.js') || extensions.has('.mjs'),
isPython: extensions.has('.py'),
isRust: extensions.has('.rs'),
isCLI: structure.some(item =>
item.name === 'cli.js' ||
item.name === 'main.py' ||
item.path.includes('bin/')
)
};
};
const generatePrompt = (analysis) => {
const { metadata, structure } = analysis;
const projectName = metadata.name || 'Project';
const description = metadata.description || 'A well-documented project';
const repoName = metadata.repository?.url?.split('/').pop()?.replace('.git', '') || 'repository-name';
const userName = metadata.repository?.url?.split('/').slice(-2, -1)[0] || 'username';
// Detect all technologies used
const techStack = detectTechnologies(structure);
return `
Generate a professional README.md with this exact structure:
<div align="center">
<h1 align="center" style="font-size: 2.5em; margin-bottom: 0.5em;">
<img src="https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/ec559a9f6bfd399b82bb44393651661b08aaf7ba/icons/folder-markdown-open.svg" width="100" />
<br>${projectName.toUpperCase()}
</h1>
<h3 align="center" style="font-size: 1.2em; margin-top: 0;">${description}</h3>
<div align="center" style="margin: 1.5em 0;">
${techStack.map(tech => {
const badgeUrl = getTechBadgeUrl(tech);
return `<img src="${badgeUrl}" alt="${tech}" />`;
}).join('\n ')}
</div>
<div align="center" style="margin: 1.5em 0;">
<img src="https://img.shields.io/github/license/${userName}/${repoName}?style=for-the-badge&color=blue" alt="License" />
<img src="https://img.shields.io/github/last-commit/${userName}/${repoName}?style=for-the-badge" alt="Last Commit" />
<img src="https://img.shields.io/npm/v/${repoName}?style=for-the-badge" alt="npm version" />
</div>
</div>
# Detected Structure
${generateStructureTree(structure)}
# Required Sections
1. Detected Structure
2. Installation (using npm or pip after cloning the repository)
3. Contributing Guidelines (not compulary)
4. License (if present)
Format using GitHub Flavored Markdown with:
- Proper headings hierarchy
- Code blocks for examples
- Tables for CLI commands if applicable
`;
function getRepoInfo(pkg) {
try {
if (pkg.repository) {
const repoStr = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository.url;
const match = repoStr.match(/github\.com[/:]([^/]+)\/([^/]+?)(\.git)?$/);
if (match) return { user: match[1], repo: match[2] };
}
} catch (e) {
console.log(chalk.yellow('⚠ Could not parse repository URL'));
}
return { user: 'username', repo: 'repository' };
}
// Helper function to detect all technologies used
function detectTechnologies(structure) {
const extensions = new Set();
const techStack = new Set();
// Scan all files for extensions
structure.forEach(item => {
if (item.type === 'file') extensions.add(item.ext);
if (item.type === 'directory') {
item.contents.forEach(file => extensions.add(file.ext));
}
});
// Map extensions to technologies
const techMap = {
'.js': 'JavaScript',
'.ts': 'TypeScript',
'.jsx': 'React',
'.tsx': 'TypeScript',
'.py': 'Python',
'.rb': 'Ruby',
'.java': 'Java',
'.go': 'Go',
'.rs': 'Rust',
'.php': 'PHP',
'.sh': 'Bash'
};
extensions.forEach(ext => {
if (techMap[ext]) techStack.add(techMap[ext]);
});
return Array.from(techStack);
}
// Helper function to generate badges for all detected technologies
function getTechBadgeUrl(tech) {
const badgeMap = {
'JavaScript': 'https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black',
'TypeScript': 'https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white',
'Python': 'https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white',
'React': 'https://img.shields.io/badge/React-61DAFB?style=for-the-badge&logo=react&logoColor=black',
'Ruby': 'https://img.shields.io/badge/Ruby-CC342D?style=for-the-badge&logo=ruby&logoColor=white',
'Java': 'https://img.shields.io/badge/Java-007396?style=for-the-badge&logo=java&logoColor=white',
'Go': 'https://img.shields.io/badge/Go-00ADD8?style=for-the-badge&logo=go&logoColor=white',
'Rust': 'https://img.shields.io/badge/Rust-000000?style=for-the-badge&logo=rust&logoColor=white',
'PHP': 'https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white',
'Bash': 'https://img.shields.io/badge/Bash-4EAA25?style=for-the-badge&logo=gnubash&logoColor=white'
};
return badgeMap[tech] || '';
}
// Helper function to generate directory tree
function generateStructureTree(structure, depth = 0, prefix = '') {
let tree = '';
const lastIndex = structure.length - 1;
const indent = depth > 0 ? '│ '.repeat(depth - 1) : '';
structure.forEach((item, index) => {
const isLast = index === lastIndex;
const pointer = isLast ? '└── ' : '├── ';
if (item.type === 'directory') {
tree += `${indent}${prefix}${pointer}📂 ${item.name}/\n`;
tree += generateStructureTree(
item.contents,
depth + 1,
isLast ? ' ' : '│ '
);
} else {
tree += `${indent}${prefix}${pointer}📄 ${item.name}\n`;
}
});
return tree;
}
};
// --- Main Execution ---
(async () => {
try {
console.log(chalk.cyan('Analyzing project...'));
const apiToken = getApiToken();
const pkg = readPackageJson();
const structure = scanDirectory();
const projectType = detectProjectType(structure);
const analysis = {
metadata: {
...pkg,
type: projectType
},
structure
};
console.log(chalk.cyan('📄 Generating README...'));
const client = ModelClient(CONFIG.ENDPOINT, new AzureKeyCredential(apiToken));
const response = await client.path('/chat/completions').post({
body: {
messages: [
{
role: 'system',
content: 'You are a senior developer that creates perfect READMEs by analyzing project structure. Be concise but thorough.'
},
{
role: 'user',
content: generatePrompt(analysis)
}
],
model: CONFIG.MODEL,
...CONFIG.TEMPLATE_PARAMS
}
});
if (isUnexpected(response)) {
throw new Error(response.body.error?.message || 'API request failed');
}
const readmeContent = response.body.choices[0].message.content;
fs.writeFileSync('README.md', readmeContent.trim());
console.log(chalk.green('✔ README.md generated successfully!'));
console.log(chalk.blue('Detected:'),
Object.entries(projectType)
.filter(([_, value]) => value)
.map(([key]) => key.replace('is', ''))
.join(', ') || 'Unknown project type');
} catch (error) {
console.error(chalk.red('✖ Error:'), error.message);
process.exit(1);
}
})();