/ cli / src / opensandbox_cli / main.py
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)