@jayanithu/readmi
Version:
Modern README generator powered by AI
647 lines (569 loc) • 21.6 kB
JavaScript
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import fs from 'fs/promises';
import inquirer from 'inquirer';
import { GoogleGenerativeAI } from '@google/generative-ai';
import chalk from 'chalk';
import Conf from 'conf';
import ora from 'ora';
import path from 'path';
import boxen from 'boxen';
import gradient from 'gradient-string';
import { execSync } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const args = process.argv.slice(2);
const config = new Conf({
projectName: 'readmi',
defaults: {
apiKey: null,
preferredModel: null,
preferredLanguage: 'en'
}
});
async function analyzeProject(dir) {
const info = {
name: '',
description: '',
version: '',
dependencies: {},
devDependencies: {},
scripts: {},
files: [],
hasTests: false,
hasDocker: false,
hasGithubActions: false
};
try {
const packageJson = JSON.parse(await fs.readFile(join(dir, 'package.json'), 'utf8'));
Object.assign(info, packageJson);
const files = await fs.readdir(dir);
info.files = files;
info.hasTests = files.some(f => f.includes('test') || f.includes('spec'));
info.hasDocker = files.includes('Dockerfile') || files.includes('docker-compose.yml');
info.hasGithubActions = files.includes('.github');
return info;
} catch (error) {
return info;
}
}
class ReadmeGenerator {
constructor() {
this.currentDir = process.cwd();
this.spinner = ora();
}
async init() {
try {
this.showHeader();
if (args.includes('-v') || args.includes('--version')) {
this.showVersion();
return;
}
if (args.includes('--update')) {
const spinner = ora('Checking for updates...').start();
try {
execSync('npm install -g @jayanithu/readmi@latest');
spinner.succeed(chalk.green('Successfully updated to the latest version!'));
process.exit(0);
} catch (error) {
spinner.fail(chalk.red('Update failed: ') + error.message);
process.exit(1);
}
}
if (args.includes('-h') || args.includes('--help')) {
this.showHelp();
return;
}
if (args[0] === 'models') {
await this.listAvailableModels();
return;
}
if (args[0] === 'config') {
await this.handleConfig();
return;
}
if (args[0] === 'language' || args[0] === 'lang') {
await this.selectLanguage();
return;
}
this.spinner.start(chalk.blue('Initializing ReadMI...'));
const apiKey = await this.getApiKey();
const projectInfo = await analyzeProject(this.currentDir);
let language = config.get('preferredLanguage') || 'en';
if (args.includes('--select-language') || args.includes('-sl')) {
language = await this.selectLanguage();
}
await this.generateReadme(apiKey, projectInfo, language);
} catch (error) {
this.handleError(error);
}
}
async listAvailableModels() {
const apiKey = await this.getApiKey();
this.spinner.start(chalk.blue('Fetching available models...'));
try {
const genAI = new GoogleGenerativeAI(apiKey);
const modelList = await genAI.listModels();
this.spinner.stop();
console.log(
'\n' +
boxen(
gradient.pastel.multiline(
[
'📋 Available Gemini Models:',
'',
...modelList.models.map(
model =>
`• ${chalk.cyan(model.name)}\n ${chalk.gray(model.description || 'No description available')}`
)
].join('\n')
),
{ padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green' }
)
);
} catch (error) {
this.spinner.fail(chalk.red('Failed to fetch models: ') + error.message);
process.exit(1);
}
}
showHeader() {
console.log(
'\n' +
boxen(
gradient.pastel.multiline(
[
'╭─────────────────────────────╮',
'│ │',
'│ ReadMI - README Builder │',
'│ v2.3.2 │',
'│ │',
'╰─────────────────────────────╯'
].join('\n')
),
{ padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan' }
)
);
}
showVersion() {
console.log(chalk.cyan('ReadMI v2.3.2'));
process.exit(0);
}
showHelp() {
console.log(
'\n' +
boxen(
gradient.pastel.multiline(
[
'📘 ReadMI Commands:',
'',
' readmi Generate README',
' readmi models List available AI models',
' readmi config Manage configuration',
' readmi config -r Remove saved API key',
' readmi config -rm Remove preferred model',
' readmi config -rl Remove preferred language',
' readmi config -l Set preferred language',
' readmi -v Display version',
' readmi --update Update to latest version',
' readmi -h Display help',
'',
'✨ Visit: https://github.com/jayanithu/readmi'
].join('\n')
),
{ padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue' }
)
);
process.exit(0);
}
async getApiKey() {
const savedApiKey = config.get('apiKey');
if (savedApiKey) return savedApiKey;
this.spinner.stop();
const { apiKey, saveKey } = await inquirer.prompt([
{
type: 'password',
name: 'apiKey',
message: chalk.cyan('Enter your Google AI API Key:'),
mask: '*'
},
{
type: 'confirm',
name: 'saveKey',
message: chalk.cyan('Save this API key for future use?'),
default: true
}
]);
if (saveKey) {
config.set('apiKey', apiKey);
this.spinner.succeed(chalk.green('API key saved'));
}
return apiKey;
}
async generateReadme(apiKey, projectInfo, language = 'en') {
this.spinner.start(chalk.blue('Analyzing project...'));
try {
const genAI = new GoogleGenerativeAI(apiKey);
const preferredModels = [
'gemini-2.0-flash',
'gemini-2.0-flash-lite',
'gemini-1.5-pro',
'gemini-1.5-flash',
'gemini-1.5-flash-8b',
'gemini-pro',
'gemini-1.0-pro'
];
let selectedModel = null;
let workingModel = null;
for (const modelName of preferredModels) {
try {
const tempModel = genAI.getGenerativeModel({
model: modelName,
generationConfig: {
temperature: modelName.includes('flash') ? 0.9 : 0.7,
maxOutputTokens: modelName.includes('pro') ? 2048 : 1024,
topP: modelName.includes('flash') ? 0.9 : 0.8,
topK: modelName.includes('pro') ? 40 : 32
}
});
await tempModel.generateContent([{ text: 'test' }]);
workingModel = tempModel;
selectedModel = modelName;
this.spinner.info(chalk.blue(`Found working model: ${modelName}`));
break;
} catch (error) {
this.spinner.warn(chalk.yellow(`Model ${modelName} not available, trying next...`));
continue;
}
}
if (!workingModel || !selectedModel) {
workingModel = genAI.getGenerativeModel({
model: 'gemini-pro',
generationConfig: {
temperature: 0.7,
maxOutputTokens: 1024,
topP: 0.8,
topK: 32
}
});
}
const modelType = selectedModel?.toLowerCase() || '';
if (modelType.includes('2.0')) {
this.spinner.info(chalk.green('✨ Using Gemini 2.0 with enhanced capabilities'));
} else if (modelType.includes('1.5')) {
this.spinner.info(chalk.green('✨ Using Gemini 1.5 with improved performance'));
} else if (modelType.includes('flash')) {
this.spinner.info(chalk.green('⚡ Using Flash model for faster generation'));
}
this.spinner.info(chalk.blue(`Generating README in ${this.getLanguageName(language)}`));
const projectType = this.determineProjectType(projectInfo);
const promptText = `Create a comprehensive, accurate README.md for the project "${projectInfo.name}" in ${this.getLanguageName(language)}.
Project Details:
${JSON.stringify(projectInfo, null, 2)}
Project Type Analysis:
${projectType.join('\n')}
Generate a README with these sections (adapt as needed for the specific project type):
1. 🚀 Project Title and Description
- Clear project name with appropriate badges (npm, license, build status, etc.)
- Concise description that explains the project's purpose
- If applicable: version information, status (beta, stable, etc.)
2. ✨ Features
- List 4-6 key features with emoji prefixes
- Focus on what makes this project unique or valuable
- Highlight technical capabilities and user benefits
3. 📦 Installation
- Provide clear, step-by-step installation instructions
- Include all prerequisites and dependencies
- Show multiple installation methods if applicable (npm, yarn, bun, pip, etc.)
- Include platform-specific instructions if needed
4. 🎮 Quick Start / Usage
- Show a minimal working example to get started quickly
- Include code snippets with proper syntax highlighting
- Explain key configuration options or environment variables
- For APIs: show basic request/response examples
5. 📖 Documentation
- Link to or include API documentation if applicable
- Explain core concepts and architecture
- Provide examples for common use cases
- Include diagrams or screenshots if helpful
6. ⚙️ Configuration
- Document all configuration options
- Explain default values and possible alternatives
- Show configuration file examples
- Explain environment variables
7. 🧪 Testing
- Instructions for running tests
- Explanation of test coverage
- How to write new tests
8. 🤝 Contributing
- Guidelines for contributors
- Code of conduct
- Development setup instructions
- Pull request process
9. 📝 License
- Specify the license type
- Link to the full license text
10. 👏 Acknowledgements
- Credit contributors, inspirations, and dependencies
- Link to related projects
Adapt the sections based on the project type:
- For libraries/frameworks: Focus on API documentation, usage examples
- For CLI tools: Focus on command reference, options, examples
- For web applications: Include deployment instructions, screenshots
- For mobile apps: Include store links, platform requirements
- For data science projects: Include dataset information, model details
- For games: Include controls, gameplay instructions
Style Guidelines:
- Use clear, concise language
- Include relevant code examples with proper syntax highlighting
- Use emojis for section headers to improve readability
- Use tables for structured data when appropriate
- Include badges for important information (build status, version, etc.)
- Organize information hierarchically with proper heading levels
- Ensure all links are functional and relevant
- Include visuals (screenshots, diagrams) where they add value
Technical Accuracy:
- Ensure all command examples work as written
- Verify all API examples are syntactically correct
- Check that installation instructions are complete
- Confirm compatibility information is accurate
- Ensure version numbers are consistent throughout
Write the entire README in ${this.getLanguageName(language)}.
Format the output as clean markdown without any wrapper code blocks.`;
try {
const result = await workingModel.generateContent([{ text: promptText }]);
const response = await result.response;
const readmeContent = response.text();
if (!readmeContent) {
throw new Error('Generated content is empty');
}
const processedContent = this.postProcessReadme(readmeContent);
let filename = 'README.md';
if (language !== 'en') {
filename = `README.${language}.md`;
}
await fs.writeFile(filename, processedContent);
this.spinner.succeed(chalk.green(`✨ ${filename} generated successfully in ${this.getLanguageName(language)}!`));
} catch (error) {
if (error.message.includes('not found for API version')) {
throw new Error('API configuration error. Please check your API key and try again. Error: ' + error.message);
}
throw new Error(`README generation failed: ${error.message}`);
}
} catch (error) {
throw error;
}
}
determineProjectType(projectInfo) {
const projectType = [];
if (projectInfo.bin || Object.keys(projectInfo.bin || {}).length > 0) {
projectType.push('- This is a command-line interface (CLI) tool');
}
else if (projectInfo.main) {
projectType.push('- This is a library/package meant to be imported by other projects');
}
if (projectInfo.dependencies) {
if (projectInfo.dependencies.react) {
projectType.push('- This is a React application/component');
} else if (projectInfo.dependencies.vue) {
projectType.push('- This is a Vue.js application/component');
} else if (projectInfo.dependencies.express || projectInfo.dependencies['@nestjs/core']) {
projectType.push('- This is a Node.js backend/API service');
} else if (projectInfo.dependencies.electron) {
projectType.push('- This is an Electron desktop application');
} else if (projectInfo.dependencies['react-native']) {
projectType.push('- This is a React Native mobile application');
}
}
if (projectInfo.devDependencies) {
const testingTools = [];
if (projectInfo.devDependencies.jest) testingTools.push('Jest');
if (projectInfo.devDependencies.mocha) testingTools.push('Mocha');
if (projectInfo.devDependencies.cypress) testingTools.push('Cypress');
if (projectInfo.devDependencies.playwright) testingTools.push('Playwright');
if (testingTools.length > 0) {
projectType.push(`- Testing is done with: ${testingTools.join(', ')}`);
}
}
if (projectInfo.hasDocker) {
projectType.push('- This project has Docker support');
}
if (projectInfo.hasGithubActions) {
projectType.push('- This project uses GitHub Actions for CI/CD');
}
const fileExtensions = projectInfo.files.map(file => {
const ext = file.split('.').pop();
return ext;
});
if (fileExtensions.includes('py')) {
projectType.push('- This project uses Python');
} else if (fileExtensions.includes('go')) {
projectType.push('- This project uses Go');
} else if (fileExtensions.includes('rs')) {
projectType.push('- This project uses Rust');
} else if (fileExtensions.includes('java') || fileExtensions.includes('kt')) {
projectType.push('- This project uses Java/Kotlin');
} else if (fileExtensions.includes('rb')) {
projectType.push('- This project uses Ruby');
} else if (fileExtensions.includes('php')) {
projectType.push('- This project uses PHP');
} else if (fileExtensions.includes('ts') || fileExtensions.includes('tsx')) {
projectType.push('- This project uses TypeScript');
} else if (fileExtensions.includes('js') || fileExtensions.includes('jsx')) {
projectType.push('- This project uses JavaScript');
}
if (projectType.length === 0) {
projectType.push('- Project type could not be automatically determined');
projectType.push('- Generating a generic README structure');
}
return projectType;
}
getLanguageMap() {
return {
'en': 'English',
'es': 'Spanish (Español)',
'fr': 'French (Français)',
'de': 'German (Deutsch)',
'zh': 'Chinese (中文)',
'ja': 'Japanese (日本語)',
'pt': 'Portuguese (Português)',
'ru': 'Russian (Русский)',
'hi': 'Hindi (हिन्दी)',
'ar': 'Arabic (العربية)'
};
}
getLanguageName(code) {
return this.getLanguageMap()[code] || code;
}
postProcessReadme(content) {
let processed = content.replace(/```markdown/g, '```');
if (processed.startsWith('```')) {
processed = processed.replace(/^```[a-z]*\n?/, '');
}
processed = processed
.replace(/\n(#+)\s/g, '\n\n$1 ')
.replace(/\n-\s/g, '\n• ')
.replace(/\n{3,}/g, '\n\n')
.trim();
processed += '\n\n---\n_Made with ❤️ using ReadMI by jayanithu_\n';
return processed;
}
async handleConfig() {
if (args[1] === '--remove-key' || args[1] === '-r') {
if (config.has('apiKey')) {
config.delete('apiKey');
console.log(chalk.green('✅ API key removed successfully'));
} else {
console.log(chalk.yellow('ℹ️ No API key found in configuration'));
}
return;
}
if (args[1] === '--remove-model' || args[1] === '-rm') {
if (config.has('preferredModel')) {
config.delete('preferredModel');
console.log(chalk.green('✅ Preferred model removed successfully'));
} else {
console.log(chalk.yellow('ℹ️ No preferred model found in configuration'));
}
return;
}
if (args[1] === '--remove-language' || args[1] === '-rl') {
if (config.has('preferredLanguage')) {
config.delete('preferredLanguage');
console.log(chalk.green('✅ Preferred language removed successfully'));
} else {
console.log(chalk.yellow('ℹ️ No preferred language found in configuration'));
}
return;
}
if (args[1] === '--language' || args[1] === '-l') {
await this.selectLanguage();
return;
}
console.log(
'\n' +
boxen(
gradient.pastel.multiline(
[
'⚙️ Configuration Commands:',
'',
' readmi config -r Remove saved API key',
' readmi config -rm Remove preferred model',
' readmi config -rl Remove preferred language',
' readmi config -l Set preferred language',
'',
'📝 Current Configuration:',
` API Key: ${config.has('apiKey') ? '🔑 Saved' : '❌ Not saved'}`,
` Preferred Model: ${config.has('preferredModel') ? chalk.cyan(config.get('preferredModel')) : '❌ Not set'}`,
` Preferred Language: ${config.has('preferredLanguage') ? chalk.cyan(this.getLanguageName(config.get('preferredLanguage'))) : '❌ Not set'}`
].join('\n')
),
{ padding: 1, margin: 1, borderStyle: 'round', borderColor: 'magenta' }
)
);
}
getLanguageName(code) {
const languageMap = {
'en': 'English',
'es': 'Spanish (Español)',
'fr': 'French (Français)',
'de': 'German (Deutsch)',
'zh': 'Chinese (中文)',
'ja': 'Japanese (日本語)',
'pt': 'Portuguese (Português)',
'ru': 'Russian (Русский)',
'hi': 'Hindi (हिन्दी)',
'ar': 'Arabic (العربية)'
};
return languageMap[code] || code;
}
handleError(error) {
this.spinner.fail(chalk.red('Error: ') + error.message);
process.exit(1);
}
async selectLanguage() {
const savedLanguage = config.get('preferredLanguage');
const languages = [
{ name: 'English', value: 'en' },
{ name: 'Spanish (Español)', value: 'es' },
{ name: 'French (Français)', value: 'fr' },
{ name: 'German (Deutsch)', value: 'de' },
{ name: 'Chinese (中文)', value: 'zh' },
{ name: 'Japanese (日本語)', value: 'ja' },
{ name: 'Portuguese (Português)', value: 'pt' },
{ name: 'Russian (Русский)', value: 'ru' },
{ name: 'Hindi (हिन्दी)', value: 'hi' },
{ name: 'Arabic (العربية)', value: 'ar' }
];
this.spinner.stop();
const { language } = await inquirer.prompt([
{
type: 'list',
name: 'language',
message: chalk.cyan('Select README language:'),
choices: languages,
default: languages.findIndex(lang => lang.value === savedLanguage) || 0
}
]);
const { saveLanguage } = await inquirer.prompt([
{
type: 'confirm',
name: 'saveLanguage',
message: chalk.cyan('Save this language as preferred?'),
default: true
}
]);
if (saveLanguage) {
config.set('preferredLanguage', language);
this.spinner.succeed(chalk.green(`Language preference saved: ${language}`));
}
return language;
}
}
console.clear();
new ReadmeGenerator()
.init()
.catch(error => {
console.error(chalk.red('Fatal error:'), error);
process.exit(1);
});