Add content-only workspace read modes
Make the human workspace read commands easier to use in chat transcripts and shell pipelines by adding CLI-only --content-only to workspace file read and workspace disk read. Keep JSON, SDK, and MCP behavior unchanged while fixing the default human rendering so content without a trailing newline is cleanly separated from the summary footer. Update the 3.9.0 docs and roadmap status, and add CLI regression coverage plus a real guest-backed smoke for live and stopped-disk reads.
This commit is contained in:
parent
407c805ce2
commit
22d284b1f5
13 changed files with 314 additions and 46 deletions
|
|
@ -45,6 +45,12 @@ def _write_stream(text: str, *, stream: Any) -> None:
|
|||
stream.flush()
|
||||
|
||||
|
||||
def _print_read_content(text: str, *, content_only: bool) -> None:
|
||||
_write_stream(text, stream=sys.stdout)
|
||||
if not content_only and text != "" and not text.endswith("\n"):
|
||||
_write_stream("\n", stream=sys.stdout)
|
||||
|
||||
|
||||
def _print_run_human(payload: dict[str, Any]) -> None:
|
||||
stdout = str(payload.get("stdout", ""))
|
||||
stderr = str(payload.get("stderr", ""))
|
||||
|
|
@ -339,8 +345,12 @@ def _print_workspace_disk_list_human(payload: dict[str, Any]) -> None:
|
|||
print(line)
|
||||
|
||||
|
||||
def _print_workspace_disk_read_human(payload: dict[str, Any]) -> None:
|
||||
_write_stream(str(payload.get("content", "")), stream=sys.stdout)
|
||||
def _print_workspace_disk_read_human(
|
||||
payload: dict[str, Any], *, content_only: bool = False
|
||||
) -> None:
|
||||
_print_read_content(str(payload.get("content", "")), content_only=content_only)
|
||||
if content_only:
|
||||
return
|
||||
print(
|
||||
"[workspace-disk-read] "
|
||||
f"workspace_id={str(payload.get('workspace_id', 'unknown'))} "
|
||||
|
|
@ -397,8 +407,12 @@ def _print_workspace_file_list_human(payload: dict[str, Any]) -> None:
|
|||
print(line)
|
||||
|
||||
|
||||
def _print_workspace_file_read_human(payload: dict[str, Any]) -> None:
|
||||
_write_stream(str(payload.get("content", "")), stream=sys.stdout)
|
||||
def _print_workspace_file_read_human(
|
||||
payload: dict[str, Any], *, content_only: bool = False
|
||||
) -> None:
|
||||
_print_read_content(str(payload.get("content", "")), content_only=content_only)
|
||||
if content_only:
|
||||
return
|
||||
print(
|
||||
"[workspace-file-read] "
|
||||
f"workspace_id={str(payload.get('workspace_id', 'unknown'))} "
|
||||
|
|
@ -1223,7 +1237,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"Read one regular text file under `/workspace`. This is bounded and does not "
|
||||
"follow symlinks."
|
||||
),
|
||||
epilog="Example:\n pyro workspace file read WORKSPACE_ID src/app.py",
|
||||
epilog=dedent(
|
||||
"""
|
||||
Examples:
|
||||
pyro workspace file read WORKSPACE_ID src/app.py
|
||||
pyro workspace file read WORKSPACE_ID src/app.py --content-only
|
||||
"""
|
||||
),
|
||||
formatter_class=_HelpFormatter,
|
||||
)
|
||||
workspace_file_read_parser.add_argument("workspace_id", metavar="WORKSPACE_ID")
|
||||
|
|
@ -1234,7 +1254,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
default=DEFAULT_WORKSPACE_FILE_READ_MAX_BYTES,
|
||||
help="Maximum number of bytes to return in the decoded text response.",
|
||||
)
|
||||
workspace_file_read_parser.add_argument(
|
||||
workspace_file_read_output_group = workspace_file_read_parser.add_mutually_exclusive_group()
|
||||
workspace_file_read_output_group.add_argument(
|
||||
"--content-only",
|
||||
action="store_true",
|
||||
help="Print only the decoded file content with no human summary footer.",
|
||||
)
|
||||
workspace_file_read_output_group.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print structured JSON instead of human-readable output.",
|
||||
|
|
@ -1535,7 +1561,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
"Read one regular file from a stopped workspace rootfs without booting the guest. "
|
||||
"Relative paths resolve inside `/workspace`; absolute paths inspect any guest path."
|
||||
),
|
||||
epilog="Example:\n pyro workspace disk read WORKSPACE_ID note.txt --max-bytes 4096",
|
||||
epilog=dedent(
|
||||
"""
|
||||
Examples:
|
||||
pyro workspace disk read WORKSPACE_ID note.txt --max-bytes 4096
|
||||
pyro workspace disk read WORKSPACE_ID note.txt --content-only
|
||||
"""
|
||||
),
|
||||
formatter_class=_HelpFormatter,
|
||||
)
|
||||
workspace_disk_read_parser.add_argument("workspace_id", metavar="WORKSPACE_ID")
|
||||
|
|
@ -1546,7 +1578,13 @@ def _build_parser() -> argparse.ArgumentParser:
|
|||
default=DEFAULT_WORKSPACE_DISK_READ_MAX_BYTES,
|
||||
help="Maximum number of decoded UTF-8 bytes to return.",
|
||||
)
|
||||
workspace_disk_read_parser.add_argument(
|
||||
workspace_disk_read_output_group = workspace_disk_read_parser.add_mutually_exclusive_group()
|
||||
workspace_disk_read_output_group.add_argument(
|
||||
"--content-only",
|
||||
action="store_true",
|
||||
help="Print only the decoded file content with no human summary footer.",
|
||||
)
|
||||
workspace_disk_read_output_group.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print structured JSON instead of human-readable output.",
|
||||
|
|
@ -2505,7 +2543,10 @@ def main() -> None:
|
|||
if bool(args.json):
|
||||
_print_json(payload)
|
||||
else:
|
||||
_print_workspace_file_read_human(payload)
|
||||
_print_workspace_file_read_human(
|
||||
payload,
|
||||
content_only=bool(getattr(args, "content_only", False)),
|
||||
)
|
||||
return
|
||||
if args.workspace_file_command == "write":
|
||||
text = (
|
||||
|
|
@ -2698,7 +2739,10 @@ def main() -> None:
|
|||
if bool(args.json):
|
||||
_print_json(payload)
|
||||
else:
|
||||
_print_workspace_disk_read_human(payload)
|
||||
_print_workspace_disk_read_human(
|
||||
payload,
|
||||
content_only=bool(getattr(args, "content_only", False)),
|
||||
)
|
||||
return
|
||||
if args.workspace_command == "shell":
|
||||
if args.workspace_shell_command == "open":
|
||||
|
|
|
|||
|
|
@ -51,12 +51,12 @@ PUBLIC_CLI_WORKSPACE_CREATE_FLAGS = (
|
|||
)
|
||||
PUBLIC_CLI_WORKSPACE_DISK_EXPORT_FLAGS = ("--output", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_DISK_LIST_FLAGS = ("--recursive", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_DISK_READ_FLAGS = ("--max-bytes", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_DISK_READ_FLAGS = ("--max-bytes", "--content-only", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_EXEC_FLAGS = ("--timeout-seconds", "--secret-env", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_DIFF_FLAGS = ("--json",)
|
||||
PUBLIC_CLI_WORKSPACE_EXPORT_FLAGS = ("--output", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_FILE_LIST_FLAGS = ("--recursive", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_FILE_READ_FLAGS = ("--max-bytes", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_FILE_READ_FLAGS = ("--max-bytes", "--content-only", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_FILE_WRITE_FLAGS = ("--text", "--text-file", "--json")
|
||||
PUBLIC_CLI_WORKSPACE_LIST_FLAGS = ("--json",)
|
||||
PUBLIC_CLI_WORKSPACE_PATCH_APPLY_FLAGS = ("--patch", "--patch-file", "--json")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from typing import Any
|
|||
from pyro_mcp.runtime import DEFAULT_PLATFORM, RuntimePaths
|
||||
|
||||
DEFAULT_ENVIRONMENT_VERSION = "1.0.0"
|
||||
DEFAULT_CATALOG_VERSION = "3.8.0"
|
||||
DEFAULT_CATALOG_VERSION = "3.9.0"
|
||||
OCI_MANIFEST_ACCEPT = ", ".join(
|
||||
(
|
||||
"application/vnd.oci.image.index.v1+json",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue