From ecd263d77563ae49b87cb31588a0401e22c54395 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 3 Apr 2026 18:10:50 +0800 Subject: [PATCH 1/2] feat(cli): enable sage studio plugin command and align studio CLI-manager APIO Made studio plugin registration argparse-compatible - Added argparse bridge handler that dispatches to existing Typer app: - sage-studio/src/sage/studio/cli.py - Updated register_studio_command to support both Typer and argparse callers: - sage-studio/src/sage/studio/cli.py - Disabled wrapper-local help so sage studio --help reaches Typer help: - sage-studio/src/sage/studio/cli.py align studio CLI-manager API add dynamic loading for sage CLI plugins via sage.cli.plugins entry points register studio command into argparse subcommands from plugin entry point forward plugin args safely with parse_known_args so studio help/subcommands work fix studio start/restart calls to use StudioManager.start(port=...) instead of frontend_port fix studio open command to call open_browser() align studio logs command with manager signature by removing unsupported gateway/lines forwarding preserve clear error behavior for unknown args on non-plugin commands --- src/sage/studio/_version.py | 2 +- src/sage/studio/cli.py | 54 ++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/sage/studio/_version.py b/src/sage/studio/_version.py index 0f1f248..e445b03 100644 --- a/src/sage/studio/_version.py +++ b/src/sage/studio/_version.py @@ -1,6 +1,6 @@ """Version information for sage-studio package.""" # 独立硬编码版本 -__version__ = "0.2.4.40" +__version__ = "0.2.4.41" __author__ = "IntelliStream Team" __email__ = "shuhao_zhang@hust.edu.cn" diff --git a/src/sage/studio/cli.py b/src/sage/studio/cli.py index 808c62f..4e661af 100644 --- a/src/sage/studio/cli.py +++ b/src/sage/studio/cli.py @@ -9,6 +9,8 @@ from __future__ import annotations +import argparse + import typer from rich.console import Console @@ -44,7 +46,7 @@ def start( """Start SAGE Studio (frontend + backend).""" manager = _get_studio_manager() manager.start( - frontend_port=frontend_port, + port=frontend_port, backend_port=backend_port, host=host, dev=dev, @@ -87,26 +89,24 @@ def restart( console.print("🔄 Restarting Studio...") # 🔧 FIX: 重启时停止 LLM 服务(避免端口冲突),但保留 Gateway(共享服务) manager.stop(stop_gateway=False, stop_llm=True) - manager.start(frontend_port=frontend_port, dev=dev, skip_confirm=skip_confirm) + manager.start(port=frontend_port, dev=dev, skip_confirm=skip_confirm) @app.command() def logs( backend: bool = typer.Option(False, "--backend", help="Show backend logs"), - gateway: bool = typer.Option(False, "--gateway", help="Show gateway logs"), follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"), - lines: int = typer.Option(50, "--lines", "-n", help="Number of lines to show"), ): """Show Studio logs.""" manager = _get_studio_manager() - manager.logs(backend=backend, gateway=gateway, follow=follow, lines=lines) + manager.logs(backend=backend, follow=follow) @app.command() def open(): """Open Studio in default browser.""" manager = _get_studio_manager() - manager.open() + manager.open_browser() @app.command() @@ -139,10 +139,46 @@ def npm( manager.run_npm_command(args) +def _run_studio_argparse(args: argparse.Namespace) -> int: + """Dispatch argparse-captured studio args to the Typer app.""" + studio_args = list(args.studio_args or []) + if studio_args and studio_args[0] == "--": + studio_args = studio_args[1:] + + try: + app(args=studio_args, prog_name="sage studio", standalone_mode=False) + except typer.Exit as exc: + return int(exc.exit_code) + + return 0 + + # For SAGE CLI integration -def register_studio_command(sage_app: typer.Typer) -> None: +def register_studio_command(sage_cli: object) -> None: """Register studio commands to SAGE CLI app. - This function is called by SAGE CLI to dynamically add studio commands. + Supports both: + - Typer root app (``add_typer``) + - argparse subparsers action (``add_parser``) """ - sage_app.add_typer(app, name="studio") + if hasattr(sage_cli, "add_typer"): + sage_cli.add_typer(app, name="studio") + return + + if hasattr(sage_cli, "add_parser"): + parser = sage_cli.add_parser( + "studio", + help="Studio visual workflow builder", + add_help=False, + ) + parser.add_argument( + "studio_args", + nargs=argparse.REMAINDER, + help="Arguments passed through to 'sage studio'", + ) + parser.set_defaults(_handler=_run_studio_argparse) + return + + raise TypeError( + "Unsupported SAGE CLI object for studio registration; expected Typer app or argparse subparsers" + ) From da80fc250587ff989253abc27f787d1bb073ef5d Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Fri, 3 Apr 2026 18:42:04 +0800 Subject: [PATCH 2/2] refactor(cli): standardize studio manager contract and align tests --- src/sage/studio/_version.py | 2 +- src/sage/studio/cli.py | 8 ++-- tests/integration/test_studio_cli.py | 8 ++-- tests/integration/test_studio_lifecycle.py | 50 +++++++++++----------- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/sage/studio/_version.py b/src/sage/studio/_version.py index e445b03..c66aee7 100644 --- a/src/sage/studio/_version.py +++ b/src/sage/studio/_version.py @@ -1,6 +1,6 @@ """Version information for sage-studio package.""" # 独立硬编码版本 -__version__ = "0.2.4.41" +__version__ = "0.2.4.42" __author__ = "IntelliStream Team" __email__ = "shuhao_zhang@hust.edu.cn" diff --git a/src/sage/studio/cli.py b/src/sage/studio/cli.py index 4e661af..e5156ac 100644 --- a/src/sage/studio/cli.py +++ b/src/sage/studio/cli.py @@ -33,7 +33,7 @@ def _get_studio_manager(): @app.command() def start( - frontend_port: int | None = typer.Option( + port: int | None = typer.Option( None, "--port", "-p", help="Frontend port (default: 5173 dev, 8889 prod)" ), backend_port: int | None = typer.Option( @@ -46,7 +46,7 @@ def start( """Start SAGE Studio (frontend + backend).""" manager = _get_studio_manager() manager.start( - port=frontend_port, + port=port, backend_port=backend_port, host=host, dev=dev, @@ -80,7 +80,7 @@ def status(): @app.command() def restart( - frontend_port: int | None = typer.Option(None, "--port", "-p", help="Frontend port"), + port: int | None = typer.Option(None, "--port", "-p", help="Frontend port"), dev: bool = typer.Option(True, "--dev/--prod", help="Development or production mode"), skip_confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"), ): @@ -89,7 +89,7 @@ def restart( console.print("🔄 Restarting Studio...") # 🔧 FIX: 重启时停止 LLM 服务(避免端口冲突),但保留 Gateway(共享服务) manager.stop(stop_gateway=False, stop_llm=True) - manager.start(port=frontend_port, dev=dev, skip_confirm=skip_confirm) + manager.start(port=port, dev=dev, skip_confirm=skip_confirm) @app.command() diff --git a/tests/integration/test_studio_cli.py b/tests/integration/test_studio_cli.py index 7870fba..256a363 100644 --- a/tests/integration/test_studio_cli.py +++ b/tests/integration/test_studio_cli.py @@ -38,7 +38,7 @@ def load_config(self): def start( self, - frontend_port=None, + port=None, backend_port=None, gateway_port=None, host=None, @@ -52,8 +52,8 @@ def start( skip_confirm=False, no_embedding=False, ): - if frontend_port: - self._config["port"] = frontend_port + if port: + self._config["port"] = port if host: self._config["host"] = host self._running = True @@ -76,7 +76,7 @@ def install(self): def build(self): return True - def open(self): + def open_browser(self): return True def clean(self): diff --git a/tests/integration/test_studio_lifecycle.py b/tests/integration/test_studio_lifecycle.py index 444f714..b014aec 100644 --- a/tests/integration/test_studio_lifecycle.py +++ b/tests/integration/test_studio_lifecycle.py @@ -33,12 +33,25 @@ def is_running(self): def load_config(self) -> dict: return dict(self._config) - def start(self, **kwargs) -> bool: - self.last_start_kwargs = kwargs - if kwargs.get("frontend_port"): - self._config["port"] = kwargs["frontend_port"] - if kwargs.get("host"): - self._config["host"] = kwargs["host"] + def start( + self, + port: int | None = None, + backend_port: int | None = None, + host: str | None = None, + dev: bool = True, + skip_confirm: bool = False, + ) -> bool: + self.last_start_kwargs = { + "port": port, + "backend_port": backend_port, + "host": host, + "dev": dev, + "skip_confirm": skip_confirm, + } + if port: + self._config["port"] = port + if host: + self._config["host"] = host self._running = True return True @@ -51,8 +64,11 @@ def stop(self, stop_gateway: bool = False, stop_llm: bool = False) -> bool: def status(self): return {"running": self._running, "config": self._config} - def logs(self, **kwargs) -> list: - self.last_logs_kwargs = kwargs + def logs(self, follow: bool = False, backend: bool = False) -> list: + self.last_logs_kwargs = { + "follow": follow, + "backend": backend, + } return [] def install(self) -> bool: @@ -64,7 +80,7 @@ def build(self) -> bool: def clean(self) -> bool: return True - def open(self) -> bool: + def open_browser(self) -> bool: return True def run_npm_command(self, args: list[str]) -> bool: @@ -99,7 +115,7 @@ def test_start_happy_path(fake_manager): def test_start_forwards_port(fake_manager): - """``start --port 9001`` passes frontend_port=9001 to manager.""" + """``start --port 9001`` passes port=9001 to manager.""" result = invoke(fake_manager, "start", "--port", "9001") assert result.exit_code == 0, result.stdout assert fake_manager._config["port"] == 9001 @@ -202,17 +218,3 @@ def test_logs_follow_flag(fake_manager): result = invoke(fake_manager, "logs", "--follow") assert result.exit_code == 0, result.stdout assert fake_manager.last_logs_kwargs.get("follow") is True - - -def test_logs_lines_option(fake_manager): - """``logs --lines 100`` passes lines=100 to manager.logs().""" - result = invoke(fake_manager, "logs", "--lines", "100") - assert result.exit_code == 0, result.stdout - assert fake_manager.last_logs_kwargs.get("lines") == 100 - - -def test_logs_gateway_flag(fake_manager): - """``logs --gateway`` passes gateway=True to manager.logs().""" - result = invoke(fake_manager, "logs", "--gateway") - assert result.exit_code == 0, result.stdout - assert fake_manager.last_logs_kwargs.get("gateway") is True