Skip to main content

#!/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()