diff --git a/src/openjarvis/cli/__init__.py b/src/openjarvis/cli/__init__.py index 10a70a94..6ff4ab66 100644 --- a/src/openjarvis/cli/__init__.py +++ b/src/openjarvis/cli/__init__.py @@ -26,6 +26,7 @@ from openjarvis.cli.operators_cmd import operators from openjarvis.cli.optimize_cmd import optimize_group from openjarvis.cli.quickstart_cmd import quickstart from openjarvis.cli.registry_cmd import registry +from openjarvis.cli.scan_cmd import scan from openjarvis.cli.scheduler_cmd import scheduler from openjarvis.cli.serve import serve from openjarvis.cli.skill_cmd import skill @@ -87,6 +88,7 @@ cli.add_command(gateway, "gateway") cli.add_command(tool, "tool") cli.add_command(registry, "registry") cli.add_command(config, "config") +cli.add_command(scan, "scan") def main() -> None: diff --git a/src/openjarvis/cli/init_cmd.py b/src/openjarvis/cli/init_cmd.py index 95c946f3..8d729b7e 100644 --- a/src/openjarvis/cli/init_cmd.py +++ b/src/openjarvis/cli/init_cmd.py @@ -11,6 +11,7 @@ from rich.markup import escape from rich.panel import Panel from openjarvis.cli.model import find_model_spec, hf_download, ollama_pull +from openjarvis.cli.scan_cmd import PrivacyScanner from openjarvis.core.config import ( DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_PATH, @@ -156,6 +157,23 @@ def _next_steps_text(engine: str, model: str = "") -> str: return steps.get(engine, steps["ollama"]) +def _quick_privacy_check(console: Console) -> None: + """Run critical privacy checks and print compact summary.""" + scanner = PrivacyScanner() + results = scanner.run_quick() + if results: + console.print(" [bold]Privacy check:[/bold]") + for r in results: + if r.status == "ok": + console.print(f" [green]\u2713[/green] {r.message}") + elif r.status == "warn": + console.print(f" [yellow]![/yellow] {r.message}") + elif r.status == "fail": + console.print(f" [red]\u2717[/red] {r.message}") + console.print() + console.print(" Run [cyan]jarvis scan[/cyan] for a full environment audit.") + + def _do_download(engine: str, model: str, spec, console: Console) -> None: """Dispatch model download based on engine type.""" import os @@ -358,6 +376,7 @@ def init( if click.confirm(prompt, default=True): _do_download(selected_engine, model, spec, console) + _quick_privacy_check(console) console.print() console.print( Panel( diff --git a/tests/cli/test_init_guidance.py b/tests/cli/test_init_guidance.py index 6e2cd67f..74924c53 100644 --- a/tests/cli/test_init_guidance.py +++ b/tests/cli/test_init_guidance.py @@ -19,6 +19,7 @@ class TestInitShowsNextSteps: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp", "--no-download"]) assert result.exit_code == 0 @@ -33,6 +34,7 @@ class TestInitShowsNextSteps: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp", "--no-download"]) assert result.exit_code == 0 @@ -92,6 +94,7 @@ class TestMinimalConfig: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "ollama", "--no-download"]) assert result.exit_code == 0 @@ -109,6 +112,7 @@ class TestMinimalConfig: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--full", "--engine", "ollama", "--no-download"]) assert result.exit_code == 0 @@ -126,6 +130,7 @@ class TestInitDownloadPrompt: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "ollama"], input="n\n") assert result.exit_code == 0 @@ -137,6 +142,7 @@ class TestInitDownloadPrompt: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "ollama", "--no-download"]) assert result.exit_code == 0 @@ -151,6 +157,7 @@ class TestInitEmptyModelFallback: mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), mock.patch("openjarvis.cli.init_cmd.recommend_model", return_value=""), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "llamacpp"]) assert result.exit_code == 0 @@ -179,6 +186,7 @@ class TestInitDownloadDispatch: mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), mock.patch("openjarvis.cli.init_cmd.ollama_pull", return_value=True) as mock_pull, + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "ollama"], input="y\n") assert result.exit_code == 0 @@ -190,7 +198,29 @@ class TestInitDownloadDispatch: with ( mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner"), ): result = CliRunner().invoke(cli, ["init", "--engine", "vllm"], input="y\n") assert result.exit_code == 0 assert "automatically" in result.output + + +class TestInitPrivacyHook: + def test_init_shows_privacy_summary(self, tmp_path: Path) -> None: + config_dir = tmp_path / ".openjarvis" + config_path = config_dir / "config.toml" + with ( + mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_DIR", config_dir), + mock.patch("openjarvis.cli.init_cmd.DEFAULT_CONFIG_PATH", config_path), + mock.patch("openjarvis.cli.init_cmd.PrivacyScanner") as MockScanner, + ): + from openjarvis.cli.scan_cmd import ScanResult + instance = MockScanner.return_value + instance.run_quick.return_value = [ + ScanResult("FileVault", "ok", "FileVault enabled", "darwin"), + ] + result = CliRunner().invoke( + cli, ["init", "--engine", "llamacpp", "--no-download"] + ) + assert result.exit_code == 0 + assert "jarvis scan" in result.output