Completed new latest CLI (#2426)
This commit is contained in:
committed by
GitHub
parent
74cd8f7b45
commit
2216ce66d3
@@ -102,7 +102,13 @@ This quickstart guide will walk you through getting Skyvern up and running on yo
|
|||||||
skyvern run ui
|
skyvern run ui
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **Run task**
|
5. **Check component status**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
skyvern status
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Run task**
|
||||||
|
|
||||||
Run a skyvern task locally:
|
Run a skyvern task locally:
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -2,24 +2,32 @@ import typer
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from .docs import docs_app
|
from .docs import docs_app
|
||||||
from .init_command import init, init_browser, init_mcp
|
from .init_command import init, init_browser
|
||||||
from .run_commands import run_app
|
from .run_commands import run_app
|
||||||
from .setup_commands import setup_mcp_command
|
from .status import status_app
|
||||||
from .tasks import tasks_app
|
from .tasks import tasks_app
|
||||||
from .workflow import workflow_app
|
from .workflow import workflow_app
|
||||||
|
|
||||||
cli_app = typer.Typer()
|
cli_app = typer.Typer(
|
||||||
cli_app.add_typer(run_app, name="run")
|
help=("""[bold]Skyvern CLI[/bold]\nManage and run your local Skyvern environment."""),
|
||||||
cli_app.add_typer(workflow_app, name="workflow")
|
no_args_is_help=True,
|
||||||
cli_app.add_typer(tasks_app, name="tasks")
|
rich_markup_mode="rich",
|
||||||
cli_app.add_typer(docs_app, name="docs")
|
)
|
||||||
setup_app = typer.Typer()
|
cli_app.add_typer(
|
||||||
cli_app.add_typer(setup_app, name="setup")
|
run_app,
|
||||||
init_app = typer.Typer(invoke_without_command=True)
|
name="run",
|
||||||
|
help="Run Skyvern services like the API server, UI, and MCP.",
|
||||||
|
)
|
||||||
|
cli_app.add_typer(workflow_app, name="workflow", help="Workflow management commands.")
|
||||||
|
cli_app.add_typer(tasks_app, name="tasks", help="Task management commands.")
|
||||||
|
cli_app.add_typer(docs_app, name="docs", help="Open Skyvern documentation.")
|
||||||
|
cli_app.add_typer(status_app, name="status", help="Check if Skyvern services are running.")
|
||||||
|
init_app = typer.Typer(
|
||||||
|
invoke_without_command=True,
|
||||||
|
help="Interactively configure Skyvern and its dependencies.",
|
||||||
|
)
|
||||||
cli_app.add_typer(init_app, name="init")
|
cli_app.add_typer(init_app, name="init")
|
||||||
|
|
||||||
setup_app.command(name="mcp")(setup_mcp_command)
|
|
||||||
|
|
||||||
|
|
||||||
@init_app.callback()
|
@init_app.callback()
|
||||||
def init_callback(
|
def init_callback(
|
||||||
@@ -37,12 +45,6 @@ def init_browser_command() -> None:
|
|||||||
init_browser()
|
init_browser()
|
||||||
|
|
||||||
|
|
||||||
@init_app.command(name="mcp")
|
|
||||||
def init_mcp_command() -> None:
|
|
||||||
"""Initialize only the MCP server configuration."""
|
|
||||||
init_mcp()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover - manual CLI invocation
|
if __name__ == "__main__": # pragma: no cover - manual CLI invocation
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
cli_app()
|
cli_app()
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
"""Documentation-related CLI helpers."""
|
"""Documentation-related CLI helpers."""
|
||||||
|
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
from .console import console
|
from .console import console
|
||||||
|
|
||||||
docs_app = typer.Typer()
|
DOCS_URL = "https://docs.skyvern.com"
|
||||||
|
|
||||||
|
docs_app = typer.Typer(
|
||||||
|
invoke_without_command=True,
|
||||||
|
help="Open Skyvern documentation in your browser.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@docs_app.command()
|
@docs_app.callback()
|
||||||
def placeholder() -> None:
|
def docs_callback(ctx: typer.Context) -> None:
|
||||||
"""Placeholder command for documentation actions."""
|
"""Open the Skyvern documentation in a browser."""
|
||||||
console.print("Documentation commands are not yet implemented.")
|
if ctx.invoked_subcommand is None:
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
f"[bold blue]Opening Skyvern docs at [link={DOCS_URL}]{DOCS_URL}[/link][/bold blue]",
|
||||||
|
border_style="cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
webbrowser.open(DOCS_URL)
|
||||||
|
except Exception as exc: # pragma: no cover - CLI safeguard
|
||||||
|
console.print(f"[red]Failed to open documentation: {exc}[/red]")
|
||||||
|
|||||||
@@ -142,8 +142,3 @@ def init_browser() -> None:
|
|||||||
progress.add_task("[bold blue]Downloading Chromium, this may take a moment...", total=None)
|
progress.add_task("[bold blue]Downloading Chromium, this may take a moment...", total=None)
|
||||||
subprocess.run(["playwright", "install", "chromium"], check=True)
|
subprocess.run(["playwright", "install", "chromium"], check=True)
|
||||||
console.print("✅ [green]Chromium installation complete.[/green]")
|
console.print("✅ [green]Chromium installation complete.[/green]")
|
||||||
|
|
||||||
|
|
||||||
def init_mcp() -> None:
|
|
||||||
"""Initialize only the MCP server configuration."""
|
|
||||||
setup_mcp()
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from skyvern.utils import detect_os
|
|||||||
|
|
||||||
from .console import console
|
from .console import console
|
||||||
|
|
||||||
run_app = typer.Typer()
|
run_app = typer.Typer(help="Commands to run Skyvern services such as the API server or UI.")
|
||||||
|
|
||||||
mcp = FastMCP("Skyvern")
|
mcp = FastMCP("Skyvern")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from .mcp import setup_mcp
|
|
||||||
|
|
||||||
|
|
||||||
def setup_mcp_command() -> None:
|
|
||||||
"""Wrapper command to configure the MCP server."""
|
|
||||||
setup_mcp()
|
|
||||||
48
skyvern/cli/status.py
Normal file
48
skyvern/cli/status.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
import typer
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
from .console import console
|
||||||
|
|
||||||
|
status_app = typer.Typer(help="Check status of Skyvern components.", invoke_without_command=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_port(port: int) -> bool:
|
||||||
|
"""Return True if a local port is accepting connections."""
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("localhost", port), timeout=0.5):
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _status_table() -> Table:
|
||||||
|
api_port = int(os.getenv("PORT", 8000))
|
||||||
|
ui_port = 8080
|
||||||
|
db_port = 5432
|
||||||
|
|
||||||
|
components = [
|
||||||
|
("API server", _check_port(api_port), "skyvern run server"),
|
||||||
|
("UI server", _check_port(ui_port), "skyvern run ui"),
|
||||||
|
("PostgreSQL", _check_port(db_port), "skyvern init --no-postgres false"),
|
||||||
|
]
|
||||||
|
|
||||||
|
table = Table(title="Skyvern Component Status")
|
||||||
|
table.add_column("Component", style="bold")
|
||||||
|
table.add_column("Running")
|
||||||
|
table.add_column("Start Command")
|
||||||
|
|
||||||
|
for name, running, cmd in components:
|
||||||
|
status = "[green]Yes[/green]" if running else "[red]No[/red]"
|
||||||
|
table.add_row(name, status, cmd)
|
||||||
|
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
@status_app.callback(invoke_without_command=True)
|
||||||
|
def status_callback(ctx: typer.Context) -> None:
|
||||||
|
"""Show status for API server, UI, and database."""
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
console.print(_status_table())
|
||||||
@@ -1,13 +1,62 @@
|
|||||||
"""Task-related CLI helpers."""
|
"""Task-related CLI helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from skyvern.client import Skyvern
|
||||||
|
from skyvern.config import settings
|
||||||
|
|
||||||
from .console import console
|
from .console import console
|
||||||
|
|
||||||
tasks_app = typer.Typer()
|
tasks_app = typer.Typer(help="Manage Skyvern tasks and operations.")
|
||||||
|
|
||||||
|
|
||||||
@tasks_app.command()
|
@tasks_app.callback()
|
||||||
def placeholder() -> None:
|
def tasks_callback(
|
||||||
"""Placeholder command for task management."""
|
ctx: typer.Context,
|
||||||
console.print("Task operations are not yet implemented.")
|
api_key: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--api-key",
|
||||||
|
help="Skyvern API key",
|
||||||
|
envvar="SKYVERN_API_KEY",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Store API key in Typer context."""
|
||||||
|
ctx.obj = {"api_key": api_key}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client(api_key: Optional[str] = None) -> Skyvern:
|
||||||
|
"""Instantiate a Skyvern SDK client using environment variables."""
|
||||||
|
load_dotenv()
|
||||||
|
load_dotenv(".env")
|
||||||
|
key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY
|
||||||
|
return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key)
|
||||||
|
|
||||||
|
|
||||||
|
def _list_workflow_tasks(client: Skyvern, run_id: str) -> list[dict]:
|
||||||
|
"""Return tasks for the given workflow run."""
|
||||||
|
resp = client.agent._client_wrapper.httpx_client.request(
|
||||||
|
"api/v1/tasks",
|
||||||
|
method="GET",
|
||||||
|
params={"workflow_run_id": run_id, "page_size": 100, "page": 1},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
|
@tasks_app.command("list")
|
||||||
|
def list_tasks(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workflow_run_id: str = typer.Option(..., "--workflow-run-id", "-r", help="Workflow run ID"),
|
||||||
|
) -> None:
|
||||||
|
"""List tasks for a workflow run."""
|
||||||
|
client = _get_client(ctx.obj.get("api_key") if ctx.obj else None)
|
||||||
|
tasks = _list_workflow_tasks(client, workflow_run_id)
|
||||||
|
console.print(Panel(json.dumps(tasks, indent=2), border_style="cyan"))
|
||||||
|
|||||||
@@ -1,13 +1,115 @@
|
|||||||
"""Workflow-related CLI helpers."""
|
"""Workflow-related CLI helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
from skyvern.client import Skyvern
|
||||||
|
from skyvern.config import settings
|
||||||
|
|
||||||
from .console import console
|
from .console import console
|
||||||
|
from .tasks import _list_workflow_tasks
|
||||||
|
|
||||||
workflow_app = typer.Typer()
|
workflow_app = typer.Typer(help="Manage Skyvern workflows.")
|
||||||
|
|
||||||
|
|
||||||
@workflow_app.command()
|
@workflow_app.callback()
|
||||||
def placeholder() -> None:
|
def workflow_callback(
|
||||||
"""Placeholder command for workflow operations."""
|
ctx: typer.Context,
|
||||||
console.print("Workflow operations are not yet implemented.")
|
api_key: Optional[str] = typer.Option(
|
||||||
|
None,
|
||||||
|
"--api-key",
|
||||||
|
help="Skyvern API key",
|
||||||
|
envvar="SKYVERN_API_KEY",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Store the provided API key in the Typer context."""
|
||||||
|
ctx.obj = {"api_key": api_key}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_client(api_key: Optional[str] = None) -> Skyvern:
|
||||||
|
"""Instantiate a Skyvern SDK client using environment variables."""
|
||||||
|
load_dotenv()
|
||||||
|
load_dotenv(".env")
|
||||||
|
key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY
|
||||||
|
return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key)
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_app.command("start")
|
||||||
|
def start_workflow(
|
||||||
|
ctx: typer.Context,
|
||||||
|
workflow_id: str = typer.Argument(..., help="Workflow permanent ID"),
|
||||||
|
parameters: str = typer.Option("{}", "--parameters", "-p", help="JSON parameters for the workflow"),
|
||||||
|
title: Optional[str] = typer.Option(None, "--title", help="Title for the workflow run"),
|
||||||
|
max_steps: Optional[int] = typer.Option(None, "--max-steps", help="Override the workflow max steps"),
|
||||||
|
) -> None:
|
||||||
|
"""Dispatch a workflow run."""
|
||||||
|
try:
|
||||||
|
params_dict = json.loads(parameters) if parameters else {}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
console.print(f"[red]Invalid JSON for parameters: {parameters}[/red]")
|
||||||
|
raise typer.Exit(code=1)
|
||||||
|
|
||||||
|
client = _get_client(ctx.obj.get("api_key") if ctx.obj else None)
|
||||||
|
run_resp = client.agent.run_workflow(
|
||||||
|
workflow_id=workflow_id,
|
||||||
|
parameters=params_dict,
|
||||||
|
title=title,
|
||||||
|
max_steps_override=max_steps,
|
||||||
|
)
|
||||||
|
console.print(
|
||||||
|
Panel(
|
||||||
|
f"Started workflow run [bold]{run_resp.run_id}[/bold]",
|
||||||
|
border_style="green",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_app.command("stop")
|
||||||
|
def stop_workflow(
|
||||||
|
ctx: typer.Context,
|
||||||
|
run_id: str = typer.Argument(..., help="ID of the workflow run"),
|
||||||
|
) -> None:
|
||||||
|
"""Cancel a running workflow."""
|
||||||
|
client = _get_client(ctx.obj.get("api_key") if ctx.obj else None)
|
||||||
|
client.agent.cancel_run(run_id=run_id)
|
||||||
|
console.print(Panel(f"Stop signal sent for run {run_id}", border_style="red"))
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_app.command("status")
|
||||||
|
def workflow_status(
|
||||||
|
ctx: typer.Context,
|
||||||
|
run_id: str = typer.Argument(..., help="ID of the workflow run"),
|
||||||
|
tasks: bool = typer.Option(False, "--tasks", help="Show task executions"),
|
||||||
|
) -> None:
|
||||||
|
"""Retrieve status information for a workflow run."""
|
||||||
|
client = _get_client(ctx.obj.get("api_key") if ctx.obj else None)
|
||||||
|
run = client.agent.get_run(run_id=run_id)
|
||||||
|
console.print(Panel(run.model_dump_json(indent=2), border_style="cyan"))
|
||||||
|
if tasks:
|
||||||
|
task_list = _list_workflow_tasks(client, run_id)
|
||||||
|
console.print(Panel(json.dumps(task_list, indent=2), border_style="magenta"))
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_app.command("list")
|
||||||
|
def list_workflows(
|
||||||
|
ctx: typer.Context,
|
||||||
|
page: int = typer.Option(1, "--page", help="Page number"),
|
||||||
|
page_size: int = typer.Option(10, "--page-size", help="Number of workflows to return"),
|
||||||
|
template: bool = typer.Option(False, "--template", help="List template workflows"),
|
||||||
|
) -> None:
|
||||||
|
"""List workflows for the organization."""
|
||||||
|
client = _get_client(ctx.obj.get("api_key") if ctx.obj else None)
|
||||||
|
resp = client.agent._client_wrapper.httpx_client.request(
|
||||||
|
"api/v1/workflows",
|
||||||
|
method="GET",
|
||||||
|
params={"page": page, "page_size": page_size, "template": template},
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
console.print(Panel(json.dumps(resp.json(), indent=2), border_style="cyan"))
|
||||||
|
|||||||
Reference in New Issue
Block a user