#!/usr/bin/env python3 """ Generate READMEs
Automatically generates README.md files for directories missing them.
Usage: python generate-readmes.py [path] [--recursive] [--force]
Examples: python generate-readmes.py docs/ # Generate for docs/ python generate-readmes.py --recursive # Generate all missing python generate-readmes.py --force # Overwrite existing """
import argparse import os import sys from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional
ANSI colors
Shared Colors module (consolidates 36 duplicate definitions)
_script_dir = Path(file).parent sys.path.insert(0, str(_script_dir / "core")) from colors import Colors
class GeneratedReadme: """Represents a generated README.""" path: Path template: str sections: List[str]
README templates
MINIMAL_TEMPLATE = """# {title}
{description}
Contents
{contents} """
STANDARD_TEMPLATE = """# {title}
Overview
{description}
Contents
{contents}
Usage
{usage}
Generated by CODITECT README Generator """
COMPREHENSIVE_TEMPLATE = """# {title}
Overview
{description}
Quick Start
# Quick start commands here
Prerequisites
- Prerequisite 1
- Prerequisite 2
Installation
# Installation steps
Usage
{usage}
Contents
{contents}
Documentation
Contributing
See CONTRIBUTING.md for guidelines.
License
See LICENSE.
Generated by CODITECT README Generator """
def get_file_description(path: Path) -> str: """Get a brief description of a file based on its name and type.""" name = path.stem suffix = path.suffix.lower()
descriptions = {
'.md': 'Documentation',
'.py': 'Python script',
'.ts': 'TypeScript module',
'.tsx': 'React component',
'.js': 'JavaScript module',
'.json': 'Configuration',
'.yaml': 'Configuration',
'.yml': 'Configuration',
'.sh': 'Shell script',
'.sql': 'SQL script',
}
base_desc = descriptions.get(suffix, 'File')
# Try to make name more readable
readable_name = name.replace('-', ' ').replace('_', ' ').title()
return f"{base_desc}: {readable_name}"
def generate_contents_table(path: Path) -> str: """Generate contents table for a directory.""" try: items = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name.lower())) except PermissionError: return "| Item | Description |\n|------|-------------|"
# Filter out hidden files and common excludes
items = [i for i in items if not i.name.startswith('.') and
i.name not in ['node_modules', 'venv', '.venv', '__pycache__', 'README.md']]
if not items:
return "| Item | Description |\n|------|-------------|\n| (empty) | - |"
lines = ["| Item | Description |", "|------|-------------|"]
for item in items[:20]: # Limit to 20 items
if item.is_dir():
lines.append(f"| [`{item.name}/`]({item.name}/) | Directory |")
else:
desc = get_file_description(item)
lines.append(f"| [{item.name}]({item.name}) | {desc} |")
if len(items) > 20:
lines.append(f"| ... | *{len(items) - 20} more items* |")
return '\n'.join(lines)
def select_template(path: Path) -> str: """Select appropriate template for a directory.""" name = path.name.lower()
# Check if it's a project root
has_license = (path / 'LICENSE').exists() or (path / 'LICENSE.md').exists()
has_package = any((path / f).exists() for f in ['package.json', 'pyproject.toml', 'Cargo.toml'])
if has_license or has_package:
return 'comprehensive'
# Check content count
try:
items = [i for i in path.iterdir() if not i.name.startswith('.')]
file_count = len(items)
except PermissionError:
file_count = 0
if file_count < 5:
return 'minimal'
return 'standard'
def generate_readme(path: Path, template_type: str = 'auto') -> GeneratedReadme: """Generate README content for a directory.""" if template_type == 'auto': template_type = select_template(path)
# Generate title from directory name
title = path.name.replace('-', ' ').replace('_', ' ').title()
# Generate description
description = f"This directory contains {title.lower()} files and resources."
# Generate contents
contents = generate_contents_table(path)
# Generate usage (generic)
usage = "See individual file documentation for usage details."
# Select template
templates = {
'minimal': MINIMAL_TEMPLATE,
'standard': STANDARD_TEMPLATE,
'comprehensive': COMPREHENSIVE_TEMPLATE
}
template = templates.get(template_type, STANDARD_TEMPLATE)
content = template.format(
title=title,
description=description,
contents=contents,
usage=usage
)
return GeneratedReadme(
path=path / 'README.md',
template=template_type,
sections=['title', 'overview', 'contents']
)
def find_missing_readmes(path: Path, recursive: bool = True) -> List[Path]: """Find directories missing README.md.""" missing = []
skip_dirs = {'.git', 'node_modules', 'venv', '.venv', '__pycache__', '.pytest_cache'}
# Check current directory
if not (path / 'README.md').exists():
missing.append(path)
if recursive:
for item in path.iterdir():
if item.is_dir() and item.name not in skip_dirs and not item.name.startswith('.'):
missing.extend(find_missing_readmes(item, recursive))
return missing
def write_readme(path: Path, force: bool = False) -> bool: """Write README to a directory.""" readme_path = path / 'README.md'
if readme_path.exists() and not force:
return False
result = generate_readme(path)
# Generate content
title = path.name.replace('-', ' ').replace('_', ' ').title()
description = f"This directory contains {title.lower()} files and resources."
contents = generate_contents_table(path)
template_type = select_template(path)
templates = {
'minimal': MINIMAL_TEMPLATE,
'standard': STANDARD_TEMPLATE,
'comprehensive': COMPREHENSIVE_TEMPLATE
}
template = templates.get(template_type, STANDARD_TEMPLATE)
content = template.format(
title=title,
description=description,
contents=contents,
usage="See individual file documentation for usage details."
)
try:
readme_path.write_text(content)
return True
except Exception as e:
print(f"Error writing {readme_path}: {e}", file=sys.stderr)
return False
def main(): parser = argparse.ArgumentParser( description='Generate README.md files for directories' ) parser.add_argument('path', nargs='?', default='.', help='Path to generate for') parser.add_argument('--recursive', '-r', action='store_true', help='Generate recursively') parser.add_argument('--force', '-f', action='store_true', help='Overwrite existing') parser.add_argument('--dry-run', action='store_true', help='Show what would be done') parser.add_argument('--missing', action='store_true', help='Only generate for missing')
args = parser.parse_args()
path = Path(args.path).resolve()
if not path.exists():
print(f"Error: Path does not exist: {path}", file=sys.stderr)
sys.exit(1)
if args.missing or args.recursive:
# Find all directories needing READMEs
dirs = find_missing_readmes(path, recursive=args.recursive)
else:
# Just the specified directory
dirs = [path] if not (path / 'README.md').exists() or args.force else []
if not dirs:
print(f"{Colors.GREEN}All directories have README.md files!{Colors.RESET}")
sys.exit(0)
print(f"\n{Colors.BOLD}README Generation{Colors.RESET}")
print(f"Directories to process: {len(dirs)}\n")
generated = 0
for d in dirs:
template = select_template(d)
if args.dry_run:
print(f" Would generate: {d}/README.md ({template})")
generated += 1
else:
if write_readme(d, force=args.force):
print(f" {Colors.GREEN}Generated:{Colors.RESET} {d}/README.md ({template})")
generated += 1
else:
print(f" {Colors.YELLOW}Skipped:{Colors.RESET} {d}/README.md (exists)")
print(f"\n{Colors.GREEN}Generated {generated} README files{Colors.RESET}\n")
sys.exit(0)
if name == 'main': main()