feat: enable remote SDK and resilient device orchestration

This commit is contained in:
somew1nd committed 2026-08-30 00:41:07 -07:00
1 parent 3cc711df51
commit ddfa420945
110 files changed
+14450 -12954

No files matched your search

+1
View File
@@ -16,6 +16,7 @@
import asyncio
import json
import os
from pathlib import Path
from typing import Annotated
+3 -3
View File
@@ -367,9 +367,9 @@ def run_command(
def on_status(sess_info):
st = sess_info.get("status")
if st == "queued":
console.print(f"[yellow]⏳ Task queued in scheduler, waiting for device...[/yellow]")
console.print("[yellow]⏳ Task queued in scheduler, waiting for device...[/yellow]")
elif st == "running":
console.print(f"[green]▶ Task executing on mobile device...[/green]")
console.print("[green]▶ Task executing on mobile device...[/green]")
try:
final_res = wait_for_daemon_task(
@@ -380,7 +380,7 @@ def run_command(
)
final_st = final_res.get("status")
if final_st in ("completed", "success"):
console.print(f"\n[bold green]✅ Task completed successfully![/bold green]")
console.print("\n[bold green]✅ Task completed successfully![/bold green]")
return
else:
err = final_res.get("error") or final_res.get("explanation") or ""
@@ -17,6 +17,7 @@
from __future__ import annotations
import datetime
import time
from typing import Annotated
import webbrowser
@@ -25,8 +26,8 @@ from rich.panel import Panel
from rich.table import Table
import typer
from artemis.interfaces.cli.commands.ui import ui_command
from artemis.runtime.daemon_client import spawn_daemon
from artemis.interfaces.cli.commands.ui import ensure_showcase_built, 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,
@@ -55,8 +56,15 @@ def _format_uptime(seconds: float | None) -> str:
def restart_command(
host: Annotated[
str,
typer.Option("--host", "-H", help="Bind host address for the UI server."),
] = "0.0.0.0",
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."),
@@ -86,14 +94,23 @@ def restart_command(
daemon: Annotated[
bool,
typer.Option(
"--daemon",
"-d",
help="Run the new server as a detached background process instead of foreground.",
"--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)."
),
),
] = False,
] = 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"
@@ -111,7 +128,7 @@ def restart_command(
else:
console.print(f" [dim]ℹ No active Artemis server found on port {port}.[/dim]")
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=4.0)
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:
@@ -122,23 +139,54 @@ def restart_command(
# 2. Launch fresh server instance
local_url = f"http://localhost:{port}"
if daemon:
console.print(f" [cyan]🚀 Starting Artemis in detached background daemon mode...[/cyan]")
ensure_showcase_built(console)
console.print(" [cyan]🚀 Starting Artemis in detached background daemon mode...[/cyan]")
proc = spawn_daemon(host=host, port=port)
if proc:
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(" [red]❌ Failed to spawn background daemon.[/red]\n")
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
# Default foreground mode
console.print(f" [cyan]🚀 Launching unified Artemis Showcase UI in current terminal...[/cyan]\n")
# 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)
@@ -165,7 +213,7 @@ def stop_command(
else:
console.print(f" [dim]Scanning port {port}...[/dim]")
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=4.0)
success, msg, stopped_pids = stop_server(port=port, force=force, timeout=12.0)
if stopped_pids:
console.print(
Panel(
+59 -19
View File
@@ -29,6 +29,33 @@ from rich.panel import Panel
import typer
def running_via_console_script_shim() -> bool:
"""Return True when this CLI was launched through a console-script ``.exe`` shim.
On Windows, ``uv``/pip install entry points as trampoline executables
(``Scripts/artemis.exe``) that stay resident for the lifetime of the CLI
process and hold a lock on their own file. A long-running foreground server
started through the shim therefore blocks ``uv sync`` from reinstalling the
package ("os error 32") until the server exits. Launching via
``python -m artemis`` avoids the shim entirely.
"""
if sys.platform != "win32":
return False
argv0 = Path(sys.argv[0] or "")
return argv0.suffix.lower() == ".exe" and argv0.name.lower() != "python.exe"
def warn_if_console_script_shim(console: Console) -> None:
"""Print a hint when a foreground server run will pin the console-script exe."""
if running_via_console_script_shim():
console.print(
" [dim]⚠ Running in foreground via the [bold]artemis.exe[/bold] launcher: package "
"syncs (uv sync / uv run) cannot replace it while this server is running.\n"
" Prefer [bold]artemis restart[/bold] (background daemon) or launch with "
"[bold]uv run python -m artemis ui[/bold] to avoid this.[/dim]\n"
)
def _resolve_npm_executable(platform_name: str | None = None) -> str | None:
"""Return a directly executable npm path for the current platform."""
platform_name = platform_name or sys.platform
@@ -57,6 +84,25 @@ def _showcase_build_required(showcase_dir: Path) -> bool:
return any(path.exists() and path.stat().st_mtime > build_time for path in input_paths)
def ensure_showcase_built(console: Console) -> None:
"""Rebuild the Angular Showcase UI when its sources are newer than the dist build."""
from artemis.config.paths import ROOT_DIR
showcase_dir = ROOT_DIR / "apps" / "showcase_ui"
if not _showcase_build_required(showcase_dir):
return
npm_executable = _resolve_npm_executable()
if not npm_executable:
return
console.print(" [yellow]🎨 Showcase UI sources changed. Compiling Angular Showcase UI...[/yellow]")
try:
subprocess.run([npm_executable, "install", "--silent"], cwd=showcase_dir, check=True)
subprocess.run([npm_executable, "run", "build"], cwd=showcase_dir, check=True)
console.print(" [green]✓ Showcase UI built successfully.[/green]\n")
except Exception as e:
console.print(f" [red]⚠ Failed to auto-build Showcase UI: {e}[/red]\n")
def _poll_and_open_browser(url: str, stop_event: threading.Event, timeout: float = 15.0) -> None:
"""Poll the UI server until it responds, then launch the user's default browser."""
start = time.time()
@@ -75,8 +121,15 @@ def _poll_and_open_browser(url: str, stop_event: threading.Event, timeout: float
def ui_command(
host: Annotated[
str,
typer.Option("--host", "-h", help="Bind host address for the UI server."),
] = "0.0.0.0",
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."),
@@ -116,7 +169,7 @@ def ui_command(
if restart:
console.print(f" [yellow]🔄 Restart requested. Recycling port {port}...[/yellow]")
stop_server(port=port, timeout=4.0)
stop_server(port=port, timeout=12.0)
elif is_port_in_use(port):
pids = find_server_pids(port)
pid_str = f" (PID: {', '.join(map(str, pids))})" if pids else ""
@@ -132,7 +185,7 @@ def ui_command(
choice = "1"
if choice == "2":
console.print(f"\n [yellow]🔄 Terminating previous Artemis instance on port {port}...[/yellow]")
stop_server(port=port, timeout=4.0)
stop_server(port=port, timeout=12.0)
elif choice == "3":
return
else:
@@ -178,21 +231,8 @@ def ui_command(
except Exception:
pass
showcase_dir = ROOT_DIR / "apps" / "showcase_ui"
if _showcase_build_required(showcase_dir):
npm_executable = _resolve_npm_executable()
if npm_executable:
console.print(
" [yellow]🎨 Showcase UI sources changed. Compiling Angular Showcase UI...[/yellow]"
)
try:
subprocess.run(
[npm_executable, "install", "--silent"], cwd=showcase_dir, check=True
)
subprocess.run([npm_executable, "run", "build"], cwd=showcase_dir, check=True)
console.print(" [green]✓ Showcase UI built successfully.[/green]\n")
except Exception as e:
console.print(f" [red]⚠ Failed to auto-build Showcase UI: {e}[/red]\n")
ensure_showcase_built(console)
warn_if_console_script_shim(console)
stop_event = threading.Event()
if open_browser: