/
ump-team
/
ump-infra
Обзор
Документация
Войти
/
ump-team
/
ump-infra
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev/test
scripts/preflight_check.py
403 строки
18 KB
Dmitry Kochenov
Audit v0.0.2: security fixes
19 июл 2026, 19:19
19 июл 2026, 19:19
8a849eb
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ Preflight Check — проверка готовности проекта к работе. Проверяет: 1. Python + UV установлены 2. Node.js + npm установлены 3. Git настроен (user.name, user.email) 4. Обязательные файлы проекта на месте 5. .gigacode_vsc/ структура цела 6. plans/ YAML-файлы валидны 7. VitePress docs/ структура цела 8. Зависимости установлены (uv sync, npm install) 9. Git remote настроен 10. Makefile работает Запуск: uv run python3 scripts/preflight_check.py make preflight Exit codes: 0 — всё готово 1 — есть проблемы (выводит инструкции по исправлению) """ from __future__ import annotations import hashlib import shlex import shutil import subprocess import sys from pathlib import Path # Единая точка правды для BASE-резолвинга. try: from ump.paths import detect_base, resolve_infra_dir except ImportError: _here = Path(__file__).resolve().parent sys.path.insert(0, str(_here.parent)) from ump.paths import detect_base, resolve_infra_dir # type: ignore[no-redef] BASE = detect_base(__file__) INFRA_DIR = resolve_infra_dir(BASE) # .ump/ в submodule-layout, BASE в standalone # Цвета для вывода GREEN = '\033[92m' RED = '\033[91m' YELLOW = '\033[93m' RESET = '\033[0m' BOLD = '\033[1m' def check(name: str, condition: bool, fix_instruction: str = '') -> bool: """Вывести результат проверки.""" if condition: print(f' {GREEN}✓{RESET} {name}') return True else: print(f' {RED}✗{RESET} {name}') if fix_instruction: print(f' {YELLOW}→ {fix_instruction}{RESET}') return False def run(cmd: str | list[str]) -> str: """Запустить команду, вернуть stdout. P0-7 fix: ранее использовался subprocess.run(cmd, shell=True, ...), что позволяло инъекции через интерполируемые переменные. Теперь — list-form через shlex.split() (для str) или прямой list (для list[str]). Без shell=True: команды с `2>/dev/null`, `||`, `|`, `&&`, `;` не работают как раньше. callers должны переписать на Python-логику или использовать list-form без shell-конструктов. """ try: args = shlex.split(cmd) if isinstance(cmd, str) else cmd r = subprocess.run(args, capture_output=True, text=True, cwd=BASE, timeout=10) return r.stdout.strip() except Exception: return '' def check_tool(cmd: str, name: str, install_cmd: str) -> bool: """Проверить, установлен ли инструмент.""" path = shutil.which(cmd) return check(f'{name} установлен', path is not None, f'Установите: {install_cmd}') def main(): print(f'\n{BOLD}=== PREFLIGHT CHECK — Universal Modular Platform ==={RESET}\n') all_ok = True manual_actions = [] # === 1. Инструменты === print(f'{BOLD}1. Инструменты{RESET}') # Python py_version = run(['python3', '--version']) all_ok &= check(f'Python ({py_version})', bool(py_version), 'Установите Python 3.12+: https://www.python.org/downloads/') # UV uv_version = run(['uv', '--version']) all_ok &= check(f'UV ({uv_version})', bool(uv_version), 'Установите UV: curl -LsSf https://astral.sh/uv/install.sh | sh') # Node.js node_version = run(['node', '--version']) all_ok &= check(f'Node.js ({node_version})', bool(node_version), 'Установите Node.js 24: https://nodejs.org/ или через nvm') # npm npm_version = run(['npm', '--version']) all_ok &= check(f'npm ({npm_version})', bool(npm_version), 'Устанавливается вместе с Node.js') # Git git_version = run(['git', '--version']) all_ok &= check(f'Git ({git_version})', bool(git_version), 'Установите Git: https://git-scm.com/downloads') # gh CLI (опционально, для PR) # P0-7 fix: list-form без shell. `2>/dev/null | head -1` заменено на # Python-обработку: capture_output подавляет stderr, [:1] берёт первую строку. gh_version = run(['gh', '--version']) if gh_version: gh_version = gh_version.splitlines()[0] has_gh = bool(gh_version) if has_gh: print(f' {GREEN}✓{RESET} GitHub CLI ({gh_version}) — PR будут создаваться автоматически') else: print(f' {YELLOW}⚠{RESET} GitHub CLI не установлен — PR нужно создавать вручную') manual_actions.append('Установите gh CLI для автоматических PR: https://cli.github.com/') # === 2. Git настройка === print(f'\n{BOLD}2. Git настройка{RESET}') git_name = run(['git', 'config', 'user.name']) all_ok &= check('git config user.name задан', bool(git_name), 'git config --global user.name "Ваше Имя"') git_email = run(['git', 'config', 'user.email']) all_ok &= check('git config user.email задан', bool(git_email), 'git config --global user.email "your_email@example.com"') git_remote = run(['git', 'remote', '-v']) all_ok &= check('git remote настроен', bool(git_remote), 'git remote add origin <URL> — см. docs/руководство/gitverse-репозиторий.md') # P0-7 fix: list-form без shell. `2>/dev/null` не нужно — capture_output подавляет stderr. git_branch = run(['git', 'branch', '--show-current']) all_ok &= check(f'Текущая ветка: {git_branch}', bool(git_branch), 'git init -b main (если репозиторий не инициализирован)') # === 3. Обязательные файлы проекта === print(f'\n{BOLD}3. Обязательные файлы{RESET}') required_files = [ 'AGENTS.md', '00-manifest.md', 'STEP_STATE.template.md', 'Makefile', 'pyproject.toml', 'package.json', '.gitignore', '.nvmrc', ] for f in required_files: # В submodule-layout эти файлы могут лежать как в BASE, так и в INFRA_DIR ok = (BASE / f).exists() or (INFRA_DIR / f).exists() all_ok &= check(f'Файл: {f}', ok, 'Файл отсутствует — архив повреждён') # === 4. .agent/ структура === print(f'\n{BOLD}4. .agent/ структура{RESET}') agent_rules = [ '00-agent-protocol.md', '01-step-lifecycle.md', '02-documentation-rules.md', '03-context-loading.md', '04-git-workflow.md', '05-subagent-rules.md', '06-interactive-protocol.md', '07-plan-first-mode.md', '08-antipatterns.md', '09-runtime-rules.md', ] # .agent/ в submodule-layout лежит в INFRA_DIR, в standalone — в BASE agent_dir = INFRA_DIR / '.agent' if (INFRA_DIR / '.agent').exists() else BASE / '.agent' for f in agent_rules: all_ok &= check(f'.agent/rules/{f}', (agent_dir / 'rules' / f).exists(), 'Архив повреждён — отсутствует файл правил') agent_prompts = ['README.md', 'adr-writer.md', 'debug-fixer.md', 'step-rollback.md', 'test-runner.md'] for f in agent_prompts: all_ok &= check(f'.agent/prompts/{f}', (agent_dir / 'prompts' / f).exists(), 'Архив повреждён — отсутствует промпт') agent_cards = ['phase1.card.md', 'implementation.card.md', 'phase2a.card.md'] for f in agent_cards: all_ok &= check(f'.agent/cards/{f}', (agent_dir / 'cards' / f).exists(), 'Архив повреждён — отсутствует карточка') # === 5. plans/ YAML-файлы === print(f'\n{BOLD}5. plans/ YAML-планы{RESET}') plans_dir = BASE / 'plans' yaml_count = len(list(plans_dir.glob('*.yaml'))) all_ok &= check(f'YAML-планы: {yaml_count} файлов', yaml_count >= 1, 'Создайте план: python3 .ump/scripts/ump.py setup --example web-app') # === 6. scripts/ === print(f'\n{BOLD}6. scripts/ скрипты{RESET}') required_scripts = [ 'agent_log.py', 'detect_manual_changes.py', 'extract_section.py', 'finalize_step.py', 'gen_progress.py', 'git_commit_guard.sh', 'orchestrate_step.py', 'run_phase.py', 'validate_md.py', 'validate_plans.py', 'verify_step_completion.py', 'preflight_check.py', 'protect_files.py', 'log_git.sh', ] scripts_dir = INFRA_DIR / 'scripts' if (INFRA_DIR / 'scripts').exists() else BASE / 'scripts' for f in required_scripts: all_ok &= check(f'scripts/{f}', (scripts_dir / f).exists(), f'Скрипт отсутствует: scripts/{f}') # === 6.5 immutable-файлы === print(f'\n{BOLD}6.5 immutable-файлы{RESET}') all_ok &= check('.agent/immutable.txt', (agent_dir / 'immutable.txt').exists(), 'Архив повреждён — отсутствует список immutable-файлов') snapshot = INFRA_DIR / '_meta' / 'immutable-snapshot.json' if not snapshot.exists(): snapshot = BASE / '_meta' / 'immutable-snapshot.json' if not snapshot.exists(): print(f' {YELLOW}⚠{RESET} immutable-snapshot.json не существует') print(f' {YELLOW}→ Запустите: make protect-init{RESET}') print(f' {YELLOW} (создаст snapshot SHA-256 всех immutable-файлов){RESET}') # Не считаем критичной ошибкой — пользователь может инициализировать позже else: print(f' {GREEN}✓{RESET} immutable-snapshot.json существует') # === 7. VitePress docs/ === print(f'\n{BOLD}7. VitePress документация{RESET}') docs_dir = BASE / 'docs' if not docs_dir.exists(): docs_dir = INFRA_DIR / 'docs' all_ok &= check('docs/.vitepress/config.mts', (docs_dir / '.vitepress' / 'config.mts').exists(), 'VitePress config отсутствует') all_ok &= check('docs/index.md', (docs_dir / 'index.md').exists(), 'VitePress index отсутствует') docs_rukovodstvo = list((docs_dir / 'руководство').glob('*.md')) all_ok &= check(f'docs/руководство/: {len(docs_rukovodstvo)} страниц', len(docs_rukovodstvo) >= 8, 'Недостаточно страниц руководства') # === 8. Зависимости === print(f'\n{BOLD}8. Зависимости{RESET}') # uv sync venv_exists = (BASE / '.venv').exists() all_ok &= check('.venv/ создан (uv sync)', venv_exists, 'Запустите: uv sync --dev') # node_modules node_modules_exists = (BASE / 'node_modules').exists() all_ok &= check('node_modules/ создан (npm install)', node_modules_exists, 'Запустите: npm install') # === 9. CI/CD === print(f'\n{BOLD}9. CI/CD workflows{RESET}') all_ok &= check('.gitverse/workflows/deploy-docs.yml', (BASE / '.gitverse' / 'workflows' / 'deploy-docs.yml').exists(), 'Workflow для деплоя документации отсутствует') # === 10. Pre-commit hook === print(f'\n{BOLD}10. Pre-commit hook{RESET}') source_hook = BASE / 'scripts' / 'git_commit_guard.sh' target_hook = BASE / '.git' / 'hooks' / 'pre-commit' if not source_hook.exists(): all_ok &= check('scripts/git_commit_guard.sh существует', False, 'Скрипт git_commit_guard.sh отсутствует') else: source_hash = hashlib.sha256(source_hook.read_bytes()).hexdigest() if target_hook.exists(): target_hash = hashlib.sha256(target_hook.read_bytes()).hexdigest() if source_hash == target_hash: print(f' {GREEN}✓{RESET} pre-commit hook установлен и актуален') else: print(f' {YELLOW}⚠{RESET} pre-commit hook устарел — обновляю...') try: shutil.copy2(source_hook, target_hook) target_hook.chmod(0o755) print(f' {GREEN}✓{RESET} pre-commit hook обновлён') except Exception as e: all_ok &= check('Обновление pre-commit hook', False, f'Не удалось обновить: {e}') else: print(f' {YELLOW}⚠{RESET} pre-commit hook не установлен — устанавливаю...') git_hooks_dir = BASE / '.git' / 'hooks' if not git_hooks_dir.exists(): git_hooks_dir.mkdir(parents=True, exist_ok=True) try: shutil.copy2(source_hook, target_hook) target_hook.chmod(0o755) print(f' {GREEN}✓{RESET} pre-commit hook установлен: {target_hook}') except Exception as e: all_ok &= check('Установка pre-commit hook', False, f'Не удалось установить: {e}') # === 11. GigaCode auto-discovery === print(f'\n{BOLD}11. GigaCode auto-discovery{RESET}') vscode_settings = BASE / '.vscode' / 'settings.json' if vscode_settings.exists(): try: import json as _json settings = _json.loads(vscode_settings.read_text(encoding='utf-8')) # Реальные настройки: language, notifications.*, sounds.*, showTaskTimeline if 'gigacode.new.rules' in settings: print(f' {RED}✗{RESET} .vscode/settings.json содержит gigacode.new.rules — НЕ СУЩЕСТВУЕТ в GigaCode v26.7.43!') print(f' {YELLOW}→ Удалите этот ключ. Реальный механизм: auto-discovery через{RESET}') print(f' AGENTS.md + .agent/rules/ (см. .agent/rules/README.md){RESET}') all_ok = False else: print(f' {GREEN}✓{RESET} .vscode/settings.json: нет несуществующего gigacode.new.rules (OK)') except Exception as e: print(f' {YELLOW}⚠{RESET} .vscode/settings.json: не удалось прочитать ({e})') else: print(f' {YELLOW}⚠{RESET} .vscode/settings.json не найден — см. docs/руководство/gigacode-настройка.md') # Проверка auto-discovery путей print(f'\n{BOLD}11.1. GigaCode auto-discovery пути{RESET}') auto_discovery_paths = [ ('AGENTS.md', 'Основная точка входа (читается автоматически)'), ('.agent/rules/', 'Auto-discovery правила (v33)'), ('.gigacoderules', 'Альтернативный путь (опционально)'), ] for path, desc in auto_discovery_paths: full = BASE / path.rstrip('/') if path.endswith('/'): if full.is_dir(): count = len(list(full.glob('*.md'))) print(f' {GREEN}✓{RESET} {path} — {count} .md файлов ({desc})') else: if path == '.agent/rules/': print(f' {YELLOW}⚠{RESET} {path} не найден — auto-discovery будет работать только через AGENTS.md') else: print(f' {YELLOW}⚠{RESET} {path} не найден (опционально)') else: if full.exists(): print(f' {GREEN}✓{RESET} {path} — существует ({desc})') else: print(f' {RED}✗{RESET} {path} — отсутствует ({desc})') if path == 'AGENTS.md': all_ok = False # Проверка .agent/skills/ (v33: НЕ работает в GigaCode v26.7.43) skills_dir = BASE / 'skills' if skills_dir.is_dir(): print(f'\n{BOLD}11.2. .agent/skills/ (статус: НЕ работает в GigaCode v26.7.43){RESET}') print(f' {YELLOW}⚠{RESET} .agent/skills/ существует, но GigaCode v26.7.43 НЕ загружает skills автоматически') print(' (см. .agent/skills/README.md — Skills System Priority: P2, runtime не интегрирован)') print(' Используйте AGENTS.md + .agent/rules/ вместо .agent/skills/') # === ИТОГ === print(f'\n{"=" * 60}') if all_ok: print(f'{GREEN}{BOLD}✓ ВСЁ ГОТОВО К РАБОТЕ{RESET}') print('\nДальнейшие шаги:') print(' 1. Откройте чат GigaCode в VSCode') print(' 2. Скажите: выполни шаг 1.0') print(' 3. Агент проверит клон и начнёт работу') sys.exit(0) else: print(f'{RED}{BOLD}✗ ЕСТЬ ПРОБЛЕМЫ — нужно исправить{RESET}') if manual_actions: print(f'\n{YELLOW}Ручные действия:{RESET}') for action in manual_actions: print(f' → {action}') print(f'\n{YELLOW}Автоматические команды для исправления:{RESET}') print(' make setup # установить npm + uv зависимости') print(' make preflight # повторная проверка') print('') print('Если git не настроен:') print(' git config --global user.name "Ваше Имя"') print(' git config --global user.email "your_email@example.com"') print(' git remote add origin <URL_репозитория>') sys.exit(1) if __name__ == '__main__': main()