forked from innovacion/Mayacontigo
ic
This commit is contained in:
326
.mise/tasks/new.py
Executable file
326
.mise/tasks/new.py
Executable file
@@ -0,0 +1,326 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user