4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
295 lines
9.7 KiB
Python
295 lines
9.7 KiB
Python
"""OpenSRE CLI - open-source SRE agent for automated incident investigation.
|
|
|
|
Enable shell tab-completion (add to your shell profile for persistence):
|
|
|
|
bash: eval "$(_OPENSRE_COMPLETE=bash_source opensre)"
|
|
zsh: eval "$(_OPENSRE_COMPLETE=zsh_source opensre)"
|
|
fish: _OPENSRE_COMPLETE=fish_source opensre | source
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import signal
|
|
import sys
|
|
from contextlib import suppress
|
|
from typing import TYPE_CHECKING
|
|
|
|
from config.platform_bootstrap import ensure_project_platform_package
|
|
|
|
ensure_project_platform_package()
|
|
|
|
import click # noqa: E402
|
|
|
|
from config.version import get_opensre_version # noqa: E402
|
|
from surfaces.cli.group import LazyRichGroup, ThemeParamType # noqa: E402
|
|
from surfaces.cli.invocation import ( # noqa: E402
|
|
ensure_utf8_stdio,
|
|
is_fast_version_invocation,
|
|
print_fast_version,
|
|
resolve_command_parts,
|
|
)
|
|
from surfaces.cli.telemetry import ( # noqa: E402
|
|
build_cli_invoked_properties,
|
|
capture_cli_invoked,
|
|
capture_exception,
|
|
capture_first_run_if_needed,
|
|
init_sentry,
|
|
load_structured_error_type,
|
|
render_landing,
|
|
render_structured_error,
|
|
report_exception,
|
|
should_report_exception,
|
|
shutdown_analytics,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from platform.analytics.provider import Properties
|
|
|
|
_CAPTURE_CLI_ANALYTICS = "capture_cli_analytics"
|
|
_CLI_ANALYTICS_CAPTURED = "cli_analytics_captured"
|
|
_CLI_ARGV = "cli_argv"
|
|
|
|
|
|
def _cli_invoked_properties(ctx: click.Context) -> Properties:
|
|
raw_argv = ctx.obj.get(_CLI_ARGV, []) if ctx.obj else []
|
|
command_parts = resolve_command_parts(
|
|
ctx.command,
|
|
raw_argv if isinstance(raw_argv, list) else [],
|
|
)
|
|
obj = ctx.obj if ctx.obj else {}
|
|
return build_cli_invoked_properties(
|
|
entrypoint="opensre",
|
|
command_parts=command_parts,
|
|
json_output=bool(obj.get("json", False)),
|
|
verbose=bool(obj.get("verbose", False)),
|
|
debug=bool(obj.get("debug", False)),
|
|
yes=bool(obj.get("yes", False)),
|
|
interactive=bool(obj.get("interactive", True)),
|
|
)
|
|
|
|
|
|
def _capture_accepted_cli_invocation(ctx: click.Context) -> None:
|
|
if not ctx.obj.get(_CAPTURE_CLI_ANALYTICS, False):
|
|
return
|
|
if ctx.obj.get(_CLI_ANALYTICS_CAPTURED, False):
|
|
return
|
|
ctx.obj[_CLI_ANALYTICS_CAPTURED] = True
|
|
capture_first_run_if_needed()
|
|
capture_cli_invoked(_cli_invoked_properties(ctx))
|
|
|
|
|
|
@click.group(
|
|
cls=LazyRichGroup,
|
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
invoke_without_command=True,
|
|
)
|
|
@click.version_option(version=get_opensre_version(), prog_name="opensre")
|
|
@click.option(
|
|
"--json", "-j", "json_output", is_flag=True, help="Emit machine-readable JSON output."
|
|
)
|
|
@click.option("--verbose", is_flag=True, help="Print extra diagnostic information.")
|
|
@click.option("--debug", is_flag=True, help="Print debug-level logs and traces.")
|
|
@click.option("--yes", "-y", is_flag=True, help="Auto-confirm all interactive prompts.")
|
|
@click.option(
|
|
"--interactive/--no-interactive",
|
|
default=True,
|
|
help="Disable the interactive shell and print the landing page instead.",
|
|
)
|
|
@click.option(
|
|
"--resume",
|
|
"resume_session_id",
|
|
default=None,
|
|
metavar="SESSION-ID",
|
|
help="Resume a previous interactive shell session by ID, prefix, or name substring.",
|
|
)
|
|
@click.option(
|
|
"--layout",
|
|
type=click.Choice(["classic", "pinned"]),
|
|
default=None,
|
|
help="Interactive-shell layout: 'classic' (scrolling) or 'pinned' (fixed "
|
|
"input bar). Overrides OPENSRE_LAYOUT env var and ~/.opensre/config.yml.",
|
|
)
|
|
@click.option(
|
|
"--theme",
|
|
type=ThemeParamType(),
|
|
default=None,
|
|
help="Interactive-shell color palette. Overrides OPENSRE_THEME env var "
|
|
"and ~/.opensre/config.yml interactive.theme.",
|
|
)
|
|
@click.pass_context
|
|
def cli(
|
|
ctx: click.Context,
|
|
json_output: bool,
|
|
verbose: bool,
|
|
debug: bool,
|
|
yes: bool,
|
|
interactive: bool,
|
|
resume_session_id: str | None,
|
|
layout: str | None,
|
|
theme: str | None,
|
|
) -> None:
|
|
"""OpenSRE - open-source SRE agent for automated incident investigation and root cause analysis."""
|
|
ctx.ensure_object(dict)
|
|
ctx.obj["json"] = json_output
|
|
ctx.obj["verbose"] = verbose
|
|
ctx.obj["debug"] = debug
|
|
ctx.obj["yes"] = yes
|
|
ctx.obj["interactive"] = interactive
|
|
|
|
from surfaces.cli.runtime_flags import sync_runtime_flags_from_click
|
|
|
|
sync_runtime_flags_from_click(ctx)
|
|
|
|
if verbose or debug:
|
|
os.environ["TRACER_VERBOSE"] = "1"
|
|
|
|
from config.repl_config import ReplConfig
|
|
|
|
_capture_accepted_cli_invocation(ctx)
|
|
|
|
if ctx.invoked_subcommand is None:
|
|
if sys.stdin.isatty() and sys.stdout.isatty():
|
|
from surfaces.interactive_shell import run_repl
|
|
|
|
config = ReplConfig.load(
|
|
cli_enabled=interactive or resume_session_id is not None,
|
|
cli_layout=layout,
|
|
cli_theme=theme,
|
|
)
|
|
if config.enabled or resume_session_id:
|
|
raise SystemExit(
|
|
run_repl(
|
|
config=config,
|
|
resume_session_id=resume_session_id,
|
|
)
|
|
)
|
|
click.echo("🚧 OpenSRE is in Public Beta — features may change.", err=True)
|
|
render_landing(cli)
|
|
raise SystemExit(0)
|
|
|
|
# Apply interactive.theme / OPENSRE_THEME / --theme for subcommands (onboard, etc.).
|
|
ReplConfig.load(cli_theme=theme)
|
|
|
|
|
|
def _install_sigint_handler() -> None:
|
|
"""Handle Ctrl+C between prompts (when prompt_toolkit is not active).
|
|
|
|
prompt_toolkit intercepts Ctrl+C internally while a prompt is running, so
|
|
the key binding in prompt_support.py handles that case. This SIGINT handler
|
|
covers everything else: long-running operations, streaming output, etc.
|
|
"""
|
|
|
|
def _handler(_signum: int, _frame: object) -> None:
|
|
from platform.terminal.prompt_support import handle_ctrl_c_press
|
|
|
|
handle_ctrl_c_press()
|
|
|
|
signal.signal(signal.SIGINT, _handler)
|
|
|
|
|
|
def _is_update_invocation(argv: list[str]) -> bool:
|
|
command_parts = resolve_command_parts(cli, argv)
|
|
return bool(command_parts) and command_parts[0] == "update"
|
|
|
|
|
|
def _sentry_entrypoint_for_invocation(argv: list[str]) -> str:
|
|
command_parts = resolve_command_parts(cli, argv)
|
|
if command_parts and command_parts[0] == "debug":
|
|
return "debug"
|
|
return "cli"
|
|
|
|
|
|
def _should_capture_cli_exception(exc: click.ClickException) -> bool:
|
|
"""Return whether a Click error represents an unexpected internal failure."""
|
|
return should_report_exception(exc)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""Entry point for the ``opensre`` console script."""
|
|
ensure_utf8_stdio()
|
|
cli_argv = list(sys.argv[1:] if argv is None else argv)
|
|
if is_fast_version_invocation(cli_argv):
|
|
print_fast_version(cli_argv)
|
|
return 0
|
|
|
|
from config.local_env import bootstrap_opensre_env_once
|
|
|
|
bootstrap_opensre_env_once(override=False)
|
|
try:
|
|
init_sentry(entrypoint=_sentry_entrypoint_for_invocation(cli_argv))
|
|
except ModuleNotFoundError as exc:
|
|
if exc.name != "sentry_sdk" or not _is_update_invocation(cli_argv):
|
|
raise
|
|
# Wire CLI-flavored implementations into the observability ports
|
|
# (ProgressTracker, debug_print) so any core code under core/domain,
|
|
# tools/investigation, utils that calls into the abstractions routes
|
|
# through the Rich-aware adapters during this process.
|
|
from surfaces.interactive_shell.ui.output.boundary import (
|
|
install_product_adapters,
|
|
)
|
|
|
|
install_product_adapters()
|
|
from platform.terminal.prompt_support import (
|
|
install_questionary_ctrl_c_double_exit,
|
|
install_questionary_escape_cancel,
|
|
)
|
|
|
|
install_questionary_escape_cancel()
|
|
install_questionary_ctrl_c_double_exit()
|
|
_install_sigint_handler()
|
|
StructuredError = load_structured_error_type()
|
|
|
|
try:
|
|
cli(
|
|
args=cli_argv,
|
|
standalone_mode=False,
|
|
obj={_CAPTURE_CLI_ANALYTICS: True, _CLI_ARGV: cli_argv},
|
|
)
|
|
except KeyboardInterrupt:
|
|
# A KeyboardInterrupt that escapes cli() was not handled by our
|
|
# double-exit logic (e.g. click.prompt, an unpatched library prompt).
|
|
# Print a newline so the terminal cursor lands on a clean line, then
|
|
# exit quietly — Click's "Aborted!" message is intentionally suppressed.
|
|
print(flush=True)
|
|
return 0
|
|
except click.Abort:
|
|
# Click raises Abort for some prompt-level cancel paths. Treat it as a
|
|
# clean user cancel, not as an unexpected CLI failure.
|
|
print(flush=True)
|
|
return 0
|
|
except click.ClickException as exc:
|
|
if _should_capture_cli_exception(exc):
|
|
report_exception(exc, context="surfaces.cli.main")
|
|
exc.show()
|
|
return exc.exit_code
|
|
except StructuredError as exc:
|
|
# A structured error raised by non-CLI code (tools/integrations) is not
|
|
# a ClickException, so render it as a clean panel (no traceback) here.
|
|
return render_structured_error(exc)
|
|
except click.exceptions.Exit as exc:
|
|
return exc.exit_code
|
|
except SystemExit as exc:
|
|
if isinstance(exc.code, int):
|
|
return exc.code
|
|
if exc.code is not None:
|
|
click.echo(exc.code, err=True)
|
|
return 1
|
|
return 0
|
|
except BaseException as exc:
|
|
if not isinstance(exc, KeyboardInterrupt):
|
|
capture_exception(exc, context="surfaces.cli.main.unhandled")
|
|
with suppress(Exception):
|
|
import sentry_sdk as _sentry_sdk
|
|
|
|
_sentry_sdk.flush(timeout=2)
|
|
raise
|
|
finally:
|
|
# Non-blocking: daemon worker may still drain in-flight events briefly.
|
|
shutdown_analytics(flush=False)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|