@click.command()
@click.option(
"--frontend-port", default=5173, show_default=True, type=click.IntRange(1, 65535)
)
@click.option(
"--api-port", default=8000, show_default=True, type=click.IntRange(1, 65535)
)
@click.option("--no-server", is_flag=True, help="Do not start the API server.")
@click.option("--no-browser", is_flag=True, help="Only start the frontend.")
def gui(frontend_port: int, api_port: int, no_server: bool, no_browser: bool) -> None:
"""Start the browser-based graphical mode in the default browser.
This command is intended for source checkouts. For an installed desktop
application, launch OpenJarvis from the operating system menu instead.
"""
console = Console(stderr=True)
frontend = _frontend_dir()
if frontend is None:
raise click.ClickException(
"The graphical frontend is not available in this installation. "
"Download the OpenJarvis desktop app or run this command "
"from a source checkout."
)
npm = shutil.which("npm") or shutil.which("npm.cmd")
if npm is None:
raise click.ClickException(
"Node.js/npm is required for graphical mode. Install Node.js 22 or newer."
)
_check_frontend_port(frontend_port)
_ensure_frontend_dependencies(frontend, npm)
# `jarvis start` exits non-zero when a daemon is already registered, so
# without this a perfectly healthy API turned into the misleading "Could
# not start the OpenJarvis API server." Adopt it instead.
if not no_server and _api_healthy(api_port):
if not _daemon_pid_alive(api_port, require_ready=True):
raise click.ClickException(
f"API port {api_port} is already serving an unregistered service. "
"Stop it or choose another port. Use --no-server only when you "
"intend to use an externally managed OpenJarvis backend."
)
console.print(
f"[green]Reusing the API server already running on port {api_port}.[/green]"
)
no_server = True
launched_api = not no_server
if launched_api:
uv = shutil.which("uv")
if uv is None:
raise click.ClickException(
"uv is required to start the API with desktop dependencies. "
"Install uv or use --no-server with an already-running API."
)
server = subprocess.run(
[
uv,
"run",
"--extra",
"desktop",
"jarvis",
"start",
"--port",
str(api_port),
],
cwd=frontend.parent,
check=False,
)
if server.returncode != 0:
raise click.ClickException("Could not start the OpenJarvis API server.")
env = os.environ.copy()
# Let browser requests use Vite's same-origin proxy at any frontend port.
# VITE_API_URL is exposed to browser code, so clear an inherited override.
env["VITE_API_URL"] = ""
env["OPENJARVIS_VITE_PROXY_TARGET"] = f"http://127.0.0.1:{api_port}"
spawn_kwargs = {}
if sys.platform == "win32":
spawn_kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
else:
spawn_kwargs["start_new_session"] = True
process = subprocess.Popen(
[
npm,
"run",
"dev",
"--",
"--host",
"127.0.0.1",
"--port",
str(frontend_port),
"--strictPort",
],
cwd=frontend,
env=env,
**spawn_kwargs,
)
owner = _FrontendOwner(process, frontend_port)
shutdown = owner.shutdown
handlers = {}
atexit.register(shutdown)
try:
for signum in (signal.SIGTERM, getattr(signal, "SIGBREAK", None)):
if signum is not None:
with contextlib.suppress(OSError, ValueError):
previous = signal.getsignal(signum)
signal.signal(signum, lambda *_: sys.exit(1))
handlers[signum] = previous
owner.register()
if not _wait_for_port(
process, "127.0.0.1", frontend_port, on_poll=owner.refresh
):
raise click.ClickException(
"The graphical frontend did not start on the requested port."
)
owner.refresh()
url = f"http://127.0.0.1:{frontend_port}"
console.print(f"[green]OpenJarvis graphical mode is ready:[/green] {url}")
if not no_browser:
webbrowser.open(url)
watch_api = _api_healthy(api_port)
failures = 0
while True:
owner.refresh()
try:
process.wait(timeout=_API_POLL_SECONDS)
break
except subprocess.TimeoutExpired:
pass
healthy = _api_healthy(api_port)
if healthy:
watch_api = True
failures = 0
continue
if not watch_api and not launched_api:
continue
failures += 1
if failures < _API_FAILURES_BEFORE_DOWN or _daemon_pid_alive(api_port):
continue
console.print(
"[red]The API server stopped or failed to start; "
"shutting down the frontend.[/red]\n"
f" Check {DEFAULT_CONFIG_DIR / 'server.log'}, "
f"then run 'jarvis gui' again."
)
raise SystemExit(1)
except KeyboardInterrupt:
pass
finally:
atexit.unregister(shutdown)
for signum, previous in handlers.items():
with contextlib.suppress(OSError, ValueError):
signal.signal(signum, previous)
shutdown()