Add host bootstrap and repair helpers

Add a dedicated pyro host surface for supported chat hosts so Claude Code, Codex, and OpenCode users can connect or repair the canonical MCP setup without hand-writing raw commands or config edits.

Implement the shared host helper layer and wire it through the CLI with connect, print-config, doctor, and repair, all generated from the same canonical pyro mcp serve command shape and project-source flags. Update the docs, public contract, examples, changelog, and roadmap so the helper flow becomes the primary onramp while raw host-specific commands remain as reference material.

Harden the verification path that this milestone exposed: temp git repos in tests now disable commit signing, socket-based port tests skip cleanly when the sandbox forbids those primitives, and make test still uses multiple cores by default but caps xdist workers to a stable value so make check stays fast and deterministic here.

Validation:
- uv lock
- UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make check
- UV_OFFLINE=1 UV_CACHE_DIR=.uv-cache make dist-check
This commit is contained in:
Thales Maciel 2026-03-13 16:46:10 -03:00
parent 535efc6919
commit 899a6760c4
25 changed files with 1658 additions and 58 deletions

View file

@ -9,6 +9,7 @@ from typing import Any, cast
import pytest
import pyro_mcp.cli as cli
from pyro_mcp.host_helpers import HostDoctorEntry
def _subparser_choice(parser: argparse.ArgumentParser, name: str) -> argparse.ArgumentParser:
@ -31,10 +32,11 @@ def test_cli_help_guides_first_run() -> None:
assert "pyro env list" in help_text
assert "pyro env pull debian:12" in help_text
assert "pyro run debian:12 -- git --version" in help_text
assert "pyro mcp serve" in help_text
assert "pyro host connect claude-code" in help_text
assert "Connect a chat host after that:" in help_text
assert "claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve" in help_text
assert "codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve" in help_text
assert "pyro host connect claude-code" in help_text
assert "pyro host connect codex" in help_text
assert "pyro host print-config opencode" in help_text
assert "If you want terminal-level visibility into the workspace model:" in help_text
assert "pyro workspace exec WORKSPACE_ID -- cat note.txt" in help_text
assert "pyro workspace snapshot create WORKSPACE_ID checkpoint" in help_text
@ -57,6 +59,31 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "pyro env pull debian:12" in env_help
assert "downloads from public Docker Hub" in env_help
host_help = _subparser_choice(parser, "host").format_help()
assert "Connect or repair the supported Claude Code, Codex, and OpenCode" in host_help
assert "pyro host connect claude-code" in host_help
assert "pyro host repair opencode" in host_help
host_connect_help = _subparser_choice(
_subparser_choice(parser, "host"), "connect"
).format_help()
assert "--installed-package" in host_connect_help
assert "--project-path" in host_connect_help
assert "--repo-url" in host_connect_help
assert "--repo-ref" in host_connect_help
assert "--no-project-source" in host_connect_help
host_print_config_help = _subparser_choice(
_subparser_choice(parser, "host"), "print-config"
).format_help()
assert "--output" in host_print_config_help
host_doctor_help = _subparser_choice(_subparser_choice(parser, "host"), "doctor").format_help()
assert "--config-path" in host_doctor_help
host_repair_help = _subparser_choice(_subparser_choice(parser, "host"), "repair").format_help()
assert "--config-path" in host_repair_help
doctor_help = _subparser_choice(parser, "doctor").format_help()
assert "Check host prerequisites and embedded runtime health" in doctor_help
assert "pyro doctor --json" in doctor_help
@ -316,6 +343,94 @@ def test_cli_subcommand_help_includes_examples_and_guidance() -> None:
assert "Close a persistent workspace shell" in workspace_shell_close_help
def test_cli_host_connect_dispatch(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
pass
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="host",
host_command="connect",
host="codex",
installed_package=False,
profile="workspace-core",
project_path=None,
repo_url=None,
repo_ref=None,
no_project_source=False,
)
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
monkeypatch.setattr(
cli,
"connect_cli_host",
lambda host, *, config: {
"host": host,
"server_command": ["uvx", "--from", "pyro-mcp", "pyro", "mcp", "serve"],
"verification_command": ["codex", "mcp", "list"],
},
)
cli.main()
captured = capsys.readouterr()
assert captured.out == (
"Connected pyro to codex.\n"
"Server command: uvx --from pyro-mcp pyro mcp serve\n"
"Verify with: codex mcp list\n"
)
assert captured.err == ""
def test_cli_host_doctor_prints_human(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
class StubPyro:
pass
class StubParser:
def parse_args(self) -> argparse.Namespace:
return argparse.Namespace(
command="host",
host_command="doctor",
installed_package=False,
profile="workspace-core",
project_path=None,
repo_url=None,
repo_ref=None,
no_project_source=False,
config_path=None,
)
monkeypatch.setattr(cli, "_build_parser", lambda: StubParser())
monkeypatch.setattr(cli, "Pyro", StubPyro)
monkeypatch.setattr(
cli,
"doctor_hosts",
lambda **_: [
HostDoctorEntry(
host="codex",
installed=True,
configured=False,
status="missing",
details="codex entry missing",
repair_command="pyro host repair codex",
)
],
)
cli.main()
captured = capsys.readouterr()
assert "codex: missing installed=yes configured=no" in captured.out
assert "repair: pyro host repair codex" in captured.out
assert captured.err == ""
def test_cli_run_prints_json(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
@ -2823,30 +2938,38 @@ def test_chat_host_docs_and_examples_recommend_workspace_core() -> None:
claude_code = Path("examples/claude_code_mcp.md").read_text(encoding="utf-8")
codex = Path("examples/codex_mcp.md").read_text(encoding="utf-8")
opencode = json.loads(Path("examples/opencode_mcp_config.json").read_text(encoding="utf-8"))
claude_helper = "pyro host connect claude-code"
codex_helper = "pyro host connect codex"
opencode_helper = "pyro host print-config opencode"
claude_cmd = "claude mcp add pyro -- uvx --from pyro-mcp pyro mcp serve"
codex_cmd = "codex mcp add pyro -- uvx --from pyro-mcp pyro mcp serve"
assert "## Chat Host Quickstart" in readme
assert "uvx --from pyro-mcp pyro mcp serve" in readme
assert claude_cmd in readme
assert codex_cmd in readme
assert claude_helper in readme
assert codex_helper in readme
assert opencode_helper in readme
assert "examples/opencode_mcp_config.json" in readme
assert "pyro host doctor" in readme
assert "bare `pyro mcp serve` starts `workspace-core`" in readme
assert "auto-detects\nthe current Git checkout" in readme
assert "auto-detects the current Git checkout" in readme.replace("\n", " ")
assert "--project-path /abs/path/to/repo" in readme
assert "--repo-url https://github.com/example/project.git" in readme
assert "## 5. Connect a chat host" in install
assert "uvx --from pyro-mcp pyro mcp serve" in install
assert claude_cmd in install
assert codex_cmd in install
assert claude_helper in install
assert codex_helper in install
assert opencode_helper in install
assert "workspace-full" in install
assert "--project-path /abs/path/to/repo" in install
assert claude_cmd in first_run
assert codex_cmd in first_run
assert claude_helper in first_run
assert codex_helper in first_run
assert opencode_helper in first_run
assert "--project-path /abs/path/to/repo" in first_run
assert claude_helper in integrations
assert codex_helper in integrations
assert opencode_helper in integrations
assert "Bare `pyro mcp serve` starts `workspace-core`." in integrations
assert "auto-detects the current Git checkout" in integrations
assert "examples/claude_code_mcp.md" in integrations
@ -2862,13 +2985,17 @@ def test_chat_host_docs_and_examples_recommend_workspace_core() -> None:
assert "codex_mcp.md" in mcp_config
assert "opencode_mcp_config.json" in mcp_config
assert claude_helper in claude_code
assert claude_cmd in claude_code
assert "claude mcp list" in claude_code
assert "pyro host repair claude-code" in claude_code
assert "workspace-full" in claude_code
assert "--project-path /abs/path/to/repo" in claude_code
assert codex_helper in codex
assert codex_cmd in codex
assert "codex mcp list" in codex
assert "pyro host repair codex" in codex
assert "workspace-full" in codex
assert "--project-path /abs/path/to/repo" in codex