#!/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)