mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-30 10:52:15 +00:00
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:
co-authored by
Claude Opus 4.6
parent
f6c02c13d8
commit
30a6aa585d
@@ -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:
|
||||
|
||||
@@ -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]")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user