Polish onboarding and CLI help

This commit is contained in:
Thales Maciel 2026-03-09 21:12:56 -03:00
parent 38b6aeba68
commit b2ea56db4c
7 changed files with 561 additions and 58 deletions

View file

@ -5,6 +5,7 @@ from __future__ import annotations
import argparse
import json
import sys
from textwrap import dedent
from typing import Any
from pyro_mcp import __version__
@ -142,55 +143,284 @@ def _print_doctor_human(payload: dict[str, Any]) -> None:
print(f"- {issue}")
class _HelpFormatter(
argparse.RawDescriptionHelpFormatter,
argparse.ArgumentDefaultsHelpFormatter,
):
"""Help formatter with examples and default values."""
def _get_help_string(self, action: argparse.Action) -> str:
if action.default is None and action.help is not None:
return action.help
help_string = super()._get_help_string(action)
if help_string is None:
return ""
return help_string
def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="pyro CLI for curated ephemeral Linux environments."
description=(
"Run ephemeral Firecracker microVM workflows from the CLI on supported "
"Linux x86_64 KVM hosts."
),
epilog=dedent(
"""
Suggested first run:
pyro doctor
pyro env pull debian:12
pyro run debian:12 -- git --version
Use `pyro mcp serve` only after the CLI validation path works.
"""
),
formatter_class=_HelpFormatter,
)
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
env_parser = subparsers.add_parser("env", help="Inspect and manage curated environments.")
env_subparsers = env_parser.add_subparsers(dest="env_command", required=True)
list_parser = env_subparsers.add_parser("list", help="List official environments.")
list_parser.add_argument("--json", action="store_true")
env_parser = subparsers.add_parser(
"env",
help="Inspect and manage curated environments.",
description="Inspect, install, and prune curated Linux environments.",
epilog=dedent(
"""
Examples:
pyro env list
pyro env pull debian:12
pyro env inspect debian:12
"""
),
formatter_class=_HelpFormatter,
)
env_subparsers = env_parser.add_subparsers(dest="env_command", required=True, metavar="ENV")
list_parser = env_subparsers.add_parser(
"list",
help="List official environments.",
description="List the shipped environment catalog and show local install status.",
epilog="Example:\n pyro env list",
formatter_class=_HelpFormatter,
)
list_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
pull_parser = env_subparsers.add_parser(
"pull",
help="Install an environment into the local cache.",
description="Download and install one official environment into the local cache.",
epilog="Example:\n pyro env pull debian:12",
formatter_class=_HelpFormatter,
)
pull_parser.add_argument(
"environment",
metavar="ENVIRONMENT",
help="Environment name from `pyro env list`, for example `debian:12`.",
)
pull_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
inspect_parser = env_subparsers.add_parser(
"inspect",
help="Inspect one environment.",
description="Show catalog and local cache details for one environment.",
epilog="Example:\n pyro env inspect debian:12",
formatter_class=_HelpFormatter,
)
inspect_parser.add_argument(
"environment",
metavar="ENVIRONMENT",
help="Environment name from `pyro env list`, for example `debian:12`.",
)
inspect_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
prune_parser = env_subparsers.add_parser(
"prune",
help="Delete stale cached environments.",
description="Remove cached environment installs that are no longer referenced.",
epilog="Example:\n pyro env prune",
formatter_class=_HelpFormatter,
)
prune_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
pull_parser.add_argument("environment")
pull_parser.add_argument("--json", action="store_true")
inspect_parser = env_subparsers.add_parser("inspect", help="Inspect one environment.")
inspect_parser.add_argument("environment")
inspect_parser.add_argument("--json", action="store_true")
prune_parser = env_subparsers.add_parser("prune", help="Delete stale cached environments.")
prune_parser.add_argument("--json", action="store_true")
mcp_parser = subparsers.add_parser("mcp", help="Run the MCP server.")
mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True)
mcp_subparsers.add_parser("serve", help="Run the MCP server over stdio.")
mcp_parser = subparsers.add_parser(
"mcp",
help="Run the MCP server.",
description=(
"Run the MCP server after you have already validated the host and "
"guest execution with `pyro doctor` and `pyro run`."
),
epilog="Example:\n pyro mcp serve",
formatter_class=_HelpFormatter,
)
mcp_subparsers = mcp_parser.add_subparsers(dest="mcp_command", required=True, metavar="MCP")
mcp_subparsers.add_parser(
"serve",
help="Run the MCP server over stdio.",
description="Expose pyro tools over stdio for an MCP client.",
epilog=dedent(
"""
Example:
pyro mcp serve
run_parser = subparsers.add_parser("run", help="Run one command inside an ephemeral VM.")
run_parser.add_argument("environment")
run_parser.add_argument("--vcpu-count", type=int, default=DEFAULT_VCPU_COUNT)
run_parser.add_argument("--mem-mib", type=int, default=DEFAULT_MEM_MIB)
run_parser.add_argument("--timeout-seconds", type=int, default=30)
run_parser.add_argument("--ttl-seconds", type=int, default=600)
run_parser.add_argument("--network", action="store_true")
run_parser.add_argument("--allow-host-compat", action="store_true")
run_parser.add_argument("--json", action="store_true")
run_parser.add_argument("command_args", nargs="*")
Use this from an MCP client config after the CLI evaluation path works.
"""
),
formatter_class=_HelpFormatter,
)
doctor_parser = subparsers.add_parser("doctor", help="Inspect runtime and host diagnostics.")
doctor_parser.add_argument("--platform", default=DEFAULT_PLATFORM)
doctor_parser.add_argument("--json", action="store_true")
run_parser = subparsers.add_parser(
"run",
help="Run one command inside an ephemeral VM.",
description="Run one non-interactive command in a temporary Firecracker microVM.",
epilog=dedent(
"""
Examples:
pyro run debian:12 -- git --version
pyro run debian:12 --network -- git ls-remote https://github.com/octocat/Hello-World.git
"""
),
formatter_class=_HelpFormatter,
)
run_parser.add_argument(
"environment",
metavar="ENVIRONMENT",
help="Curated environment to boot, for example `debian:12`.",
)
run_parser.add_argument(
"--vcpu-count",
type=int,
default=DEFAULT_VCPU_COUNT,
help="Number of virtual CPUs to allocate to the guest.",
)
run_parser.add_argument(
"--mem-mib",
type=int,
default=DEFAULT_MEM_MIB,
help="Guest memory allocation in MiB.",
)
run_parser.add_argument(
"--timeout-seconds",
type=int,
default=30,
help="Maximum time allowed for the guest command.",
)
run_parser.add_argument(
"--ttl-seconds",
type=int,
default=600,
help="Time-to-live for temporary VM artifacts before cleanup.",
)
run_parser.add_argument(
"--network",
action="store_true",
help="Enable outbound guest networking. Requires TAP/NAT privileges on the host.",
)
run_parser.add_argument(
"--allow-host-compat",
action="store_true",
help=(
"Opt into host-side compatibility execution if guest boot or guest exec "
"is unavailable."
),
)
run_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
run_parser.add_argument(
"command_args",
nargs="*",
metavar="ARG",
help=(
"Command and arguments to run inside the guest. Prefix them with `--`, "
"for example `pyro run debian:12 -- git --version`."
),
)
demo_parser = subparsers.add_parser("demo", help="Run built-in demos.")
demo_subparsers = demo_parser.add_subparsers(dest="demo_command")
demo_parser.add_argument("--network", action="store_true")
ollama_parser = demo_subparsers.add_parser("ollama", help="Run the Ollama MCP demo.")
ollama_parser.add_argument("--base-url", default=DEFAULT_OLLAMA_BASE_URL)
ollama_parser.add_argument("--model", default=DEFAULT_OLLAMA_MODEL)
ollama_parser.add_argument("-v", "--verbose", action="store_true")
doctor_parser = subparsers.add_parser(
"doctor",
help="Inspect runtime and host diagnostics.",
description="Check host prerequisites and embedded runtime health before your first run.",
epilog=dedent(
"""
Examples:
pyro doctor
pyro doctor --json
"""
),
formatter_class=_HelpFormatter,
)
doctor_parser.add_argument(
"--platform",
default=DEFAULT_PLATFORM,
help="Runtime platform to inspect.",
)
doctor_parser.add_argument(
"--json",
action="store_true",
help="Print structured JSON instead of human-readable output.",
)
demo_parser = subparsers.add_parser(
"demo",
help="Run built-in demos.",
description="Run built-in demos after the basic CLI validation path works.",
epilog=dedent(
"""
Examples:
pyro demo
pyro demo --network
pyro demo ollama --verbose
"""
),
formatter_class=_HelpFormatter,
)
demo_subparsers = demo_parser.add_subparsers(dest="demo_command", metavar="DEMO")
demo_parser.add_argument(
"--network",
action="store_true",
help="Enable outbound guest networking for the deterministic demo.",
)
ollama_parser = demo_subparsers.add_parser(
"ollama",
help="Run the Ollama MCP demo.",
description="Run the Ollama tool-calling demo against the `vm_run` and lifecycle tools.",
epilog=dedent(
"""
Example:
pyro demo ollama --model llama3.2:3b --verbose
"""
),
formatter_class=_HelpFormatter,
)
ollama_parser.add_argument(
"--base-url",
default=DEFAULT_OLLAMA_BASE_URL,
help="OpenAI-compatible base URL for the Ollama server.",
)
ollama_parser.add_argument(
"--model",
default=DEFAULT_OLLAMA_MODEL,
help="Ollama model name to use for tool calling.",
)
ollama_parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Print full tool loop output instead of only the summary.",
)
return parser