Files
Mayacontigo/.mise/tasks/new.py
Rogelio 325f1ef439 ic
2025-10-13 18:16:25 +00:00

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)