feat(cli): add jarvis quickstart command

5-step guided setup: detect hardware, write config, check engine,
verify model, run test query. Skips config step if already present
unless --force is used. Exits with helpful message on engine failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jon Saad-Falcon
2026-02-28 17:23:37 +00:00
co-authored by Claude Opus 4.6
parent f6c02c13d8
commit 30a6aa585d
3 changed files with 244 additions and 0 deletions
+2
View File
@@ -17,6 +17,7 @@ from openjarvis.cli.doctor_cmd import doctor
from openjarvis.cli.init_cmd import init
from openjarvis.cli.memory_cmd import memory
from openjarvis.cli.model import model
from openjarvis.cli.quickstart_cmd import quickstart
from openjarvis.cli.scheduler_cmd import scheduler
from openjarvis.cli.serve import serve
from openjarvis.cli.skill_cmd import skill
@@ -52,6 +53,7 @@ cli.add_command(status, "status")
cli.add_command(vault, "vault")
cli.add_command(add, "add")
cli.add_command(operators, "operators")
cli.add_command(quickstart, "quickstart")
def main() -> None:
+123
View File
@@ -0,0 +1,123 @@
"""``jarvis quickstart`` — guided 5-step setup for new users."""
from __future__ import annotations
import click
from rich.console import Console
from openjarvis.core.config import (
DEFAULT_CONFIG_DIR,
DEFAULT_CONFIG_PATH,
detect_hardware,
generate_default_toml,
recommend_engine,
)
def _check_engine_health(engine_key: str) -> bool:
"""Return True if the recommended engine is reachable."""
try:
from openjarvis.core.config import load_config
from openjarvis.core.registry import EngineRegistry
from openjarvis.engine import _discovery
import openjarvis.engine # noqa: F401 — trigger registration
config = load_config()
if engine_key not in EngineRegistry.keys():
return False
engine = _discovery._make_engine(engine_key, config)
return engine.health()
except Exception:
return False
def _check_model_available(engine_key: str) -> bool:
"""Return True if at least one model is available on the engine."""
try:
from openjarvis.core.config import load_config
from openjarvis.core.registry import EngineRegistry
from openjarvis.engine import _discovery
config = load_config()
if engine_key not in EngineRegistry.keys():
return False
engine = _discovery._make_engine(engine_key, config)
return bool(engine.list_models())
except Exception:
return False
def _test_query(engine_key: str) -> str:
"""Run a quick test query and return the response text."""
try:
from openjarvis import Jarvis
j = Jarvis(engine_key=engine_key)
response = j.ask("Say hello in one sentence.")
j.close()
return response
except Exception as exc:
return f"(query failed: {exc})"
@click.command()
@click.option("--force", is_flag=True, help="Redo all steps even if already done.")
def quickstart(force: bool) -> None:
"""Guided 5-step setup for new users."""
console = Console()
# Step 1: Detect hardware
console.print("[bold cyan][1/5][/bold cyan] Detecting hardware...")
hw = detect_hardware()
console.print(f" Platform : {hw.platform}")
console.print(f" CPU : {hw.cpu_brand} ({hw.cpu_count} cores)")
console.print(f" RAM : {hw.ram_gb} GB")
if hw.gpu:
console.print(
f" GPU : {hw.gpu.name} ({hw.gpu.vram_gb} GB VRAM, x{hw.gpu.count})"
)
else:
console.print(" GPU : none detected")
engine_key = recommend_engine(hw)
# Step 2: Write config
console.print()
console.print("[bold cyan][2/5][/bold cyan] Writing config...")
if DEFAULT_CONFIG_PATH.exists() and not force:
console.print(f" [dim]Config already exists at {DEFAULT_CONFIG_PATH} (skip)[/dim]")
else:
toml_content = generate_default_toml(hw)
DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
DEFAULT_CONFIG_PATH.write_text(toml_content)
console.print(f" [green]Config written to {DEFAULT_CONFIG_PATH}[/green]")
# Step 3: Check engine
console.print()
console.print(f"[bold cyan][3/5][/bold cyan] Checking engine ({engine_key})...")
if not _check_engine_health(engine_key):
console.print(f" [red bold]Engine '{engine_key}' is not reachable.[/red bold]")
console.print()
console.print(f" Start the {engine_key} server and try again.")
console.print(f" Run [bold]jarvis doctor[/bold] for detailed diagnostics.")
raise SystemExit(1)
console.print(f" [green]Engine '{engine_key}' is healthy.[/green]")
# Step 4: Verify model
console.print()
console.print("[bold cyan][4/5][/bold cyan] Checking for available models...")
if not _check_model_available(engine_key):
console.print(" [yellow]No models found.[/yellow]")
console.print(" Pull a model first (e.g. [bold]ollama pull qwen3:8b[/bold]).")
raise SystemExit(1)
console.print(" [green]Models available.[/green]")
# Step 5: Test query
console.print()
console.print("[bold cyan][5/5][/bold cyan] Running test query...")
response = _test_query(engine_key)
console.print(f" [green]Response:[/green] {response[:200]}")
console.print()
console.print("[bold green]Setup complete![/bold green] Try: [bold]jarvis ask \"Hello\"[/bold]")
+119
View File
@@ -0,0 +1,119 @@
"""Tests for ``jarvis quickstart`` command."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from click.testing import CliRunner
from openjarvis.cli import cli
class TestQuickstartCommand:
def test_registered(self):
"""quickstart should be a registered CLI command."""
runner = CliRunner()
result = runner.invoke(cli, ["quickstart", "--help"])
assert result.exit_code == 0
assert "quickstart" in result.output.lower() or "--help" in result.output
def test_happy_path(self, tmp_path):
"""Full quickstart succeeds when hardware detected and engine healthy."""
config_path = tmp_path / "config.toml"
hw = MagicMock()
hw.platform = "linux"
hw.cpu_brand = "Test CPU"
hw.cpu_count = 8
hw.ram_gb = 32
hw.gpu = MagicMock(name="Test GPU", vram_gb=24, count=1, vendor="nvidia")
with (
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 0
assert "1/5" in result.output
assert "5/5" in result.output
def test_skips_config_if_exists(self, tmp_path):
"""Config step is skipped when config already exists."""
config_path = tmp_path / "config.toml"
config_path.write_text("[engine]\n")
hw = MagicMock()
hw.platform = "linux"
hw.cpu_brand = "Test CPU"
hw.cpu_count = 8
hw.ram_gb = 32
hw.gpu = None
with (
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 0
assert "already exists" in result.output.lower() or "skip" in result.output.lower()
def test_force_regenerates_config(self, tmp_path):
"""--force should regenerate config even if it exists."""
config_path = tmp_path / "config.toml"
config_path.write_text("[old]\n")
hw = MagicMock()
hw.platform = "linux"
hw.cpu_brand = "Test CPU"
hw.cpu_count = 8
hw.ram_gb = 32
hw.gpu = None
with (
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\nnew = true\n"),
patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=True),
patch("openjarvis.cli.quickstart_cmd._check_model_available", return_value=True),
patch("openjarvis.cli.quickstart_cmd._test_query", return_value="Hello!"),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart", "--force"])
assert result.exit_code == 0
assert "new = true" in config_path.read_text()
def test_engine_not_found(self, tmp_path):
"""Helpful message when engine is unreachable."""
config_path = tmp_path / "config.toml"
hw = MagicMock()
hw.platform = "linux"
hw.cpu_brand = "Test CPU"
hw.cpu_count = 8
hw.ram_gb = 32
hw.gpu = None
with (
patch("openjarvis.cli.quickstart_cmd.detect_hardware", return_value=hw),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_PATH", config_path),
patch("openjarvis.cli.quickstart_cmd.DEFAULT_CONFIG_DIR", tmp_path),
patch("openjarvis.cli.quickstart_cmd.generate_default_toml", return_value="[engine]\n"),
patch("openjarvis.cli.quickstart_cmd.recommend_engine", return_value="ollama"),
patch("openjarvis.cli.quickstart_cmd._check_engine_health", return_value=False),
):
runner = CliRunner()
result = runner.invoke(cli, ["quickstart"])
assert result.exit_code == 1
assert "engine" in result.output.lower() or "not reachable" in result.output.lower()