import os import sys from typing import List, Optional import typer from rich.console import Console from rich.table import Table from .config import ConfigManager from .container import ContainerManager from .models import SessionStatus app = typer.Typer(help="Monadical Container Tool") session_app = typer.Typer(help="Manage MC sessions") driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True) app.add_typer(session_app, name="session", no_args_is_help=True) app.add_typer(driver_app, name="driver", no_args_is_help=True) console = Console() config_manager = ConfigManager() container_manager = ContainerManager(config_manager) @app.callback(invoke_without_command=True) def main(ctx: typer.Context) -> None: """Monadical Container Tool""" # If no command is specified, create a session if ctx.invoked_subcommand is None: create_session(driver=None, project=None, env=[], name=None) @app.command() def version() -> None: """Show MC version information""" from importlib.metadata import version as get_version try: version_str = get_version("mcontainer") console.print(f"MC - Monadical Container Tool v{version_str}") except Exception: console.print("MC - Monadical Container Tool (development version)") @session_app.command("list") def list_sessions() -> None: """List active MC sessions""" sessions = container_manager.list_sessions() if not sessions: console.print("No active sessions found") return table = Table(show_header=True, header_style="bold") table.add_column("ID") table.add_column("Name") table.add_column("Driver") table.add_column("Status") table.add_column("Ports") table.add_column("Project") for session in sessions: ports_str = ", ".join( [ f"{container_port}:{host_port}" for container_port, host_port in session.ports.items() ] ) status_color = { SessionStatus.RUNNING: "green", SessionStatus.STOPPED: "red", SessionStatus.CREATING: "yellow", SessionStatus.FAILED: "red", }.get(session.status, "white") table.add_row( session.id, session.name, session.driver, f"[{status_color}]{session.status}[/{status_color}]", ports_str, session.project or "", ) console.print(table) @session_app.command("create") def create_session( driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), project: Optional[str] = typer.Option(None, "--project", "-p", help="Project repository URL"), env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" ), name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), ) -> None: """Create a new MC session""" # Use default driver if not specified if not driver: driver = config_manager.config.defaults.get("driver", "goose") # Parse environment variables environment = {} for var in env: if "=" in var: key, value = var.split("=", 1) environment[key] = value else: console.print( f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]" ) with console.status(f"Creating session with driver '{driver}'..."): session = container_manager.create_session( driver_name=driver, project=project, environment=environment, session_name=name, ) if session: console.print("[green]Session created successfully![/green]") console.print(f"Session ID: {session.id}") console.print(f"Driver: {session.driver}") if session.ports: console.print("Ports:") for container_port, host_port in session.ports.items(): console.print(f" {container_port} -> {host_port}") console.print( f"\nConnect to the session with:\n mc session connect {session.id}" ) else: console.print("[red]Failed to create session[/red]") @session_app.command("close") def close_session( session_id: str = typer.Argument(..., help="Session ID to close"), ) -> None: """Close a MC session""" with console.status(f"Closing session {session_id}..."): success = container_manager.close_session(session_id) if success: console.print(f"[green]Session {session_id} closed successfully[/green]") else: console.print(f"[red]Failed to close session {session_id}[/red]") @session_app.command("connect") def connect_session( session_id: str = typer.Argument(..., help="Session ID to connect to"), ) -> None: """Connect to a MC session""" console.print(f"Connecting to session {session_id}...") success = container_manager.connect_session(session_id) if not success: console.print(f"[red]Failed to connect to session {session_id}[/red]") @session_app.command("logs") def session_logs( session_id: str = typer.Argument(..., help="Session ID to get logs from"), follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"), ) -> None: """Stream logs from a MC session""" if follow: console.print(f"Streaming logs from session {session_id}... (Ctrl+C to exit)") container_manager.get_session_logs(session_id, follow=True) else: logs = container_manager.get_session_logs(session_id) if logs: console.print(logs) @app.command() def stop() -> None: """Stop the current MC session (from inside the container)""" # Check if running inside a container if not os.path.exists("/.dockerenv"): console.print( "[red]This command can only be run from inside a MC container[/red]" ) return # Stop the container from inside console.print("Stopping the current session...") os.system("kill 1") # Send SIGTERM to PID 1 (container's init process) # Main CLI entry point that handles project repository URLs @app.command(name="") def quick_create( project: Optional[str] = typer.Argument(..., help="Project repository URL"), driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"), env: List[str] = typer.Option( [], "--env", "-e", help="Environment variables (KEY=VALUE)" ), name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"), ) -> None: """Create a new MC session with a project repository""" create_session(driver=driver, project=project, env=env, name=name) @driver_app.command("list") def list_drivers() -> None: """List available MC drivers""" drivers = config_manager.list_drivers() if not drivers: console.print("No drivers found") return table = Table(show_header=True, header_style="bold") table.add_column("Name") table.add_column("Description") table.add_column("Version") table.add_column("Maintainer") table.add_column("Image") for name, driver in drivers.items(): table.add_row( driver.name, driver.description, driver.version, driver.maintainer, driver.image, ) console.print(table) @driver_app.command("build") def build_driver( driver_name: str = typer.Argument(..., help="Driver name to build"), tag: str = typer.Option("latest", "--tag", "-t", help="Image tag"), push: bool = typer.Option(False, "--push", "-p", help="Push image to registry after building"), ) -> None: """Build a driver Docker image""" # Get driver path driver_path = config_manager.get_driver_path(driver_name) if not driver_path: console.print(f"[red]Driver '{driver_name}' not found[/red]") return # Check if Dockerfile exists dockerfile_path = driver_path / "Dockerfile" if not dockerfile_path.exists(): console.print(f"[red]Dockerfile not found in {driver_path}[/red]") return # Build image name image_name = f"monadical/mc-{driver_name}:{tag}" # Build the image with console.status(f"Building image {image_name}..."): result = os.system(f"cd {driver_path} && docker build -t {image_name} .") if result != 0: console.print("[red]Failed to build driver image[/red]") return console.print(f"[green]Successfully built image: {image_name}[/green]") # Push if requested if push: with console.status(f"Pushing image {image_name}..."): result = os.system(f"docker push {image_name}") if result != 0: console.print("[red]Failed to push driver image[/red]") return console.print(f"[green]Successfully pushed image: {image_name}[/green]") @driver_app.command("info") def driver_info( driver_name: str = typer.Argument(..., help="Driver name to get info for"), ) -> None: """Show detailed information about a driver""" driver = config_manager.get_driver(driver_name) if not driver: console.print(f"[red]Driver '{driver_name}' not found[/red]") return console.print(f"[bold]Driver: {driver.name}[/bold]") console.print(f"Description: {driver.description}") console.print(f"Version: {driver.version}") console.print(f"Maintainer: {driver.maintainer}") console.print(f"Image: {driver.image}") if driver.ports: console.print("\n[bold]Ports:[/bold]") for port in driver.ports: console.print(f" {port}") # Get driver path driver_path = config_manager.get_driver_path(driver_name) if driver_path: console.print(f"\n[bold]Path:[/bold] {driver_path}") # Check for README readme_path = driver_path / "README.md" if readme_path.exists(): console.print("\n[bold]README:[/bold]") with open(readme_path, "r") as f: console.print(f.read()) if __name__ == "__main__": app()