#!/usr/bin/env python3
"""
Docs Index Generator
Генерирует INDEX.md с иерархией всех документов платформы.
Режимы:
--all Вся платформа (архитектор)
--project NAME Только проект
--section NAME Только секция (architect, system, etc.)
Использование:
python3 docs_index.py --all
python3 docs_index.py --project pirotehnika
python3 docs_index.py --section architect
"""
import os
import re
import argparse
from pathlib import Path
from datetime import datetime
WORKSPACE = os.environ.get('WORKSPACE', '/opt/claude-workspace')
EXCLUDE_DIRS = {'node_modules', 'venv', '.venv', '__pycache__', '.git', 'vendor'}
EXCLUDE_FILES = {'package-lock.json', 'yarn.lock'}
def get_doc_info(filepath: Path) -> dict:
"""Извлекает метаданные из .md файла"""
info = {
'path': str(filepath),
'name': filepath.name,
'title': filepath.stem,
'version': None,
'description': None
}
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read(2000) # Первые 2KB
# Заголовок
title_match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
if title_match:
info['title'] = title_match.group(1).strip()
# Версия
version_match = re.search(r'\*\*Версия:\*\*\s*(\S+)', content)
if version_match:
info['version'] = version_match.group(1)
# Первый абзац после заголовка как описание
desc_match = re.search(r'^#.+\n\n(.+?)(?:\n\n|$)', content, re.MULTILINE | re.DOTALL)
if desc_match:
desc = desc_match.group(1).strip()
desc = re.sub(r'\*\*[^*]+\*\*[:\s]*\S+\s*', '', desc) # Убираем **Версия:** и т.д.
desc = desc.strip()
if desc and len(desc) > 10:
info['description'] = desc[:100] + '...' if len(desc) > 100 else desc
except Exception:
pass
return info
def scan_directory(root_path: Path, prefix: str = "") -> list:
"""Рекурсивно сканирует директорию"""
items = []
if not root_path.exists():
return items
entries = sorted(root_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
for entry in entries:
if entry.name.startswith('.') or entry.name in EXCLUDE_DIRS:
continue
if entry.is_dir():
# Проверяем есть ли .md файлы внутри
has_md = any(entry.rglob('*.md'))
if has_md:
items.append({
'type': 'dir',
'name': entry.name,
'path': str(entry.relative_to(WORKSPACE)),
'children': scan_directory(entry, prefix + " ")
})
elif entry.suffix.lower() == '.md':
info = get_doc_info(entry)
info['type'] = 'file'
info['rel_path'] = str(entry.relative_to(WORKSPACE))
items.append(info)
return items
def render_tree(items: list, indent: int = 0) -> str:
"""Рендерит дерево в markdown"""
lines = []
prefix = " " * indent
for item in items:
if item['type'] == 'dir':
lines.append(f"{prefix}📁 **{item['name']}/**")
if item.get('children'):
lines.append(render_tree(item['children'], indent + 1))
else:
# Файл
version = f" (v{item['version']})" if item.get('version') else ""
desc = f" — {item['description']}" if item.get('description') else ""
link = f"[{item['title']}]({item['rel_path']})"
lines.append(f"{prefix}📄 {link}{version}{desc}")
return "\n".join(lines)
def generate_index(mode: str = 'all', target: str = None) -> str:
"""Генерирует INDEX.md"""
now = datetime.now().strftime('%Y-%m-%d %H:%M')
header = f"""# Документация платформы
**Сгенерировано:** {now}
**Режим:** {mode}
"""
if mode == 'all':
# Вся платформа
sections = [
('architect', 'Методология'),
('system', 'Система'),
('infra', 'Инфраструктура'),
('projects', 'Проекты'),
]
content = header + "\n---\n\n"
# Корневые документы
root_docs = [f for f in Path(WORKSPACE).iterdir()
if f.suffix.lower() == '.md' and not f.name.startswith('.')]
if root_docs:
content += "## Корневые документы\n\n"
for doc in sorted(root_docs):
info = get_doc_info(doc)
version = f" (v{info['version']})" if info.get('version') else ""
content += f"📄 [{info['title']}]({doc.name}){version}\n\n"
for section, title in sections:
section_path = Path(WORKSPACE) / section
if section_path.exists():
items = scan_directory(section_path)
if items:
content += f"## {title}\n\n"
content += render_tree(items) + "\n\n"
elif mode == 'project':
# Только проект
project_path = Path(WORKSPACE) / 'projects' / target
if not project_path.exists():
return f"# Ошибка\n\nПроект '{target}' не найден."
content = header + f"**Проект:** {target}\n\n---\n\n"
items = scan_directory(project_path)
content += render_tree(items)
elif mode == 'section':
# Только секция
section_path = Path(WORKSPACE) / target
if not section_path.exists():
return f"# Ошибка\n\nСекция '{target}' не найдена."
content = header + f"**Секция:** {target}\n\n---\n\n"
items = scan_directory(section_path)
content += render_tree(items)
return content
def main():
parser = argparse.ArgumentParser(description='Docs Index Generator')
parser.add_argument('--all', action='store_true', help='Вся платформа')
parser.add_argument('--project', '-p', help='Только проект')
parser.add_argument('--section', '-s', help='Только секция')
parser.add_argument('--output', '-o', help='Файл вывода (по умолчанию stdout)')
parser.add_argument('--save', action='store_true', help='Сохранить в INDEX.md')
args = parser.parse_args()
if args.project:
content = generate_index('project', args.project)
elif args.section:
content = generate_index('section', args.section)
else:
content = generate_index('all')
if args.save:
output_path = Path(WORKSPACE) / 'INDEX.md'
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Сохранено: {output_path}")
elif args.output:
with open(args.output, 'w', encoding='utf-8') as f:
f.write(content)
print(f"Сохранено: {args.output}")
else:
print(content)
if __name__ == '__main__':
main()