diff --git a/src/coding_scaffold/cli.py b/src/coding_scaffold/cli.py index e6e3bdb..9a4f11a 100644 --- a/src/coding_scaffold/cli.py +++ b/src/coding_scaffold/cli.py @@ -670,6 +670,11 @@ def build_parser() -> argparse.ArgumentParser: ) doctor.add_argument("--target", type=Path, default=Path.cwd(), help="Project directory.") doctor.add_argument("--json", action="store_true", help="Print machine-readable JSON.") + doctor.add_argument( + "--verbose", + action="store_true", + help="Also print the legacy hardware/provider recommendation snapshot.", + ) pilot = sub.add_parser( "pilot", @@ -934,8 +939,9 @@ def main(argv: list[str] | None = None) -> int: print(json.dumps(report.to_dict(), indent=2, sort_keys=True)) else: print(format_doctor_text(report)) - # Keep the original system snapshot at the end for continuity. - _print_doctor() + if getattr(args, "verbose", False): + print() + _print_doctor() return 0 if args.command == "pilot": diff --git a/src/coding_scaffold/pilot.py b/src/coding_scaffold/pilot.py index 3a20ec4..f822c3d 100644 --- a/src/coding_scaffold/pilot.py +++ b/src/coding_scaffold/pilot.py @@ -110,7 +110,8 @@ def run_pilot(target: Path | None = None, tool: str = "opencode") -> PilotReport env_info["tool"] = {"name": tool, "binary": tool_binary, "installed": tool_present} if not tool_present: warnings.append( - f"`{tool_binary}` is not on PATH. The setup step below offers --install to add it; " + f"`{tool_binary}` is not on PATH. The setup step below offers --install-tools " + "to add it; " "this pilot wrapper never installs anything itself." ) @@ -130,7 +131,12 @@ def run_pilot(target: Path | None = None, tool: str = "opencode") -> PilotReport "or install Ollama before running it." ) - environment_ok = python_ok and bool(git_path) and (bool(found_keys) or bool(local_runtime_binaries)) + environment_ok = ( + python_ok + and bool(git_path) + and tool_present + and (bool(found_keys) or bool(local_runtime_binaries)) + ) # Build the printable recipe. Same shape regardless of tool, with the tool name woven in. steps = _build_steps(root, tool=tool, tool_present=tool_present) @@ -149,7 +155,7 @@ def run_pilot(target: Path | None = None, tool: str = "opencode") -> PilotReport def _build_steps(root: Path, *, tool: str, tool_present: bool) -> list[str]: setup_args = f"--target {_format_target(root)} --tool {tool} --mode beginner" if not tool_present: - setup_args += " --install" + setup_args += " --install-tools" return [ f"coding-scaffold setup run {setup_args}", f"coding-scaffold pr-template init --target {_format_target(root)}", diff --git a/tests/test_cli_ux.py b/tests/test_cli_ux.py index 8301cb3..6b6fbd6 100644 --- a/tests/test_cli_ux.py +++ b/tests/test_cli_ux.py @@ -1,10 +1,12 @@ from __future__ import annotations import json +import shlex from pathlib import Path import pytest +import coding_scaffold.pilot as pilot_module from coding_scaffold.cli import build_parser, main from coding_scaffold.doctor import run_doctor from coding_scaffold.pilot import SUPPORTED_TOOLS, run_pilot @@ -117,10 +119,21 @@ def test_doctor_cli_text_output_runs( captured = capsys.readouterr() assert rc == 0 assert "CodingScaffold doctor" in captured.out + assert captured.out.count("CodingScaffold doctor") == 1 assert "Recommended next steps:" in captured.out assert "Ignore for now" in captured.out +def test_doctor_cli_verbose_includes_legacy_snapshot( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + rc = main(["doctor", "--target", str(tmp_path), "--verbose"]) + captured = capsys.readouterr() + assert rc == 0 + assert captured.out.count("CodingScaffold doctor") == 2 + assert "Python package is runnable" in captured.out + + # --------------------------------------------------------------------------- # pilot # --------------------------------------------------------------------------- @@ -139,12 +152,49 @@ def test_pilot_prints_three_step_recipe(tmp_path: Path) -> None: def test_pilot_does_not_attempt_to_install_anything(tmp_path: Path) -> None: # The pilot is read-only: even when the tool is missing, the printed recipe asks the - # USER to run `setup run --install`. The pilot itself never writes files or installs. + # USER to run `setup run --install-tools`. The pilot itself never writes files or installs. run_pilot(tmp_path, tool="opencode") # No files are written to the target directory. assert not list(tmp_path.iterdir()) +def test_pilot_missing_tool_recipe_uses_valid_setup_run_flag( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + def fake_which(name: str) -> str | None: + if name in {"git", "ollama"}: + return f"/usr/bin/{name}" + return None + + monkeypatch.setattr(pilot_module.shutil, "which", fake_which) + + report = run_pilot(tmp_path, tool="opencode") + setup_step = report.steps[0] + assert "--install-tools" in setup_step + assert "--install " not in f"{setup_step} " + # Regression guard: the printed recipe should be parseable by the real CLI parser. + build_parser().parse_args(shlex.split(setup_step)[1:]) + + +def test_pilot_environment_not_ok_when_selected_tool_missing( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + def fake_which(name: str) -> str | None: + if name in {"git", "ollama"}: + return f"/usr/bin/{name}" + return None + + monkeypatch.setattr(pilot_module.shutil, "which", fake_which) + + report = run_pilot(tmp_path, tool="opencode") + assert report.environment["git"] is True + assert report.environment["local_runtime_cli"] == ["ollama"] + tool_info = report.environment["tool"] + assert isinstance(tool_info, dict) + assert tool_info["installed"] is False + assert report.environment_ok is False + + def test_pilot_rejects_unknown_tool(tmp_path: Path) -> None: with pytest.raises(ValueError, match="Unknown tool"): run_pilot(tmp_path, tool="not-a-tool")