# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """CLI commands for Artemis server lifecycle management: restart, stop, and status.""" from __future__ import annotations import datetime import time from typing import Annotated import webbrowser from rich.console import Console from rich.panel import Panel from rich.table import Table import typer from artemis.interfaces.cli.commands.ui import ( ensure_showcase_built, load_session_reconciler, ui_command, ) from artemis.runtime.daemon_client import daemon_log_path, is_daemon_running, spawn_daemon from artemis.runtime.server_lifecycle import ( find_server_pids, get_server_status, stop_server, ) console = Console() def _format_uptime(seconds: float | None) -> str: """Format seconds into human-friendly duration (e.g. 1h 23m 45s).""" if seconds is None: return "Unknown" sec = int(seconds) hours, remainder = divmod(sec, 3600) minutes, remainder_sec = divmod(remainder, 60) parts = [] if hours > 0: parts.append(f"{hours}h") if minutes > 0 or hours > 0: parts.append(f"{minutes}m") parts.append(f"{remainder_sec}s") return " ".join(parts) def restart_command( host: Annotated[ str, typer.Option( "--host", "-H", help=( "Bind host address for the UI server. Defaults to loopback; for remote " "access prefer a Tailscale/SSH tunnel over a wide bind." ), ), ] = "127.0.0.1", port: Annotated[ int, typer.Option("--port", "-p", help="Port to run the unified UI server on."), ] = 8000, open_browser: Annotated[ bool, typer.Option( "--open/--no-open", help="Automatically open the Showcase UI in default web browser.", ), ] = True, force: Annotated[ bool, typer.Option( "--force", "-f", help="Force kill previous server immediately without waiting for graceful exit.", ), ] = False, reload: Annotated[ bool, typer.Option( "--reload/--no-reload", help="Enable uvicorn live auto-reload (for development).", ), ] = False, daemon: Annotated[ bool, typer.Option( "--daemon/--foreground", "-d/-F", help=( "Run the new server as a detached background daemon (default) or attached " "to the current terminal. The daemon is launched via 'python -m' so the " "console-script executable is never pinned by a long-lived server " "(which would block 'uv sync' reinstalls on Windows)." ), ), ] = True, ) -> None: """Restart the Artemis Web UI server, terminating any existing instance on the port.""" console.print() if reload and daemon: # uvicorn auto-reload requires an attached terminal session. console.print(" [dim]ℹ --reload requested: switching to foreground mode.[/dim]") daemon = False console.print( Panel( f"[bold cyan]🔄 Artemis Server Restart[/bold cyan]\n" f"[dim]Inspecting port {port} and recycling any running Artemis instances...[/dim]", border_style="cyan", expand=False, ) ) # 1. Stop existing server existing_pids = find_server_pids(port) if existing_pids: pid_str = ", ".join(map(str, existing_pids)) console.print(f" [yellow]🛑 Terminating running Artemis instance (PID: {pid_str})...[/yellow]") else: console.print(f" [dim]ℹ No active Artemis server found on port {port}.[/dim]") load_session_reconciler() success, msg, stopped_pids = stop_server(port=port, force=force, timeout=12.0) if stopped_pids: console.print(f" [green]✓ {msg}[/green]\n") elif not success: console.print(f" [red]⚠ {msg}[/red]\n") else: console.print(f" [green]✓ Port {port} is clear and ready.[/green]\n") # 2. Launch fresh server instance local_url = f"http://localhost:{port}" if daemon: ensure_showcase_built(console) console.print(" [cyan]🚀 Starting Artemis in detached background daemon mode...[/cyan]") proc = spawn_daemon(host=host, port=port) if not proc: console.print(" [red]❌ Failed to spawn background daemon.[/red]\n") raise typer.Exit(code=1) # Wait until the daemon answers HTTP before declaring success (cold # start includes DB/trace initialization and can take tens of seconds). ready = False deadline = time.monotonic() + 60.0 while time.monotonic() < deadline: if is_daemon_running(host="127.0.0.1", port=port, timeout=0.5): ready = True break if proc.poll() is not None: # Parent exited: either it crashed, or it handed the socket off # to a child worker. Give the worker a short grace period. grace = time.monotonic() + 5.0 while time.monotonic() < grace: if is_daemon_running(host="127.0.0.1", port=port, timeout=0.5): ready = True break time.sleep(0.3) break time.sleep(0.3) log_path = daemon_log_path(port) if ready: console.print( f" [bold green]✓ Artemis server running in background (PID: {proc.pid})[/bold green]\n" f" 🌐 [cyan]{local_url}[/cyan]\n" f" 🛠️ [cyan]{local_url}/admin[/cyan]\n" f" 📄 [dim]Logs: {log_path}[/dim]\n" f" [dim]💡 Stop with [bold]artemis stop[/bold]; run in-terminal with [bold]artemis restart --foreground[/bold].[/dim]\n" ) if open_browser: webbrowser.open(local_url) else: console.print( f" [red]❌ Background daemon did not become ready on port {port}.[/red]\n" f" 📄 [dim]Check logs: {log_path}[/dim]\n" ) raise typer.Exit(code=1) return # Foreground mode (--foreground / --reload) console.print(" [cyan]🚀 Launching unified Artemis Showcase UI in current terminal...[/cyan]\n") ui_command(host=host, port=port, open_browser=open_browser, reload=reload) def stop_command( port: Annotated[ int, typer.Option("--port", "-p", help="Port of the Artemis server to stop."), ] = 8000, force: Annotated[ bool, typer.Option( "--force", "-f", help="Force kill immediately without waiting for graceful shutdown.", ), ] = False, ) -> None: """Stop any running Artemis Web UI / server instance on the given port.""" console.print() existing_pids = find_server_pids(port) if existing_pids: pid_str = ", ".join(map(str, existing_pids)) console.print(f" [yellow]🛑 Stopping Artemis server on port {port} (PID: {pid_str})...[/yellow]") else: console.print(f" [dim]Scanning port {port}...[/dim]") load_session_reconciler() success, msg, stopped_pids = stop_server(port=port, force=force, timeout=12.0) if stopped_pids: console.print( Panel( f"[bold green]✓ Artemis server stopped successfully.[/bold green]\n\n" f"[dim]{msg}[/dim]", title="🛑 Server Stopped", border_style="green", expand=False, ) ) elif not success: console.print( Panel( f"[bold red]⚠ Failed to fully stop server on port {port}[/bold red]\n\n{msg}", title="⚠ Warning", border_style="red", expand=False, ) ) raise typer.Exit(code=1) else: console.print( Panel( f"[dim]No active Artemis server found on port {port}. Port is free.[/dim]", title="ℹ Status", border_style="dim", expand=False, ) ) def status_command( port: Annotated[ int, typer.Option("--port", "-p", help="Port to query for Artemis server status."), ] = 8000, ) -> None: """Check whether the Artemis server is currently running and display runtime metadata.""" console.print() status = get_server_status(port=port) if status["running"]: pids_str = ", ".join(map(str, status["pids"])) if status["pids"] else str(status["active_pid"] or "Unknown") uptime_str = _format_uptime(status["uptime_seconds"]) metadata = status.get("metadata") or {} started_at_str = "Unknown" if metadata.get("started_at"): try: dt = datetime.datetime.fromtimestamp(metadata["started_at"]) started_at_str = dt.strftime("%Y-%m-%d %H:%M:%S") except Exception: pass table = Table(show_header=False, box=None, padding=(0, 2)) table.add_row("[bold cyan]Status[/bold cyan]", "[bold green]● ONLINE / RUNNING[/bold green]") table.add_row("[bold cyan]PID(s)[/bold cyan]", f"[yellow]{pids_str}[/yellow]") table.add_row("[bold cyan]Port[/bold cyan]", f"[white]{port}[/white]") table.add_row("[bold cyan]Uptime[/bold cyan]", f"[white]{uptime_str}[/white]") table.add_row("[bold cyan]Started At[/bold cyan]", f"[dim]{started_at_str}[/dim]") table.add_row("[bold cyan]Showcase UI[/bold cyan]", f"[cyan]{status['url']}[/cyan]") table.add_row("[bold cyan]Admin Debug[/bold cyan]", f"[magenta]{status['admin_url']}[/magenta]") if metadata.get("cwd"): table.add_row("[bold cyan]Workspace[/bold cyan]", f"[dim]{metadata['cwd']}[/dim]") console.print( Panel( table, title="🚀 Artemis Server Status", border_style="green", expand=False, ) ) console.print(" [dim]💡 To restart: [bold]artemis restart[/bold] | To stop: [bold]artemis stop[/bold][/dim]\n") else: console.print( Panel( f"[dim]○ OFFLINE / STOPPED[/dim]\n\n" f"Port [cyan]{port}[/cyan] is currently free.\n" f"[dim]No active Artemis server process detected.[/dim]", title="ℹ Artemis Server Status", border_style="dim", expand=False, ) ) console.print(" [dim]💡 To start: [bold]artemis ui[/bold] or [bold]./start.sh[/bold][/dim]\n")