project-standalo-note-to-app/skills/guardrail-orchestrator/scripts/detect_framework.py

462 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Framework Detection Utility for Eureka Deployments.
Detects project framework from package.json, requirements.txt, go.mod, or Cargo.toml
and returns appropriate Dockerfile template information.
"""
import json
import sys
from pathlib import Path
from typing import Dict, Any, Optional, List
# Framework definitions with Dockerfile requirements
FRAMEWORKS: Dict[str, Dict[str, Any]] = {
# Full-stack frameworks
'nextjs-prisma': {
'name': 'Next.js + Prisma',
'base_image': 'node:20-alpine3.18',
'port': 3000,
'requirements': ['output: standalone in next.config.js', 'openssl1.1-compat'],
'build_cmd': 'npm run build',
'start_cmd': 'node server.js',
},
'nextjs': {
'name': 'Next.js',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': ['output: standalone in next.config.js'],
'build_cmd': 'npm run build',
'start_cmd': 'node server.js',
},
'nuxt': {
'name': 'Nuxt.js 3',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node .output/server/index.mjs',
},
'remix': {
'name': 'Remix',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'npm start',
},
'sveltekit': {
'name': 'SvelteKit',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node build',
},
# Frontend frameworks (SPA)
'react-vite': {
'name': 'React (Vite)',
'base_image': 'nginx:alpine',
'port': 80,
'requirements': ['nginx.conf for SPA routing'],
'build_cmd': 'npm run build',
'build_output': 'dist',
'spa': True,
},
'react-cra': {
'name': 'React (Create React App)',
'base_image': 'nginx:alpine',
'port': 80,
'requirements': ['nginx.conf for SPA routing'],
'build_cmd': 'npm run build',
'build_output': 'build',
'spa': True,
},
'vue-vite': {
'name': 'Vue.js (Vite)',
'base_image': 'nginx:alpine',
'port': 80,
'requirements': ['nginx.conf for SPA routing'],
'build_cmd': 'npm run build',
'build_output': 'dist',
'spa': True,
},
'angular': {
'name': 'Angular',
'base_image': 'nginx:alpine',
'port': 80,
'requirements': ['nginx.conf for SPA routing'],
'build_cmd': 'npm run build --prod',
'build_output': 'dist/*/browser',
'spa': True,
},
# Backend frameworks
'nestjs-prisma': {
'name': 'NestJS + Prisma',
'base_image': 'node:20-alpine3.18',
'port': 3000,
'requirements': ['openssl1.1-compat'],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/main.js',
},
'nestjs': {
'name': 'NestJS',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/main.js',
},
'fastify-prisma': {
'name': 'Fastify + Prisma',
'base_image': 'node:20-alpine3.18',
'port': 3000,
'requirements': ['openssl1.1-compat'],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/index.js',
},
'fastify': {
'name': 'Fastify',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/index.js',
},
'express-prisma': {
'name': 'Express + Prisma',
'base_image': 'node:20-alpine3.18',
'port': 3000,
'requirements': ['openssl1.1-compat'],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/index.js',
},
'express': {
'name': 'Express.js',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/index.js',
},
'hono': {
'name': 'Hono',
'base_image': 'node:20-alpine',
'port': 3000,
'requirements': [],
'build_cmd': 'npm run build',
'start_cmd': 'node dist/index.js',
},
# Python frameworks
'fastapi': {
'name': 'FastAPI',
'base_image': 'python:3.12-slim',
'port': 8000,
'requirements': ['uvicorn'],
'start_cmd': 'uvicorn main:app --host 0.0.0.0 --port 8000',
},
'flask': {
'name': 'Flask',
'base_image': 'python:3.12-slim',
'port': 5000,
'requirements': ['gunicorn'],
'start_cmd': 'gunicorn --bind 0.0.0.0:5000 app:app',
},
'django': {
'name': 'Django',
'base_image': 'python:3.12-slim',
'port': 8000,
'requirements': ['gunicorn', 'collectstatic'],
'start_cmd': 'gunicorn --bind 0.0.0.0:8000 project.wsgi:application',
},
# Other languages
'go': {
'name': 'Go',
'base_image': 'golang:1.22-alpine',
'runtime_image': 'alpine:latest',
'port': 8080,
'requirements': ['CGO_ENABLED=0 for static binary'],
'build_cmd': 'go build -o main .',
'start_cmd': './main',
},
'rust': {
'name': 'Rust',
'base_image': 'rust:1.75-alpine',
'runtime_image': 'alpine:latest',
'port': 8080,
'requirements': ['musl-dev for static linking'],
'build_cmd': 'cargo build --release',
'start_cmd': './app',
},
}
def has_prisma(project_path: Path, deps: Dict[str, str]) -> bool:
"""Check if project uses Prisma ORM."""
# Check for prisma in dependencies
if 'prisma' in deps or '@prisma/client' in deps:
return True
# Check for prisma directory
if (project_path / 'prisma').exists():
return True
return False
def detect_node_framework(project_path: Path) -> Optional[str]:
"""Detect Node.js framework from package.json."""
package_json_path = project_path / 'package.json'
if not package_json_path.exists():
return None
try:
with open(package_json_path) as f:
pkg = json.load(f)
except (json.JSONDecodeError, IOError):
return None
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
uses_prisma = has_prisma(project_path, deps)
# Full-stack frameworks (order matters - check specific ones first)
if 'next' in deps:
return 'nextjs-prisma' if uses_prisma else 'nextjs'
if 'nuxt' in deps:
return 'nuxt'
if '@remix-run/node' in deps or '@remix-run/react' in deps:
return 'remix'
if '@sveltejs/kit' in deps:
return 'sveltekit'
# Frontend frameworks (SPA)
if 'vite' in deps and 'react' in deps:
return 'react-vite'
if 'react-scripts' in deps:
return 'react-cra'
if 'vite' in deps and 'vue' in deps:
return 'vue-vite'
if '@angular/core' in deps:
return 'angular'
# Backend frameworks
if '@nestjs/core' in deps:
return 'nestjs-prisma' if uses_prisma else 'nestjs'
if 'fastify' in deps:
return 'fastify-prisma' if uses_prisma else 'fastify'
if 'express' in deps:
return 'express-prisma' if uses_prisma else 'express'
if 'hono' in deps:
return 'hono'
return None
def detect_python_framework(project_path: Path) -> Optional[str]:
"""Detect Python framework from requirements.txt or pyproject.toml."""
requirements_path = project_path / 'requirements.txt'
pyproject_path = project_path / 'pyproject.toml'
requirements_content = ''
if requirements_path.exists():
try:
requirements_content = requirements_path.read_text().lower()
except IOError:
pass
if pyproject_path.exists():
try:
requirements_content += pyproject_path.read_text().lower()
except IOError:
pass
if not requirements_content:
return None
if 'fastapi' in requirements_content:
return 'fastapi'
if 'flask' in requirements_content:
return 'flask'
if 'django' in requirements_content:
return 'django'
return None
def detect_go_project(project_path: Path) -> Optional[str]:
"""Detect Go project from go.mod."""
if (project_path / 'go.mod').exists():
return 'go'
return None
def detect_rust_project(project_path: Path) -> Optional[str]:
"""Detect Rust project from Cargo.toml."""
if (project_path / 'Cargo.toml').exists():
return 'rust'
return None
def detect_framework(project_path: Path) -> Dict[str, Any]:
"""
Detect project framework and return Dockerfile configuration.
Args:
project_path: Path to the project root
Returns:
Dictionary with framework info and Dockerfile requirements
"""
project_path = Path(project_path).resolve()
# Try detection in order
framework_id = (
detect_node_framework(project_path) or
detect_python_framework(project_path) or
detect_go_project(project_path) or
detect_rust_project(project_path)
)
if not framework_id:
return {
'detected': False,
'framework_id': 'unknown',
'name': 'Unknown',
'error': 'Could not detect framework. Check package.json, requirements.txt, go.mod, or Cargo.toml',
}
framework = FRAMEWORKS[framework_id].copy()
framework['detected'] = True
framework['framework_id'] = framework_id
# Add validation checks
checks: List[Dict[str, Any]] = []
# Check Next.js standalone config
if framework_id in ['nextjs', 'nextjs-prisma']:
next_config = project_path / 'next.config.js'
next_config_mjs = project_path / 'next.config.mjs'
next_config_ts = project_path / 'next.config.ts'
config_found = False
standalone_configured = False
for config_path in [next_config, next_config_mjs, next_config_ts]:
if config_path.exists():
config_found = True
try:
content = config_path.read_text()
if 'standalone' in content:
standalone_configured = True
except IOError:
pass
checks.append({
'name': 'Next.js standalone output',
'passed': standalone_configured,
'message': 'Add output: "standalone" to next.config.js' if not standalone_configured else 'OK',
'critical': True,
})
# Check nginx.conf for SPAs
if framework.get('spa'):
nginx_conf = project_path / 'nginx.conf'
checks.append({
'name': 'nginx.conf exists',
'passed': nginx_conf.exists(),
'message': 'Create nginx.conf for SPA routing' if not nginx_conf.exists() else 'OK',
'critical': False, # Can be generated
})
# Check Dockerfile exists
dockerfile = project_path / 'Dockerfile'
checks.append({
'name': 'Dockerfile exists',
'passed': dockerfile.exists(),
'message': 'Dockerfile will be generated' if not dockerfile.exists() else 'OK',
'critical': False,
})
framework['checks'] = checks
framework['all_checks_passed'] = all(c['passed'] or not c['critical'] for c in checks)
return framework
def format_output(result: Dict[str, Any], format_type: str = 'json') -> str:
"""Format the detection result for output."""
if format_type == 'json':
return json.dumps(result, indent=2)
if format_type == 'summary':
lines = []
if result['detected']:
lines.append(f"Framework: {result['name']}")
lines.append(f"Base Image: {result['base_image']}")
lines.append(f"Port: {result['port']}")
if result.get('requirements'):
lines.append(f"Requirements: {', '.join(result['requirements'])}")
if result.get('checks'):
lines.append("\nValidation Checks:")
for check in result['checks']:
status = "" if check['passed'] else ""
lines.append(f" {status} {check['name']}: {check['message']}")
else:
lines.append(f"{result['error']}")
return '\n'.join(lines)
return str(result)
def main():
"""CLI entry point."""
import argparse
parser = argparse.ArgumentParser(
description='Detect project framework for Eureka deployment'
)
parser.add_argument(
'path',
nargs='?',
default='.',
help='Project path (default: current directory)'
)
parser.add_argument(
'--format',
choices=['json', 'summary'],
default='json',
help='Output format (default: json)'
)
parser.add_argument(
'--check',
action='store_true',
help='Exit with error code if critical checks fail'
)
args = parser.parse_args()
project_path = Path(args.path)
if not project_path.exists():
print(f"Error: Path not found: {project_path}", file=sys.stderr)
sys.exit(1)
result = detect_framework(project_path)
print(format_output(result, args.format))
if args.check and result.get('detected') and not result.get('all_checks_passed'):
sys.exit(1)
elif not result.get('detected'):
sys.exit(1)
if __name__ == '__main__':
main()