Добавлен анализатор GitHub проектов и зависимостей

Этот коммит содержится в:
Виктор
2026-05-11 22:24:22 +09:00
родитель e8d9eda61a
Коммит 7a277df9b0
+171
Просмотреть файл
@@ -0,0 +1,171 @@
import json
import os
import re
import shutil
import subprocess
from dataclasses import dataclass, asdict
from pathlib import Path
from urllib.parse import urlparse
DATA_DIR = Path(os.getenv('DEVCONSOLE_DATA_DIR', '/var/lib/devconsole'))
PROJECTS_DIR = DATA_DIR / 'projects'
@dataclass
class DependencyStep:
title: str
command: str
cwd: str
safe: bool = True
@dataclass
class ProjectAnalysis:
repo_url: str
workspace: str
detected_stack: list[str]
files_found: list[str]
dependency_steps: list[DependencyStep]
notes: list[str]
def to_dict(self) -> dict:
data = asdict(self)
data['dependency_steps'] = [asdict(step) for step in self.dependency_steps]
return data
def _slug_from_repo_url(repo_url: str) -> str:
parsed = urlparse(repo_url)
path = parsed.path.strip('/').replace('.git', '')
slug = re.sub(r'[^a-zA-Z0-9_.-]+', '-', path)
return slug or 'project'
def clone_or_update_project(repo_url: str) -> Path:
PROJECTS_DIR.mkdir(parents=True, exist_ok=True)
slug = _slug_from_repo_url(repo_url)
workspace = PROJECTS_DIR / slug
if (workspace / '.git').exists():
subprocess.run(['git', '-C', str(workspace), 'fetch', '--all'], check=True)
subprocess.run(['git', '-C', str(workspace), 'pull', '--ff-only'], check=False)
else:
if workspace.exists():
shutil.rmtree(workspace)
subprocess.run(['git', 'clone', repo_url, str(workspace)], check=True)
return workspace
def _exists(root: Path, relative: str) -> bool:
return (root / relative).exists()
def _find_files(root: Path, names: set[str], limit: int = 80) -> list[Path]:
result: list[Path] = []
ignored_dirs = {'.git', 'node_modules', 'vendor', '.venv', 'venv', 'build', 'dist', '.dart_tool', '.gradle'}
for current, dirs, files in os.walk(root):
dirs[:] = [d for d in dirs if d not in ignored_dirs]
for file in files:
if file in names:
result.append(Path(current) / file)
if len(result) >= limit:
return result
return result
def analyze_workspace(repo_url: str, workspace: Path) -> ProjectAnalysis:
detected_stack: list[str] = []
files_found: list[str] = []
steps: list[DependencyStep] = []
notes: list[str] = []
marker_files = _find_files(
workspace,
{
'package.json', 'requirements.txt', 'pyproject.toml', 'Pipfile',
'composer.json', 'pubspec.yaml', 'build.gradle', 'build.gradle.kts',
'settings.gradle', 'settings.gradle.kts', 'Dockerfile', 'docker-compose.yml',
'docker-compose.yaml', 'go.mod', 'Cargo.toml'
}
)
for marker in marker_files:
relative = marker.relative_to(workspace).as_posix()
files_found.append(relative)
cwd = marker.parent.as_posix()
if marker.name == 'package.json':
detected_stack.append('Node.js')
package_lock = marker.parent / 'package-lock.json'
yarn_lock = marker.parent / 'yarn.lock'
pnpm_lock = marker.parent / 'pnpm-lock.yaml'
if pnpm_lock.exists():
steps.append(DependencyStep('Install Node dependencies with pnpm', 'corepack enable && pnpm install', cwd))
elif yarn_lock.exists():
steps.append(DependencyStep('Install Node dependencies with yarn', 'corepack enable && yarn install', cwd))
elif package_lock.exists():
steps.append(DependencyStep('Install Node dependencies with npm ci', 'npm ci', cwd))
else:
steps.append(DependencyStep('Install Node dependencies with npm', 'npm install', cwd))
elif marker.name == 'requirements.txt':
detected_stack.append('Python')
steps.append(DependencyStep('Create Python virtualenv', 'python3 -m venv .venv', cwd))
steps.append(DependencyStep('Install Python requirements', '.venv/bin/pip install --upgrade pip && .venv/bin/pip install -r requirements.txt', cwd))
elif marker.name == 'pyproject.toml':
detected_stack.append('Python')
steps.append(DependencyStep('Install Python project', 'python3 -m venv .venv && .venv/bin/pip install --upgrade pip && .venv/bin/pip install -e .', cwd))
elif marker.name == 'composer.json':
detected_stack.append('PHP/Composer')
steps.append(DependencyStep('Install PHP Composer dependencies', 'composer install', cwd))
elif marker.name == 'pubspec.yaml':
detected_stack.append('Flutter/Dart')
steps.append(DependencyStep('Install Flutter/Dart dependencies', 'flutter pub get', cwd))
elif marker.name in {'build.gradle', 'build.gradle.kts'}:
detected_stack.append('Gradle/Android')
gradlew = marker.parent / 'gradlew'
command = './gradlew tasks' if gradlew.exists() else 'gradle tasks'
steps.append(DependencyStep('Prepare Gradle project', command, cwd))
elif marker.name in {'docker-compose.yml', 'docker-compose.yaml'}:
detected_stack.append('Docker Compose')
steps.append(DependencyStep('Pull Docker Compose images', 'docker compose pull', cwd))
elif marker.name == 'Dockerfile':
detected_stack.append('Docker')
image_name = _slug_from_repo_url(repo_url).replace('/', '-').lower()
steps.append(DependencyStep('Build Docker image', f'docker build -t {image_name}:dev .', cwd))
elif marker.name == 'go.mod':
detected_stack.append('Go')
steps.append(DependencyStep('Download Go modules', 'go mod download', cwd))
elif marker.name == 'Cargo.toml':
detected_stack.append('Rust')
steps.append(DependencyStep('Fetch Cargo dependencies', 'cargo fetch', cwd))
if not files_found:
notes.append('No known dependency manifest found. AI/manual analysis required.')
unique_stack = list(dict.fromkeys(detected_stack))
unique_files = list(dict.fromkeys(files_found))
return ProjectAnalysis(
repo_url=repo_url,
workspace=workspace.as_posix(),
detected_stack=unique_stack,
files_found=unique_files,
dependency_steps=steps,
notes=notes,
)
def analyze_github_project(repo_url: str) -> ProjectAnalysis:
workspace = clone_or_update_project(repo_url)
return analyze_workspace(repo_url, workspace)