Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

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())