forked from innovacion/Mayacontigo
327 lines
11 KiB
Python
Executable File
327 lines
11 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
#MISE silent=true
|
|
#MISE description="Create a new project from our templates"
|
|
# /// script
|
|
# dependencies = ["rich", "pyyaml"]
|
|
# ///
|
|
"""
|
|
Slick app generator script with Rich formatting
|
|
"""
|
|
|
|
import sys
|
|
import subprocess
|
|
import shutil
|
|
import yaml
|
|
from pathlib import Path
|
|
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TimeElapsedColumn
|
|
from rich.text import Text
|
|
from rich.prompt import Prompt
|
|
from rich.table import Table
|
|
from rich import box
|
|
|
|
console = Console()
|
|
|
|
def show_banner():
|
|
"""Display a stylish banner"""
|
|
banner = Text("🚀 RAG TEMPLATE", style="bold magenta")
|
|
banner.stylize("bold cyan", 0, 4) # Style the rocket emoji
|
|
|
|
panel = Panel(
|
|
banner,
|
|
box=box.DOUBLE,
|
|
border_style="bright_blue",
|
|
padding=(1, 2)
|
|
)
|
|
console.print(panel)
|
|
|
|
def validate_app_name(app_name):
|
|
"""Validate the app name"""
|
|
if not app_name:
|
|
return False, "App name cannot be empty"
|
|
|
|
if not app_name.replace('_', '').replace('-', '').isalnum():
|
|
return False, "App name can only contain letters, numbers, hyphens, and underscores"
|
|
|
|
if len(app_name) < 2:
|
|
return False, "App name must be at least 2 characters long"
|
|
|
|
if len(app_name) > 50:
|
|
return False, "App name must be less than 50 characters long"
|
|
|
|
return True, "Valid app name"
|
|
|
|
def run_command(command, description, cwd=None):
|
|
"""Run a command with progress indicator"""
|
|
try:
|
|
with Progress(
|
|
SpinnerColumn(),
|
|
TextColumn("[progress.description]{task.description}"),
|
|
BarColumn(),
|
|
TimeElapsedColumn(),
|
|
console=console,
|
|
transient=True
|
|
) as progress:
|
|
task = progress.add_task(description, total=None)
|
|
|
|
result = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True
|
|
)
|
|
|
|
progress.update(task, completed=True)
|
|
|
|
if result.returncode != 0:
|
|
console.print(f"❌ [red]Error running command:[/red] {command}")
|
|
console.print(f"[red]Error output:[/red] {result.stderr}")
|
|
return False
|
|
|
|
return True
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Exception occurred:[/red] {str(e)}")
|
|
return False
|
|
|
|
class CustomYAMLDumper(yaml.SafeDumper):
|
|
"""Custom YAML dumper that preserves list formatting"""
|
|
|
|
def write_line_break(self, data=None):
|
|
super().write_line_break(data)
|
|
if len(self.indents) == 1:
|
|
super().write_line_break()
|
|
|
|
def custom_yaml_dump(data, stream=None):
|
|
"""Custom YAML dump function that maintains proper list formatting"""
|
|
class ListDumper(yaml.SafeDumper):
|
|
def increase_indent(self, flow=False, indentless=False):
|
|
return super().increase_indent(flow, False)
|
|
|
|
def represent_list(self, data):
|
|
return self.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=False)
|
|
|
|
def represent_dict(self, data):
|
|
return self.represent_mapping('tag:yaml.org,2002:map', data, flow_style=False)
|
|
|
|
ListDumper.add_representer(list, ListDumper.represent_list)
|
|
ListDumper.add_representer(dict, ListDumper.represent_dict)
|
|
|
|
return yaml.dump(data, stream=stream, Dumper=ListDumper,
|
|
default_flow_style=False, indent=2, sort_keys=False,
|
|
allow_unicode=True, width=1000)
|
|
|
|
def find_available_port(compose_path):
|
|
"""Find the next available port for the new service"""
|
|
used_ports = set()
|
|
|
|
try:
|
|
with open(compose_path, 'r') as f:
|
|
compose_data = yaml.safe_load(f)
|
|
|
|
# Extract used ports from existing services
|
|
services = compose_data.get('services', {})
|
|
for service_name, service_config in services.items():
|
|
if service_name == 'traefik': # Skip traefik
|
|
continue
|
|
ports = service_config.get('ports', [])
|
|
for port_mapping in ports:
|
|
if isinstance(port_mapping, str):
|
|
external_port = int(port_mapping.split(':')[0])
|
|
used_ports.add(external_port)
|
|
|
|
# Find next available port starting from 8001
|
|
port = 8001
|
|
while port in used_ports:
|
|
port += 1
|
|
|
|
return port
|
|
except Exception as e:
|
|
console.print(f"⚠️ [yellow]Could not determine available port, using 8001:[/yellow] {str(e)}")
|
|
return 8001
|
|
|
|
def generate_service_config(app_name, port):
|
|
"""Generate Docker Compose service configuration for new app"""
|
|
return {
|
|
'image': f'mayacontigo/{app_name}:latest',
|
|
'build': {
|
|
'context': '.',
|
|
'dockerfile': '.containers/unit/Dockerfile',
|
|
'args': {
|
|
'PACKAGE': app_name
|
|
}
|
|
},
|
|
'x-bake': {
|
|
'tags': [f'mayacontigo/{app_name}:latest']
|
|
},
|
|
'ports': [f'{port}:80'],
|
|
'labels': [
|
|
'traefik.enable=true',
|
|
f'traefik.http.routers.{app_name}.rule=PathPrefix(`/api/maya{app_name}`)',
|
|
f'traefik.http.routers.{app_name}.entrypoints=web',
|
|
f'traefik.http.routers.{app_name}.middlewares={app_name}-strip',
|
|
f'traefik.http.middlewares.{app_name}-strip.stripprefix.prefixes=/api/maya{app_name}'
|
|
]
|
|
}
|
|
|
|
def add_service_to_compose(app_name):
|
|
"""Add new service to compose.yaml file"""
|
|
compose_path = Path('compose.yaml')
|
|
|
|
if not compose_path.exists():
|
|
console.print(f"⚠️ [yellow]compose.yaml not found, skipping service addition.[/yellow]")
|
|
return True
|
|
|
|
try:
|
|
# Create backup
|
|
backup_path = compose_path.with_suffix('.yaml.backup')
|
|
shutil.copy2(compose_path, backup_path)
|
|
|
|
# Load existing compose file
|
|
with open(compose_path, 'r') as f:
|
|
compose_data = yaml.safe_load(f)
|
|
|
|
# Check if service already exists
|
|
services = compose_data.get('services', {})
|
|
if app_name in services:
|
|
console.print(f"⚠️ [yellow]Service '{app_name}' already exists in compose.yaml[/yellow]")
|
|
return True
|
|
|
|
# Find available port
|
|
port = find_available_port(compose_path)
|
|
|
|
# Generate service configuration
|
|
service_config = generate_service_config(app_name, port)
|
|
|
|
# Add service to compose data
|
|
services[app_name] = service_config
|
|
|
|
# Write updated compose file with proper formatting
|
|
with open(compose_path, 'w') as f:
|
|
custom_yaml_dump(compose_data, f)
|
|
|
|
console.print(f"✅ [green]Added service '{app_name}' to compose.yaml (port {port})[/green]")
|
|
|
|
# Remove backup on success
|
|
backup_path.unlink()
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
console.print(f"❌ [red]Failed to add service to compose.yaml:[/red] {str(e)}")
|
|
|
|
# Restore backup if it exists
|
|
if backup_path.exists():
|
|
shutil.copy2(backup_path, compose_path)
|
|
backup_path.unlink()
|
|
console.print("🔄 [yellow]Restored original compose.yaml from backup[/yellow]")
|
|
|
|
return False
|
|
|
|
def show_summary(app_name):
|
|
"""Show a summary of what was created"""
|
|
table = Table(title="🎉 App Created Successfully!", box=box.ROUNDED)
|
|
table.add_column("Component", style="cyan", no_wrap=True)
|
|
table.add_column("Status", style="green")
|
|
table.add_column("Location", style="yellow")
|
|
|
|
table.add_row("📁 Project Directory", "✅ Created", f"./apps/{app_name}")
|
|
table.add_row("📦 Dependencies", "✅ Installed", "package.json + requirements.txt")
|
|
table.add_row("🔧 Template", "✅ Applied", "RAG template")
|
|
table.add_row("🐍 Python Environment", "✅ Synced", "uv environment")
|
|
table.add_row("🐳 Docker Service", "✅ Added", "compose.yaml")
|
|
|
|
console.print(table)
|
|
|
|
# Next steps
|
|
next_steps = Panel(
|
|
f"[bold green]Next Steps:[/bold green]\n\n"
|
|
f"1. [cyan]mise dev --app {app_name}[/cyan]\n"
|
|
f"2. [cyan]Start building your RAG app! 🚀[/cyan]",
|
|
title="🎯 What's Next?",
|
|
border_style="green",
|
|
box=box.ROUNDED
|
|
)
|
|
console.print(next_steps)
|
|
|
|
def main():
|
|
"""Main function"""
|
|
show_banner()
|
|
|
|
# Get app name from command line or prompt
|
|
if len(sys.argv) < 2:
|
|
console.print("[yellow]No app name provided as argument.[/yellow]")
|
|
app_name = Prompt.ask("Enter your app name", default="my-rag-app")
|
|
else:
|
|
app_name = sys.argv[1]
|
|
|
|
# Validate app name
|
|
is_valid, message = validate_app_name(app_name)
|
|
if not is_valid:
|
|
console.print(f"❌ [red]Invalid app name:[/red] {message}")
|
|
sys.exit(1)
|
|
|
|
# Show what we're about to do
|
|
console.print(f"[bold]Creating app:[/bold] [cyan]{app_name}[/cyan]")
|
|
console.print()
|
|
|
|
# Check if directory already exists
|
|
app_path = Path(f"apps/{app_name}")
|
|
if app_path.exists():
|
|
console.print(f"⚠️ [yellow]Directory 'apps/{app_name}' already exists![/yellow]")
|
|
if not Prompt.ask("Do you want to continue?", choices=["y", "n"], default="n") == "y":
|
|
console.print("Operation cancelled.")
|
|
sys.exit(0)
|
|
|
|
# Step 1: Copy template
|
|
console.print("📂 [bold]Step 1:[/bold] Copying RAG template...")
|
|
if not run_command(
|
|
f"uvx copier copy .templates/rag apps --data project_name={app_name} --trust",
|
|
"Copying template files..."
|
|
):
|
|
console.print("❌ [red]Failed to copy template. Make sure you're in the right directory.[/red]")
|
|
sys.exit(1)
|
|
|
|
# Step 2: Install npm dependencies
|
|
console.print("📦 [bold]Step 2:[/bold] Installing npm dependencies...")
|
|
if not run_command(
|
|
"pnpm install",
|
|
"Installing npm packages...",
|
|
cwd=f"apps/{app_name}"
|
|
):
|
|
console.print("❌ [red]Failed to install npm dependencies.[/red]")
|
|
sys.exit(1)
|
|
|
|
# Step 3: Sync Python environment
|
|
console.print("🐍 [bold]Step 3:[/bold] Setting up Python environment...")
|
|
if not run_command(
|
|
"uv sync",
|
|
"Syncing Python environment...",
|
|
cwd=f"apps/{app_name}"
|
|
):
|
|
console.print("❌ [red]Failed to sync Python environment.[/red]")
|
|
sys.exit(1)
|
|
|
|
# Step 4: Add service to compose.yaml
|
|
console.print("🐳 [bold]Step 4:[/bold] Adding service to compose.yaml...")
|
|
if not add_service_to_compose(app_name):
|
|
console.print("❌ [red]Failed to add service to compose.yaml.[/red]")
|
|
console.print("⚠️ [yellow]You can manually add the service later.[/yellow]")
|
|
# Don't exit here, this is not critical
|
|
|
|
# Success!
|
|
console.print()
|
|
show_summary(app_name)
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]Operation cancelled by user.[/yellow]")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
console.print(f"\n❌ [red]Unexpected error:[/red] {str(e)}")
|
|
sys.exit(1)
|