Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sage/studio/_version.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Version information for sage-studio package."""

# 独立硬编码版本
__version__ = "0.2.4.40"
__version__ = "0.2.4.42"
__author__ = "IntelliStream Team"
__email__ = "shuhao_zhang@hust.edu.cn"
58 changes: 47 additions & 11 deletions src/sage/studio/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from __future__ import annotations

import argparse

import typer
from rich.console import Console

Expand All @@ -31,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(
Expand All @@ -44,7 +46,7 @@ def start(
"""Start SAGE Studio (frontend + backend)."""
manager = _get_studio_manager()
manager.start(
frontend_port=frontend_port,
port=port,
backend_port=backend_port,
host=host,
dev=dev,
Expand Down Expand Up @@ -78,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"),
):
Expand All @@ -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=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()
Expand Down Expand Up @@ -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"
)
8 changes: 4 additions & 4 deletions tests/integration/test_studio_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def load_config(self):

def start(
self,
frontend_port=None,
port=None,
backend_port=None,
gateway_port=None,
host=None,
Expand All @@ -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
Expand All @@ -76,7 +76,7 @@ def install(self):
def build(self):
return True

def open(self):
def open_browser(self):
return True

def clean(self):
Expand Down
50 changes: 26 additions & 24 deletions tests/integration/test_studio_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading