main.py
1 # Copyright 2026 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """Root Click group with global options.""" 16 17 from __future__ import annotations 18 19 from pathlib import Path 20 21 import click 22 from rich.console import Console 23 24 from opensandbox_cli import __version__ 25 from opensandbox_cli.client import ClientContext 26 from opensandbox_cli.commands.command import command_group 27 from opensandbox_cli.commands.config_cmd import config_group 28 from opensandbox_cli.commands.devops import devops_group 29 from opensandbox_cli.commands.egress import egress_group 30 from opensandbox_cli.commands.file import file_group 31 from opensandbox_cli.commands.sandbox import sandbox_group 32 from opensandbox_cli.commands.skills import skills_group 33 from opensandbox_cli.config import resolve_config 34 35 # --------------------------------------------------------------------------- 36 # Banner 37 # --------------------------------------------------------------------------- 38 39 BANNER = r"""[bold cyan] 40 ____ _____ _ _ 41 / __ \ / ____| | | | 42 | | | |_ __ ___ _ _| (___ __ _ _ __ __| | |__ _____ __ 43 | | | | '_ \ / _ \ '_ \___ \ / _` | '_ \ / _` | '_ \ / _ \ \/ / 44 | |__| | |_) | __/ | | |___) | (_| | | | | (_| | |_) | (_) > < 45 \____/| .__/ \___|_| |_|____/ \__,_|_| |_|\__,_|_.__/ \___/_/\_\ 46 | | 47 |_|[/] [dim]v{version}[/] 48 """ 49 50 51 class BannerGroup(click.Group): 52 """Custom Click group that shows a banner before help text.""" 53 54 def format_help(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: 55 console = Console(stderr=False) 56 console.print(BANNER.format(version=__version__)) 57 super().format_help(ctx, formatter) 58 59 60 @click.group(cls=BannerGroup, context_settings={"help_option_names": ["-h", "--help"]}) 61 @click.option("--api-key", envvar="OPEN_SANDBOX_API_KEY", default=None, help="API key for authentication.") 62 @click.option("--domain", envvar="OPEN_SANDBOX_DOMAIN", default=None, help="API server domain (e.g. localhost:8080).") 63 @click.option("--protocol", type=click.Choice(["http", "https"]), default=None, help="Protocol (http/https).") 64 @click.option("--request-timeout", type=int, default=None, help="Request timeout in seconds.") 65 @click.option( 66 "--use-server-proxy/--no-use-server-proxy", 67 default=None, 68 help="Route execd and endpoint traffic through the sandbox server proxy.", 69 ) 70 @click.option("--config", "config_path", type=click.Path(exists=False, path_type=Path), default=None, help="Config file path.") 71 @click.option("-v", "--verbose", is_flag=True, default=False, help="Enable verbose/debug output.") 72 @click.option("--no-color", is_flag=True, default=False, help="Disable colored output.") 73 @click.version_option(version=__version__, prog_name="opensandbox") 74 @click.pass_context 75 def cli( 76 ctx: click.Context, 77 api_key: str | None, 78 domain: str | None, 79 protocol: str | None, 80 request_timeout: int | None, 81 use_server_proxy: bool | None, 82 config_path: Path | None, 83 verbose: bool, 84 no_color: bool, 85 ) -> None: 86 """OpenSandbox CLI — manage sandboxes from your terminal.""" 87 if verbose: 88 import logging 89 90 logging.basicConfig(level=logging.DEBUG) 91 92 cli_overrides = { 93 "api_key": api_key, 94 "domain": domain, 95 "protocol": protocol, 96 "request_timeout": request_timeout, 97 "use_server_proxy": use_server_proxy, 98 } 99 100 if ctx.invoked_subcommand == "config": 101 resolved = { 102 "api_key": api_key, 103 "domain": domain, 104 "protocol": protocol or "http", 105 "request_timeout": request_timeout or 30, 106 "use_server_proxy": use_server_proxy if use_server_proxy is not None else False, 107 "color": True, 108 "default_image": None, 109 "default_timeout": None, 110 } 111 else: 112 resolved = resolve_config( 113 cli_api_key=api_key, 114 cli_domain=domain, 115 cli_protocol=protocol, 116 cli_timeout=request_timeout, 117 cli_use_server_proxy=use_server_proxy, 118 config_path=config_path, 119 ) 120 resolved["color"] = not no_color and resolved.get("color", True) 121 122 effective_config_path = config_path or Path.home() / ".opensandbox" / "config.toml" 123 ctx.obj = ClientContext( 124 resolved_config=resolved, 125 config_path=effective_config_path, 126 cli_overrides=cli_overrides, 127 ) 128 ctx.call_on_close(lambda: ctx.obj.close()) 129 130 131 # Register sub-command groups 132 cli.add_command(sandbox_group) 133 cli.add_command(command_group) 134 cli.add_command(file_group) 135 cli.add_command(egress_group) 136 cli.add_command(config_group) 137 cli.add_command(devops_group) 138 cli.add_command(skills_group)